KMSAN (Kernel Memory Sanitizer)

KMSAN은 초기화되지 않은 메모리(Uninitialized Memory) 사용을 탐지하는 커널 Sanitizer입니다. 각 바이트에 Shadow와 Origin 메타데이터를 유지해 미초기화 값이 분기·출력·사용자 공간으로 전파되는 순간을 포착합니다. 오버헤드(Overhead)는 크지만 페이지(Page) 단위 할당부터 스택 변수까지 포괄적으로 디버깅(Debugging)할 수 있습니다.

전제 조건: KASAN의 Shadow Memory 개념과 SLUB 할당자의 kmalloc 흐름을 먼저 이해하면 도움이 됩니다. KMSAN은 KASAN과 같은 Shadow 구조를 쓰지만 "초기화 여부"라는 다른 속성을 추적합니다.
일상 비유: KMSAN은 잉크 미포함 프린터 카트리지와 같습니다. 새로 뜯은 카트리지(미초기화 메모리)는 아직 잉크(값)가 채워지지 않았습니다. 인쇄(사용)하기 전까지는 상관없지만, 미리 인쇄해 버리면 무슨 색이 나올지 알 수 없고(비결정성), 그걸 외부 문서(사용자 공간)에 붙여 보내면 안에 들어있던 이전 기록이 흘러나옵니다(정보 누출).

핵심 요약

  • 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에서만 사용

단계별 이해

  1. 할당
    kmalloc이 GFP_ZERO 없이 호출되면 반환 메모리의 Shadow를 0xFF로 포이즌하고, Origin에 kmalloc 호출 스택을 기록합니다.
  2. 쓰기
    프로그램이 초기화 쓰기를 수행하면 컴파일러 계측이 해당 바이트의 Shadow를 0으로 클리어합니다.
  3. 읽기/사용
    읽기·분기·산술·copy_to_user 시 Shadow가 0인지 확인합니다.
  4. 전파
    연산 결과는 피연산자 Shadow의 OR가 되고, Origin은 "먼저 미초기화였던 쪽"이 전파됩니다.
  5. 리포트
    Shadow가 0이 아닌 값이 사용자 공간·커널 로직에 영향을 미치는 순간 "uninit-value" 리포트가 출력됩니다.

개요

KMSAN은 초기화되지 않은 메모리 사용(uninitialized memory use)을 탐지합니다. 커널에서 kmalloc으로 할당한 메모리를 초기화하지 않고 사용하거나, 초기화되지 않은 스택 변수의 값에 기반한 분기/출력이 발생하면 정보 누출(information leak)이나 비결정적 동작(non-deterministic behavior)의 원인이 됩니다.

보안 취약점: 커널이 초기화되지 않은 데이터를 copy_to_user()로 사용자 공간에 복사하면 커널 스택의 민감 정보가 유출됩니다. 실제 CVE 다수가 이 패턴에 해당하며, KMSAN은 컴파일러 계측으로 이러한 누출을 런타임에 잡아냅니다.
KASAN과의 차이:
  • 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 핸들 (초기화되지 않은 데이터의 출처) 미초기화 데이터가 어디서 왔는지 추적
KMSAN Shadow·Origin 추적 흐름 ① kmalloc(size) data = [ ? ? ? ? ? ? ? ? ] shadow= [FF FF FF FF FF FF FF FF] origin= [stackdepot handle] (GFP_ZERO면 shadow=0) ② data[0..3] = 값 쓰기 data = [01 02 03 04 ? ? ? ?] shadow= [00 00 00 00 FF FF FF FF] 컴파일러 계측이 해당 바이트 Shadow를 0으로 unpoison ③ copy_to_user(uarg, data, 8) kmsan_check_memory(data, 8) → shadow[4..7] != 0 발견 BUG: KMSAN: uninit-value Origin에서 kmalloc 위치 복원 메모리 레이아웃 (1:1 Shadow + 4:1 Origin) Data 01 02 03 04 ?? ?? ?? ?? Shadow 00 00 00 00 FF FF FF FF Origin (4바이트 커버, clear) kmalloc+0x120 @ driver.c:42 전파 규칙 (Propagation Rules) 연산: c = a & bshadow_c = shadow_a | shadow_b (한 쪽이라도 미초기화면 결과도 미초기화) 분기: if (x)shadow_x != 0면 "uninit branch" 즉시 리포트 경계: copy_to_user(), memcpy(user), write(fd) 같은 외부 출력 직전 체크

컴파일러 계측 (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_tls TLS 슬롯을 경유합니다.

함수 경계와 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)도 포함합니다. 런타임 코드가 다시 계측된 코드를 호출하는 역순환을 방지합니다.

초기화 지연 리포트: KMSAN은 미초기화 값이 "사용"될 때가 아니라 "외부로 누출 직전(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 지원 차이

SanitizerGCCClang비고
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/Kconfigselect HAVE_ARCH_KMSAN 포함. Clang 빌드 필수
arm64 / RISC-V🗒 논의 단계RFC/제안 수준. 2026-04 기준 메인라인 병합 계획 없음
현재 상태 확인 방법: 사용 중인 커널이 KMSAN을 지원하는지는 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계층으로 구성됩니다.

  1. KMSAN 활성 커널 이미지LLVM=1 + CONFIG_KMSAN=y + CONFIG_KCOV=y로 빌드된 x86_64 이미지
  2. syz-manager — 4~8개의 QEMU VM을 돌리며 시스템 콜 시퀀스를 랜덤 생성·실행
  3. 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) 가능한 모든 리포트를 모으는 것이 일반적입니다.

흔한 실수와 디버깅 팁

커널 설정과 제약

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로 제외합니다.

참고자료

다음 학습:
  • KASAN — 메모리 주소 오류 탐지 (Shadow 구조 비교)
  • KFENCE — 프로덕션 샘플링 탐지
  • UBSAN — 정의되지 않은 동작 탐지
  • KCSAN — 데이터 레이스 탐지
  • 시스템 콜(System Call)copy_to_user와 사용자 공간 경계