内核地址消毒器 (KASAN)

概述

内核地址消毒器 (KASAN) 是一种动态内存安全错误检测器,旨在查找越界和释放后使用错误。

KASAN 具有三种模式

  1. 通用 KASAN

  2. 基于软件标签的 KASAN

  3. 基于硬件标签的 KASAN

通用 KASAN,通过 CONFIG_KASAN_GENERIC 启用,是用于调试的模式,类似于用户空间 ASan。此模式在许多 CPU 架构上受支持,但它具有显著的性能和内存开销。

基于软件标签的 KASAN 或 SW_TAGS KASAN,通过 CONFIG_KASAN_SW_TAGS 启用,可用于调试和内部测试,类似于用户空间 HWASan。此模式仅支持 arm64,但其适度的内存开销允许在内存受限的设备上使用真实工作负载进行测试。

基于硬件标签的 KASAN 或 HW_TAGS KASAN,通过 CONFIG_KASAN_HW_TAGS 启用,旨在用作现场内存错误检测器或安全缓解措施。此模式仅适用于支持 MTE (内存标记扩展) 的 arm64 CPU,但其内存和性能开销较低,因此可在生产中使用。

有关每种 KASAN 模式的内存和性能影响的详细信息,请参阅相应 Kconfig 选项的说明。

通用模式和基于软件标签的模式通常称为软件模式。基于软件标签的模式和基于硬件标签的模式称为基于标签的模式。

支持

架构

通用 KASAN 在 x86_64、arm、arm64、powerpc、riscv、s390、xtensa 和 loongarch 上受支持,基于标签的 KASAN 模式仅在 arm64 上受支持。

编译器

软件 KASAN 模式使用编译时插桩在每次内存访问之前插入有效性检查,因此需要提供支持的编译器版本。 基于硬件标签的模式依赖于硬件来执行这些检查,但仍然需要支持内存标记指令的编译器版本。

通用 KASAN 需要 GCC 8.3.0 或更高版本,或者内核支持的任何 Clang 版本。

基于软件标签的 KASAN 需要 GCC 11+ 或内核支持的任何 Clang 版本。

基于硬件标签的 KASAN 需要 GCC 10+ 或 Clang 12+。

内存类型

通用 KASAN 支持查找 slab、page_alloc、vmap、vmalloc、stack 和全局内存中的错误。

基于软件标签的 KASAN 支持 slab、page_alloc、vmalloc 和 stack 内存。

基于硬件标签的 KASAN 支持 slab、page_alloc 和不可执行的 vmalloc 内存。

对于 slab,软件 KASAN 模式都支持 SLUB 和 SLAB 分配器,而基于硬件标签的 KASAN 仅支持 SLUB。

用法

要启用 KASAN,请使用以下命令配置内核

CONFIG_KASAN=y

并在 CONFIG_KASAN_GENERIC (启用通用 KASAN)、CONFIG_KASAN_SW_TAGS (启用基于软件标签的 KASAN) 和 CONFIG_KASAN_HW_TAGS (启用基于硬件标签的 KASAN) 之间进行选择。

对于软件模式,还可以在 CONFIG_KASAN_OUTLINECONFIG_KASAN_INLINE 之间进行选择。 Outline 和 inline 是编译器插桩类型。 前者产生较小的二进制文件,而后者速度最多快 2 倍。

要将受影响的 slab 对象的 alloc 和 free 堆栈跟踪包含到报告中,请启用 CONFIG_STACKTRACE。 要将受影响的物理页面的 alloc 和 free 堆栈跟踪包含到报告中,请启用 CONFIG_PAGE_OWNER 并使用 page_owner=on 引导。

启动参数

KASAN 受通用 panic_on_warn 命令行参数的影响。 启用后,KASAN 会在打印错误报告后使内核崩溃。

默认情况下,KASAN 仅打印第一个无效内存访问的错误报告。 使用 kasan_multi_shot,KASAN 会打印每个无效访问的报告。 这实际上为 KASAN 报告禁用了 panic_on_warn

或者,独立于 panic_on_warnkasan.fault= 启动参数可用于控制崩溃和报告行为

  • kasan.fault=report=panic=panic_on_write 控制是仅打印 KASAN 报告、使内核崩溃还是仅在无效写入时使内核崩溃 (默认值: report)。 即使启用了 kasan_multi_shot,也会发生崩溃。 请注意,当使用基于硬件标签的 KASAN 的异步模式时,kasan.fault=panic_on_write 始终会因异步检查的访问 (包括读取) 而崩溃。

