← Documents Documentation/dev-tools/kmsan.rst GitHub 원문 ↗

Linux 6.18.37 · Dev Tools

Kernel Memory Sanitizer (KMSAN)

KMSAN의 미초기화 값 검출 방식, shadow와 origin 전파, Clang 계측 API, task별 런타임 상태와 메타데이터 배치를 설명합니다.

Source pathDocumentation/dev-tools/kmsan.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약과 해설

kmsan.rst:1-435

KMSAN은 커널 메모리의 각 비트가 초기화되었는지를 shadow memory로 추적하고, 초기화되지 않은 값이 만들어지고 저장된 경로를 origin chain으로 기록합니다. 조건식, 주소 계산, 사용자 공간 복사, 함수 인수와 반환값에서 poison된 값이 소비되면 생성 위치와 전파 경로를 함께 보고합니다.

정밀한 컴파일러 계측과 별도 메타데이터가 필요해 메모리와 실행 시간 비용이 크므로 프로덕션용이 아닙니다. 보고서를 읽을 때는 사용, 저장, 생성 스택을 구분하고, `__no_sanitize_memory` 같은 예외가 shadow와 origin을 왜곡해 false positive를 만들 수 있다는 점을 함께 확인해야 합니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 .. SPDX-License-Identifier: GPL-2.0
2 .. Copyright (C) 2022, Google LLC.
3
4 ===============================
5 Kernel Memory Sanitizer (KMSAN)
6 ===============================
7
8 KMSAN is a dynamic error detector aimed at finding uses of uninitialized
9 values. It is based on compiler instrumentation, and is quite similar to the
10 userspace `MemorySanitizer tool`_.
11
12 An important note is that KMSAN is not intended for production use, because it
13 drastically increases kernel memory footprint and slows the whole system down.
14
15 Usage
16 =====
17
18 Building the kernel
19 -------------------
20
21 In order to build a kernel with KMSAN you will need a fresh Clang (14.0.6+).
22 Please refer to `LLVM documentation`_ for the instructions on how to build Clang.
23
24 Now configure and build the kernel with CONFIG_KMSAN enabled.
25
26 Example report
27 --------------
28
29 Here is an example of a KMSAN report::
30
31 =====================================================
32 BUG: KMSAN: uninit-value in test_uninit_kmsan_check_memory+0x1be/0x380 [kmsan_test]
33 test_uninit_kmsan_check_memory+0x1be/0x380 mm/kmsan/kmsan_test.c:273
34 kunit_run_case_internal lib/kunit/test.c:333
35 kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374
36 kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28
37 kthread+0x721/0x850 kernel/kthread.c:327
38 ret_from_fork+0x1f/0x30 ??:?
39
40 Uninit was stored to memory at:
41 do_uninit_local_array+0xfa/0x110 mm/kmsan/kmsan_test.c:260
42 test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271
43 kunit_run_case_internal lib/kunit/test.c:333
44 kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374
45 kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28
46 kthread+0x721/0x850 kernel/kthread.c:327
47 ret_from_fork+0x1f/0x30 ??:?
48
49 Local variable uninit created at:
50 do_uninit_local_array+0x4a/0x110 mm/kmsan/kmsan_test.c:256
51 test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271
52
53 Bytes 4-7 of 8 are uninitialized
54 Memory access of size 8 starts at ffff888083fe3da0
55
56 CPU: 0 PID: 6731 Comm: kunit_try_catch Tainted: G B E 5.16.0-rc3+ #104
57 Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
58 =====================================================
59
60 The report says that the local variable ``uninit`` was created uninitialized in
61 ``do_uninit_local_array()``. The third stack trace corresponds to the place
62 where this variable was created.
63
64 The first stack trace shows where the uninit value was used (in
65 ``test_uninit_kmsan_check_memory()``). The tool shows the bytes which were left
66 uninitialized in the local variable, as well as the stack where the value was
67 copied to another memory location before use.
68
69 A use of uninitialized value ``v`` is reported by KMSAN in the following cases:
70
71 - in a condition, e.g. ``if (v) { ... }``;
72 - in an indexing or pointer dereferencing, e.g. ``array[v]`` or ``*v``;
73 - when it is copied to userspace or hardware, e.g. ``copy_to_user(..., &v, ...)``;
74 - when it is passed as an argument to a function, and
75 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL`` is enabled (see below).
76
77 The mentioned cases (apart from copying data to userspace or hardware, which is
78 a security issue) are considered undefined behavior from the C11 Standard point
79 of view.
80
81 Disabling the instrumentation
82 -----------------------------
83
84 A function can be marked with ``__no_kmsan_checks``. Doing so makes KMSAN
85 ignore uninitialized values in that function and mark its output as initialized.
86 As a result, the user will not get KMSAN reports related to that function.
87
88 Another function attribute supported by KMSAN is ``__no_sanitize_memory``.
89 Applying this attribute to a function will result in KMSAN not instrumenting
90 it, which can be helpful if we do not want the compiler to interfere with some
91 low-level code (e.g. that marked with ``noinstr`` which implicitly adds
92 ``__no_sanitize_memory``).
93
94 This however comes at a cost: stack allocations from such functions will have
95 incorrect shadow/origin values, likely leading to false positives. Functions
96 called from non-instrumented code may also receive incorrect metadata for their
97 parameters.
98
99 As a rule of thumb, avoid using ``__no_sanitize_memory`` explicitly.
100
101 It is also possible to disable KMSAN for a single file (e.g. main.o)::
102
103 KMSAN_SANITIZE_main.o := n
104
105 or for the whole directory::
106
107 KMSAN_SANITIZE := n
108
109 in the Makefile. Think of this as applying ``__no_sanitize_memory`` to every
110 function in the file or directory. Most users won't need KMSAN_SANITIZE, unless
111 their code gets broken by KMSAN (e.g. runs at early boot time).
112
113 KMSAN checks can also be temporarily disabled for the current task using
114 ``kmsan_disable_current()`` and ``kmsan_enable_current()`` calls. Each
115 ``kmsan_enable_current()`` call must be preceded by a
116 ``kmsan_disable_current()`` call; these call pairs may be nested. One needs to
117 be careful with these calls, keeping the regions short and preferring other
118 ways to disable instrumentation, where possible.
119
120 Support
121 =======
122
123 In order for KMSAN to work the kernel must be built with Clang, which so far is
124 the only compiler that has KMSAN support. The kernel instrumentation pass is
125 based on the userspace `MemorySanitizer tool`_.
126
127 The runtime library only supports x86_64 at the moment.
128
129 How KMSAN works
130 ===============
131
132 KMSAN shadow memory
133 -------------------
134
135 KMSAN associates a metadata byte (also called shadow byte) with every byte of
136 kernel memory. A bit in the shadow byte is set if the corresponding bit of the
137 kernel memory byte is uninitialized. Marking the memory uninitialized (i.e.
138 setting its shadow bytes to ``0xff``) is called poisoning, marking it
139 initialized (setting the shadow bytes to ``0x00``) is called unpoisoning.
140
141 When a new variable is allocated on the stack, it is poisoned by default by
142 instrumentation code inserted by the compiler (unless it is a stack variable
143 that is immediately initialized). Any new heap allocation done without
144 ``__GFP_ZERO`` is also poisoned.
145
146 Compiler instrumentation also tracks the shadow values as they are used along
147 the code. When needed, instrumentation code invokes the runtime library in
148 ``mm/kmsan/`` to persist shadow values.
149
150 The shadow value of a basic or compound type is an array of bytes of the same
151 length. When a constant value is written into memory, that memory is unpoisoned.
152 When a value is read from memory, its shadow memory is also obtained and
153 propagated into all the operations which use that value. For every instruction
154 that takes one or more values the compiler generates code that calculates the
155 shadow of the result depending on those values and their shadows.
156
157 Example::
158
159 int a = 0xff; // i.e. 0x000000ff
160 int b;
161 int c = a | b;
162
163 In this case the shadow of ``a`` is ``0``, shadow of ``b`` is ``0xffffffff``,
164 shadow of ``c`` is ``0xffffff00``. This means that the upper three bytes of
165 ``c`` are uninitialized, while the lower byte is initialized.
166
167 Origin tracking
168 ---------------
169
170 Every four bytes of kernel memory also have a so-called origin mapped to them.
171 This origin describes the point in program execution at which the uninitialized
172 value was created. Every origin is associated with either the full allocation
173 stack (for heap-allocated memory), or the function containing the uninitialized
174 variable (for locals).
175
176 When an uninitialized variable is allocated on stack or heap, a new origin
177 value is created, and that variable's origin is filled with that value. When a
178 value is read from memory, its origin is also read and kept together with the
179 shadow. For every instruction that takes one or more values, the origin of the
180 result is one of the origins corresponding to any of the uninitialized inputs.
181 If a poisoned value is written into memory, its origin is written to the
182 corresponding storage as well.
183
184 Example 1::
185
186 int a = 42;
187 int b;
188 int c = a + b;
189
190 In this case the origin of ``b`` is generated upon function entry, and is
191 stored to the origin of ``c`` right before the addition result is written into
192 memory.
193
194 Several variables may share the same origin address, if they are stored in the
195 same four-byte chunk. In this case every write to either variable updates the
196 origin for all of them. We have to sacrifice precision in this case, because
197 storing origins for individual bits (and even bytes) would be too costly.
198
199 Example 2::
200
201 int combine(short a, short b) {
202 union ret_t {
203 int i;
204 short s[2];
205 } ret;
206 ret.s[0] = a;
207 ret.s[1] = b;
208 return ret.i;
209 }
210
211 If ``a`` is initialized and ``b`` is not, the shadow of the result would be
212 0xffff0000, and the origin of the result would be the origin of ``b``.
213 ``ret.s[0]`` would have the same origin, but it will never be used, because
214 that variable is initialized.
215
216 If both function arguments are uninitialized, only the origin of the second
217 argument is preserved.
218
219 Origin chaining
220 ~~~~~~~~~~~~~~~
221
222 To ease debugging, KMSAN creates a new origin for every store of an
223 uninitialized value to memory. The new origin references both its creation stack
224 and the previous origin the value had. This may cause increased memory
225 consumption, so we limit the length of origin chains in the runtime.
226
227 Clang instrumentation API
228 -------------------------
229
230 Clang instrumentation pass inserts calls to functions defined in
231 ``mm/kmsan/nstrumentation.c`` into the kernel code.
232
233 Shadow manipulation
234 ~~~~~~~~~~~~~~~~~~~
235
236 For every memory access the compiler emits a call to a function that returns a
237 pair of pointers to the shadow and origin addresses of the given memory::
238
239 typedef struct {
240 void *shadow, *origin;
241 } shadow_origin_ptr_t
242
243 shadow_origin_ptr_t __msan_metadata_ptr_for_load_{1,2,4,8}(void *addr)
244 shadow_origin_ptr_t __msan_metadata_ptr_for_store_{1,2,4,8}(void *addr)
245 shadow_origin_ptr_t __msan_metadata_ptr_for_load_n(void *addr, uintptr_t size)
246 shadow_origin_ptr_t __msan_metadata_ptr_for_store_n(void *addr, uintptr_t size)
247
248 The function name depends on the memory access size.
249
250 The compiler makes sure that for every loaded value its shadow and origin
251 values are read from memory. When a value is stored to memory, its shadow and
252 origin are also stored using the metadata pointers.
253
254 Handling locals
255 ~~~~~~~~~~~~~~~
256
257 A special function is used to create a new origin value for a local variable and
258 set the origin of that variable to that value::
259
260 void __msan_poison_alloca(void *addr, uintptr_t size, char *descr)
261
262 Access to per-task data
263 ~~~~~~~~~~~~~~~~~~~~~~~
264
265 At the beginning of every instrumented function KMSAN inserts a call to
266 ``__msan_get_context_state()``::
267
268 kmsan_context_state *__msan_get_context_state(void)
269
270 ``kmsan_context_state`` is declared in ``include/linux/kmsan.h``::
271
272 struct kmsan_context_state {
273 char param_tls[KMSAN_PARAM_SIZE];
274 char retval_tls[KMSAN_RETVAL_SIZE];
275 char va_arg_tls[KMSAN_PARAM_SIZE];
276 char va_arg_origin_tls[KMSAN_PARAM_SIZE];
277 u64 va_arg_overflow_size_tls;
278 char param_origin_tls[KMSAN_PARAM_SIZE];
279 depot_stack_handle_t retval_origin_tls;
280 };
281
282 This structure is used by KMSAN to pass parameter shadows and origins between
283 instrumented functions (unless the parameters are checked immediately by
284 ``CONFIG_KMSAN_CHECK_PARAM_RETVAL``).
285
286 Passing uninitialized values to functions
287 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
288
289 Clang's MemorySanitizer instrumentation has an option,
290 ``-fsanitize-memory-param-retval``, which makes the compiler check function
291 parameters passed by value, as well as function return values.
292
293 The option is controlled by ``CONFIG_KMSAN_CHECK_PARAM_RETVAL``, which is
294 enabled by default to let KMSAN report uninitialized values earlier.
295 Please refer to the `LKML discussion`_ for more details.
296
297 Because of the way the checks are implemented in LLVM (they are only applied to
298 parameters marked as ``noundef``), not all parameters are guaranteed to be
299 checked, so we cannot give up the metadata storage in ``kmsan_context_state``.
300
301 String functions
302 ~~~~~~~~~~~~~~~~
303
304 The compiler replaces calls to ``memcpy()``/``memmove()``/``memset()`` with the
305 following functions. These functions are also called when data structures are
306 initialized or copied, making sure shadow and origin values are copied alongside
307 with the data::
308
309 void *__msan_memcpy(void *dst, void *src, uintptr_t n)
310 void *__msan_memmove(void *dst, void *src, uintptr_t n)
311 void *__msan_memset(void *dst, int c, uintptr_t n)
312
313 Error reporting
314 ~~~~~~~~~~~~~~~
315
316 For each use of a value the compiler emits a shadow check that calls
317 ``__msan_warning()`` in the case that value is poisoned::
318
319 void __msan_warning(u32 origin)
320
321 ``__msan_warning()`` causes KMSAN runtime to print an error report.
322
323 Inline assembly instrumentation
324 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
325
326 KMSAN instruments every inline assembly output with a call to::
327
328 void __msan_instrument_asm_store(void *addr, uintptr_t size)
329
330 , which unpoisons the memory region.
331
332 This approach may mask certain errors, but it also helps to avoid a lot of
333 false positives in bitwise operations, atomics etc.
334
335 Sometimes the pointers passed into inline assembly do not point to valid memory.
336 In such cases they are ignored at runtime.
337
338
339 Runtime library
340 ---------------
341
342 The code is located in ``mm/kmsan/``.
343
344 Per-task KMSAN state
345 ~~~~~~~~~~~~~~~~~~~~
346
347 Every task_struct has an associated KMSAN task state that holds the KMSAN
348 context (see above) and a per-task counter disallowing KMSAN reports::
349
350 struct kmsan_context {
351 ...
352 unsigned int depth;
353 struct kmsan_context_state cstate;
354 ...
355 }
356
357 struct task_struct {
358 ...
359 struct kmsan_context kmsan;
360 ...
361 }
362
363 KMSAN contexts
364 ~~~~~~~~~~~~~~
365
366 When running in a kernel task context, KMSAN uses ``current->kmsan.cstate`` to
367 hold the metadata for function parameters and return values.
368
369 But in the case the kernel is running in the interrupt, softirq or NMI context,
370 where ``current`` is unavailable, KMSAN switches to per-cpu interrupt state::
371
372 DEFINE_PER_CPU(struct kmsan_ctx, kmsan_percpu_ctx);
373
374 Metadata allocation
375 ~~~~~~~~~~~~~~~~~~~
376
377 There are several places in the kernel for which the metadata is stored.
378
379 1. Each ``struct page`` instance contains two pointers to its shadow and
380 origin pages::
381
382 struct page {
383 ...
384 struct page *shadow, *origin;
385 ...
386 };
387
388 At boot-time, the kernel allocates shadow and origin pages for every available
389 kernel page. This is done quite late, when the kernel address space is already
390 fragmented, so normal data pages may arbitrarily interleave with the metadata
391 pages.
392
393 This means that in general for two contiguous memory pages their shadow/origin
394 pages may not be contiguous. Consequently, if a memory access crosses the
395 boundary of a memory block, accesses to shadow/origin memory may potentially
396 corrupt other pages or read incorrect values from them.
397
398 In practice, contiguous memory pages returned by the same ``alloc_pages()``
399 call will have contiguous metadata, whereas if these pages belong to two
400 different allocations their metadata pages can be fragmented.
401
402 For the kernel data (``.data``, ``.bss`` etc.) and percpu memory regions
403 there also are no guarantees on metadata contiguity.
404
405 In the case ``__msan_metadata_ptr_for_XXX_YYY()`` hits the border between two
406 pages with non-contiguous metadata, it returns pointers to fake shadow/origin regions::
407
408 char dummy_load_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE)));
409 char dummy_store_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE)));
410
411 ``dummy_load_page`` is zero-initialized, so reads from it always yield zeroes.
412 All stores to ``dummy_store_page`` are ignored.
413
414 2. For vmalloc memory and modules, there is a direct mapping between the memory
415 range, its shadow and origin. KMSAN reduces the vmalloc area by 3/4, making only
416 the first quarter available to ``vmalloc()``. The second quarter of the vmalloc
417 area contains shadow memory for the first quarter, the third one holds the
418 origins. A small part of the fourth quarter contains shadow and origins for the
419 kernel modules. Please refer to ``arch/x86/include/asm/pgtable_64_types.h`` for
420 more details.
421
422 When an array of pages is mapped into a contiguous virtual memory space, their
423 shadow and origin pages are similarly mapped into contiguous regions.
424
425 References
426 ==========
427
428 E. Stepanov, K. Serebryany. `MemorySanitizer: fast detector of uninitialized
429 memory use in C++
430 <https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43308.pdf>`_.
431 In Proceedings of CGO 2015.
432
433 .. _MemorySanitizer tool: https://clang.llvm.org/docs/MemorySanitizer.html
434 .. _LLVM documentation: https://llvm.org/docs/GettingStarted.html
435 .. _LKML discussion: https://lore.kernel.org/all/20220614144853.3693273-1-glider@google.com/
436

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

