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);
}
  • 역할: 메모리를 오염(poison) 상태로 표시
  • 분기 로직:
  • - 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);
    }
  • 역할: 메모리를 초기화됨(unpoisoned) 상태로 표시
  • 분기 로직:
  • - 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 비교

    항목KMSANKASAN (Generic)KASAN (SW/HW)
    **타겟**초기화되지 않은 메모리UAF, 버퍼 오버플로우UAF, 버퍼 오버플로우
    **그림자 메모리**필요 (바이트별)필요 (바이트별/8바이트별)하드웨어 지원
    **컴파일러**Clang (-fsanitize=kernel-memory)GCC/Clang (-fsanitize=kernel-address)GCC/Clang + 하드웨어
    **메모리 오버헤드**높음 (그림자+기원)중간 (그림자만)낮음 (하드웨어)
    **설정**CONFIG_KMSANCONFIG_KASANCONFIG_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-valueuse-after-free
    `REASON_COPY_TO_USER`kernel-infoleakkernel-infoleak-after-free
    `REASON_SUBMIT_URB`kernel-usb-infoleakkernel-usb-infoleak-after-free

    그림자 값 상태 비교

    그림자 바이트상태의미
    0x00초기화됨메모리가 정상적으로 초기화됨
    0xFF (-1)오염됨초기화되지 않음 (poison 상태)
    기타 값부분 초기화특정 바이트만 초기화됨

    관련 문서

  • 메모리 관리 개요 — 전체 메모리 관리 조감도
  • SLUB 할당자 — KMSAN slab 훅과의 연동
  • vmalloc — vmalloc 메모리의 KMSAN 처리
  • 페이지 중독 — 유사한 소프트웨어 디버깅 메커니즘
  • 메모리 누수 추적 실습 — kmemleak/page_owner와의 비교
  • 디버그 도구 — KASAN/KFENCE/KLEAK와의 관계

  • 참고 사항

    메모리 할당/해제 시 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()으로 재귀 호출 방지