基于软件和硬件标签的 KASAN 模式 (请参阅下面有关各种模式的部分) 支持更改堆栈跟踪收集行为

  • kasan.stacktrace=off=on 禁用或启用 alloc 和 free 堆栈跟踪收集 (默认值: on)。

  • kasan.stack_ring_size=<条目数> 指定堆栈环中的条目数 (默认值: 32768)。

基于硬件标签的 KASAN 模式旨在用于生产中,作为一种安全缓解措施。 因此,它支持其他启动参数,允许完全禁用 KASAN 或控制其功能

  • kasan=off=on 控制是否启用 KASAN (默认值: on)。

  • kasan.mode=sync=async=asymm 控制是否在同步、异步或非对称执行模式下配置 KASAN (默认值: sync)。 同步模式:当发生标签检查故障时,会立即检测到错误的访问。 异步模式:会延迟错误的访问检测。 当发生标签检查故障时,信息存储在硬件中 (对于 arm64,存储在 TFSR_EL1 寄存器中)。 内核定期检查硬件,并且仅在这些检查期间报告标签故障。 非对称模式:在读取时同步检测到错误的访问,而在写入时异步检测到错误的访问。

  • kasan.vmalloc=off=on 禁用或启用 vmalloc 分配的标记 (默认值: on)。

  • kasan.page_alloc.sample=<采样间隔> 使 KASAN 仅标记每第 N 个 page_alloc 分配,其中顺序等于或大于 kasan.page_alloc.sample.order,其中 N 是 sample 参数的值 (默认值: 1,或者标记每个此类分配)。 此参数旨在缓解 KASAN 引入的性能开销。 请注意,启用此参数会使基于硬件标签的 KASAN 跳过抽样选择的分配的检查,从而错过对这些分配的错误访问。 使用默认值进行准确的错误检测。

  • kasan.page_alloc.sample.order=<最小页面顺序> 指定受抽样影响的分配的最小顺序 (默认值: 3)。 仅当 kasan.page_alloc.sample 设置为大于 1 的值时才适用。 此参数旨在仅允许抽样大型 page_alloc 分配,这是性能开销的最大来源。

错误报告

典型的 KASAN 报告如下所示

==================================================================
BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0xa8/0xbc [kasan_test]
Write of size 1 at addr ffff8801f44ec37b by task insmod/2760

CPU: 1 PID: 2760 Comm: insmod Not tainted 4.19.0-rc3+ #698
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1 04/01/2014
Call Trace:
 dump_stack+0x94/0xd8
 print_address_description+0x73/0x280
 kasan_report+0x144/0x187
 __asan_report_store1_noabort+0x17/0x20
 kmalloc_oob_right+0xa8/0xbc [kasan_test]
 kmalloc_tests_init+0x16/0x700 [kasan_test]
 do_one_initcall+0xa5/0x3ae
 do_init_module+0x1b6/0x547
 load_module+0x75df/0x8070
 __do_sys_init_module+0x1c6/0x200
 __x64_sys_init_module+0x6e/0xb0
 do_syscall_64+0x9f/0x2c0
 entry_SYSCALL_64_after_hwframe+0x44/0xa9
RIP: 0033:0x7f96443109da
RSP: 002b:00007ffcf0b51b08 EFLAGS: 00000202 ORIG_RAX: 00000000000000af
RAX: ffffffffffffffda RBX: 000055dc3ee521a0 RCX: 00007f96443109da
RDX: 00007f96445cff88 RSI: 0000000000057a50 RDI: 00007f9644992000
RBP: 000055dc3ee510b0 R08: 0000000000000003 R09: 0000000000000000
R10: 00007f964430cd0a R11: 0000000000000202 R12: 00007f96445cff88
R13: 000055dc3ee51090 R14: 0000000000000000 R15: 0000000000000000

Allocated by task 2760:
 save_stack+0x43/0xd0
 kasan_kmalloc+0xa7/0xd0
 kmem_cache_alloc_trace+0xe1/0x1b0
 kmalloc_oob_right+0x56/0xbc [kasan_test]
 kmalloc_tests_init+0x16/0x700 [kasan_test]
 do_one_initcall+0xa5/0x3ae
 do_init_module+0x1b6/0x547
 load_module+0x75df/0x8070
 __do_sys_init_module+0x1c6/0x200
 __x64_sys_init_module+0x6e/0xb0
 do_syscall_64+0x9f/0x2c0
 entry_SYSCALL_64_after_hwframe+0x44/0xa9

Freed by task 815:
 save_stack+0x43/0xd0
 __kasan_slab_free+0x135/0x190
 kasan_slab_free+0xe/0x10
 kfree+0x93/0x1a0
 umh_complete+0x6a/0xa0
 call_usermodehelper_exec_async+0x4c3/0x640
 ret_from_fork+0x35/0x40