KMSAN 개요

1-14

SPDX 라이선스 식별자: GPL-2.0

저작권 (C) 2022, Google LLC.

Kernel Memory Sanitizer (KMSAN)

KMSAN은 초기화되지 않은 값의 사용을 찾기 위한 동적 오류 검출기입니다. 컴파일러 계측을 기반으로 하며 사용자 공간의 `MemorySanitizer tool`과 매우 유사합니다.

중요한 점은 KMSAN이 프로덕션 사용을 목적으로 하지 않는다는 것입니다. 커널 메모리 사용량을 크게 늘리고 시스템 전체를 느리게 만들기 때문입니다.

빌드, 보고서 해석과 검출 조건

15-80

사용법

커널 빌드

KMSAN을 포함한 커널을 빌드하려면 최신 Clang 14.0.6 이상이 필요합니다. Clang 빌드 방법은 `LLVM documentation`을 참조하십시오.

그런 다음 CONFIG_KMSAN을 활성화하여 커널을 구성하고 빌드합니다.

보고서 예

다음은 KMSAN 보고서의 예입니다.

=====================================================
BUG: KMSAN: uninit-value in test_uninit_kmsan_check_memory+0x1be/0x380 [kmsan_test]
 test_uninit_kmsan_check_memory+0x1be/0x380 mm/kmsan/kmsan_test.c:273
 kunit_run_case_internal lib/kunit/test.c:333
 kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374
 kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28
 kthread+0x721/0x850 kernel/kthread.c:327
 ret_from_fork+0x1f/0x30 ??:?

