内核地址消毒器 (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 选项的描述。

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

支持

架构

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

编译器

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

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

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

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

内存类型

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

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

基于硬件标签的 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 对象的分配和释放堆栈跟踪包含在报告中,请启用 CONFIG_STACKTRACE。要包含受影响的物理页面的分配和释放堆栈跟踪,请启用 CONFIG_PAGE_OWNER 并使用 page_owner=on 引导。

引导参数

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

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

或者,与 panic_on_warn 无关,可以使用 kasan.fault= 引导参数来控制崩溃和报告行为。

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

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

  • kasan.stacktrace=off=on 禁用或启用分配和释放堆栈跟踪收集(默认值: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 个字节可以是可访问的、部分可访问的、已释放的或红区的一部分。KASAN 对每个影子字节使用以下编码:00 表示相应内存区域的所有 8 个字节都可访问;数字 N (1 <= N <= 7) 表示前 N 个字节可访问,其他 (8 - N) 个字节不可访问;任何负值表示整个 8 字节字不可访问。KASAN 使用不同的负值来区分不同类型的不可访问内存,例如红区或已释放的内存(请参阅 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 的内核内存专用于其影子内存(在 x86_64 上使用 16TB 来覆盖 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))。这些函数通过检查相应的影子内存来检查内存访问是否有效。

使用内联检测,编译器不会进行函数调用,而是直接插入代码来检查影子内存。此选项会显著扩大内核,但与外部检测内核相比,性能提升 1.1-2 倍。

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

基于软件标签的 KASAN

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

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

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

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

基于软件标签的 KASAN 也有两种检测模式(外部模式,它发出回调以检查内存访问;以及内联模式,它内联执行影子内存检查)。使用外部检测模式,错误报告从执行访问检查的函数打印。使用内联检测,编译器会发出 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 文档