The buggy address belongs to the object at ffff8801f44ec300
 which belongs to the cache kmalloc-128 of size 128
The buggy address is located 123 bytes inside of
 128-byte region [ffff8801f44ec300, ffff8801f44ec380)
The buggy address belongs to the page:
page:ffffea0007d13b00 count:1 mapcount:0 mapping:ffff8801f7001640 index:0x0
flags: 0x200000000000100(slab)
raw: 0200000000000100 ffffea0007d11dc0 0000001a0000001a ffff8801f7001640
raw: 0000000000000000 0000000080150015 00000001ffffffff 0000000000000000
page dumped because: kasan: bad access detected

Memory state around the buggy address:
 ffff8801f44ec200: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
 ffff8801f44ec280: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
>ffff8801f44ec300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03
                                                                ^
 ffff8801f44ec380: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
 ffff8801f44ec400: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
==================================================================

报告标头总结了发生的错误类型以及导致错误的访问类型。 后面是错误访问的堆栈跟踪、访问的内存的分配位置的堆栈跟踪 (如果访问了 slab 对象) 以及对象释放位置的堆栈跟踪 (如果是释放后使用错误报告)。 接下来是访问的 slab 对象的描述以及有关访问的内存页面的信息。

最后,报告显示了访问地址周围的内存状态。 在内部,KASAN 分别跟踪每个内存粒度的内存状态,这取决于 KASAN 模式是 8 个或 16 个对齐的字节。 报告的内存状态部分中的每个数字都显示了围绕访问地址的内存粒度之一的状态。

对于通用 KASAN,每个内存粒度的大小为 8。 每个粒度的状态都编码在一个影子字节中。 这 8 个字节可以是可访问的、部分可访问的、已释放的或属于 redzone 的一部分。 KASAN 使用以下编码用于每个影子字节:00 表示相应内存区域的所有 8 个字节都可以访问; 数字 N (1 <= N <= 7) 表示前 N 个字节可以访问,其他 (8 - N) 个字节不可访问; 任何负值都表示整个 8 字节字都不可访问。 KASAN 使用不同的负值来区分不同类型的不可访问内存,例如 redzone 或释放的内存 (请参阅 mm/kasan/kasan.h)。

在上面的报告中,箭头指向影子字节 03,这意味着访问的地址是部分可访问的。

对于基于标签的 KASAN 模式,此最后的报告部分显示了访问地址周围的内存标签 (请参阅实现细节部分)。

请注意,KASAN 错误标题 (如 slab-out-of-boundsuse-after-free) 是尽力而为:KASAN 根据它拥有的有限信息打印最可能的错误类型。 错误的实际类型可能不同。

通用 KASAN 还会报告最多两个辅助调用堆栈跟踪。 这些堆栈跟踪指向代码中与对象交互但在错误访问堆栈跟踪中未直接显示的位置。 目前,这包括 call_rcu() 和工作队列排队。

CONFIG_KASAN_EXTRA_INFO

启用 CONFIG_KASAN_EXTRA_INFO 允许 KASAN 记录和报告更多信息。 当前支持的额外信息是在分配和释放时的 CPU 编号和时间戳。 更多信息有助于查找错误原因并将错误与其他系统事件相关联,代价是使用额外的内存来记录更多信息 (更多成本详细信息请参阅 CONFIG_KASAN_EXTRA_INFO 的帮助文本)。

这是启用 CONFIG_KASAN_EXTRA_INFO 的报告 (仅显示不同的部分)

==================================================================
...
Allocated by task 134 on cpu 5 at 229.133855s:
...
Freed by task 136 on cpu 3 at 230.199335s:
...
==================================================================

实现细节

通用 KASAN

软件 KASAN 模式使用影子内存来记录内存的每个字节是否可以安全访问,并使用编译时插桩在每次内存访问之前插入影子内存检查。

通用 KASAN 将内核内存的 1/8 专用于其影子内存 (16TB 以覆盖 x86_64 上的 128TB),并使用带有比例和偏移量的直接映射将内存地址转换为其相应的影子地址。

这是将地址转换为其相应影子地址的函数

static inline void *kasan_mem_to_shadow(const void *addr)
{
    return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
            + KASAN_SHADOW_OFFSET;
}

其中 KASAN_SHADOW_SCALE_SHIFT = 3

编译时插桩用于插入内存访问检查。 编译器在每次大小为 1、2、4、8 或 16 的内存访问之前插入函数调用 (__asan_load*(addr), __asan_store*(addr))。 这些函数通过检查相应的影子内存来检查内存访问是否有效。