Uninit was stored to memory at:
 do_uninit_local_array+0xfa/0x110 mm/kmsan/kmsan_test.c:260
 test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271
 kunit_run_case_internal lib/kunit/test.c:333
 kunit_try_run_case+0x206/0x420 lib/kunit/test.c:374
 kunit_generic_run_threadfn_adapter+0x6d/0xc0 lib/kunit/try-catch.c:28
 kthread+0x721/0x850 kernel/kthread.c:327
 ret_from_fork+0x1f/0x30 ??:?

Local variable uninit created at:
 do_uninit_local_array+0x4a/0x110 mm/kmsan/kmsan_test.c:256
 test_uninit_kmsan_check_memory+0x1a2/0x380 mm/kmsan/kmsan_test.c:271

Bytes 4-7 of 8 are uninitialized
Memory access of size 8 starts at ffff888083fe3da0

CPU: 0 PID: 6731 Comm: kunit_try_catch Tainted: G    B       E     5.16.0-rc3+ #104
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
=====================================================
KMSAN 보고서 추적 단계
단계보고 내용대표 위치
1. 사용초기화되지 않은 값이 실제로 소비된 호출 스택test_uninit_kmsan_check_memory()
2. 저장값이 사용 전 다른 메모리 위치로 복사된 호출 스택do_uninit_local_array()
3. 생성지역 변수 uninit이 초기화되지 않은 채 만들어진 위치mm/kmsan/kmsan_test.c:256
바이트8바이트 접근 중 초기화되지 않은 구간Bytes 4-7 of 8

