Kernel Address Sanitizer (KASAN)
Kernel Address Sanitizer (KASAN)是一种动态内存安全错误检测器,旨在发现越界和使用已释放内存的错误。KASAN有三种模式:
- 通用KASAN
- 基于软件标签的KASAN
- 基于硬件标签的KASAN
通用KASAN是一种调试模式,类似于用户空间ASan,启用CONFIG_KASAN_GENERIC。该模式支持许多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支持的架构
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_OUTLINE和CONFIG_KASAN_INLINE之间进行选择。
Outline和inline是编译器插装类型。前者生成较小的二进制文件,而后者速度最高可达2倍。
- 要将受影响的slab对象的alloc和free堆栈跟踪包含在报告中,请启用CONFIG_STACKTRACE。
- 要将受影响的物理页面的alloc和free堆栈跟踪包含在报告中,请启用CONFIG_PAGE_OWNER并使用page_owner=on引导。
Boot parameters
KASAN受到通用的panic_on_warn命令行参数的影响。当启用该参数时,KASAN在打印错误报告后会使内核崩溃。
默认情况下,KASAN仅为第一个无效的内存访问打印错误报告。使用kasan_multi_shot,KASAN会在每次无效访问时打印报告。这有效地禁用了KASAN报告的panic_on_warn。
另外,与panic_on_warn无关,可以使用kasan.fault=引导参数来控制panic和报告行为:
- kasan.fault=report、=panic或=panic_on_write控制是否仅打印KASAN报告、使内核崩溃或仅在无效写入时使内核崩溃(默认值:report)。即使启用了kasan_multi_shot,也会发生panic。请注意,当使用硬件标签为基础的KASAN的异步模式时,kasan.fault=panic_on_write总是在异步检查访问(包括读取)时发生panic。
软件和硬件标签为基础的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仅对每个顺序等于或大于kasan.page_alloc.sample.order的第N个page_alloc分配进行标记,其中N是采样参数的值(默认值:1,或标记每个这样的分配)。此参数旨在减轻KASAN引入的性能开销。
请注意,启用此参数会使硬件标签为基础的KASAN跳过由采样选择的分配的检查,从而错过对这些分配的错误访问。为了准确检测错误,请使用默认值。 - kasan.page_alloc.sample.order=<最小页面顺序>指定受采样影响的分配的最小顺序(默认值:3)。仅适用于kasan.page_alloc.sample设置为大于1的值。此参数旨在仅允许对大的page_alloc分配进行采样,这是性能开销最大的来源。
Error reports
一个典型的KASAN报告看起来像这样:
================================================================== BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0xa8/0xbc [test_kasan] 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 [test_kasan] kmalloc_tests_init+0x16/0x700 [test_kasan] 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 [test_kasan] kmalloc_tests_init+0x16/0x700 [test_kasan] 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 ==================================================================
这是一个内核错误报告,指示在kmalloc_oob_right函数中发生了一个内存越界写入错误。错误发生在地址ffff8801f44ec37b处,由进程insmod/2760执行。错误的大小为1字节,写入了地址ffff8801f44ec37b处的内存。
错误的堆栈跟踪显示了错误发生的位置和调用链。错误发生在kmalloc_tests_init函数中,该函数是test_kasan模块的初始化函数。错误发生时,test_kasan模块正在加载。
错误的内存访问导致了一个内核错误报告,并且指示错误的地址属于一个大小为128字节的kmalloc-128缓存中的对象。错误的地址位于128字节区域[ffff8801f44ec300, ffff8801f44ec380)内的第123个字节。
错误的地址所在的页面是ffffea0007d13b00,它的计数为1,映射计数为0,映射为ffff8801f7001640。页面的标志为0x200000000000100,表示它是一个slab页面。
错误发生时,附近的内存状态显示了错误地址周围的内存内容。在ffff8801f44ec300处,内存被写入了8个字节的0x03。
这个错误可能是由于内核模块test_kasan中的代码错误导致的。建议检查kmalloc_tests_init函数中的代码,特别是与kmalloc和内存访问相关的部分,以找出并修复错误。
下面来解析这个报告
- KASAN报告头部概述了发生的错误类型和导致错误的访问类型。
- 接下来是错误访问的堆栈跟踪,访问内存分配的堆栈跟踪(如果访问了slab对象),以及释放对象的堆栈跟踪(如果是use-after-free错误报告)。
- 接下来是访问的slab对象的描述和有关访问内存页面的信息。
- 最后,报告显示了访问地址周围的内存状态。在内部,KASAN为每个内存粒度单独跟踪内存状态,每个内存粒度都是8或16个对齐的字节,具体取决于KASAN模式。
报告中内存状态部分的每个数字显示了围绕访问地址的一个内存粒度的状态。对于通用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-bounds或use-after-free)是最佳努力的:
- KASAN根据其拥有的有限信息打印最可能的错误类型。
- 实际的错误类型可能不同。
- 通用KASAN还报告了最多两个辅助调用堆栈跟踪。
- 这些堆栈跟踪指向与对象交互但不直接出现在错误访问堆栈跟踪中的代码位置。目前,这包括call_rcu()和workqueue排队。
Implementation details
Generic KASAN
Generic KASAN使用影子内存记录每个字节的内存是否可以安全访问,并使用编译时插装在每次内存访问之前插入影子内存检查。Generic 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倍。
Generic KASAN是唯一一个通过隔离区(quarantine)延迟重用释放对象的模式(请参阅mm/kasan/quarantine.c进行实现)。
Software Tag-Based KASAN
Software Tag-Based KASAN使用软件内存标记方法来检查访问的有效性。目前仅在arm64架构上实现。
Software Tag-Based KASAN使用arm64 CPU的Top Byte Ignore (TBI)特性,在内核指针的最高字节中存储一个指针标记。它使用影子内存来存储与每个16字节内存单元相关联的内存标记(因此,它将1/16的内核内存用于影子内存)。
在每次内存分配时,Software Tag-Based KASAN生成一个随机标记,用该标记标记分配的内存,并将相同的标记嵌入返回的指针中。
Software Tag-Based KASAN使用编译时插装在每次内存访问之前插入检查。这些检查确保正在访问的内存的标记与用于访问该内存的指针的标记相等。如果标记不匹配,Software Tag-Based KASAN会打印一个错误报告。
Software Tag-Based KASAN还有两种插装模式(outline和inline)。使用outline插装模式时,错误报告将从执行访问检查的函数中打印出来。使用inline插装时,编译器会发出一个brk指令,并使用专用的brk处理程序来打印错误报告。
Software Tag-Based KASAN将0xFF用作匹配所有指针标记(通过带有0xFF指针标记的指针进行的访问不会被检查)。值0xFE目前保留用于标记释放的内存区域。
Hardware Tag-Based KASAN
Hardware Tag-Based KASAN在概念上与软件模式类似,但是使用硬件内存标记支持而不是编译器插装和影子内存。
Hardware Tag-Based KASAN目前仅在arm64架构上实现,并基于arm64 Memory Tagging Extension (MTE)(引入了ARMv8.5指令集架构)和Top Byte Ignore (TBI)。
特殊的arm64指令用于为每个分配分配内存标记。指向这些分配的指针被分配相同的标记。在每次内存访问时,硬件确保正在访问的内存的标记与用于访问该内存的指针的标记相等。如果标记不匹配,将生成一个故障,并打印一个报告。
Hardware Tag-Based KASAN将0xFF用作匹配所有指针标记(通过带有0xFF指针标记的指针进行的访问不会被检查)。值0xFE目前保留用于标记释放的内存区域。
如果硬件不支持MTE(早于ARMv8.5),则不会启用Hardware Tag-Based KASAN。在这种情况下,所有KASAN引导参数都将被忽略。
请注意,启用CONFIG_KASAN_HW_TAGS始终会导致内核TBI被启用。即使提供了kasan.mode=off或硬件不支持MTE(但支持TBI)。
Hardware Tag-Based KASAN仅报告第一个发现的错误。之后,MTE标记检查将被禁用。
Shadow memory
本节内容仅适用于软件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模式,要在特定文件或目录上禁用插装,可以在相应的内核Makefile中添加KASAN_SANITIZE注释:
对于单个文件(例如main.o):
KASAN_SANITIZE_main.o := n
对于一个目录中的所有文件:
KASAN_SANITIZE := n
对于软件KASAN模式,要按函数基础禁用插装,可以使用KASAN特定的__no_sanitize_address
函数属性或通用的noinstr
属性。
请注意,禁用编译器插装(无论是按文件还是按函数)会使得软件KASAN模式忽略直接发生在该代码中的访问。但是,当访问间接发生(通过调用插装函数)或使用不使用编译器插装的硬件标签KASAN时,这并不起作用。
对于软件KASAN模式,要在内核代码的某个部分中禁用KASAN报告,可以使用kasan_disable_current()
/kasan_enable_current()
部分对该代码进行注释。这也会禁用通过函数调用间接发生的访问的报告。
对于基于标签的KASAN模式,要禁用访问检查,可以使用kasan_reset_tag()
或page_kasan_tag_reset()
。请注意,通过page_kasan_tag_reset()
临时禁用访问检查需要保存和恢复每页的KASAN标签,可以使用page_kasan_tag
和page_kasan_tag_set
来实现。
测试
有KASAN测试允许验证KASAN是否工作,并可以检测某些类型的内存损坏。测试包括两部分:
1.与KUnit测试框架集成的测试。使用CONFIG_KASAN_KUIT_TEST启用。这些测试可以以几种不同的方式自动运行和部分验证;请参阅下面的说明。
2.当前与KUnit不兼容的测试。使用CONFIG_KASAN_MODULE_TEST启用,只能作为模块运行。这些测试只能通过加载内核模块并检查内核日志中的KASAN报告来手动验证。
如果检测到错误,每个与KUnit兼容的KASAN测试都会打印多个KASAN报告中的一个。然后,测试打印其编号和状态。
- 当一个测试通过时:ok 28 - kmalloc_double_kzfree
- 当一个测试因为kmalloc失败而失败时:# kmalloc_large_oob_right: 在lib/test_kasan.c的第163行断言失败,预期的ptr不为空,但不是ok 4 - kmalloc_large_oob_right
- 当一个测试因为缺少KASAN报告而失败时:# kmalloc_double_kzfree: 在lib/test_kasan.c的第974行预期失败,期望在’kfree_sensitive(ptr)'中发生KASAN失败,但没有发生 not ok 44 - kmalloc_double_kzfree
最后打印所有KASAN测试的累积状态。如果成功:ok 1 - kasan。如果其中一个测试失败:not ok 1 - kasan。
有几种运行KUnit兼容的KASAN测试的方法。
- 可加载模块:启用CONFIG_KUNIT后,KASAN-KUnit测试可以作为可加载模块构建,并通过使用insmod或modprobe加载test_kasan.ko来运行。
- 内置:启用CONFIG_KUNIT后,KASAN-KUnit测试也可以作为内置模块构建。在这种情况下,测试将作为late-init调用在启动时运行。
- 使用kunit_tool:启用CONFIG_KUNIT和CONFIG_KASAN_KUNIT_TEST后,还可以使用kunit_tool以更可读的方式查看KUnit测试的结果。这不会打印通过的测试的KASAN报告。有关kunit_tool的更多最新信息,请参阅KUnit文档。