使用内联插桩,编译器直接插入代码以检查影子内存,而不是进行函数调用。 此选项显著扩大了内核,但与轮廓插桩内核相比,它提供了 x1.1-x2 的性能提升。

通用 KASAN 是唯一通过隔离区延迟重用已释放对象的模式 (有关实现,请参阅 mm/kasan/quarantine.c)。

基于软件标签的 KASAN

基于软件标签的 KASAN 使用软件内存标记方法来检查访问有效性。 目前仅为 arm64 架构实现了该方法。

基于软件标签的 KASAN 使用 arm64 CPU 的顶部字节忽略 (TBI) 功能在内核指针的顶部字节中存储指针标签。 它使用影子内存来存储与每个 16 字节内存单元关联的内存标签 (因此,它将内核内存的 1/16 专用于影子内存)。

在每次内存分配时,基于软件标签的 KASAN 都会生成一个随机标签,使用此标签标记分配的内存,并将相同的标签嵌入到返回的指针中。

基于软件标签的 KASAN 使用编译时插桩在每次内存访问之前插入检查。 这些检查确保被访问的内存的标签等于用于访问此内存的指针的标签。 如果标签不匹配,则基于软件标签的 KASAN 会打印错误报告。

基于软件标签的 KASAN 也有两种插桩模式 (outline,它发出回调以检查内存访问;以及 inline,它内联执行影子内存检查)。 使用 outline 插桩模式,错误报告从执行访问检查的函数中打印。 使用 inline 插桩,编译器发出 brk 指令,并且使用专用的 brk 处理程序来打印错误报告。

基于软件标签的 KASAN 使用 0xFF 作为匹配所有指针标签 (不检查通过带有 0xFF 指针标签的指针进行的访问)。 值 0xFE 当前保留用于标记释放的内存区域。

基于硬件标签的 KASAN

基于硬件标签的 KASAN 在概念上与软件模式相似,但使用硬件内存标记支持而不是编译器插桩和影子内存。

基于硬件标签的 KASAN 目前仅为 arm64 架构实现,并且基于 ARMv8.5 指令集架构中引入的 arm64 内存标记扩展 (MTE) 和顶部字节忽略 (TBI)。

专用 arm64 指令用于为每次分配分配内存标签。 相同的标签分配给指向这些分配的指针。 在每次内存访问时,硬件都会确保被访问的内存的标签等于用于访问此内存的指针的标签。 如果标签不匹配,则会生成故障并打印报告。

基于硬件标签的 KASAN 使用 0xFF 作为匹配所有指针标签 (不检查通过带有 0xFF 指针标签的指针进行的访问)。 值 0xFE 当前保留用于标记释放的内存区域。

如果硬件不支持 MTE (pre ARMv8.5),则不会启用基于硬件标签的 KASAN。 在这种情况下,所有 KASAN 启动参数都将被忽略。

请注意,启用 CONFIG_KASAN_HW_TAGS 始终会导致启用内核内 TBI。 即使提供了 kasan.mode=off 或当硬件不支持 MTE (但支持 TBI) 时。

基于硬件标签的 KASAN 仅报告第一个找到的错误。 之后,MTE 标签检查将被禁用。

影子内存

本节的内容仅适用于软件 KASAN 模式。

内核在地址空间的几个不同部分中映射内存。 内核虚拟地址的范围很大:没有足够的真实内存来支持每个可由内核访问的地址的真实影子区域。 因此,KASAN 仅为地址的某些部分映射真实影子。

默认行为

默认情况下,架构仅将真实内存映射到线性映射的影子区域上 (以及可能其他小区域)。 对于所有其他区域 (例如 vmalloc 和 vmemmap 空间),单个只读页面映射到影子区域上。 此只读影子页面声明所有内存访问都已允许。

这给模块带来了问题:它们不在线性映射中,而是在专用的模块空间中。 通过挂钩到模块分配器,KASAN 临时映射真实影子内存以覆盖它们。 这允许检测到对模块全局变量的无效访问,例如。

这也创建了与 VMAP_STACK 的不兼容性:如果堆栈位于 vmalloc 空间中,它将被只读页面隐藏,并且内核在尝试为堆栈变量设置影子数据时将出现故障。

CONFIG_KASAN_VMALLOC

使用 CONFIG_KASAN_VMALLOC,KASAN 可以覆盖 vmalloc 空间,但代价是更大的内存使用量。 目前,x86、arm64、riscv、s390 和 powerpc 支持此功能。

这通过挂钩到 vmalloc 和 vmap 并动态分配真实影子内存来支持映射来实现。