초기화되지 않은 값의 사용 지점에서 생성 지점까지 이어지는 세 개의 추적과 바이트 범위를 구조화했습니다.

보고서는 지역 변수 `uninit`이 `do_uninit_local_array()`에서 초기화되지 않은 채 생성되었다고 알려 줍니다. 세 번째 스택 추적이 이 변수가 만들어진 위치에 해당합니다.

첫 번째 스택 추적은 초기화되지 않은 값이 `test_uninit_kmsan_check_memory()`에서 사용된 위치를 보여 줍니다. 도구는 지역 변수에서 초기화되지 않은 채 남은 바이트와, 그 값이 사용되기 전에 다른 메모리 위치로 복사된 스택도 함께 표시합니다.

KMSAN은 초기화되지 않은 값 `v`가 다음과 같이 사용될 때 보고합니다.

조건식에서 사용될 때. 예: `if (v) { ... }`.

인덱싱이나 포인터 역참조에 사용될 때. 예: `array[v]` 또는 `*v`.

사용자 공간이나 하드웨어로 복사될 때. 예: `copy_to_user(..., &v, ...)`.

함수 인수로 전달되고 CONFIG_KMSAN_CHECK_PARAM_RETVAL이 활성화되어 있을 때. 자세한 내용은 뒤에서 설명합니다.

