Poison, redzones and shadows: inside KASAN
AI 深度解读
背景
Linux 内核代码库中 Rust 的使用正在缓慢增长,但该项目绝大部分仍以 C 语言编写。C 语言是编写底层代码的事实标准,但同时也带来了大量出错的可能性,其后果表现为多种形式:未定义行为、缓冲区溢出、段错误……内核本身也无法免疫这些问题,因此内核开发者需要专门的工具及早发现这些错误。由于许多问题根源于内存管理,Kernel Address Sanitizer(简称 KASAN)是每个内核黑客工具箱中不可或缺的工具之一。本文不旨在教授如何使用 KASAN(内核文档已有详细说明),而是深入探索 KASAN 的内部实现,主要理解其影响(包括构建时和运行时),并在一定程度上欣赏其实现中蕴含的优雅工程。
核心内容
Kernel Address Sanitizer 概述
KASAN 是内核中实现的运行时检查器,能够捕获两类内存操作错误:越界访问(out-of-bounds access)和释放后使用(use-after-free)。该工具必须在内核构建时显式启用,因为它依赖于两个主要部分:
- 构建时插桩:由编译器插入。GCC 或 LLVM 借助 Address Sanitizer 库,在所有内存访问指令之前插入额外的代码。
- 影子内存(shadow memory):插桩后的代码需要一种方式来区分合法与非法内存访问。KASAN 依赖一种称为影子内存的特殊内存区域,由内核维护,用于跟踪其能够使用的所有内存的状态。
启用 KASAN
启用 KASAN 非常简单:主要需要设置 CONFIG_KASAN=y 并重新编译内核。KASAN 还提供几个可调参数:
- 插桩方式可以是内联(inlined)或外联(outlined),取决于可接受的权衡。
- 如果架构支持,可以启用硬件支持(如软件标签和硬件标签)以降低 KASAN 开销。
- 可以调整跟踪的内存类型(如 vmalloc 内存、栈内存等)。
为了学习和实验 KASAN,建议在虚拟化环境中运行测试内核,以避免破坏宿主内核。使用 Andrea Righi 的 virtme-ng 是最简单的解决方案之一。以下是基本设置流程,用于生成 KASAN 报告:
$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
$ cd linux && git checkout linux-7.1.y
$ vng -b \
--configitem CONFIG_KASAN=y \
--configitem CONFIG_KUNIT=y \
--configitem CONFIG_KASAN_KUNIT_TEST=m
$ vng --user root
上述命令指示 virtme-ng 配置并构建一个带有基本 KASAN 支持和相应 kunit 测试的内核。最后的 vng 命令在 QEMU 环境中启动该内核,模拟与构建内核主机相同的架构,并附带最小根文件系统。启动后,可以通过加载测试内核模块来生成并研究 KASAN 报告:
# modprobe kasan_test
# dmesg
内核日志缓冲区现在包含大量关于错误内存访问的 KASAN 报告,例如以下示例:
==================================================================
BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0x705/0x7d0 [kasan_test]
Write of size 1 at addr ffff888005fa5f73 by task kunit_try_catch/249
CPU: 2 UID: 0 PID: 249 Comm: kunit_try_catch Tainted: G N 7.1.2-virtme #6
PREEMPT(lazy) Tainted: [N]=TEST
Hardware name: Bochs Bochs, BIOS Bochs 01/01/2011
Call Trace:
<TASK>
dump_stack_lvl+0x4d/0x70
print_report+0x14b/0x4b0
...
kmalloc_oob_right+0x705/0x7d0 [kasan_test]
...
ret_from_fork_asm+0x1a/0x30
</TASK>
Allocated by task 249:
kasan_save_stack+0x2f/0x50
kasan_save_track+0x14/0x30
__kasan_kmalloc+0x7f/0x90
kmalloc_oob_right+0xae/0x7d0 [kasan_test]
...
The buggy address belongs to the object at ffff888005fa5f00
which belongs to the cache kmalloc-128 of size 128
The buggy address is located 0 bytes to the right of
allocated 115-byte region [ffff888005fa5f00, ffff888005fa5f73)
...
Memory state around the buggy address:
ffff888005fa5e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fc
ffff888005fa5e80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff888005fa5f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 fc
^
ffff888005fa5f80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ffff888005fa6000: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
报告解读
该报告提供了大量关于错误访问性质的信息:
- 第一行直接说明了错误访问的具体性质:这是一个在动态分配的缓冲区上的越界访问(slab-out-of-bounds)。报告还显示执行该访问的函数是
kasan_test模块中的kmalloc_oob_right,写操作大小为 1 字节,访问地址为0xffff888005fa5f73。 - 接下来是错误执行的上下文和平台信息:发生错误的 CPU、正在运行的任务、内核版本、平台名称。
- 随后是完整的调用栈。
- 最后,根据内存类型,可能包含更具体的数据:由于错误访问发生在 slab 分配的区域,报告显示了对应的分配大小、分配调用栈、用于分配该对象的 slab 缓存,以及该 1 字节写入尝试正好在合法区域之后(这与执行错误访问的测试代码相符)。
- 报告末尾的“内存状态”区域并非被访问内存的内容,而是用于跟踪内存状态的影子内存的值。
KASAN 影子内存
当启用 KASAN 时,内核不会使用全部可用内存,而是保留一部分内存用于跟踪其余内核虚拟地址空间的状态。这部分保留的内存就是影子内存。影子内存采用压缩的映射方式:通常每 1 字节的常规内存对应 1 字节的影子内存(但具体比例取决于配置和架构)。影子内存中的每个字节编码了对应常规内存区域的状态,例如“可访问”、“不可访问”(已释放或越界区域)、“部分可访问”等。在报告末尾的 Memory state 中,00 表示可访问,fc 表示不可访问(红色区域),03 表示可访问但仅限前 3 字节等。通过检查影子内存,KASAN 能够快速判断内存访问是否合法。
关键要点
- KASAN 是 Linux 内核中的运行时内存错误检测工具,专门捕获越界访问和释放后使用错误。
- 它依赖于两项关键技术:编译时插桩(由 GCC/LLVM 的 Address Sanitizer