vmalloc 空间中的大多数映射都很小,需要小于整个页面的影子空间。 因此,为每个映射分配整个影子页面将是浪费的。 此外,为了确保不同的映射使用不同的影子页面,映射必须与 KASAN_GRANULE_SIZE * PAGE_SIZE 对齐。

相反,KASAN 在多个映射之间共享后备空间。 当 vmalloc 空间中的映射使用影子区域的特定页面时,它会分配一个后备页面。 此页面可以稍后由其他 vmalloc 映射共享。

KASAN 挂钩到 vmap 基础结构以延迟清理未使用的影子内存。

为了避免围绕交换映射的困难,KASAN 希望覆盖 vmalloc 空间的那部分影子区域不会被早期的影子页面覆盖,而是保持未映射状态。 这将需要在特定于架构的代码中进行更改。

这允许 x86 上支持 VMAP_STACK,并可以简化对没有固定模块区域的架构的支持。

面向开发者

忽略访问

软件 KASAN 模式使用编译器插桩来插入有效性检查。 此类插桩可能与内核的某些部分不兼容,因此需要禁用。

内核的其他部分可能会访问已分配对象的元数据。 通常,KASAN 会检测并报告此类访问,但在某些情况下 (例如,在内存分配器中),这些访问是有效的。

对于软件 KASAN 模式,要禁用特定文件或目录的插桩,请将 KASAN_SANITIZE 注释添加到相应的内核 Makefile

  • 对于单个文件 (例如 main.o)

    KASAN_SANITIZE_main.o := n
    
  • 对于一个目录中的所有文件

    KASAN_SANITIZE := n
    

对于软件 KASAN 模式,要在每个函数的基础上禁用插桩,请使用 KASAN 特定的 __no_sanitize_address 函数属性或通用的 noinstr 函数属性。

请注意,禁用编译器插桩 (无论是按文件还是按函数) 会使 KASAN 忽略软件 KASAN 模式下直接在该代码中发生的访问。 当访问间接发生 (通过调用插桩函数) 或使用不使用编译器插桩的基于硬件标签的 KASAN 时,它无济于事。

对于软件 KASAN 模式,要在内核代码的一部分中为当前任务禁用 KASAN 报告,请使用 kasan_disable_current()/kasan_enable_current() 部分注释代码的这一部分。 这也会禁用通过函数调用发生的间接访问的报告。

对于基于标签的 KASAN 模式,要禁用访问检查,请使用 kasan_reset_tag()page_kasan_tag_reset()。 请注意,通过 page_kasan_tag_reset() 临时禁用访问检查需要通过 page_kasan_tag/page_kasan_tag_set 保存和恢复每个页面的 KASAN 标签。

测试

有一些 KASAN 测试允许验证 KASAN 是否工作以及是否可以检测某些类型的内存损坏。

所有 KASAN 测试都与 KUnit 测试框架集成,可以通过 CONFIG_KASAN_KUNIT_TEST 启用。 这些测试可以以几种不同的方式运行和部分自动验证; 请参阅下面的说明。

如果检测到错误,每个 KASAN 测试都会打印多个 KASAN 报告之一。 然后,测试会打印其编号和状态。

当测试通过时

ok 28 - kmalloc_double_kzfree

当测试因 kmalloc 失败而失败时

# kmalloc_large_oob_right: ASSERTION FAILED at mm/kasan/kasan_test.c:245
Expected ptr is not null, but is
not ok 5 - kmalloc_large_oob_right

当测试因缺少 KASAN 报告而失败时

# kmalloc_double_kzfree: EXPECTATION FAILED at mm/kasan/kasan_test.c:709
KASAN failure expected in "kfree_sensitive(ptr)", but none occurred
not ok 28 - kmalloc_double_kzfree

最后,打印所有 KASAN 测试的累积状态。 成功时

ok 1 - kasan

或者,如果其中一个测试失败

not ok 1 - kasan

有几种方法可以运行 KASAN 测试。

  1. 可加载模块

    启用 CONFIG_KUNIT 后,可以将测试构建为可加载模块,并通过使用 insmodmodprobe 加载 kasan_test.ko 来运行它们。

  2. 内置

    如果内置了 CONFIG_KUNIT,测试也可以内置。在这种情况下,测试将在启动时作为晚期初始化调用运行。

  3. 使用 kunit_tool

    如果内置了 CONFIG_KUNITCONFIG_KASAN_KUNIT_TEST,也可以使用 kunit_tool 以更易读的方式查看 KUnit 测试的结果。 这将不会打印已通过测试的 KASAN 报告。 有关 kunit_tool 的最新信息,请参阅 KUnit 文档