KMSAN (Kernel Memory Sanitizer)
KMSAN은 초기화되지 않은 메모리(Uninitialized Memory) 사용을 탐지하는 커널 Sanitizer입니다. 각 바이트에 Shadow와 Origin 메타데이터를 유지해 미초기화 값이 분기·출력·사용자 공간으로 전파되는 순간을 포착합니다. 오버헤드(Overhead)는 크지만 페이지(Page) 단위 할당부터 스택 변수까지 포괄적으로 디버깅(Debugging)할 수 있습니다.
핵심 요약
- Shadow Memory — 각 바이트의 "초기화 여부"를 비트 단위로 추적(0=초기화, 1=미초기화)
- Origin Memory — 미초기화 값의 "출처"(kmalloc 호출 스택 등)를 4바이트당 1개 저장
- 사용 시점 체크 — 분기·산술·
copy_to_user에서 Shadow 확인, 0이 아니면 리포트 - Clang 전용 — GCC는 미지원.
LLVM=1로 빌드, x86_64/s390 지원 - 퍼징 전용 — 오버헤드가 약 3배라 프로덕션 불가. syzkaller·KUnit·CI에서만 사용
단계별 이해
- 할당
kmalloc이GFP_ZERO없이 호출되면 반환 메모리의 Shadow를0xFF로 포이즌하고, Origin에 kmalloc 호출 스택을 기록합니다. - 쓰기
프로그램이 초기화 쓰기를 수행하면 컴파일러 계측이 해당 바이트의 Shadow를 0으로 클리어합니다. - 읽기/사용
읽기·분기·산술·copy_to_user시 Shadow가 0인지 확인합니다. - 전파
연산 결과는 피연산자 Shadow의 OR가 되고, Origin은 "먼저 미초기화였던 쪽"이 전파됩니다. - 리포트
Shadow가 0이 아닌 값이 사용자 공간·커널 로직에 영향을 미치는 순간 "uninit-value" 리포트가 출력됩니다.
개요
KMSAN은 초기화되지 않은 메모리 사용(uninitialized memory use)을 탐지합니다. 커널에서 kmalloc으로 할당한 메모리를 초기화하지 않고 사용하거나, 초기화되지 않은 스택 변수의 값에 기반한 분기/출력이 발생하면 정보 누출(information leak)이나 비결정적 동작(non-deterministic behavior)의 원인이 됩니다.
copy_to_user()로
사용자 공간에 복사하면 커널 스택의 민감 정보가 유출됩니다. 실제 CVE 다수가 이 패턴에
해당하며, KMSAN은 컴파일러 계측으로 이러한 누출을 런타임에 잡아냅니다.
- KASAN — 잘못된 주소 접근(OOB/UAF)을 잡음. Shadow는 "접근 허용 여부"
- KMSAN — 초기화 안 된 값을 잡음. Shadow는 "초기화 여부"
- 두 도구는 같은 Shadow 구조를 공유하지 않으며(레이아웃 다름), 동시에 켤 수 없습니다.
Shadow와 Origin 추적
KMSAN은 KASAN과 유사하게 Shadow Memory를 사용하지만, 목적이 다릅니다. 각 메모리 바이트에 대해 두 종류의 메타데이터를 유지합니다:
| 메타데이터 | 비율 | 값의 의미 | 용도 |
|---|---|---|---|
| Shadow Memory | 1:1 | 0 = 초기화됨, 비트별 1 = 해당 비트 미초기화 | 초기화 상태 추적 |
| Origin Memory | 4:1 (4바이트당 1개 origin) | Stack Depot 핸들 (초기화되지 않은 데이터의 출처) | 미초기화 데이터가 어디서 왔는지 추적 |
컴파일러 계측 (Clang Instrumentation)
KMSAN은 Clang 전용입니다. 컴파일러가 LLVM Pass 레벨에서 모든 메모리 접근에
__msan_*/__kmsan_* 호출을 삽입합니다. GCC에는 대응 Pass가 없어
현재까지 미구현입니다.
mm/kmsan/instrumentation.c가 내보내는 실제 헬퍼는
__msan_metadata_ptr_for_load_N(shadow·origin 포인터 얻기),
__msan_warning(경고 발행),
__msan_get_context_state(struct kmsan_ctx 접근),
__msan_chain_origin(origin 체인 확장),
__msan_poison_alloca/__msan_unpoison_alloca(스택 변수 진입/해제),
__msan_memcpy/__msan_memset/__msan_memmove 등입니다.
아래 C 코드는 LLVM IR을 읽기 쉽게 풀어낸 개념적 예시이며, 실제 생성되는 심볼은 타겟 타입/크기별로
__msan_metadata_ptr_for_load_1~_8/_n 형태로 분기됩니다.
/* Clang이 원본 커널 코드에 자동 삽입하는 계측 (개념적) */
/* 원본 */
int sample(int *p)
{
return *p + 1;
}
/* 계측 후 (Clang이 합성한 IR을 C로 역번역한 모습) */
int sample(int *p)
{
/* 1. *p를 읽기 전: shadow/origin 포인터 획득 */
struct shadow_origin_ptr so =
__msan_metadata_ptr_for_load_4(p);
u32 shadow = *(u32 *)so.shadow;
/* 2. 값이 미초기화이면 경고 발행 (Origin 포함) */
if (shadow)
__msan_warning(*(u32 *)so.origin);
int v = *p;
int r = v + 1;
/* 3. retval shadow는 per-task cstate.retval_tls에 기록 */
struct kmsan_ctx *ctx = __msan_get_context_state();
*(u32 *)ctx->cstate.retval_tls = shadow;
return r;
}
코드 설명
-
12-14행
__msan_metadata_ptr_for_load_4()는 주소p에 대응하는 shadow와 origin 포인터 쌍을 반환합니다. 크기별로_1/_2/_4/_8/_n변형이 있습니다. -
17-18행
Shadow가 0이 아니면
__msan_warning()을 호출해 Origin과 함께 리포트합니다. 실제 리포트 출력은 값이 "사용자 공간으로 나갈 때"까지 지연(Latency)되기도 합니다. -
24-25행
반환 값의 shadow는 현재 task의
kmsan_ctx.cstate.retval_tls에 저장되어 호출자에게 전파됩니다. 함수 인자 shadow도param_tls/param_origin_tlsTLS 슬롯을 경유합니다.
함수 경계와 Origin 전파
함수를 넘나드는 Shadow/Origin 전파는 task_struct에 붙은 struct kmsan_ctx의
per-task TLS 영역을 통해 이루어집니다. 실제 정의(include/linux/kmsan_types.h)는
struct kmsan_context_state가 800바이트 고정 크기 TLS 버퍼(Buffer) 5개를 갖는 구조이며,
컴파일러가 각 슬롯에 파라미터와 반환 값을 직렬화(Serialization)해 주고받습니다.
| 경로 | 저장 위치 (kmsan_ctx.cstate.*) | 타이밍 |
|---|---|---|
| 함수 인자 Shadow | param_tls[800] |
호출 직전 caller가 기록, callee가 읽음 |
| 함수 반환 Shadow | retval_tls[800] |
return 직전 callee가 기록, 호출자 읽음 |
| 함수 인자 Origin | param_origin_tls[800] |
인자 shadow와 동기 저장/읽기 |
| 반환 값 Origin | retval_origin_tls (단일 u32) |
return 직전 기록 |
| 가변 인자 Shadow | va_arg_tls[800] + va_arg_overflow_size_tls |
printk 같은 가변 인자 함수에서 사용 |
| 가변 인자 Origin | va_arg_origin_tls[800] |
가변 인자 shadow와 동기 |
kmsan_ctx 자체는 cstate 외에 재귀 방지 플래그(kmsan_in_runtime)와
호출 깊이 카운터(depth)도 포함합니다. 런타임 코드가 다시 계측된 코드를 호출하는 역순환을 방지합니다.
copy_to_user, write(), ioctl 응답 등)"에
리포트하는 것이 기본 정책입니다. 산술·분기에서 즉시 트랩하면 폭풍 경고가 나기 때문입니다.
핵심 체크 로직
/* mm/kmsan/core.c - KMSAN 핵심 체크 (간략화) */
/* 메모리에서 값을 읽을 때 초기화 상태 체크 */
void kmsan_check_memory(const void *addr, size_t size)
{
u8 *shadow = kmsan_get_shadow(addr);
u32 *origin = kmsan_get_origin(addr);
for (size_t i = 0; i < size; i++) {
if (shadow[i] != 0) {
/* 초기화되지 않은 바이트 발견! */
kmsan_report(addr + i, size,
"uninit-value", origin[i / 4]);
break;
}
}
}
/* kmalloc 시 Shadow/Origin 초기화 */
void kmsan_kmalloc_hook(const void *addr,
size_t size, gfp_t flags)
{
if (flags & __GFP_ZERO) {
/* GFP_ZERO: 초기화됨으로 표시 */
kmsan_internal_unpoison(addr, size);
} else {
/* 미초기화: Shadow를 0xFF로, Origin 기록 */
kmsan_internal_poison(addr, size);
kmsan_set_origin(addr, size,
kmsan_save_stack());
}
}
/* copy_to_user 시 체크 (정보 누출 방지) */
void kmsan_copy_to_user(void __user *to,
const void *from, size_t size)
{
/* 사용자 공간으로 복사하는 데이터가 초기화되었는지 검증 */
kmsan_check_memory(from, size);
}
코드 설명
-
4-16행
kmsan_check_memory()는 메모리 영역의 Shadow를 검사합니다. 0이 아닌 비트가 있으면 해당 바이트가 초기화되지 않았음을 의미하며, Origin 정보와 함께 리포트합니다. -
21-31행
GFP_ZERO로 할당하면 초기화됨(shadow=0)으로 표시하고, 그 외에는 미초기화(shadow=0xFF)로 표시합니다. Origin에는 kmalloc 호출 스택이 기록됩니다. -
35-38행
copy_to_user()시 커널 데이터가 초기화되었는지 검증합니다. 미초기화 데이터가 사용자 공간(User Space)으로 복사되면 정보 누출(information disclosure) 취약점(Vulnerability)이 됩니다.
리포트 해석
=====================================================
BUG: KMSAN: uninit-value in copy_to_user+0x78/0xf0
Uninit was stored to memory at:
kmsan_save_stack+0x20/0x40
kmsan_internal_poison+0x50/0x80
kmsan_kmalloc_hook+0x34/0x60
__kmalloc+0x120/0x180
some_driver_ioctl+0x30/0x100 ← kmalloc 위치 (미초기화)
Uninit was created at:
__kmalloc+0x120/0x180
some_driver_ioctl+0x30/0x100
Bytes 4-7 of 16 are uninitialized
=====================================================
분석: some_driver_ioctl()에서 16바이트 kmalloc 후
일부(4~7바이트)를 초기화하지 않고
copy_to_user()로 사용자 공간에 복사 → 정보 누출
실전 사례: 구조체(Struct) 패딩(Padding) 정보 누출
C 컴파일러는 정렬 요구에 맞추기 위해 구조체 필드 사이에 암묵적 패딩을 삽입합니다.
이 패딩 바이트는 개발자가 명시적으로 초기화하지 않는 한 스택 쓰레기 값이 그대로 남아 있으며,
구조체 전체를 copy_to_user()로 보내면 커널 스택의 이전 내용이 사용자 공간으로 누출됩니다.
이런 패턴은 KMSAN 도입 이후 네트워크·tc action·netlink 응답 계열에서 반복적으로 발견되어 오며,
최근 사례는 syzbot KMSAN 대시보드에서 "uninit-value"
필터로 확인할 수 있습니다.
/* 취약 코드 - 사용자에게 구조체 반환 시 패딩 미초기화 */
struct response {
u32 status; /* 4바이트 */
/* 패딩 4바이트 (정렬을 위해 컴파일러 삽입) */
u64 timestamp; /* 8바이트 */
};
static long get_status_ioctl(void __user *uarg)
{
struct response resp;
resp.status = 0;
resp.timestamp = ktime_get_ns();
/* BUG: 4바이트 패딩이 초기화되지 않음!
커널 스택의 이전 데이터가 사용자에게 누출됨 */
copy_to_user(uarg, &resp, sizeof(resp));
/* KMSAN: "uninit-value in copy_to_user" 리포트 */
/* 해결: memset(&resp, 0, sizeof(resp)); 후 필드 설정 */
}
= {0} 또는
memset(&s, 0, sizeof(s))로 시작하고 필드를 채우세요.
__GFP_ZERO 할당이 가능하면 그것이 가장 안전합니다.
커널 네트워크 ABI는 nla_put 같은 API로 TLV 구조를 쓰기 때문에 패딩이 자동 제거됩니다.
어노테이션과 예외 처리
특정 함수나 파일에서 KMSAN 계측을 끄려면 다음 패턴을 사용합니다:
| 방법 | 적용 범위 | 용도 |
|---|---|---|
__no_sanitize_memory |
함수 단위 | 인스펙터·저수준 코드 (예: 스택 스캐너) |
noinstr |
함수 단위 | 엔트리 경로(entry-common.c) 등 계측 금지 영역 |
KMSAN_SANITIZE := n |
Makefile / 디렉토리 | 부트스트랩·페이지 할당자(Page Allocator) 자체 |
kmsan_disable_current() / kmsan_enable_current() |
런타임 블록 | 특정 임계 구간만 일시 비활성화 |
__msan_unpoison(ptr, size) |
명시적 호출 | 외부 소스(DMA, MMIO)에서 받은 데이터를 "초기화됨"으로 표시 |
GCC/Clang 지원 차이
| Sanitizer | GCC | Clang | 비고 |
|---|---|---|---|
| KASAN Generic | ✅ | ✅ | 두 컴파일러 모두 지원 |
| KASAN SW-Tag / HW-Tag | ❌ | ✅ | ARM64 MTE는 Clang 전용 |
| KMSAN | ❌ | ✅ | GCC는 MSan Pass 미포팅 |
| UBSAN | ✅ | ✅ | 표준 유틸리티 |
| KCSAN | ✅ | ✅ | TSan 모델 공통 |
# KMSAN 빌드 예시 (Clang 필수)
make LLVM=1 defconfig
./scripts/config -e CONFIG_KMSAN
./scripts/config -e CONFIG_KMSAN_CHECK_PARAM_RETVAL
./scripts/config -e CONFIG_DEBUG_INFO
make LLVM=1 -j$(nproc)
아키텍처 지원 현황
KMSAN의 가장 큰 제약은 지원 아키텍처입니다. 공식 문서(Documentation/dev-tools/kmsan.rst)
기준으로 런타임 라이브러리는 x86_64만 지원합니다. Clang MSan Pass가
Shadow/Origin을 위한 고정 가상 주소(Virtual Address) 영역을 가정하므로, 다른 아키텍처로 포팅하려면 가상 주소 공간(Address Space) 재배치(Relocation)와
어셈블리(Assembly) 인라인 경로 어노테이션을 모두 준비해야 합니다.
| 아키텍처 | 상태 | 비고 |
|---|---|---|
| x86_64 | ✅ 정식 지원 | KMSAN의 오리지널 타겟. 가장 안정적인 플랫폼 |
| s390 | ✅ 정식 지원 (6.15+) | v7 패치(Patch) 시리즈가 머지되어 arch/s390/Kconfig에 select HAVE_ARCH_KMSAN 포함. Clang 빌드 필수 |
| arm64 / RISC-V | 🗒 논의 단계 | RFC/제안 수준. 2026-04 기준 메인라인 병합 계획 없음 |
grep -r HAVE_ARCH_KMSAN arch/ 또는 make menuconfig에서
"Kernel hacking → Memory Debugging → KMSAN"이 선택 가능한지로 판정할 수 있습니다.
Clang 빌드 필수(LLVM=1)이고, GCC에서는 CONFIG_KMSAN 자체가 노출되지 않습니다.
Stack Depot 공유
KASAN·KMSAN·KFENCE·page_owner·slub_debug가 공유하는 lib/stackdepot.c는
최근 수 년간 여러 단계 최적화를 거쳤습니다. stack_depot_save_flags()로 태그 분할 저장,
stack_depot_put()으로 참조 해제, 부팅 초기 초기화 분리 등이 순차 머지되었으며,
KMSAN은 x86_64 빌드에서 이 공용 Stack Depot을 통해 Origin 체인을 해석합니다.
커버리지: 스택·힙·vmalloc·percpu
KMSAN은 "어디서 할당된 메모리든" 커널 관점에서 추적 가능한 곳이면 Shadow를 매핑(Mapping)합니다. 영역별 특성과 주의점은 다음과 같습니다.
| 영역 | Shadow/Origin 매핑 | 주의점 |
|---|---|---|
| 스택(Stack) | 함수 진입 시 __msan_poison_alloca()로 Shadow 포이즌 |
로컬 변수 초기화 누락이 가장 흔한 uninit-value 소스 |
| kmalloc/slab(Heap) | kmsan_kmalloc_hook이 __GFP_ZERO 여부로 분기 |
__GFP_ZERO 또는 kzalloc 사용 시 shadow=0 |
| 페이지 할당자(Page Allocator) | alloc_pages 계열도 hook으로 Shadow 초기화 |
드라이버가 __get_free_page로 받은 페이지는 기본 미초기화 |
| vmalloc / ioremap | vmalloc 영역 전용 Shadow 매핑 경로 존재 | MMIO·펌웨어(Firmware) 주소 영역은 "외부 입력"이므로 __msan_unpoison_memory_region 필요 |
| percpu 변수 | percpu chunk Shadow도 x86_64에서 관리 | DEFINE_PER_CPU 초기화 매크로(Macro)는 대부분 KMSAN이 자동 처리 |
| DMA·MMIO | 외부에서 들어오는 값은 KMSAN이 볼 수 없음 | 받자마자 __msan_unpoison(ptr, size)로 "초기화됨" 표시 필수 |
syzkaller와의 표준 연동
KMSAN의 최대 소비자는 syzkaller 퍼저(Fuzzer)입니다. 실제 메인라인에 머지된 패딩 누출·uninit read CVE 다수가
ci-upstream-kmsan-gce 잡에서 발견된 것이며, 운영 체계는 다음 3계층으로 구성됩니다.
- KMSAN 활성 커널 이미지 —
LLVM=1+CONFIG_KMSAN=y+CONFIG_KCOV=y로 빌드된 x86_64 이미지 - syz-manager — 4~8개의 QEMU VM을 돌리며 시스템 콜 시퀀스를 랜덤 생성·실행
- syzbot 대시보드 —
uninit-value in ...리포트를 자동 분류하고 관련 stable 태그를 트래킹
{
"name": "linux-kmsan",
"target": "linux/amd64",
"type": "qemu",
"vm": {
"count": 4,
"kernel": "/root/linux/arch/x86/boot/bzImage",
"cmdline": "kmsan.panic=1 panic_on_warn=1"
}
}
kmsan.panic=1은 첫 uninit-value 발견 시 즉시 panic을 유도해 syzkaller의 자동 최소화
경로를 활용합니다. 로컬 재현 시에는 끄고(kmsan.panic=0) 가능한 모든 리포트를 모으는 것이 일반적입니다.
흔한 실수와 디버깅 팁
- "KMSAN이 아무것도 안 잡는다" —
CONFIG_KMSAN_CHECK_PARAM_RETVAL=y가 꺼져 있으면 함수 경계 전파가 제한됩니다. y로 켜세요. - 폭주하는 false positive — DMA나 MMIO로 외부에서 들어온 데이터는 KMSAN 관점에서는 "출처 불명".
__msan_unpoison()을 명시 호출하거나 해당 파일에KMSAN_SANITIZE := n을 지정하세요. - 인라인 어셈블리에서 uninit 보고 — Clang은
asm블록을 분석하지 못합니다. 출력 오퍼랜드에__msan_unpoison_memory_region()을 수동으로 호출해 주세요. - 부팅 초기 패닉 — KMSAN이 활성화되기 전의
early_param()/setup_arch()단계는noinstr로 표시되어야 합니다. - 성능 문제 — KMSAN은 약 3x 느려집니다. 퍼징·CI 전용. 프로덕션은 불가.
커널 설정과 제약
CONFIG_KMSAN=y
CONFIG_KMSAN_CHECK_PARAM_RETVAL=y # 함수 인자/반환값 검사 (권장)
CONFIG_DEBUG_INFO=y # Origin 스택 해석에 필요
CONFIG_KCOV=y # syzkaller 연동 (선택)
- Clang 컴파일러 전용 (GCC 미지원).
LLVM=1로 빌드하세요. - 지원 아키텍처: x86_64, s390 (6.15~). ARM64/RISC-V 포팅 중.
- 오버헤드가 크므로(~3x) 프로덕션 사용 불가. 퍼징/CI 전용.
- 일부 저수준 코드는
__no_sanitize_memory속성이나KMSAN_SANITIZE := n로 제외합니다.
참고자료
- KernelMemorySanitizer (KMSAN) — 커널 공식 문서
- Google KMSAN 프로젝트
- KMSAN: Kernel Memory Sanitizer (Linux Plumbers 2022)
- syzbot KMSAN 대시보드
- 소스 코드:
mm/kmsan/core.c,mm/kmsan/shadow.c,mm/kmsan/instrumentation.c,include/linux/kmsan.h
- KASAN — 메모리 주소 오류 탐지 (Shadow 구조 비교)
- KFENCE — 프로덕션 샘플링 탐지
- UBSAN — 정의되지 않은 동작 탐지
- KCSAN — 데이터 레이스 탐지
- 시스템 콜(System Call) —
copy_to_user와 사용자 공간 경계