사용자 공간이나 하드웨어로 데이터를 복사하는 경우는 보안 문제이며, 이를 제외한 나머지 사례도 C11 표준 관점에서는 정의되지 않은 동작입니다.

계측 비활성화와 지원 범위

81-128

계측 비활성화

함수에 `__no_kmsan_checks`를 표시할 수 있습니다. 그러면 KMSAN은 그 함수 안의 초기화되지 않은 값을 무시하고 함수 출력을 초기화된 것으로 표시합니다. 따라서 사용자는 그 함수와 관련된 KMSAN 보고서를 받지 않습니다.

KMSAN이 지원하는 또 다른 함수 속성은 `__no_sanitize_memory`입니다. 이 속성을 함수에 적용하면 KMSAN이 해당 함수를 계측하지 않습니다. 컴파일러가 일부 저수준 코드에 개입하지 않게 해야 할 때 유용합니다. 예를 들어 `noinstr`로 표시한 코드는 암시적으로 `__no_sanitize_memory`를 추가합니다.

하지만 비용이 있습니다. 이런 함수의 스택 할당에는 잘못된 shadow 또는 origin 값이 들어가 false positive를 일으킬 가능성이 큽니다. 계측되지 않은 코드에서 호출된 함수도 매개변수에 잘못된 메타데이터를 받을 수 있습니다.

일반적인 원칙으로 `__no_sanitize_memory`를 명시적으로 사용하지 마십시오.

단일 파일, 예를 들어 main.o에서 KMSAN을 끌 수도 있습니다.

KMSAN_SANITIZE_main.o := n

전체 디렉터리에서 끄려면 Makefile에 다음을 지정합니다.

KMSAN_SANITIZE := n

이는 파일이나 디렉터리의 모든 함수에 `__no_sanitize_memory`를 적용하는 것과 같습니다. KMSAN 때문에 코드가 깨지는 경우, 예를 들어 아주 이른 부팅 단계에서 실행되는 코드가 아니라면 대부분의 사용자는 KMSAN_SANITIZE를 사용할 필요가 없습니다.

