引言
Kasan 是 Kernel Address Sanitizer 的缩写,它是一个动态检测内存错误的工具,主要功能是检查内存越界访问和使用已释放的内存等问题。Kasan 集成在 Linux 内核中,随 Linux 内核代码一起发布,并由内核社区维护和发展。
背景
Kasan 可以追溯到 LLVM 的 sanitizers 项目(https://github.com/google/sanitizers),这个项目包含了 AddressSanitizer,MemorySanitizer,ThreadSanitizer 和 LeakSanitizer 等工具。但这些工具只能检测用户空间的内存问题。通过在编译时加入指定的选项,就可以给用户程序加入 Address Sanitizer 功能。
清单 1. 用户空间内存错误代码实例
当运行以上有内存使用错误的程序时,加入 Address Sanitizer 功能的的版本会报告如下的错误信息,而没有任何选项的版本则会正常结束程序。
清单 2. Address Sanitizer 运行结果
Andrey Ryabinin 借鉴了 AddressSanitizer 的思想,并在 Linux 内核中实现了 Kernel Address Sanitizer。所以 Kasan 也可以看成是用于内核空间的 Address Sanitizer。
原理
Kasan 的原理是利用“额外”的内存来标记那些可以被使用的内存的状态。这些做标记的区域被称为影子区域(shadow region)。了解 Linux 内存管理的读者知道,内存中的每个物理页在内存中都会有一个 struct page 这样的结构体来表示,即每 4KB 的页需要 40B 的结构体,大约 1% 的内存用来表示内存本身。Kasan 与其类似但“浪费”更为严重,影子区域的比例是 1:8,即总内存的九分之一会被“浪费”。用官方文档中的例子,如果有 128TB 的可用内存,需要有额外 16TB 的内存用来做标记。
做标记的方法比较简单,将可用内存按照 8 子节的大小分组,如果每组中所有 8 个字节都可以访问,则影子内存中相应的地方用全零(0x00)表示;如果可用内存的前 N(1 到 7 范围之间)个字节可用,则影子内存中响应的位置用 N 表示;其它情况影子内存用负数表示该内存不可用。
图 1. Kasan 内存布局原理
使用
Kasan 是内核的一部分,使用时需要重新配置、编译并安装内核。Kasan 在 Linux 内核 4.0 版本时被引入内核,所以选择的内核代码需要高于 4.0 版本。另外,最基本的 Kasan 功能需要 GCC4.9.2 支持,更多的支持则需要 GCC5.0 及以上版本。
首先是配置和编译内核。
运行如下命令启动图形配置界面:
清单 3. Linux 图形配置命令
make menuconfig
图 2. Kasan 内核选项配置界面
图 3. Kasan 模式选项
然后重新编译并安装内核即可,除了通用的编译和安装命令,在 Fedora 这种发行版本中,还需要更新 grub。
清单 4. Linux 内核编译、安装命令
清单 5. Grub 配置命令
测试
学习 Linux 的同学一定对 Linus 的名言“Talk is cheap, show me the code.”耳熟能详。由于 Kasan 这部分代码一直在变化之中,更重要的是代码也比较难懂,本文暂时不去讨论具体的实现细节,而是从测试的角度研究其原理。
幸运的是 Linux 内核的源码中已经包含了针对 Kasan 的测试代码,其位置在 linux/lib/test_kasan.c。编译内核或者单独编译 lib 模块的时候,会生成 test_kasan.ko 模块。当向内核插入该模块的时候,就会执行测试代码。
例如,下面的代码模拟了内存越界的情况:申请了 124 字节的空间,却写访问第 125 个字节的内容,则会造成越界访问的问题。
清单 6. Kasan 内存右侧越界测试代码
当运行以上测试代码的时候,在内核日志中会详细打印以下内容:
清单 7. 内核日志
[18319.272272] kasan test: kmalloc_oob_right out-of-bounds to right
[18319.272288] kasan test: kmalloc_oob_right ptr address:
0xffff8800d40b9798
[18319.272292] =====================================================
[18319.272996] BUG: KASAN: slab-out-of-bounds in
kmalloc_oob_right+0xb4/0xdb [test_kasan] at addr ffff8800d40b9814
[18319.274250] Write of size 1 by task insmod/4852
[18319.274992] ===========================================
==================================
[18319.275982] BUG kmalloc-128 (Tainted: G B
OE ): kasan: bad access detected
[18319.276103] ----------------------------------------------------
[18319.276103] INFO: Allocated in 0xffff8800d40b9d08
age=18446723238827703540 cpu=0 pid=0
[18319.276103] kmalloc_oob_right+0x53/0xdb [test_kasan]
[18319.276103] ___slab_alloc+0x4da/0x540
[18319.276103] __slab_alloc+0x20/0x40
[18319.276103] kmem_cache_alloc_trace+0x1f8/0x270
[18319.276103] kmalloc_oob_right+0x53/0xdb [test_kasan]
[18319.276103] kmalloc_tests_init+0x9/0xf25 [test_kasan]
[18319.276103] do_one_initcall+0xa9/0x230
[18319.276103] do_init_module+0x1d0/0x4de
[18319.276103] load_module+0x74d0/0x9ca0
[18319.276103] SYSC_finit_module+0x190/0x1d0
[18319.276103] SyS_finit_module+0xe/0x10
[18319.276103] entry_SYSCALL_64_fastpath+0x1e/0xa8
[18319.276103] INFO: Freed in 0x10044bcf2 age=18446723238827703542 cpu=0 pid=0
[18319.276103] load_elf_binary+0x219/0x4400
[18319.276103] __slab_free+0x17f/0x2d0
[18319.276103] kfree+0x18a/0x1d0
[18319.276103] load_elf_binary+0x219/0x4400
[18319.276103] search_binary_handler+0x151/0x420
[18319.276103] do_execveat_common.isra.36+0xfd9/0x1d20
[18319.276103] SyS_execve+0x3a/0x50
[18319.276103] do_syscall_64+0x19c/0x3b0
[18319.276103] return_from_SYSCALL_64+0x0/0x6a
[18319.276103] INFO: Slab 0xffffea0003502e00 objects=17
used=14 fp=0xffff8800d40b8748 flags=0x1ffff0000004080
[18319.276103] INFO: Object 0xffff8800d40b9790
@offset=6032 fp=0xcccccccccccccccc
[18319.276103] Redzone ffff8800d40b9788: 5a 5a 5a 5a 5a 5a 5a 5a
ZZZZZZZZ
[18319.276103] Object ffff8800d40b9790: cc cc cc cc cc cc
cc cc 6b 6b 6b 6b 6b 6b 6b 6b ........kkkkkkkk
[18319.276103] Object ffff8800d40b97a0: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Object ffff8800d40b97b0: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Object ffff8800d40b97c0: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Object ffff8800d40b97d0: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Object ffff8800d40b97e0: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Object ffff8800d40b97f0: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Object ffff8800d40b9800: 6b 6b 6b 6b 6b
6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkkkkkkkkkk
[18319.276103] Redzone ffff8800d40b9810: 6b 6b 6b 6b 6b 6b 6b a5
kkkkkkk.
[18319.276103] Padding ffff8800d40b9950: f0 bc 44 00 01 00 00 00
..D.....
[18319.276103] CPU: 0 PID: 4852 Comm: insmod Tainted: G B
OE 4.7.0-rc4+ #25
[18319.276103] Hardware name: innotek GmbH
VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
[18319.276103] 0000000000000000 00000000bfaee01f
ffff8800d40a7810 ffffffff81b6fd61
[18319.276103] ffff88011f603440 ffff8800d40b9790
ffff8800d40a7840 ffffffff8157b472
[18319.276103] ffff88011f603440 ffffea0003502e00
ffff8800d40b9790 ffffffffc03780db
[18319.276103] Call Trace:
[18319.276103] [<ffffffff81b6fd61>] dump_stack+0x63/0x82
[18319.276103] [<ffffffff8157b472>] print_trailer+0x112/0x1a0
[18319.276103] [<ffffffffc03780db>] ? kmalloc_oob_right+0xdb/0xdb [test_kasan]
[18319.276103] [<ffffffff815815f4>] object_err+0x34/0x40
[18319.276103] [<ffffffff81583a82>] kasan_report_error+0x222/0x540
[18319.276103] [<ffffffff8146af5c>] ? power_down+0xc4/0xc4
[18319.276103] [<ffffffffc03780db>] ? kmalloc_oob_right+0xdb/0xdb [test_kasan]
[18319.276103] [<ffffffff81584031>] __asan_report_store1_noabort+0x61/0x70
[18319.276103] [<ffffffffc03780b4>] ? kmalloc_oob_right+0xb4/0xdb [test_kasan]
[18319.276103] [<ffffffffc03780b4>] kmalloc_oob_right+0xb4/0xdb [test_kasan]
[18319.276103] [<ffffffffc03780e4>] kmalloc_tests_init+0x9/0xf25 [test_kasan]
[18319.276103] [<ffffffff81002299>] do_one_initcall+0xa9/0x230
[18319.276103] [<ffffffff810021f0>] ? initcall_blacklisted+0x180/0x180
[18319.276103] [<ffffffff815830c6>] ? kasan_unpoison_shadow+0x36/0x50
[18319.276103] [<ffffffff815830c6>] ? kasan_unpoison_shadow+0x36/0x50
[18319.276103] [<ffffffff8158313e>] ? kasan_kmalloc+0x5e/0x70
[18319.276103] [<ffffffff815830c6>] ? kasan_unpoison_shadow+0x36/0x50
[18319.276103] [<ffffffff815831d7>] ? __asan_register_globals+0x87/0xa0
[18319.276103] [<ffffffff8146b8a3>] do_init_module+0x1d0/0x4de
[18319.276103] [<ffffffff812eea50>] load_module+0x74d0/0x9ca0
[18319.276103] [<ffffffff812e3d00>] ? m_show+0x4a0/0x4a0
[18319.276103] [<ffffffff812e7580>] ? module_frob_arch_sections+0x20/0x20
[18319.276103] [<ffffffff815c421d>] ? rw_verify_area+0xbd/0x2b0
[18319.276103] [<ffffffff81534d05>] ? __vmalloc_node_range+0x485/0x630
[18319.276103] [<ffffffff815d4a89>] ? kernel_read_file_from_fd+0x49/0x80
[18319.276103] [<ffffffff812f15d0>] SYSC_finit_module+0x190/0x1d0
[18319.276103] [<ffffffff812f1440>] ? SYSC_init_module+0x220/0x220
[18319.276103] [<ffffffff814d1790>] ? vma_is_stack_for_task+0x90/0x90
[18319.276103] [<ffffffff815d1036>] ? vfs_getattr+0x26/0x30
[18319.276103] [<ffffffff812f162e>] SyS_finit_module+0xe/0x10
[18319.276103] [<ffffffff828bb8b6>] entry_SYSCALL_64_fastpath+0x1e/0xa8
[18319.276103] Memory state around the buggy address:
[18319.276103] ffff8800d40b9700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[18319.276103] ffff8800d40b9780: fc fc fc 00 00 00 00 00 00 00 00 00 00 00 00 00
[18319.276103] >ffff8800d40b9800: 00 00 04 fc fc fc fc fc fc fc fc fc fc fc fc fc
[18319.276103] ^
[18319.276103] ffff8800d40b9880: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[18319.276103] ffff8800d40b9900: fc fc fc fc fc fc fc fc fc fc fc fc fc 00 00 00
[18319.276103] ==================================================================
[18319.333357] kasan test: kmalloc_oob_right ptr[size] address: 0xffff8800d40b9814其中能直观看出 Kasan 原理的是 Memory state around the buggy address,在一堆 fc 字节中有一串 00 以及一个 04。如前所述,每个 00 代表 8 个可用字节,04 代表该对应的地址中前四个字节可用。这里共有 15 个 00,一个 04。15 x 8 + 4 = 124,正是代码中申请的 124 字节。当测试代码往第 125 个字节中写入数据的时候,Kasan 就会检测到该行为并报告一系列的相关信息。其中 fc 表示的是 slub 对象中的红色区域,其它填充值的意义可以参考以下定义。
清单 8. 填充值的定义
#define KASAN_FREE_PAGE 0xFF /* page was freed */
#define KASAN_PAGE_REDZONE 0xFE /* redzone for kmalloc_large allocations */
#define KASAN_KMALLOC_REDZONE 0xFC /* redzone inside slub object */
#define KASAN_KMALLOC_FREE 0xFB /* object was freed (kmem_cache_free/kfree) */
#define KASAN_GLOBAL_REDZONE 0xFA /* redzone for global variable */该测试代码包含了许多其它的测试用例,有兴趣的读者可以参考如下代码清单有选择地编译并运行。
清单 9. Kasan 测试用例集
static int __init kmalloc_tests_init(void)
{
kmalloc_oob_right();
kmalloc_oob_left();
kmalloc_node_oob_right();
#ifdef CONFIG_SLUB
kmalloc_pagealloc_oob_right();
#endif
kmalloc_large_oob_right();
kmalloc_oob_krealloc_more();
kmalloc_oob_krealloc_less();
kmalloc_oob_16();
kmalloc_oob_in_memset();
kmalloc_oob_memset_2();
kmalloc_oob_memset_4();
kmalloc_oob_memset_8();
kmalloc_oob_memset_16();
kmalloc_uaf();
kmalloc_uaf_memset();
kmalloc_uaf2();
kmem_cache_oob();
kasan_stack_oob();
kasan_global_oob();
ksize_unpoisons_memory();
copy_user_test();
return -EAGAIN;
}
对比
和 Kasan 功能类似的工具还有 kmemcheck,它比 Kasan 更早加入内核,但是运行速度没有 Kasan 快,这是因为 Kasan 利用了编译器的特性,可以将代码编译为內联模式。但 Kasan 也有自己的不足,目前 Kasan 不能检测出读取未初始化内存的错误,而这一点 kmemcheck 是支持的。
此外,内核还包含了一些配置选项可以打开其它的内存检测功能,如 SLAB_DEBUG 和 DEBUG_SLAB 选项可以激活 redzones 和 poisoning 功能,用来检测申请和释放内存的错误。当打开 DEBUG_PAGEALLOC 选项后,可以检测部分释放后使用内存的情况。
这些都是内核代码质量的保证工具,当提交代码的时候,综合使用以上工具可以预防自己的补丁引入一些低级的错误。
结束语
本文介绍了 Kasan 的配置及使用方法,并通过运行 Kasan 的测试用例说明了 Kasan 的原理。对于内核开发者来说,该工具不仅可以用来检测自己代码。对该工具有兴趣的读者,也可以给该工具增加新功能或发现并修复其中的 BUG。
相关主题
- 参考 Clang 文档 中关于 AddressSanitizer 的内容。
- 参考 LWN 文档:The kernel address sanitizer:Linux 每周新闻关于 Kasan 的介绍文章。
- 参考 LWN文档:Kernel address sainitzer (KASan) - dynamic memory error deetector:Linux 每周新闻中关于 Kasan 的建议及补丁的收录。
- 参考 Linux 内核文档,了解关于 memcheck 的介绍。
- 参考 LinuxCon North America 2015 KernelAddressSanitizer.pdf:Andrey Konovalov 和 Dmitry Vyukov 在 LinuxCon North America 2015 会议上的演讲稿。
- IBM developerWorks 中国 linux 专区:为使用 linux 的开发人员准备的技术信息和资料。这里提供产品下载、how-to 信息、支持资源以及免费技术库,包含 2000 多份技术文章、教程、最佳实践、IBM Redbook 和在线产品手册。