英语

虚拟映射内核栈支持

作者:

Shuah Khan <skhan@linuxfoundation.org>

概述

这是来自代码和原始补丁系列的信息汇编,这些补丁系列引入了虚拟映射内核栈功能 <https://lwn.net/Articles/694348/>

简介

内核栈溢出通常难以调试,并使内核容易受到攻击。问题可能在稍后出现,从而难以隔离和找到根本原因。

带有保护页的虚拟映射内核栈使内核栈溢出能够立即被捕获,而不是导致难以诊断的损坏。

HAVE_ARCH_VMAP_STACK和VMAP_STACK配置选项启用了对带有保护页的虚拟映射栈的支持。 此功能在栈溢出时会导致可靠的故障。 溢出后堆栈跟踪的可用性以及对溢出本身的响应取决于体系结构。

注意

截至本文撰写之时,arm64、powerpc、riscv、s390、um和x86都支持VMAP_STACK。

HAVE_ARCH_VMAP_STACK

可以支持虚拟映射内核栈的体系结构应启用此bool配置选项。 要求是

  • vmalloc空间必须足够大,才能容纳许多内核栈。 这可能会排除许多32位体系结构。

  • vmalloc空间中的栈需要可靠地工作。 例如,如果vmap页表是按需创建的,则此机制需要在栈指向具有未填充页表的虚拟地址时工作,或者arch代码(最可能是switch_to()和switch_mm())需要确保栈的页表条目在可能未填充的栈上运行之前已填充。

  • 如果栈溢出到保护页中,则应发生一些合理的事情。 “合理”的定义是灵活的,但是立即重新启动而不记录任何内容将是不友好的。

VMAP_STACK

启用后,VMAP_STACK bool配置选项会分配虚拟映射的任务栈。 此选项取决于HAVE_ARCH_VMAP_STACK。

  • 如果要使用带有保护页的虚拟映射内核栈,请启用此选项。 这会导致内核栈溢出立即被捕获,而不是导致难以诊断的损坏。

注意

将此功能与KASAN一起使用需要体系结构支持使用真实的影子内存来支持虚拟映射,并且必须启用KASAN_VMALLOC。

注意

VMAP_STACK启用后,无法在栈分配的数据上运行DMA。

内核配置选项和依赖项不断变化。 请参考最新的代码库

Kconfig <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/Kconfig>

分配

创建新内核线程时,线程栈是从页面级别分配器中的虚拟连续内存页分配的。 这些页面以PAGE_KERNEL保护映射到连续的内核虚拟空间中。

alloc_thread_stack_node()调用__vmalloc_node_range()以使用PAGE_KERNEL保护来分配栈。

  • 分配的栈被缓存并在以后被新线程重用,因此在将栈分配/释放给任务时,会手动执行memcg记帐。 因此,在没有__GFP_ACCOUNT的情况下调用__vmalloc_node_range。

  • 缓存vm_struct,以便能够在中断上下文中启动线程释放时找到。 可以在中断上下文中调用free_thread_stack()。

  • 在arm64上,所有VMAP的栈都需要具有相同的对齐方式,以确保VMAP的栈溢出检测能够正常工作。 体系结构特定的vmap栈分配器会处理此细节。

  • 这不解决中断栈 - 根据原始补丁

线程栈分配是从clone()、fork()、vfork()、kernel_thread()经由kernel_clone()启动的。 这些是一些用于搜索代码库以了解何时以及如何分配线程栈的提示。

大部分代码位于:kernel/fork.c <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/fork.c>

task_struct中的stack_vm_area指针跟踪虚拟分配的栈,并且非空stack_vm_area指针指示已启用虚拟映射内核栈。

struct vm_struct *stack_vm_area;

栈溢出处理

前导和尾随保护页有助于检测栈溢出。 当栈溢出到保护页中时,处理程序必须小心不要再次溢出栈。 当调用处理程序时,可能只剩下很少的栈空间。

在x86上,这是通过在双重故障栈上处理指示内核栈溢出的页面错误来完成的。

使用保护页测试VMAP分配

我们如何确保VMAP_STACK实际上是使用前导和尾随保护页进行分配的? 以下lkdtm测试可以帮助检测任何回归。

void lkdtm_STACK_GUARD_PAGE_LEADING()
void lkdtm_STACK_GUARD_PAGE_TRAILING()

结论

  • 至少在缓存命中的情况下,vmalloc栈的percpu缓存似乎比高阶栈分配快一些。

  • THREAD_INFO_IN_TASK完全摆脱了体系结构特定的thread_info,并将thread_info(仅包含标志)和“int cpu”嵌入到task_struct中。

  • 线程栈可以在任务死亡后立即释放(无需等待RCU),然后,如果正在使用vmapped栈,则缓存整个栈以在同一cpu上重复使用。