`kmsan_disable_current()`와 `kmsan_enable_current()` 호출로 현재 task의 KMSAN 검사를 일시적으로 끌 수도 있습니다. 각 `kmsan_enable_current()` 앞에는 반드시 `kmsan_disable_current()`가 있어야 하며 호출 쌍은 중첩할 수 있습니다. 이 호출은 신중하게 사용하고 영역을 짧게 유지하며 가능한 경우 계측을 끄는 다른 방법을 우선하십시오.

지원

KMSAN이 동작하려면 KMSAN을 지원하는 유일한 컴파일러인 Clang으로 커널을 빌드해야 합니다. 커널 계측 패스는 사용자 공간의 `MemorySanitizer tool`을 기반으로 합니다.

현재 런타임 라이브러리는 x86_64만 지원합니다.

Shadow memory와 값 전파

129-166

KMSAN의 동작 방식

KMSAN shadow memory

KMSAN은 커널 메모리의 각 바이트에 메타데이터 바이트, 즉 shadow byte 하나를 연결합니다. 커널 메모리 바이트의 특정 비트가 초기화되지 않았다면 shadow byte의 대응 비트가 설정됩니다. 메모리를 초기화되지 않은 상태로 표시하여 shadow byte를 `0xff`로 만드는 것을 poisoning이라 하고, 초기화된 상태로 표시하여 `0x00`으로 만드는 것을 unpoisoning이라 합니다.

새 변수가 스택에 할당되면 컴파일러가 삽입한 계측 코드가 기본적으로 이를 poison합니다. 즉시 초기화되는 스택 변수는 예외입니다. `__GFP_ZERO` 없이 수행한 새 heap 할당도 poison됩니다.

컴파일러 계측은 코드에서 shadow 값이 사용되는 과정도 추적합니다. 필요할 때 계측 코드는 `mm/kmsan/`의 런타임 라이브러리를 호출해 shadow 값을 유지합니다.

기본형이나 복합형의 shadow 값은 원래 값과 길이가 같은 바이트 배열입니다. 상수 값을 메모리에 쓰면 그 메모리는 unpoison됩니다. 메모리에서 값을 읽으면 shadow memory도 함께 가져와 그 값을 사용하는 모든 연산으로 전파합니다. 하나 이상의 값을 입력으로 받는 각 명령에 대해 컴파일러는 입력 값과 shadow에 따라 결과의 shadow를 계산하는 코드를 생성합니다.

예:

int a = 0xff;  // i.e. 0x000000ff
int b;
int c = a | b;

이 경우 `a`의 shadow는 `0`, `b`의 shadow는 `0xffffffff`, `c`의 shadow는 `0xffffff00`입니다. 따라서 `c`의 상위 3바이트는 초기화되지 않았고 하위 1바이트는 초기화되었습니다.

Origin 추적과 연결

167-226

Origin 추적

커널 메모리 4바이트마다 origin도 하나씩 매핑됩니다. origin은 초기화되지 않은 값이 프로그램 실행 중 생성된 지점을 설명합니다. 각 origin은 heap 메모리라면 전체 할당 스택에, 지역 변수라면 초기화되지 않은 변수를 포함한 함수에 연결됩니다.

초기화되지 않은 변수가 stack 또는 heap에 할당되면 새 origin 값이 생성되고 그 변수를 해당 값으로 채웁니다. 메모리에서 값을 읽을 때 origin도 읽어 shadow와 함께 유지합니다. 하나 이상의 값을 입력으로 받는 각 명령에서 결과 origin은 초기화되지 않은 입력 중 하나의 origin입니다. poison된 값을 메모리에 쓰면 origin도 대응 저장소에 기록합니다.

예 1:

int a = 42;
int b;
int c = a + b;

이 경우 함수 진입 시 `b`의 origin이 생성되고, 덧셈 결과를 메모리에 쓰기 직전에 그 origin이 `c`의 origin으로 저장됩니다.

여러 변수가 같은 4바이트 조각에 저장되면 같은 origin 주소를 공유할 수 있습니다. 이 경우 어느 변수에 쓰더라도 모든 변수의 origin이 갱신됩니다. 비트별, 심지어 바이트별 origin 저장도 비용이 지나치게 크므로 이 상황에서는 정밀도를 희생해야 합니다.

예 2:

int combine(short a, short b) {
  union ret_t {
    int i;
    short s[2];
  } ret;
  ret.s[0] = a;
  ret.s[1] = b;
  return ret.i;
}

`a`가 초기화되고 `b`가 초기화되지 않았다면 결과 shadow는 `0xffff0000`이고 결과 origin은 `b`의 origin입니다. `ret.s[0]`도 같은 origin을 갖지만 이 변수는 초기화되어 있으므로 origin이 사용되지는 않습니다.

두 함수 인수가 모두 초기화되지 않았다면 두 번째 인수의 origin만 보존됩니다.

Origin 연결

