KMSAN (Kernel Memory Sanitizer)
개요 (Overview)
KMSAN(Kernel Memory Sanitizer)는 커널에서 초기화되지 않은 메모리 읽기를 탐지하는 디버깅 도구입니다. 각 메모리 바이트마다 그림자(shadow) 메모리와 기원(origin) 메모리를 별도로 관리하여, 초기화되지 않은 값이 어디서 생성되어 어디로 전파되었는지 추적할 수 있습니다. KMSAN은 Clang 컴파일러의 -fsanitize=kernel-memory 옵션과 함께 동작하며, 커널 코드를 계측(instrumentation)하여 메모리 접근 시 그림자 값을 확인합니다.
이 메커니즘은 주로 개발/테스트 환경에서 사용되며, CONFIG_KMSAN 설정을 통해 활성화됩니다. KMSAN은 KASAN과 유사하지만 타겟이 다릅니다: KASAN은 메모리 해제 후 접근(UAF)과 버퍼 오버플로우를 탐지하는 반면, KMSAN은 초기화되지 않은 메모리의 사용을 탐지합니다. 두 도구를 동시에 활성화할 수는 없습니다.
소스 파일 경로:
├── mm/kmsan/ ← KMSAN 핵심 구현
│ ├── kmsan.h ← 핵심 헤더, 상수, 인라인 함수
│ ├── core.c ← 런타임 라이브러리 (poison/unpoison/체크)
│ ├── shadow.c ← 그림자/기원 메모리 관리
│ ├── report.c ← 오류 보고 루틴
│ ├── hooks.c ← 커널 서브시스템 훅 (slab/vmalloc/DMA)
│ ├── instrumentation.c ← Clang 컴파일러 계측 API
│ ├── init.c ← 초기화 루틴
│ └── kmsan_test.c ← 테스트 코드
├── include/linux/kmsan.h ← 외부 공개 인터페이스
└── include/linux/kmsan_types.h ← kmsan_ctx 타입 정의
빠른 점검 명령
# KMSAN 활성화 여부 확인 (부팅 파라미터)
cat /proc/cmdline | grep -o "kmsan[= ]" || echo "비활성화"
# CONFIG_KMSAN 확인
zcat /proc/config.gz 2>/dev/null | grep KMSAN || \
grep KMSAN /boot/config-$(uname -r) 2>/dev/null || echo "확인 불가"
# KMSAN 관련 심볼 확인
cat /proc/kallsyms | grep -E "kmsan_|__msan_" | head -20
# 커널 로그에서 KMSAN 관련 메시지 확인
dmesg | grep -i "kmsan\|KernelMemorySanitizer"
# KMSAN과 KASAN 동시 사용 불가 확인
zcat /proc/config.gz 2>/dev/null | grep -E "CONFIG_KMSAN|CONFIG_KASAN"
# KMSAN 컨텍스트 구조체 확인
cat /proc/kallsyms | grep kmsan_percpu_ctx
# KMSAN 메모리 체크 함수 사용처 확인
grep -r "kmsan_check_memory\|kmsan_poison_memory" \
/home/ryotta205/Chamber_linux/linux-7.0-source/mm/ \
/home/ryotta205/Chamber_linux/linux-7.0-source/include/linux/ 2>/dev/null | head -20
# KMSAN 스택 디포트 사용량 확인 (있을 경우)
cat /sys/kernel/debug/stackdepot/stacks 2>/dev/null | grep -c "kmsan" || echo "스택 디포트 접근 불가"
# KMSAN 그림자 메모리 영역 확인 (vmalloc 영역)
cat /proc/vmallocinfo 2>/dev/null | grep -i "shadow\|origin" | head -10 || echo "해당 없음"
# KMSAN 오류 발생 시 탐지된 문제 수 확인
dmesg | grep -c "BUG: KMSAN" || echo "0"
핵심 자료구조
1. `kmsan_ctx` (include/linux/kmsan_types.h)
struct kmsan_ctx {
struct kmsan_context_state cstate; // 컴파일러 계측 상태
int kmsan_in_runtime; // 런타임 진입 플래그 (중첩 호출 방지)
unsigned int depth; // kmsan_enable/disable 깊이 카운터
// 현재 태스크의 KMSAN 컨텍스트
// 인터럽트에서는 kmsan_percpu_ctx 사용
};
2. `kmsan_context_state` (include/linux/kmsan_types.h)
struct kmsan_context_state {
char param_tls[KMSAN_PARAM_SIZE]; // 매개변수 그림자 값 (TLS 저장, 800바이트)
char retval_tls[KMSAN_RETVAL_SIZE]; // 반환값 그림자 값 (TLS 저장, 800바이트)
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]; // 매개변수 기원 (TLS 저장)
u32 retval_origin_tls; // 반환값 기원
// 함수 호출 시 매개변수/반환값의 그림자/기원을 추적
};
3. `shadow_origin_ptr` (mm/kmsan/kmsan.h:41)
struct shadow_origin_ptr {
void *shadow, *origin; // 그림자 및 기원 포인터 쌍
};
// kmsan_get_shadow_origin_ptr()이 반환하는 구조체
// 메모리 접근 시 그림자/기원 메모리 위치를 동시에 제공
4. `enum kmsan_bug_reason` (mm/kmsan/kmsan.h:49)
enum kmsan_bug_reason {
REASON_ANY, // 일반 메모리 접근
REASON_COPY_TO_USER, // 사용자 공간으로 복사 (정보 누출)
REASON_SUBMIT_URB, // USB URB 제출 (USB 정보 누출)
};
// 오류 보고 시 원인 분류
// REASON_COPY_TO_USER는 커널→사용자 정보 누출 탐지
5. 그림자/기원 상수 (mm/kmsan/kmsan.h:26-36)
#define KMSAN_POISON_NOCHECK 0x0 // 오염 체크 없음
#define KMSAN_POISON_CHECK 0x1 // 오염 체크 활성화
#define KMSAN_POISON_FREE 0x2 // 해제된 메모리 (UAF 감지)
#define KMSAN_ORIGIN_SIZE 4 // 기원 엔트리 크기 (4바이트)
#define KMSAN_MAX_ORIGIN_DEPTH 7 // 최대 기원 체인 깊이
#define KMSAN_STACK_DEPTH 64 // 스택 트레이스 최대 깊이
#define KMSAN_META_SHADOW false // 그림자 메타데이터
#define KMSAN_META_ORIGIN true // 기원 메타데이터
6. `kmsan_internal_*` 함수 선언 (mm/kmsan/kmsan.h:154-167)
void kmsan_internal_memmove_metadata(void *dst, void *src, size_t n);
void kmsan_internal_poison_memory(void *address, size_t size, gfp_t flags,
unsigned int poison_flags);
void kmsan_internal_unpoison_memory(void *address, size_t size, bool checked);
void kmsan_internal_set_shadow_origin(void *address, size_t size, int b,
u32 origin, bool checked);
depot_stack_handle_t kmsan_internal_chain_origin(depot_stack_handle_t id);
// kmsan_internal_* 함수들은 런타임 체크 없이 직접 호출되는 내부 함수
// kmsan_in_runtime() 확인이 필요 없는 단순 구현
핵심 함수
1. `kmsan_internal_poison_memory()` (mm/kmsan/core.c:49)
void kmsan_internal_poison_memory(void *address, size_t size, gfp_t flags,
unsigned int poison_flags)
{
u32 extra_bits = kmsan_extra_bits(0, poison_flags & KMSAN_POISON_FREE);
bool checked = poison_flags & KMSAN_POISON_CHECK;
depot_stack_handle_t handle;
handle = kmsan_save_stack_with_flags(flags, extra_bits);
kmsan_internal_set_shadow_origin(address, size, -1, handle, checked);
}
- KMSAN_POISON_FREE 플래그 → UAF 감지를 위한 오염
- KMSAN_POISON_CHECK 플래그 → 실제 오염 수행 여부
- 스택 트레이스를 저장하여 오염 원인 추적
2. `kmsan_internal_unpoison_memory()` (mm/kmsan/core.c:61)
void kmsan_internal_unpoison_memory(void *address, size_t size, bool checked)
{
kmsan_internal_set_shadow_origin(address, size, 0, 0, checked);
}
- checked 매개변수가 true이면 실제 메타데이터에 영향
- false이면 실제 메모리 상태 변경 없이 메타데이터만 업데이트
3. `kmsan_get_shadow_origin_ptr()` (mm/kmsan/shadow.c:81)
struct shadow_origin_ptr kmsan_get_shadow_origin_ptr(void *address, u64 size,
bool store)
{
struct shadow_origin_ptr ret;
void *shadow;
if (!kmsan_enabled) goto return_dummy;
shadow = kmsan_get_metadata(address, KMSAN_META_SHADOW);
if (!shadow) goto return_dummy;
ret.shadow = shadow;
ret.origin = kmsan_get_metadata(address, KMSAN_META_ORIGIN);
return ret;
return_dummy:
if (store) { ret.shadow = dummy_store_page; ret.origin = dummy_store_page; }
else { ret.shadow = dummy_load_page; ret.origin = dummy_load_page; }
return ret;
}
- KMSAN 비활성화 시 더미 페이지 반환 (스토어 무시, 로드 0 반환)
- 그림자 메모리가 없으면 더미 페이지 반환
- 실제 메모리 접근 시 그림자/기원 메모리 위치 제공
4. `kmsan_internal_check_memory()` (mm/kmsan/core.c:253)
void kmsan_internal_check_memory(void *addr, size_t size,
const void __user *user_addr, int reason)
{
depot_stack_handle_t cur_origin = 0, new_origin = 0;
unsigned char *shadow = NULL;
int cur_off_start = -1;
// ... (생략)
while (pos < size) {
shadow = kmsan_get_metadata((void *)(addr64 + pos), KMSAN_META_SHADOW);
if (!shadow) { /* 추적되지 않은 페이지 */ continue; }
for (int i = 0; i < chunk_size; i++) {
if (!shadow[i]) { /* 초기화됨 */ continue; }
origin = kmsan_get_metadata((void *)(addr64 + pos + i),
KMSAN_META_ORIGIN);
new_origin = *origin;
if (cur_origin != new_origin) {
if (cur_origin) kmsan_report(cur_origin, addr, size, ...);
cur_origin = new_origin;
cur_off_start = pos + i;
}
}
}
}
- 그림자 바이트가 0이면 초기화됨 (무시)
- 그림자 바이트가 0이 아니면 기원(origin) 확인
- 기원이 변경되면 이전 범위 보고 후 새 기원으로 갱신
- 페이지 경계에서 청크 단위로 처리
5. `kmsan_report()` (mm/kmsan/report.c:150)
void kmsan_report(depot_stack_handle_t origin, void *address, int size,
int off_first, int off_last, const void __user *user_addr,
enum kmsan_bug_reason reason)
{
// ... (스택 트레이스 저장 및 스킵)
is_uaf = kmsan_uaf_from_eb(stack_depot_get_extra_bits(origin));
switch (reason) {
case REASON_ANY:
bug_type = is_uaf ? "use-after-free" : "uninit-value";
break;
case REASON_COPY_TO_USER:
bug_type = is_uaf ? "kernel-infoleak-after-free" : "kernel-infoleak";
break;
case REASON_SUBMIT_URB:
bug_type = is_uaf ? "kernel-usb-infoleak-after-free" : "kernel-usb-infoleak";
break;
}
pr_err("BUG: KMSAN: %s in %pSb\n", bug_type, (void *)stack_entries[skipnr]);
kmsan_print_origin(origin);
// ... (상세 정보 출력)
add_taint(TAINT_BAD_PAGE, LOCKDEP_NOW_UNRELIABLE);
if (panic_on_kmsan) panic("kmsan.panic set ...\n");
}
- kmsan_uaf_from_eb()로 UAF 여부 결정
- reason에 따라 버그 타입 문자열 결정
- panic_on_kmsan 설정 시 패닉 발생
호출 흐름
메모리 오염/초기화 흐름
kmsan_slab_alloc() / kmsan_kmalloc_large()
→ kmsan_internal_poison_memory() // __GFP_ZERO가 아닌 경우
→ kmsan_save_stack_with_flags() // 스택 트레이스 저장
→ stack_depot_save() // 스택 디포트에 저장
→ kmsan_internal_set_shadow_origin() // 그림자=-1, 기원=핸들 설정
kmsan_slab_alloc() / kmsan_kmalloc_large() // __GFP_ZERO인 경우
→ kmsan_internal_unpoison_memory()
→ kmsan_internal_set_shadow_origin() // 그림자=0, 기원=0 설정
메모리 해제 시 오염 흐름
kmsan_slab_free() / kmsan_kfree_large()
→ kmsan_internal_poison_memory()
→ kmsan_save_stack_with_flags() // 스택 트레이스 저장 (UAF 플래그 포함)
→ kmsan_internal_set_shadow_origin() // 그림자=-1, 기원=핸들 설정
// poison_flags에 KMSAN_POISON_FREE 추가 → UAF 감지용
메모리 접근 검증 흐름
__msan_metadata_ptr_for_load_N() / __msan_metadata_ptr_for_store_N()
→ kmsan_get_shadow_origin_ptr()
→ kmsan_get_metadata(shadow) // 그림자 메모리 위치 반환
→ kmsan_get_metadata(origin) // 기원 메모리 위치 반환
// 반환된 포인터로 컴파일러가 그림자 값 확인
// 그림자 ≠ 0이면 __msan_warning() 호출
__msan_warning()
→ kmsan_report()
→ kmsan_print_origin() // 기원 체인 출력
→ pr_err() // 오류 메시지 출력
복사 시 메타데이터 전파 흐름
__msan_memcpy() / __msan_memmove()
→ __memcpy() / __memmove() // 실제 복사 수행
→ kmsan_internal_memmove_metadata()
→ kmsan_get_metadata(dst/shadow/src/shadow) // 소스/대상 그림자 확인
→ kmsan_internal_chain_origin() // 기원 체인 생성
→ __memcpy(shadow_dst, shadow_src) // 그림자 복사
→ origin_dst[oiter_dst] = new_origin // 기원 업데이트
조건별 비교
KMSAN vs KASAN 비교
| 항목 | KMSAN | KASAN (Generic) | KASAN (SW/HW) |
|---|---|---|---|
| **타겟** | 초기화되지 않은 메모리 | UAF, 버퍼 오버플로우 | UAF, 버퍼 오버플로우 |
| **그림자 메모리** | 필요 (바이트별) | 필요 (바이트별/8바이트별) | 하드웨어 지원 |
| **컴파일러** | Clang (-fsanitize=kernel-memory) | GCC/Clang (-fsanitize=kernel-address) | GCC/Clang + 하드웨어 |
| **메모리 오버헤드** | 높음 (그림자+기원) | 중간 (그림자만) | 낮음 (하드웨어) |
| **설정** | CONFIG_KMSAN | CONFIG_KASAN | CONFIG_KASAN_GENERIC + ARCH_HW_TAG |
| **동시 사용** | 불가 | 불가 | 불가 |
오염 플래그 비교
| 플래그 | 값 | 용도 | 메모리 상태 |
|---|---|---|---|
| `KMSAN_POISON_NOCHECK` | 0x0 | 일반 오염 | 체크 없이 오염 |
| `KMSAN_POISON_CHECK` | 0x1 | 할당 시 오염 | 할당 시 실제 오염 수행 |
| `KMSAN_POISON_FREE` | 0x2 | 해제 시 오염 | UAF 감지용 오염 |
버그 타입 비교
| `enum kmsan_bug_reason` | `is_uaf=false` | `is_uaf=true` |
|---|---|---|
| `REASON_ANY` | uninit-value | use-after-free |
| `REASON_COPY_TO_USER` | kernel-infoleak | kernel-infoleak-after-free |
| `REASON_SUBMIT_URB` | kernel-usb-infoleak | kernel-usb-infoleak-after-free |
그림자 값 상태 비교
| 그림자 바이트 | 상태 | 의미 |
|---|---|---|
| 0x00 | 초기화됨 | 메모리가 정상적으로 초기화됨 |
| 0xFF (-1) | 오염됨 | 초기화되지 않음 (poison 상태) |
| 기타 값 | 부분 초기화 | 특정 바이트만 초기화됨 |
관련 문서
참고 사항
메모리 할당/해제 시 KMSAN 동작
할당 (__GFP_ZERO 없음):
→ kmsan_internal_poison_memory() → 그림자 = 0xFF, 기원 = 스택 핸들
할당 (__GFP_ZERO 있음):
→ kmsan_internal_unpoison_memory() → 그림자 = 0x00, 기원 = 0
해제:
→ kmsan_internal_poison_memory(KMSAN_POISON_FREE) → 그림자 = 0xFF, 기원 = UAF 스택 핸들
해제 후 재할당 (__GFP_ZERO 없음):
→ 기존 오염 상태 유지 → 사용 시 kmsan_report() 호출
초기화 흐름
kmsan_init_shadow() → 커널 텍스트/데이터 영역 그림자 초기화
kmsan_init_runtime() → kmsan_enabled = true 설정
kmsan_memblock_free_pages() → memblock→page 전환 시 메타데이터 할당
kmsan_memblock_discard() → memblock 남은 블록 정리
주의사항
1. KMSAN은 Clang 전용 (-fsanitize=kernel-memory)
2. KASAN과 동시 사용 불가
3. 오버헤드가 크므로 프로덕션 환경 사용 금지
4. 스택 디포트 한도 초과 시 기원 체인 잘림 가능
5. kmsan_in_runtime()으로 재귀 호출 방지