英文

虚拟映射内核栈支持

作者:

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

可以支持虚拟映射内核栈的架构应启用此布尔配置选项。要求如下:

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

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

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

VMAP_STACK

启用后,VMAP_STACK 布尔配置选项会分配虚拟映射的任务栈。此选项依赖于 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 记帐。因此,调用 __vmalloc_node_range 时不使用 __GFP_ACCOUNT。

  • 缓存 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 上缓存整个栈以供重用。