디버깅을 쉽게 하기 위해 KMSAN은 초기화되지 않은 값을 메모리에 저장할 때마다 새 origin을 만듭니다. 새 origin은 생성 스택과 그 값이 이전에 가졌던 origin을 모두 참조합니다. 이는 메모리 소비를 늘릴 수 있으므로 런타임에서 origin chain 길이를 제한합니다.

Origin 전파
미초기화 값 생성최초 origin과 생성 stack 기록
연산shadow와 origin을 결과로 전파
메모리 저장새 origin을 만들고 이전 origin을 연결
오류 보고chain을 따라 생성과 저장 경로 복원

초기화되지 않은 값이 생성되고 연산과 저장을 거치며 origin chain을 만드는 흐름입니다.

Clang 계측 API

227-338

Clang 계측 API

Clang 계측 패스는 커널 코드에 `mm/kmsan/nstrumentation.c`에 정의된 함수 호출을 삽입합니다. 이 경로 표기는 원문 그대로 보존했습니다.

Shadow 조작

컴파일러는 각 메모리 접근에 대해 주어진 메모리의 shadow와 origin 주소를 가리키는 포인터 쌍을 반환하는 함수를 호출합니다.

typedef struct {
  void *shadow, *origin;
} shadow_origin_ptr_t

shadow_origin_ptr_t __msan_metadata_ptr_for_load_{1,2,4,8}(void *addr)
shadow_origin_ptr_t __msan_metadata_ptr_for_store_{1,2,4,8}(void *addr)
shadow_origin_ptr_t __msan_metadata_ptr_for_load_n(void *addr, uintptr_t size)
shadow_origin_ptr_t __msan_metadata_ptr_for_store_n(void *addr, uintptr_t size)

함수 이름은 메모리 접근 크기에 따라 달라집니다.

컴파일러는 값을 읽을 때마다 그 shadow와 origin도 메모리에서 읽도록 보장합니다. 값을 메모리에 저장할 때도 메타데이터 포인터를 사용해 shadow와 origin을 함께 저장합니다.

지역 변수 처리

지역 변수에 대한 새 origin 값을 만들고 그 변수의 origin을 새 값으로 설정할 때는 특별한 함수를 사용합니다.

void __msan_poison_alloca(void *addr, uintptr_t size, char *descr)

Task별 데이터 접근

KMSAN은 계측된 모든 함수의 시작 부분에 `__msan_get_context_state()` 호출을 삽입합니다.

kmsan_context_state *__msan_get_context_state(void)

`kmsan_context_state`는 `include/linux/kmsan.h`에 다음과 같이 선언됩니다.

struct kmsan_context_state {
  char param_tls[KMSAN_PARAM_SIZE];
  char retval_tls[KMSAN_RETVAL_SIZE];
  char va_arg_tls[KMSAN_PARAM_SIZE];
  char va_arg_origin_tls[KMSAN_PARAM_SIZE];
  u64 va_arg_overflow_size_tls;
  char param_origin_tls[KMSAN_PARAM_SIZE];
  depot_stack_handle_t retval_origin_tls;
};

KMSAN은 이 구조체를 사용해 계측된 함수 사이에서 매개변수의 shadow와 origin을 전달합니다. CONFIG_KMSAN_CHECK_PARAM_RETVAL이 매개변수를 즉시 검사하는 경우는 예외입니다.

초기화되지 않은 값을 함수로 전달

Clang의 MemorySanitizer 계측에는 값으로 전달되는 함수 매개변수와 함수 반환값을 검사하는 `-fsanitize-memory-param-retval` 옵션이 있습니다.

이 옵션은 기본적으로 활성화되는 CONFIG_KMSAN_CHECK_PARAM_RETVAL이 제어하며, KMSAN이 초기화되지 않은 값을 더 일찍 보고하도록 합니다. 자세한 내용은 `LKML discussion`을 참조하십시오.

LLVM에서 검사가 구현된 방식상 `noundef`로 표시한 매개변수에만 검사가 적용되므로 모든 매개변수가 반드시 검사된다고 보장할 수 없습니다. 따라서 `kmsan_context_state`의 메타데이터 저장소를 없앨 수 없습니다.

문자열 함수

컴파일러는 `memcpy()`, `memmove()`, `memset()` 호출을 다음 함수로 바꿉니다. 데이터 구조를 초기화하거나 복사할 때도 이 함수들이 호출되어 데이터와 함께 shadow와 origin을 복사합니다.

void *__msan_memcpy(void *dst, void *src, uintptr_t n)
void *__msan_memmove(void *dst, void *src, uintptr_t n)
void *__msan_memset(void *dst, int c, uintptr_t n)

오류 보고

컴파일러는 값을 사용할 때마다 shadow 검사를 생성하며 값이 poison되어 있으면 `__msan_warning()`을 호출합니다.

void __msan_warning(u32 origin)

`__msan_warning()`은 KMSAN 런타임이 오류 보고서를 출력하게 합니다.

Inline assembly 계측

KMSAN은 모든 inline assembly 출력에 다음 호출을 삽입합니다.

