虚拟映射内核栈支持¶
- 作者:
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上重复使用。