1.. SPDX-License-Identifier: GPL-2.0 2 3.. include:: ../disclaimer-zh_CN.rst 4 5:Original: Documentation/dev-tools/kmsan.rst 6:Translator: 刘浩阳 Haoyang Liu <tttturtleruss@hust.edu.cn> 7 8======================= 9内核内存消毒剂(KMSAN) 10======================= 11 12KMSAN 是一个动态错误检测器,旨在查找未初始化值的使用。它基于编译器插桩,类似于用 13户空间的 `MemorySanitizer tool`_。 14 15需要注意的是 KMSAN 并不适合生产环境,因为它会大幅增加内核内存占用并降低系统运行速度。 16 17使用方法 18======== 19 20构建内核 21-------- 22 23要构建带有 KMSAN 的内核,你需要一个较新的 Clang (14.0.6+)。 24请参阅 `LLVM documentation`_ 了解如何构建 Clang。 25 26现在配置并构建一个启用 CONFIG_KMSAN 的内核。 27 28示例报告 29-------- 30 31以下是一个 KMSAN 报告的示例:: 32 33 ===================================================== 34 BUG: KMSAN: uninit-value in test_uninit_kmsan_check_memory+0x1be/0x380 [kmsan_test] 35 test_uninit_kmsan_check_memory+0x1be/0x380 mm/kmsan/kmsan_test.c:273 36 kunit_run_case_internal lib/kunit/test.c:333 37 kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374 38 kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28 39 kthread+0x721/0x850 kernel/kthread.c:327 40 ret_from_fork+0x1f/0x30 ??:? 41 42 Uninit was stored to memory at: 43 do_uninit_local_array+0xfa/0x110 mm/kmsan/kmsan_test.c:260 44 test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271 45 kunit_run_case_internal lib/kunit/test.c:333 46 kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374 47 kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28 48 kthread+0x721/0x850 kernel/kthread.c:327 49 ret_from_fork+0x1f/0x30 ??:? 50 51 Local variable uninit created at: 52 do_uninit_local_array+0x4a/0x110 mm/kmsan/kmsan_test.c:256 53 test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271 54 55 Bytes 4-7 of 8 are uninitialized 56 Memory access of size 8 starts at ffff888083fe3da0 57 58 CPU: 0 PID: 6731 Comm: kunit_try_catch Tainted: G B E 5.16.0-rc3+ #104 59 Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014 60 ===================================================== 61 62报告指出本地变量 ``uninit`` 在 ``do_uninit_local_array()`` 中未初始化。 63第三个堆栈跟踪对应于该变量创建的位置。 64 65第一个堆栈跟踪显示了未初始化值的使用位置(在 66``test_uninit_kmsan_check_memory()``)。 67工具显示了局部变量中未初始化的字节及其被复制到其他内存位置前的堆栈。 68 69KMSAN 会在以下情况下报告未初始化的值 ``v``: 70 71 - 在条件判断中,例如 ``if (v) { ... }``; 72 - 在索引或指针解引用中,例如 ``array[v]`` 或 ``*v``; 73 - 当它被复制到用户空间或硬件时,例如 ``copy_to_user(..., &v, ...)``; 74 - 当它作为函数参数传递,并且启用 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` 时(见下文)。 75 76这些情况(除了复制数据到用户空间或硬件外,这是一个安全问题)被视为 C11 标准下的未定义行为。 77 78禁用插桩 79-------- 80 81可以用 ``__no_kmsan_checks`` 标记函数。这样,KMSAN 会忽略该函数中的未初始化值, 82并将其输出标记为已初始化。如此,用户不会收到与该函数相关的 KMSAN 报告。 83 84KMSAN 还支持 ``__no_sanitize_memory`` 函数属性。KMSAN 不会对拥有该属性的函数进行 85插桩,这在我们不希望编译器干扰某些底层代码(例如标记为 ``noinstr`` 的代码,该 86代码隐式添加了 ``__no_sanitize_memory``)时可能很有用。 87 88然而,这会有代价:此类函数的栈分配将具有不正确的影子/初始值,可能导致误报。来 89自非插桩代码的函数也可能接收到不正确的元数据。 90 91 92作为经验之谈,避免显式使用 ``__no_sanitize_memory``。 93 94也可以通过 Makefile 禁用 KMSAN 对某个文件(例如 main.o)的作用:: 95 96 KMSAN_SANITIZE_main.o := n 97 98或者对整个目录:: 99 100 KMSAN_SANITIZE := n 101 102将其应用到文件或目录中的每个函数。大多数用户不会需要 KMSAN_SANITIZE, 103除非他们的代码被 KMSAN 破坏(例如在早期启动时运行的代码)。 104 105还可以通过调用 ``kmsan_disable_current()`` 和 ``kmsan_enable_current()`` 106暂时对当前任务禁用 KMSAN 检查。每个 ``kmsan_enable_current()`` 必须在 107``kmsan_disable_current()`` 之后调用;这些调用对可以嵌套。在调用时需要注意保持 108嵌套区域简短,并且尽可能使用其他方法禁用插桩。 109 110支持 111==== 112 113为了使用 KMSAN,内核必须使用 Clang 构建,到目前为止,Clang 是唯一支持 KMSAN 114的编译器。内核插桩过程基于用户空间的 `MemorySanitizer tool`_。 115 116目前运行时库仅支持 x86_64 架构。 117 118KMSAN 的工作原理 119================ 120 121KMSAN 阴影内存 122-------------- 123 124KMSAN 将一个元数据字节(也称为阴影字节)与每个内核内存字节关联。仅当内核内存字节 125的相应位未初始化时,阴影字节中的一个比特位才会被设置。将内存标记为未初始化(即 126将其阴影字节设置为 ``0xff``)称为中毒,将其标记为已初始化(将阴影字节设置为 127``0x00``)称为解毒。 128 129当在栈上分配新变量时,默认情况下它会中毒,这由编译器插入的插桩代码完成(除非它 130是立即初始化的栈变量)。任何未使用 ``__GFP_ZERO`` 的堆分配也会中毒。 131 132编译器插桩还跟踪阴影值在代码中的使用。当需要时,插桩代码会调用 ``mm/kmsan/`` 中 133的运行时库以持久化阴影值。 134 135基本或复合类型的阴影值是长度相同的字节数组。当常量值写入内存时,该内存会被解毒 136。当从内存读取值时,其阴影内存也会被获取,并传递到所有使用该值的操作中。对于每 137个需要一个或多个值的指令,编译器会生成代码根据这些值及其阴影来计算结果的阴影。 138 139 140示例:: 141 142 int a = 0xff; // i.e. 0x000000ff 143 int b; 144 int c = a | b; 145 146在这种情况下, ``a`` 的阴影为 ``0``, ``b`` 的阴影为 ``0xffffffff``, 147``c`` 的阴影为 ``0xffffff00``。这意味着 ``c`` 的高三个字节未初始化,而低字节已 148初始化。 149 150起源跟踪 151-------- 152 153每四字节的内核内存都有一个所谓的源点与之映射。这个源点描述了在程序执行中,未初 154始化值的创建点。每个源点都与完整的分配栈(对于堆分配的内存)或包含未初始化变 155量的函数(对于局部变量)相关联。 156 157当一个未初始化的变量在栈或堆上分配时,会创建一个新的源点值,并将该变量的初始值 158填充为这个值。当从内存中读取一个值时,其初始值也会被读取并与阴影一起保留。对于 159每个接受一个或多个值的指令,结果的源点是与任何未初始化输入相对应的源点之一。如 160果一个污染值被写入内存,其起源也会被写入相应的存储中。 161 162示例 1:: 163 164 int a = 42; 165 int b; 166 int c = a + b; 167 168在这种情况下, ``b`` 的源点是在函数入口时生成的,并在加法结果写入内存之前存储到 169``c`` 的源点中。 170 171如果几个变量共享相同的源点地址,则它们被存储在同一个四字节块中。在这种情况下, 172对任何变量的每次写入都会更新所有变量的源点。在这种情况下我们必须牺牲精度,因 173为为单独的位(甚至字节)存储源点成本过高。 174 175示例 2:: 176 177 int combine(short a, short b) { 178 union ret_t { 179 int i; 180 short s[2]; 181 } ret; 182 ret.s[0] = a; 183 ret.s[1] = b; 184 return ret.i; 185 } 186 187如果 ``a`` 已初始化而 ``b`` 未初始化,则结果的阴影为 0xffff0000,结果的源点为 188``b`` 的源点。 ``ret.s[0]`` 会有相同的起源,但它不会被使用,因为该变量已初始化。 189 190如果两个函数参数都未初始化,则只保留第二个参数的源点。 191 192源点链 193~~~~~~ 194 195为了便于调试,KMSAN 在每次将未初始化值存储到内存时都会创建一个新的源点。新的源点 196引用了其创建栈以及值的前一个起源。这可能导致内存消耗增加,因此我们在运行时限制 197了源点链的长度。 198 199Clang 插桩 API 200-------------- 201 202Clang 插桩通过在内核代码中插入定义在 ``mm/kmsan/instrumentation.c`` 中的函数调用 203来实现。 204 205 206阴影操作 207~~~~~~~~ 208 209对于每次内存访问,编译器都会发出一个函数调用,该函数返回一对指针,指向给定内存 210的阴影和原始地址:: 211 212 typedef struct { 213 void *shadow, *origin; 214 } shadow_origin_ptr_t 215 216 shadow_origin_ptr_t __msan_metadata_ptr_for_load_{1,2,4,8}(void *addr) 217 shadow_origin_ptr_t __msan_metadata_ptr_for_store_{1,2,4,8}(void *addr) 218 shadow_origin_ptr_t __msan_metadata_ptr_for_load_n(void *addr, uintptr_t size) 219 shadow_origin_ptr_t __msan_metadata_ptr_for_store_n(void *addr, uintptr_t size) 220 221函数名依赖于内存访问的大小。 222 223编译器确保对于每个加载的值,其阴影和原始值都从内存中读取。当一个值存储到内存时 224,其阴影和原始值也会通过元数据指针进行存储。 225 226处理局部变量 227~~~~~~~~~~~~ 228 229一个特殊的函数用于为局部变量创建一个新的原始值,并将该变量的原始值设置为该值:: 230 231 void __msan_poison_alloca(void *addr, uintptr_t size, char *descr) 232 233访问每个任务数据 234~~~~~~~~~~~~~~~~ 235 236在每个插桩函数的开始处,KMSAN 插入一个对 ``__msan_get_context_state()`` 的调用 237:: 238 239 kmsan_context_state *__msan_get_context_state(void) 240 241``kmsan_context_state`` 在 ``include/linux/kmsan.h`` 中声明:: 242 243 struct kmsan_context_state { 244 char param_tls[KMSAN_PARAM_SIZE]; 245 char retval_tls[KMSAN_RETVAL_SIZE]; 246 char va_arg_tls[KMSAN_PARAM_SIZE]; 247 char va_arg_origin_tls[KMSAN_PARAM_SIZE]; 248 u64 va_arg_overflow_size_tls; 249 char param_origin_tls[KMSAN_PARAM_SIZE]; 250 depot_stack_handle_t retval_origin_tls; 251 }; 252 253KMSAN 使用此结构体在插桩函数之间传递参数阴影和原始值(除非立刻通过 254 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` 检查参数)。 255 256将未初始化的值传递给函数 257~~~~~~~~~~~~~~~~~~~~~~~~ 258 259Clang 的 MemorySanitizer 插桩有一个选项 ``-fsanitize-memory-param-retval``,该 260选项使编译器检查按值传递的函数参数,以及函数返回值。 261 262该选项由 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` 控制,默认启用以便 KMSAN 更早报告 263未初始化的值。有关更多细节,请参考 `LKML discussion`_。 264 265由于 LLVM 中的实现检查的方式(它们仅应用于标记为 ``noundef`` 的参数),并不是所 266有参数都能保证被检查,因此我们不能放弃 ``kmsan_context_state`` 中的元数据存储 267。 268 269字符串函数 270~~~~~~~~~~~ 271 272编译器将对 ``memcpy()``/``memmove()``/``memset()`` 的调用替换为以下函数。这些函 273数在数据结构初始化或复制时也会被调用,确保阴影和原始值与数据一起复制:: 274 275 void *__msan_memcpy(void *dst, void *src, uintptr_t n) 276 void *__msan_memmove(void *dst, void *src, uintptr_t n) 277 void *__msan_memset(void *dst, int c, uintptr_t n) 278 279错误报告 280~~~~~~~~ 281 282对于每个值的使用,编译器发出一个阴影检查,在值中毒的情况下调用 283``__msan_warning()``:: 284 285 void __msan_warning(u32 origin) 286 287``__msan_warning()`` 使 KMSAN 运行时打印错误报告。 288 289内联汇编插桩 290~~~~~~~~~~~~ 291 292KMSAN 对每个内联汇编输出进行插桩,调用:: 293 294 void __msan_instrument_asm_store(void *addr, uintptr_t size) 295 296,该函数解除内存区域的污染。 297 298这种方法可能会掩盖某些错误,但也有助于避免许多位操作、原子操作等中的假阳性。 299 300有时传递给内联汇编的指针不指向有效内存。在这种情况下,它们在运行时被忽略。 301 302 303运行时库 304-------- 305 306代码位于 ``mm/kmsan/``。 307 308每个任务 KMSAN 状态 309~~~~~~~~~~~~~~~~~~~ 310 311每个 task_struct 都有一个关联的 KMSAN 任务状态,它保存 KMSAN 312上下文(见上文)和一个每个任务计数器以禁止 KMSAN 报告:: 313 314 struct kmsan_context { 315 ... 316 unsigned int depth; 317 struct kmsan_context_state cstate; 318 ... 319 } 320 321 struct task_struct { 322 ... 323 struct kmsan_context kmsan; 324 ... 325 } 326 327KMSAN 上下文 328~~~~~~~~~~~~ 329 330在内核任务上下文中运行时,KMSAN 使用 ``current->kmsan.cstate`` 来 331保存函数参数和返回值的元数据。 332 333但在内核运行于中断、softirq 或 NMI 上下文中, ``current`` 不可用时, 334KMSAN 切换到每 CPU 中断状态:: 335 336 DEFINE_PER_CPU(struct kmsan_ctx, kmsan_percpu_ctx); 337 338元数据分配 339~~~~~~~~~~ 340 341内核中有多个地方存储元数据。 342 3431. 每个 ``struct page`` 实例包含两个指向其影子和内存页面的指针 344:: 345 346 struct page { 347 ... 348 struct page *shadow, *origin; 349 ... 350 }; 351 352在启动时,内核为每个可用的内核页面分配影子和源页面。这是在内核地址空间已经碎片 353化时后完成的,完成的相当晚,因此普通数据页面可能与元数据页面任意交错。 354 355这意味着通常两个相邻的内存页面,它们的影子/源页面可能不是连续的。因此,如果内存 356访问跨越内存块的边界,访问影子/源内存可能会破坏其他页面或从中读取错误的值。 357 358实际上,由相同 ``alloc_pages()`` 调用返回的连续内存页面将具有连续的元数据,而 359如果这些页面属于两个不同的分配,它们的元数据页面可能会被碎片化。 360 361对于内核数据( ``.data``、 ``.bss`` 等)和每 CPU 内存区域,也没有对元数据连续 362性的保证。 363 364在 ``__msan_metadata_ptr_for_XXX_YYY()`` 遇到两个页面之间的 365非连续元数据边界时,它返回指向假影子/源区域的指针:: 366 367 char dummy_load_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE))); 368 char dummy_store_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE))); 369 370``dummy_load_page`` 被初始化为零,因此读取它始终返回零。对 ``dummy_store_page`` 的 371所有写入都被忽略。 372 3732. 对于 vmalloc 内存和模块,内存范围、影子和源之间有一个直接映射。KMSAN 将 374vmalloc 区域缩小了 3/4,仅使前四分之一可用于 ``vmalloc()``。vmalloc 375区域的第二个四分之一包含第一个四分之一的影子内存,第三个四分之一保存源。第四个 376四分之一的小部分包含内核模块的影子和源。有关更多详细信息,请参阅 377``arch/x86/include/asm/pgtable_64_types.h``。 378 379当一系列页面映射到一个连续的虚拟内存空间时,它们的影子和源页面也以连续区域的方 380式映射。 381 382参考文献 383======== 384 385E. Stepanov, K. Serebryany. `MemorySanitizer: fast detector of uninitialized 386memory use in C++ 387<https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43308.pdf>`_. 388In Proceedings of CGO 2015. 389 390.. _MemorySanitizer tool: https://clang.llvm.org/docs/MemorySanitizer.html 391.. _LLVM documentation: https://llvm.org/docs/GettingStarted.html 392.. _LKML discussion: https://lore.kernel.org/all/20220614144853.3693273-1-glider@google.com/ 393