void __msan_instrument_asm_store(void *addr, uintptr_t size)

이 함수는 해당 메모리 영역을 unpoison합니다.

이 방식은 일부 오류를 가릴 수 있지만 비트 연산과 atomic 연산 등에서 발생하는 많은 false positive를 방지하는 데도 도움이 됩니다.

때로 inline assembly에 전달되는 포인터가 유효한 메모리를 가리키지 않을 수 있습니다. 그런 포인터는 런타임에서 무시합니다.

런타임 상태와 메타데이터 할당

339-424

런타임 라이브러리

코드는 `mm/kmsan/`에 있습니다.

Task별 KMSAN 상태

각 task_struct에는 앞서 설명한 KMSAN context와 KMSAN 보고를 금지하는 task별 counter를 보관하는 KMSAN task state가 연결됩니다.

struct kmsan_context {
  ...
  unsigned int depth;
  struct kmsan_context_state cstate;
  ...
}

struct task_struct {
  ...
  struct kmsan_context kmsan;
  ...
}

KMSAN context

커널 task context에서 실행할 때 KMSAN은 `current->kmsan.cstate`에 함수 매개변수와 반환값의 메타데이터를 보관합니다.

커널이 interrupt, softirq 또는 NMI context에서 실행되어 `current`를 사용할 수 없으면 KMSAN은 CPU별 interrupt state로 전환합니다.

DEFINE_PER_CPU(struct kmsan_ctx, kmsan_percpu_ctx);

메타데이터 할당

커널에는 메타데이터를 저장하는 위치가 여러 곳 있습니다.

1. 각 `struct page` 인스턴스는 자신의 shadow page와 origin page를 가리키는 두 포인터를 포함합니다.

struct page {
  ...
  struct page *shadow, *origin;
  ...
};

부팅할 때 커널은 사용 가능한 모든 커널 page마다 shadow page와 origin page를 할당합니다. 이는 커널 주소 공간이 이미 조각난 비교적 늦은 시점에 수행되므로 일반 data page가 metadata page 사이에 임의로 끼어들 수 있습니다.

따라서 일반적으로 연속한 두 memory page의 shadow 또는 origin page가 연속한다고 보장할 수 없습니다. 메모리 접근이 한 memory block의 경계를 넘으면 shadow 또는 origin 접근이 다른 page를 손상하거나 잘못된 값을 읽을 가능성이 있습니다.

실제로 같은 `alloc_pages()` 호출이 반환한 연속 memory page는 연속한 metadata를 갖습니다. 반면 두 page가 서로 다른 할당에 속한다면 metadata page가 조각날 수 있습니다.

커널 data 영역인 `.data`, `.bss` 등과 percpu memory 영역도 metadata 연속성을 보장하지 않습니다.

`__msan_metadata_ptr_for_XXX_YYY()`가 연속하지 않은 metadata를 가진 두 page 사이의 경계에 닿으면 가짜 shadow 또는 origin 영역을 가리키는 포인터를 반환합니다.

char dummy_load_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE)));
char dummy_store_page[PAGE_SIZE] __attribute__((aligned(PAGE_SIZE)));

`dummy_load_page`는 0으로 초기화되므로 여기서 읽으면 항상 0을 얻습니다. `dummy_store_page`에 대한 모든 쓰기는 무시됩니다.

2. vmalloc memory와 module에는 실제 memory range, shadow, origin 사이에 직접 매핑이 있습니다. KMSAN은 vmalloc 영역을 4분의 3만큼 줄여 첫 번째 4분의 1만 `vmalloc()`에 제공합니다. 두 번째 4분의 1에는 첫 영역의 shadow memory를 두고 세 번째 영역에는 origin을 둡니다. 네 번째 영역의 작은 부분에는 kernel module용 shadow와 origin을 둡니다. 자세한 내용은 `arch/x86/include/asm/pgtable_64_types.h`를 참조하십시오.

page 배열을 연속한 virtual memory 공간에 매핑할 때 해당 shadow page와 origin page도 마찬가지로 연속한 영역에 매핑합니다.

vmalloc 영역의 KMSAN 메타데이터 배치
구간용도대응 관계
1/4vmalloc() 데이터실제 접근 대상
2/4Shadow memory첫 번째 구간의 초기화 비트
3/4Origin memory첫 번째 구간의 생성 이력
4/4 일부Kernel module 메타데이터module shadow와 origin

원래 vmalloc 주소 공간을 네 구간으로 나누어 데이터, shadow, origin과 module 메타데이터를 직접 대응시킵니다.

참고 문헌과 링크

425-435

참고 문헌

E. Stepanov, K. Serebryany. `MemorySanitizer: fast detector of uninitialized memory use in C++ <https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43308.pdf>`_. CGO 2015 논문집.

MemorySanitizer tool: https://clang.llvm.org/docs/MemorySanitizer.html

LLVM documentation: https://llvm.org/docs/GettingStarted.html

LKML discussion: https://lore.kernel.org/all/20220614144853.3693273-1-glider@google.com/