KASAN (Kernel Address Sanitizer) -- 커널 메모리 안전성 검사 도구

KASAN은 리눅스 커널에서 메모리 접근 오류를 런타임에 탐지하는 동적 분석 도구입니다. Shadow Memory를 활용하여 out-of-bounds, use-after-free, double-free 등의 메모리 버그를 즉시 발견하고 상세한 스택 트레이스를 제공합니다. Generic(소프트웨어 기반), SW-Tag(ARM MTE 소프트웨어 태깅), HW-Tag(하드웨어 MTE) 세 가지 모드의 내부 동작 원리부터 Shadow Memory 레이아웃, Quarantine 메커니즘, Slab 할당자 통합, 리포트 분석 방법, 성능 전략까지 전 영역을 심층적으로 다룹니다.

전제 조건: 메모리 관리(Memory Management) 개요Slab Allocator 문서를 먼저 읽으세요. KASAN은 커널 메모리 할당/해제 경로에 삽입되는 도구이므로, 페이지 할당자(Page Allocator)와 Slab 구조에 대한 기본 이해가 필요합니다.
일상 비유: KASAN은 건물 출입 감시 시스템과 비슷합니다. 건물의 각 방(메모리 영역)에 출입 권한 태그를 부착하고, 누군가 허가 없이 방에 들어가거나(out-of-bounds), 이미 철거된 방에 접근하면(use-after-free) 즉시 경보를 울립니다. Shadow Memory는 이 출입 권한 정보가 기록된 대장과 같습니다.

핵심 요약

  • KASAN -- 커널 주소 공간(Address Space)의 메모리 접근 오류를 런타임에 탐지하는 동적 분석 도구입니다.
  • Shadow Memory -- 실제 메모리 8바이트당 1바이트의 메타데이터를 유지하여 접근 가능 여부를 추적합니다.
  • Generic KASAN -- 컴파일러가 모든 메모리 접근에 체크 코드를 삽입하는 소프트웨어 방식입니다.
  • Tag-Based KASAN -- 포인터와 메모리에 태그를 부여하여 불일치를 탐지합니다(SW-Tag/HW-Tag).
  • Quarantine -- 해제된 객체를 일정 기간 격리(Isolation)하여 use-after-free 탐지 확률을 높입니다.

단계별 이해

  1. 메모리 안전성 문제 인식
    커널 메모리 버그(OOB, UAF, double-free)가 왜 치명적인지 이해합니다.
  2. Shadow Memory 개념 파악
    8:1 매핑(Mapping)으로 메모리 상태를 추적하는 원리를 학습합니다.
  3. 컴파일러 삽입 체크 이해
    GCC/Clang이 모든 메모리 접근 전에 삽입하는 체크 코드의 동작을 파악합니다.
  4. KASAN 리포트 읽기
    버그 발생 시 출력되는 리포트의 각 필드를 해석하는 방법을 익힙니다.
  5. 커널 빌드 및 실전 적용
    CONFIG_KASAN 설정과 성능 영향을 고려한 운영 전략을 수립합니다.

KASAN 개요

메모리 안전성이란?

C 언어로 작성된 리눅스 커널은 메모리 안전성(memory safety)을 언어 수준에서 보장하지 않습니다. 포인터 산술, 수동 메모리 관리, 타입 캐스팅 등으로 인해 다양한 메모리 접근 오류가 발생할 수 있으며, 이러한 버그는 데이터 손상, 권한 상승, 시스템 크래시의 원인이 됩니다.

KASAN의 탄생 배경

KASAN은 Google의 AddressSanitizer(ASan) 기술을 리눅스 커널에 적용한 것으로, Linux 4.0(2015년)에 Generic KASAN이 처음 도입되었습니다. 이후 ARM64 MTE(Memory Tagging Extension) 지원이 추가되면서 SW-Tag KASAN(Linux 5.11)과 HW-Tag KASAN(Linux 5.11)이 등장했습니다.

KASAN이 탐지하는 버그 유형

버그 유형설명위험도KASAN 탐지
Out-of-Bounds (OOB) 할당된 메모리 범위를 초과하여 읽기/쓰기 높음 Generic, SW-Tag, HW-Tag 모두 탐지
Use-After-Free (UAF) 해제된 메모리에 다시 접근 매우 높음 Generic, SW-Tag, HW-Tag 모두 탐지
Double-Free 이미 해제된 메모리를 다시 해제 높음 Generic, SW-Tag, HW-Tag 모두 탐지
Stack Out-of-Bounds 스택 변수의 범위를 초과하여 접근 높음 Generic만 탐지 (stack instrumentation)
Global Out-of-Bounds 전역 변수의 범위를 초과하여 접근 중간 Generic만 탐지 (global redzone)
Invalid-Free 할당되지 않은 주소를 해제 시도 높음 Generic, SW-Tag, HW-Tag 모두 탐지

KASAN 모드 비교 요약

항목Generic KASANSW-Tag KASANHW-Tag KASAN
구현 방식 컴파일러 삽입 (소프트웨어) 소프트웨어 태깅 하드웨어 MTE
아키텍처 x86_64, ARM64 등 ARM64 전용 ARM64 MTE 전용
메모리 오버헤드(Overhead) 1/8 (12.5%) 1/16 (6.25%) 1/16 (6.25%)
성능 오버헤드 약 1.5x ~ 3x 약 0.9x ~ 1.2x 약 5% 이하
탐지 정밀도 결정적 (바이트 수준) 확률적 (1/256) 확률적 (1/16)
스택/전역 변수 지원 미지원 미지원
프로덕션 사용 개발/테스트만 제한적 가능 프로덕션 가능

KASAN 전체 아키텍처

KASAN 전체 아키텍처 사용자 공간 (User Space) -- 시스템 콜로 커널 진입 커널 코드 kmalloc / kfree / 포인터 접근 컴파일러 삽입 체크 __asan_loadN / __asan_storeN Shadow Memory 8바이트 : 1바이트 매핑 Slab 할당자 (SLUB) Redzone / Poison 삽입 Quarantine 해제된 객체 격리 (UAF 탐지) 버그 리포트 스택 트레이스 + Shadow 덤프 불일치! Generic KASAN x86_64, ARM64 바이트 수준 정밀 탐지 스택/전역 변수 지원 오버헤드: 1.5x~3x SW-Tag KASAN ARM64 (TBI 활용) 확률적 탐지 (1/256) Slab 객체만 오버헤드: 0.9x~1.2x HW-Tag KASAN ARM64 MTE 하드웨어 확률적 탐지 (1/16) Slab 객체만 오버헤드: ~5% 그림 1. KASAN 전체 아키텍처와 세 가지 모드

KASAN 동작 원리 -- Shadow Memory 개념

Shadow Memory의 핵심 아이디어

KASAN의 핵심은 Shadow Memory입니다. 커널 주소 공간의 모든 메모리에 대해 "이 바이트에 접근해도 되는가?"라는 메타데이터를 별도의 메모리 영역에 기록합니다. 실제 메모리 8바이트마다 Shadow Memory 1바이트가 대응하므로, 매핑 비율은 8:1입니다.

Shadow 값의 의미

Shadow 바이트 값의미설명
0x00 전체 접근 가능 대응하는 8바이트 모두 유효 (정상 할당 영역)
0x01 ~ 0x07 부분 접근 가능 앞쪽 N바이트만 유효 (Slab 객체 끝부분)
0xF1 Slab Redzone (왼쪽) 할당 객체의 시작 전 패딩(Padding) 영역
0xF2 Slab Redzone (오른쪽) 할당 객체의 끝 뒤 패딩 영역
0xF3 Slab Redzone (구간) kmalloc 사이즈 정렬에 의한 내부 패딩
0xF5 해제됨 (Freed) kfree() 호출 후 아직 Quarantine에 있는 객체
0xF8 Stack 왼쪽 Redzone 스택 변수 시작 전 영역 (Generic만)
0xF9 Stack 중간 Redzone 스택 변수 사이 영역 (Generic만)
0xFA Stack 오른쪽 Redzone 스택 변수 끝 뒤 영역 (Generic만)
0xFB Stack-after-return 함수 반환 후 스택 프레임(Stack Frame) 영역
0xFC Stack-use-after-scope 스코프를 벗어난 지역 변수 영역
0xFE Global Redzone 전역 변수 인접 패딩 영역 (Generic만)

Shadow Memory 주소 계산

/* mm/kasan/kasan.h - Shadow Memory 주소 변환 매크로 */

/* Generic KASAN (x86_64 기준) */
#define KASAN_SHADOW_SCALE_SHIFT  3   /* 2^3 = 8 바이트당 1바이트 */
#define KASAN_SHADOW_OFFSET       0xdffffc0000000000ULL

/* 주소 → Shadow 주소 변환 */
static inline void *kasan_mem_to_shadow(const void *addr)
{
    return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
           + KASAN_SHADOW_OFFSET;
}

/* Shadow 주소 → 원본 주소 변환 */
static inline const void *kasan_shadow_to_mem(const void *shadow_addr)
{
    return (void *)((unsigned long)shadow_addr - KASAN_SHADOW_OFFSET)
           << KASAN_SHADOW_SCALE_SHIFT;
}
코드 설명
  • 4행 KASAN_SHADOW_SCALE_SHIFT가 3이므로 8바이트당 Shadow 1바이트. 즉, Shadow 주소 = 원본 주소 / 8 + 오프셋(Offset)입니다.
  • 5행 KASAN_SHADOW_OFFSET은 아키텍처별로 다르며, Shadow Memory가 배치되는 가상 주소(Virtual Address) 시작점입니다.
  • 8-12행 원본 주소를 3비트 오른쪽 시프트(= 8로 나누기)하고 오프셋을 더해 Shadow 주소를 얻습니다.
  • 15-19행 역변환: Shadow 주소에서 오프셋을 빼고 3비트 왼쪽 시프트(= 8 곱하기)하면 원본 주소입니다.
Shadow Memory 8:1 매핑 원리 실제 메모리 (8바이트 단위) 8B (유효) 8B (유효) 8B (유효) 8B (앞 5B 유효) Redzone Redzone 해제됨 (Freed) Shadow = addr >> 3 + OFFSET Shadow Memory (1바이트 단위) 00 00 00 05 F2 F2 F5 00 = 전체 접근 가능 01~07 = 부분 접근 F1~F3 = Redzone F5 = 해제됨 그림 2. 실제 메모리 8바이트와 Shadow Memory 1바이트 매핑

Generic KASAN (소프트웨어 기반)

컴파일러 계측(Instrumentation) 원리

Generic KASAN은 GCC 또는 Clang의 -fsanitize=kernel-address 옵션을 사용합니다. 컴파일러는 커널 코드의 모든 메모리 로드/스토어 명령어 앞에 체크 함수 호출을 삽입합니다. 이 함수는 해당 주소의 Shadow 바이트를 검사하여 접근 가능 여부를 판단합니다.

컴파일러가 변환하는 코드 예시

/* 원본 커널 코드 */
int example_read(int *ptr)
{
    return *ptr;  /* 4바이트 읽기 */
}

/* Generic KASAN 활성화 시 컴파일러가 변환한 코드 (개념적) */
int example_read(int *ptr)
{
    __asan_load4((unsigned long)ptr);  /* 삽입된 체크 */
    return *ptr;
}
코드 설명
  • 4행 원래는 단순한 4바이트 읽기 명령입니다.
  • 10행 컴파일러가 삽입한 __asan_load4는 ptr의 Shadow 바이트를 검사합니다. 접근 불가 상태이면 KASAN 리포트를 출력합니다.

__asan_load/store 체크 로직

/* mm/kasan/generic.c - 간략화된 체크 로직 */
static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
    s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr);

    if (unlikely(shadow_value)) {
        s8 last_accessible_byte = addr & KASAN_GRANULE_SIZE_MASK;
        return unlikely(last_accessible_byte >= shadow_value);
    }
    return false;  /* shadow == 0: 전체 접근 가능 */
}

static __always_inline bool memory_is_poisoned_n(
    unsigned long addr, size_t size)
{
    s8 *shadow_addr = (s8 *)kasan_mem_to_shadow((void *)addr);
    s8 *shadow_last = (s8 *)kasan_mem_to_shadow(
        (void *)addr + size - 1);

    for (s8 *s = shadow_addr; s <= shadow_last; s++) {
        if (unlikely(*s))
            return true;  /* 비정상 접근 */
    }
    return false;
}
코드 설명
  • 4행 주소를 Shadow 주소로 변환하고 Shadow 값을 읽습니다.
  • 6-9행 Shadow 값이 0이 아니면 부분 접근 가능 또는 접근 불가. 접근하려는 바이트 오프셋이 Shadow 값 이상이면 금지 영역입니다.
  • 20-23행 N바이트 접근 시 해당 범위의 모든 Shadow 바이트를 순회하며 0이 아닌 값이 있으면 에러입니다.
Generic KASAN 메모리 접근 체크 흐름 *ptr = value (메모리 쓰기) __asan_store4(addr) 호출 shadow = (addr >> 3) + OFFSET *shadow == 0 ? Yes 정상 접근 허용 No (addr & 7) + size > *shadow ? Yes KASAN 리포트! No 부분 접근 OK 그림 3. Generic KASAN의 메모리 접근 체크 판정 흐름

SW-Tag KASAN (소프트웨어 태깅)

TBI(Top Byte Ignore) 활용

ARM64는 TBI 기능을 제공합니다. 가상 주소의 상위 8비트(bit 56~63)를 무시하여 주소 접근에는 영향을 주지 않지만, 소프트웨어가 이 비트에 태그 정보를 저장할 수 있습니다. SW-Tag KASAN은 이 TBI를 활용하여 포인터에 태그를 부여합니다.

태그 할당과 검증 과정

/* mm/kasan/sw_tags.c - 태그 할당 */
static u8 kasan_random_tag(void)
{
    /* PRNG 기반 랜덤 태그 생성 (0x00 제외) */
    u8 tag = get_random_u8() % (KASAN_TAG_MAX + 1);
    if (tag == KASAN_TAG_KERNEL)
        tag++;  /* 0xFF는 커널 기본 태그이므로 회피 */
    return tag;
}

/* 포인터에 태그 설정 */
static inline void *kasan_set_tag(const void *addr, u8 tag)
{
    /* 상위 바이트에 태그 삽입 */
    return (void *)((u64)addr | ((u64)tag << 56));
}

/* Shadow에 태그 기록: 메모리 영역의 모든 그래뉼에 동일 태그 */
void kasan_poison_memory(const void *addr, size_t size, u8 tag)
{
    s8 *shadow = (s8 *)kasan_mem_to_shadow(addr);
    size_t shadow_len = size >> KASAN_SHADOW_SCALE_SHIFT;
    memset(shadow, tag, shadow_len);
}
코드 설명
  • 5행 256개 중 하나의 랜덤 태그를 생성합니다. 같은 태그가 우연히 일치할 확률은 약 1/256입니다.
  • 15행 ARM64 주소의 상위 바이트(bit 56~63)에 태그를 삽입합니다. TBI 덕분에 MMU는 이 비트를 무시합니다.
  • 20-23행 메모리 할당 시 Shadow에 태그 값을 기록합니다. 접근 시 포인터 태그와 Shadow 태그를 비교합니다.
SW-Tag KASAN 태그 매칭 원리 포인터 (64비트): Tag=0xAB bit 56~63 가상 주소: 0xFFFF800012345000 bit 0~55 (MMU가 사용하는 실제 주소) 할당된 메모리 영역 (16바이트 그래뉼) Shadow 0xAB Tag 비교 Tag 일치 (0xAB) 접근 허용 Tag 불일치! KASAN 리포트 그림 4. SW-Tag KASAN: 포인터 태그와 Shadow 태그 비교

HW-Tag KASAN (하드웨어 MTE 활용)

ARM MTE(Memory Tagging Extension) 개요

ARM MTE는 ARMv8.5-A에서 도입된 하드웨어 메모리 태깅 확장입니다. 물리 메모리(Physical Memory)의 16바이트(그래뉼) 단위마다 4비트 태그가 하드웨어적으로 부착되며, 포인터의 상위 4비트에 저장된 태그와 하드웨어가 자동으로 비교합니다. 불일치 시 CPU가 동기/비동기 예외를 발생시킵니다.

HW-Tag KASAN과 MTE 명령어

/* arch/arm64/include/asm/mte.h - MTE 태그 관련 명령 */

/* IRG: Insert Random Tag - 랜덤 태그를 포인터에 삽입 */
static inline void *mte_set_random_tag(void *addr)
{
    asm volatile("irg %0, %0" : "+r"(addr));
    return addr;
}

/* STG: Store Allocation Tag - 메모리 그래뉼에 태그 기록 */
static inline void mte_set_mem_tag(void *addr, size_t size)
{
    for (size_t i = 0; i < size; i += MTE_GRANULE_SIZE) {
        asm volatile("stg %0, [%0]"
                     : : "r"(addr + i) : "memory");
    }
}

/* LDG: Load Allocation Tag - 메모리에서 태그 읽기 */
static inline u8 mte_get_mem_tag(void *addr)
{
    asm volatile("ldg %0, [%0]" : "+r"(addr));
    return (u8)((u64)addr >> 56);
}
코드 설명
  • 6행 IRG 명령은 CPU가 랜덤 4비트 태그를 생성하여 포인터의 상위 비트에 삽입합니다.
  • 13-15행 STG 명령은 포인터 태그를 해당 메모리 그래뉼(16바이트)에 기록합니다. 이후 이 영역에 접근할 때 CPU가 자동으로 태그를 비교합니다.
  • 22행 LDG 명령은 메모리에 저장된 태그를 포인터에 로드합니다. 디버깅(Debugging) 시 현재 태그 값을 확인하는 데 사용합니다.
HW-Tag KASAN: ARM MTE 하드웨어 체크 CPU (ARM MTE 지원) Ptr Tag = 0x5 Tag 비교 유닛 LOAD/STORE 명령 실행 물리 메모리 데이터 Mem Tag=0x5 16바이트 그래뉼당 4비트 태그 (HW 저장) Tag 일치 (0x5 == 0x5) 정상 실행 계속 Tag 불일치! 동기 예외 발생 do_tag_check_fault() KASAN 리포트 출력 그림 5. HW-Tag KASAN: ARM MTE 하드웨어가 자동으로 태그를 비교하는 흐름 HW-Tag는 컴파일러 삽입 체크가 필요 없어 성능 오버헤드가 매우 낮음 (~5%)

Shadow Memory 레이아웃

x86_64 가상 주소 공간에서의 Shadow 배치

x86_64 Linux 커널은 5-level 페이징(57비트) 또는 4-level 페이징(48비트)을 사용합니다. KASAN Shadow Memory는 커널 가상 주소 공간의 특정 영역에 매핑됩니다. 전체 커널 주소 공간의 1/8에 해당하는 거대한 Shadow 영역이 필요합니다.

/* arch/x86/include/asm/kasan.h - x86_64 Shadow 레이아웃 (4-level) */

/* 커널 가상 주소 공간: 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF (128TB) */
#define KASAN_SHADOW_OFFSET   0xdffffc0000000000ULL
#define KASAN_SHADOW_START    0xffffec0000000000ULL
#define KASAN_SHADOW_END      0xfffffbffffffffffULL

/* Shadow 크기 = 커널 주소 공간 / 8 = 128TB / 8 = 16TB */
/* Shadow는 early boot 시 페이지 테이블로 매핑됨 */

/* ARM64 (48-bit VA, 4KB 페이지 기준) */
#define KASAN_SHADOW_OFFSET   ((KASAN_SHADOW_END + 1) - (1ULL << (64 - 3)))
#define KASAN_SHADOW_SCALE_SHIFT  3
x86_64 커널 주소 공간과 Shadow Memory 레이아웃 커널 가상 주소 공간 0xFFFF800000000000 Direct Mapping (64TB) vmalloc 영역 (vmalloc, vmap, ioremap) KASAN Shadow (16TB) 0xFFFFEC00~0xFFFFFBFF 모듈 영역 (Kernel Modules) Kernel Text/Data 0xFFFFFFFF80000000 Shadow Memory 상세 Direct Map Shadow vmalloc Shadow Module Shadow Kernel Text Shadow / 8 / 8 Slab 객체 Shadow 확대 Left RZ Object (유효) Pad Right RZ F1 00 00 ... 05 F3 F2 F1 = Slab Left Redzone F2 = Slab Right Redzone F3 = Internal Padding 00/05 = 유효 (05=앞5바이트) 그림 6. x86_64 Shadow Memory 레이아웃과 Slab 객체 Shadow 확대

메모리 접근 체크 흐름

__asan_loadN / __asan_storeN 전체 경로

컴파일러가 삽입하는 체크 함수는 접근 크기에 따라 __asan_load1~__asan_load16, __asan_store1~__asan_store16으로 나뉩니다. 이 함수들은 인라인 최적화되어 성능 영향을 최소화합니다.

/* mm/kasan/generic.c - 체크 함수 구현 */

#define DEFINE_ASAN_LOAD_STORE(size) \
void __asan_load##size(unsigned long addr)    \
{                                                \
    if (!kasan_arch_is_ready())                 \
        return;                                  \
    if (unlikely(memory_is_poisoned(addr, size)))\
        kasan_report(addr, size, false, _RET_IP_);\
}                                                \
void __asan_store##size(unsigned long addr)   \
{                                                \
    if (!kasan_arch_is_ready())                 \
        return;                                  \
    if (unlikely(memory_is_poisoned(addr, size)))\
        kasan_report(addr, size, true, _RET_IP_); \
}

DEFINE_ASAN_LOAD_STORE(1)
DEFINE_ASAN_LOAD_STORE(2)
DEFINE_ASAN_LOAD_STORE(4)
DEFINE_ASAN_LOAD_STORE(8)
DEFINE_ASAN_LOAD_STORE(16)
코드 설명
  • 3-16행 매크로(Macro)로 각 크기별 load/store 체크 함수를 생성합니다. memory_is_poisoned()가 true이면 kasan_report()를 호출합니다.
  • 6행 kasan_arch_is_ready()는 부팅 초기 KASAN이 아직 초기화되지 않은 시점에서 체크를 건너뜁니다.
  • 8행 unlikely()로 분기 예측(Branch Prediction) 힌트를 제공하여 정상 경로(shadow==0)의 성능 저하를 최소화합니다.
  • 18-22행 1, 2, 4, 8, 16바이트 접근에 대해 각각 체크 함수가 생성됩니다.
kmalloc -> 접근 -> kfree 전체 Shadow 변화 시간 kmalloc(29) Shadow 상태 F1 | 00 00 00 05 | F2 Redzone 설정, 29B 유효 (4*8=32B 슬랩, 앞 29B 유효) ptr[0] = 'A' 체크 결과 shadow[0] = 0x00 접근 허용 kfree(ptr) Shadow 상태 F5 F5 F5 F5 F5 F5 모든 바이트 독 처리 (Quarantine 진입) ptr[0] (UAF!) 탐지! shadow = F5 BUG 리포트 kmalloc(29) 객체의 Shadow Memory 전체 구조 F1 Left RZ 00 00 00 05 29바이트 = 3*8 + 5 F3 패딩 F2 Right RZ Track 할당 트레이스 그림 7. kmalloc(29) 생명주기에 따른 Shadow Memory 상태 변화

버그 탐지 유형

Out-of-Bounds (OOB) 접근

가장 흔한 메모리 오류입니다. 배열 끝을 넘어서 읽거나 쓰는 경우가 해당되며, KASAN은 Redzone의 Shadow 값(F1, F2, F3)으로 이를 탐지합니다.

/* Out-of-Bounds 접근 예시 */
void trigger_oob(void)
{
    char *buf = kmalloc(16, GFP_KERNEL);
    if (!buf)
        return;

    /* 정상: 인덱스 0~15 접근 */
    buf[15] = 'A';

    /* BUG: 인덱스 16은 Redzone! */
    buf[16] = 'B';  /* KASAN: out-of-bounds write */

    kfree(buf);
}

Use-After-Free (UAF)

/* Use-After-Free 접근 예시 */
void trigger_uaf(void)
{
    char *buf = kmalloc(32, GFP_KERNEL);
    if (!buf)
        return;

    kfree(buf);  /* 메모리 해제 */

    /* BUG: 해제된 메모리에 접근! */
    buf[0] = 'X';  /* KASAN: use-after-free write */
}

Double-Free

/* Double-Free 예시 */
void trigger_double_free(void)
{
    char *buf = kmalloc(64, GFP_KERNEL);
    if (!buf)
        return;

    kfree(buf);  /* 첫 번째 해제 (정상) */
    kfree(buf);  /* 두 번째 해제: KASAN: double-free */
}
버그 유형별 Shadow Memory 탐지 패턴 Out-of-Bounds F1 00 00 F2 접근! Redzone(F2) 접근 시 탐지 Use-After-Free F5 F5 F5 F5 접근! 해제됨(F5) 접근 시 탐지 Double-Free F5 F5 F5 F5 이미 F5인 객체에 kfree 재호출 시 탐지 탐지 메커니즘 비교 Generic KASAN Shadow 값으로 구분 F1/F2 = OOB F5 = UAF/Double-Free F8~FC = Stack 버그 결정적: 100% 탐지 Tag-Based KASAN 태그 불일치로 탐지 할당마다 랜덤 태그 해제 시 태그 변경 재할당 시 새 태그 확률적: SW 1/256, HW 1/16 공통 리포트 정보 접근 주소, 크기, 읽기/쓰기, 스택 트레이스, 할당/해제 트레이스, Shadow 메모리 덤프 그림 8. 버그 유형별 Shadow Memory 탐지 패턴과 메커니즘 비교

Quarantine 메커니즘

왜 Quarantine이 필요한가?

kfree() 후 해당 메모리가 즉시 재할당되면, 새로운 객체의 유효한 데이터로 채워져 use-after-free 접근이 정상처럼 보일 수 있습니다. KASAN의 Quarantine은 해제된 객체를 별도 큐에 일정 기간 격리하여, 이 시간 동안 UAF 접근을 확실히 탐지합니다.

/* mm/kasan/quarantine.c - Quarantine 구조 */
struct qlist_head {
    struct qlist_node *head;
    struct qlist_node *tail;
    size_t bytes;  /* 큐에 격리된 총 바이트 수 */
};

/* per-CPU Quarantine 큐 */
DEFINE_PER_CPU(struct qlist_head, cpu_quarantine);

/* 전역 Quarantine 큐 */
static struct qlist_head global_quarantine[QUARANTINE_BATCHES];

/* Quarantine에 객체 추가 */
void quarantine_put(struct kmem_cache *cache, void *object)
{
    struct qlist_head *q;

    /* Shadow를 F5(freed)로 설정 */
    kasan_poison_object_data(cache, object);

    /* per-CPU Quarantine에 추가 */
    q = this_cpu_ptr(&cpu_quarantine);
    qlist_put(q, object, cache_alloc_size(cache));

    /* 크기 초과 시 전역 큐로 이동 후 오래된 객체 해제 */
    if (unlikely(q->bytes > QUARANTINE_PERCPU_SIZE))
        quarantine_reduce();
}
코드 설명
  • 9행 per-CPU 큐를 사용하여 락 경합(Contention)을 최소화합니다.
  • 20행 kasan_poison_object_data()는 객체 전체를 F5로 poisoning합니다.
  • 23-24행 per-CPU 큐에 객체를 추가합니다. 이 큐에 있는 동안 재할당되지 않습니다.
  • 27-28행 per-CPU 큐가 한계를 초과하면 전역 큐로 이동하고, 가장 오래된 배치를 실제 해제합니다.
Quarantine 크기 조절: kasan.quarantine_size 부트 파라미터로 Quarantine 크기를 조절할 수 있습니다. 크기가 클수록 UAF 탐지 확률이 높아지지만 메모리 사용량이 증가합니다. 기본값은 전체 메모리의 1/32입니다.

Quarantine -- per-CPU 배칭과 메모리 회수(Memory Reclaim)

Quarantine의 2단계 큐 구조

KASAN Quarantine은 per-CPU 큐전역 큐(Global Quarantine)의 2단계로 구성됩니다. per-CPU 큐는 락 없이 빠르게 동작하며, 일정 크기를 초과하면 전역 큐로 배치 이동합니다. 전역 큐는 순환 배열(ring buffer) 구조로 QUARANTINE_BATCHES(기본 8) 개의 슬롯을 회전하며, 가장 오래된 배치의 객체를 실제 Slab으로 반환합니다.

/* mm/kasan/quarantine.c - Quarantine 2단계 큐 상세 */

#define QUARANTINE_BATCHES     8
#define QUARANTINE_PERCPU_SIZE (1 << 20)  /* 1MB per CPU */
#define QUARANTINE_FRACTION    32          /* 전체 메모리의 1/32 */

/* 전역 Quarantine: 순환 배열 */
static struct qlist_head global_quarantine[QUARANTINE_BATCHES];
static int quarantine_head;    /* 다음 삽입 위치 */
static int quarantine_tail;    /* 다음 해제 위치 */
static unsigned long quarantine_size;  /* 현재 총 크기 */

/* per-CPU 큐에서 전역 큐로 이동 */
static void quarantine_batch_move(void)
{
    struct qlist_head *cpu_q = this_cpu_ptr(&cpu_quarantine);
    struct qlist_head temp = {};

    /* per-CPU 큐 전체를 임시 리스트로 이동 (락 불필요) */
    qlist_move_all(cpu_q, &temp);

    /* 전역 큐에 삽입 (스핀락 보호) */
    spin_lock(&quarantine_lock);
    qlist_move_all(&temp, &global_quarantine[quarantine_head]);
    quarantine_head = (quarantine_head + 1) % QUARANTINE_BATCHES;
    spin_unlock(&quarantine_lock);
}

/* 오래된 배치 해제: 메모리 압력 해소 */
static void quarantine_reduce(void)
{
    unsigned long max_size = (totalram_pages() << PAGE_SHIFT)
                             / QUARANTINE_FRACTION;
    struct qlist_head to_free = {};

    spin_lock(&quarantine_lock);
    while (quarantine_size > max_size &&
           quarantine_tail != quarantine_head) {
        /* 가장 오래된 배치를 해제 리스트로 이동 */
        qlist_move_all(&global_quarantine[quarantine_tail], &to_free);
        quarantine_tail = (quarantine_tail + 1) % QUARANTINE_BATCHES;
    }
    spin_unlock(&quarantine_lock);

    /* 락 해제 후 실제 Slab으로 반환 */
    qlist_free_all(&to_free);
}
코드 설명
  • 3-5행 기본 설정값: per-CPU 큐는 1MB, 전역 Quarantine은 전체 RAM의 1/32까지 허용합니다.
  • 17-26행 per-CPU 큐를 전역 큐로 배치 이동합니다. per-CPU 접근은 락 없이, 전역 큐 삽입만 스핀락(Spinlock)으로 보호합니다.
  • 29-44행 전역 Quarantine 크기가 한계를 초과하면 가장 오래된 배치부터 해제합니다. 해제는 락 밖에서 수행하여 락 보유 시간을 최소화합니다.

Quarantine과 메모리 압력(Memory Pressure)

시스템 메모리가 부족해지면 Quarantine이 과도한 메모리를 점유하는 문제가 발생할 수 있습니다. 이를 위해 KASAN은 shrink_quarantine()을 통해 메모리 압력 상황에서 Quarantine을 축소하는 메커니즘을 제공합니다.

/* mm/kasan/quarantine.c - 메모리 압력 대응 */

/* OOM killer 이전에 Quarantine 축소 시도 */
static unsigned long kasan_quarantine_shrink_count(
    struct shrinker *shrinker,
    struct shrink_control *sc)
{
    return quarantine_size >> PAGE_SHIFT;
}

static unsigned long kasan_quarantine_shrink_scan(
    struct shrinker *shrinker,
    struct shrink_control *sc)
{
    unsigned long freed = 0;
    struct qlist_head to_free = {};

    spin_lock(&quarantine_lock);
    if (quarantine_tail != quarantine_head) {
        qlist_move_all(&global_quarantine[quarantine_tail],
                       &to_free);
        quarantine_tail = (quarantine_tail + 1) % QUARANTINE_BATCHES;
    }
    spin_unlock(&quarantine_lock);

    freed = to_free.bytes >> PAGE_SHIFT;
    qlist_free_all(&to_free);
    return freed;
}

static struct shrinker quarantine_shrinker = {
    .count_objects = kasan_quarantine_shrink_count,
    .scan_objects  = kasan_quarantine_shrink_scan,
    .seeks         = DEFAULT_SEEKS,
};

Tag-Based KASAN에서의 Quarantine 차이

항목Generic KASANSW-Tag / HW-Tag KASAN
Quarantine 사용 필수 (UAF 탐지의 핵심) 선택적 (태그 변경으로 UAF 탐지 가능)
UAF 탐지 원리 Shadow를 F5로 poisoning 해제 시 태그를 변경 → 구 포인터 태그 불일치
재할당 시 동작 Quarantine 해제 후 재할당 시 UAF 놓칠 수 있음 재할당 시 새 태그 부여 → 구 포인터로 접근 시 탐지
메모리 오버헤드 높음 (격리된 객체가 메모리 점유) 낮음 (Quarantine 없이도 기본 탐지 가능)
탐지 확률 결정적 (Quarantine 중 100%) 확률적 (SW: 255/256, HW: 15/16)
Quarantine per-CPU → Global 배칭 흐름 per-CPU Quarantine CPU 0 obj obj obj 락 불필요 CPU 1 obj obj CPU N obj bytes > QUARANTINE_PERCPU_SIZE (1MB)? quarantine_batch_move() Global Quarantine (순환 배열, 8 슬롯) Batch 0 tail ▶ Batch 1 Batch 2 ... Batch 5 Batch 6 Batch 7 ◀ head spinlock 보호 가장 오래된 quarantine_reduce() Slab으로 실제 반환 메모리 압력(Memory Pressure) OOM 상황 → shrinker 콜백 호출 → quarantine_shrink_scan() → 오래된 배치 강제 해제 그림 10. Quarantine per-CPU → Global 배칭 및 메모리 회수 흐름

KASAN 리포트 분석

리포트 예시: Use-After-Free

==================================================================
BUG: KASAN: use-after-free in trigger_uaf+0x38/0x58
Write of size 1 at addr ffff0000c5a14000 by task test/1234

CPU: 2 PID: 1234 Comm: test Not tainted 6.8.0-kasan #1
Hardware name: QEMU Virtual Machine
Call trace:
 dump_backtrace+0x0/0x1e0
 show_stack+0x18/0x24
 dump_stack_lvl+0x48/0x60
 print_report+0xf0/0x538
 kasan_report+0xac/0xe0
 __asan_store1+0x6c/0x80
 trigger_uaf+0x38/0x58
 test_module_init+0x1c/0x48

Allocated by task 1234:
 kasan_save_stack+0x24/0x50
 __kasan_kmalloc+0x88/0xa0
 kmalloc_trace+0x2c/0x40
 trigger_uaf+0x1c/0x58
 test_module_init+0x14/0x48

Freed by task 1234:
 kasan_save_stack+0x24/0x50
 kasan_save_free_info+0x30/0x48
 __kasan_slab_free+0x40/0x58
 kfree+0x94/0x130
 trigger_uaf+0x30/0x58
 test_module_init+0x18/0x48

The buggy address belongs to the object at ffff0000c5a14000
 which belongs to the cache kmalloc-32 of size 32
The buggy address is located 0 bytes inside of
 freed 32-byte region [ffff0000c5a14000, ffff0000c5a14020)

Memory state around the buggy address:
 ffff0000c5a13f00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff0000c5a13f80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff0000c5a14000: fa fa fa fa fb fb fb fb fc fc fc fc fc fc fc fc
                   ^
 ffff0000c5a14080: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff0000c5a14100: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
==================================================================

리포트 각 필드 해석

리포트 필드의미활용법
BUG: KASAN: use-after-free 버그 유형 (UAF, OOB, double-free 등) 어떤 종류의 메모리 오류인지 즉시 파악
Write of size 1 at addr 접근 크기와 주소 어떤 크기의 접근이 어디서 발생했는지 확인
Call trace 버그 발생 시점의 스택 트레이스 문제를 유발한 코드 경로 추적
Allocated by task 객체 할당 시점의 스택 트레이스 메모리를 누가 할당했는지 확인
Freed by task 객체 해제 시점의 스택 트레이스 메모리를 누가 해제했는지 확인 (UAF 분석에 핵심)
cache kmalloc-32 Slab 캐시(Cache) 이름과 크기 어떤 슬랩 캐시의 객체인지 파악
Memory state Shadow Memory 덤프(Dump) 주변 메모리 상태를 시각적으로 확인 (^가 문제 위치)

Shadow Memory 덤프 읽는 법

>ffff0000c5a14000: fa fa fa fa fb fb fb fb fc fc fc fc fc fc fc fc
                  ^
Shadow 바이트 의미:
  fa = KASAN_FREE_PAGE     (해제된 페이지)
  fb = KASAN_KMALLOC_FREE  (kfree된 kmalloc 객체)
  fc = KASAN_KMALLOC_FREETRACK (추적 정보가 있는 해제 객체)

  ^ 기호가 가리키는 위치 = 실제 접근이 발생한 Shadow 바이트
  이 경우 fa (해제된 메모리)에 대한 접근이므로 UAF

Slab 할당자와 KASAN 통합

SLUB 할당자의 KASAN 훅

SLUB 할당자는 kmalloc()kfree() 경로에서 KASAN 훅을 호출하여 Shadow Memory를 갱신합니다. 할당 시 Redzone을 설정하고, 해제 시 전체를 poisoning합니다.

/* mm/kasan/common.c - kmalloc 훅 */
void * __must_check __kasan_kmalloc(
    struct kmem_cache *cache, const void *object,
    size_t size, gfp_t flags)
{
    unsigned long redzone_start, redzone_end;

    if (unlikely(object == NULL))
        return NULL;

    /* 1. 객체 데이터 영역을 접근 가능으로 설정 (unpoison) */
    kasan_unpoison(object, size, false);

    /* 2. 요청 크기와 Slab 크기 사이의 Redzone을 poison */
    redzone_start = (unsigned long)object + size;
    redzone_end = (unsigned long)object + cache_alloc_size(cache);
    kasan_poison((void *)redzone_start, redzone_end - redzone_start,
                KASAN_SLAB_REDZONE, false);

    /* 3. 할당 스택 트레이스 저장 */
    kasan_save_alloc_info(cache, object, flags);

    return (void *)object;
}

/* mm/kasan/common.c - kfree 훅 */
bool __kasan_slab_free(
    struct kmem_cache *cache, void *object,
    bool init)
{
    /* 1. 이미 해제된 객체인지 체크 (double-free 탐지) */
    if (unlikely(kasan_check_slab_free(cache, object)))
        return true;  /* double-free 감지됨 */

    /* 2. 해제 스택 트레이스 저장 */
    kasan_save_free_info(cache, object);

    /* 3. Quarantine에 넣기 (Generic) 또는 즉시 poison (Tag-based) */
    return kasan_quarantine_put(cache, object);
}
코드 설명
  • 12행 할당된 객체의 데이터 영역을 접근 가능(shadow=0)으로 설정합니다.
  • 15-18행 요청 크기(size)와 실제 Slab 크기 사이의 차이를 Redzone으로 poison합니다. 이것이 OOB 탐지의 핵심입니다.
  • 21행 할당 시점의 스택 트레이스를 저장합니다. 나중에 버그 리포트에서 "Allocated by task" 정보를 제공합니다.
  • 33행 해제 시 먼저 double-free를 체크합니다. Shadow가 이미 freed 상태이면 에러입니다.
  • 40행 Generic KASAN은 Quarantine에, Tag-based는 새 태그로 poison 후 즉시 해제합니다.

Slab 레이아웃과 KASAN 메타데이터

구간오프셋Shadow 값설명
Left Redzone object - redzone_size ~ object 0xF1 kmalloc 객체 앞쪽 패딩 (16~128바이트)
Object Data object ~ object + size 0x00 또는 0x01~0x07 유효 데이터 영역
Right Redzone (내부) object + size ~ object + slab_size 0xF3 요청 크기와 Slab 크기 차이 패딩
Right Redzone (외부) object + slab_size ~ next_object 0xF2 객체 사이 외부 Redzone
KASAN 메타데이터 객체 끝 ~ (alloc_info + free_info) N/A 할당/해제 스택 트레이스 저장

KASAN 부팅 초기화 과정

Shadow Memory 초기화 시퀀스

KASAN Shadow Memory는 커널 부팅의 매우 초기 단계에서 초기화됩니다. 물리 메모리 전체에 대응하는 Shadow 영역의 페이지 테이블(Page Table)을 구축해야 하므로, MMU가 활성화된 직후(head64.S → start_kernel)에 Shadow를 설정합니다. 초기에는 모든 Shadow가 단일 제로 페이지(kasan_early_shadow_page)를 공유하다가, 메모리 서브시스템 초기화 후 실제 Shadow 페이지로 교체됩니다.

/* mm/kasan/init.c - Shadow Memory 초기화 (x86_64 기준) */

/* 부팅 초기 Shadow: 모든 Shadow가 이 제로 페이지를 공유 */
char kasan_early_shadow_page[PAGE_SIZE]
    __page_aligned_bss;

/* 부팅 초기 Shadow 페이지 테이블 (PGD/P4D/PUD/PMD/PTE) */
pte_t kasan_early_shadow_pte[PTRS_PER_PTE]
    __page_aligned_bss;
pmd_t kasan_early_shadow_pmd[PTRS_PER_PMD]
    __page_aligned_bss;
pud_t kasan_early_shadow_pud[PTRS_PER_PUD]
    __page_aligned_bss;
p4d_t kasan_early_shadow_p4d[PTRS_PER_P4D]
    __page_aligned_bss;

/* start_kernel() → kasan_init() 호출 */
void __init kasan_init(void)
{
    int i;

    /* 1단계: early shadow PTE를 모두 kasan_early_shadow_page로 설정 */
    for (i = 0; i < PTRS_PER_PTE; i++)
        set_pte(&kasan_early_shadow_pte[i],
                pfn_pte(virt_to_pfn(kasan_early_shadow_page),
                        PAGE_KERNEL_RO));

    /* 2단계: Direct Mapping 영역의 Shadow를 실제 페이지로 교체 */
    kasan_populate_early_shadow(
        kasan_mem_to_shadow((void *)PAGE_OFFSET),
        kasan_mem_to_shadow((void *)PAGE_OFFSET
                            + direct_map_size()));

    /* 3단계: vmalloc 영역 Shadow 초기화 (lazy 매핑 준비) */
    kasan_populate_early_shadow(
        kasan_mem_to_shadow((void *)VMALLOC_START),
        kasan_mem_to_shadow((void *)VMALLOC_END));

    /* 4단계: 커널 텍스트/모듈 영역 Shadow 설정 */
    kasan_populate_early_shadow(
        kasan_mem_to_shadow((void *)MODULES_VADDR),
        kasan_mem_to_shadow((void *)MODULES_END));

    pr_info("KASAN: shadow memory initialized\n");
}
코드 설명
  • 4-5행 kasan_early_shadow_page는 BSS 영역의 제로 페이지입니다. 부팅 초기 모든 Shadow PTE가 이 페이지를 가리킵니다(shadow=0 → 전체 접근 가능).
  • 20-25행 1단계: early shadow PTE를 제로 페이지로 설정합니다. 읽기 전용(PAGE_KERNEL_RO)으로 매핑하여 실수로 Shadow가 오염되는 것을 방지합니다.
  • 27-31행 2단계: Direct Mapping(물리 메모리 직접 매핑) 영역의 Shadow를 실제 물리 페이지로 교체합니다. 이후 이 영역의 kmalloc/kfree가 KASAN 보호를 받습니다.
  • 33-40행 3~4단계: vmalloc과 모듈 영역의 Shadow는 초기에는 early shadow를 유지하다가, 실제 vmalloc/모듈 로드 시 동적으로 실제 페이지가 할당됩니다.

ARM64에서의 KASAN 초기화

ARM64는 x86_64와 달리 swapper_pg_dir(초기 페이지 테이블)에 Shadow를 직접 매핑합니다. __pi_kasan_init_early()가 MMU 활성화 전에 호출되어 Shadow 영역의 PGD/PUD/PMD 엔트리를 설정합니다. HW-Tag KASAN의 경우 Shadow Memory 대신 MTE 태그가 하드웨어에 의해 관리되므로 Shadow 초기화가 필요하지 않습니다.

초기화 단계x86_64ARM64 (Generic/SW-Tag)ARM64 (HW-Tag)
MMU 활성화 전 head64.S에서 early PGD 설정 __pi_kasan_init_early() MTE 활성화 (SCTLR_EL1)
start_kernel kasan_init() kasan_init() kasan_init_hw_tags()
메모리 서브시스템 초기화 후 early shadow → 실제 페이지 교체 early shadow → 실제 페이지 교체 불필요 (HW 관리)
Shadow 크기 커널 주소 공간 / 8 커널 주소 공간 / 8 (또는 /16) 물리 메모리당 4비트 (HW 내장)
KASAN 부팅 초기화 타임라인 시간 head64.S Early PGD 설정 Shadow → zero page kasan_init() Direct Map Shadow 실제 페이지 교체 mem_init() Buddy 할당자 준비 추가 Shadow 매핑 kmem_cache_init() Slab 할당자 시작 KASAN 훅 활성 정상 운영 모든 메모리 접근 KASAN 보호 Shadow Memory 상태 변화 Early Shadow 모든 PTE → zero page shadow = 0x00 (전체 허용) KASAN 체크 비활성 상태 Populate Shadow 실제 물리 페이지 할당 PTE 교체 (쓰기 가능) 영역별 순차 매핑 Active Shadow kmalloc → shadow 갱신 kfree → F5 poisoning 모든 접근 체크 활성 주의: kasan_arch_is_ready() 체크 kasan_init() 완료 전에는 __asan_loadN/storeN이 호출되어도 체크를 건너뜁니다. 이는 부팅 초기 코드가 Shadow 매핑 전에 메모리에 접근하는 것을 허용하기 위함입니다. 그림 11. KASAN 부팅 초기화 타임라인과 Shadow Memory 상태 변화

스택 변수 계측 (Stack Instrumentation)

스택 Redzone의 원리

Generic KASAN은 컴파일러를 통해 스택 변수 사이에 Redzone을 삽입합니다. 함수의 프롤로그에서 각 스택 변수 주변의 Shadow를 poisoning하고, 에필로그에서 unpoisoning합니다. 이를 통해 스택 버퍼 오버플로(Buffer Overflow)를 탐지합니다. CONFIG_KASAN_STACK=y이 활성화되어야 동작합니다.

/* 컴파일러가 생성하는 스택 계측 코드 (개념적) */

/* 원본 함수 */
void original_func(void)
{
    char buf_a[16];
    int  val_b;
    char buf_c[32];
    /* ... 사용 코드 ... */
}

/* KASAN 계측 후 (개념적 변환) */
void original_func(void)
{
    /* 컴파일러가 확장한 스택 프레임 */
    struct {
        char left_redzone[32];   /* Shadow: F8 F8 F8 F8 */
        char buf_a[16];           /* Shadow: 00 00 */
        char mid_redzone_1[16];  /* Shadow: F9 F9 */
        int  val_b;                /* Shadow: 04 */
        char mid_redzone_2[28];  /* Shadow: F9 F9 F9 (+ 패딩) */
        char buf_c[32];           /* Shadow: 00 00 00 00 */
        char right_redzone[32];  /* Shadow: FA FA FA FA */
    } frame;

    /* 프롤로그: Shadow 설정 */
    __asan_set_shadow_f8(&frame.left_redzone, 32);  /* Stack Left */
    __asan_unpoison_stack_memory(&frame.buf_a, 16);
    __asan_set_shadow_f9(&frame.mid_redzone_1, 16); /* Stack Mid */
    __asan_unpoison_stack_memory(&frame.val_b, 4);
    __asan_set_shadow_f9(&frame.mid_redzone_2, 28);
    __asan_unpoison_stack_memory(&frame.buf_c, 32);
    __asan_set_shadow_fa(&frame.right_redzone, 32); /* Stack Right */

    /* ... 원래 함수 본문 ... */

    /* 에필로그: 전체 Shadow 해제 */
    __asan_unpoison_stack_memory(&frame, sizeof(frame));
}
코드 설명
  • 17행 왼쪽 Redzone(F8): 스택 프레임 시작 부분을 보호합니다. 이전 프레임의 데이터를 침범하는 것을 탐지합니다.
  • 19-21행 중간 Redzone(F9): 스택 변수 사이에 삽입되어 인접 변수로의 오버플로를 탐지합니다.
  • 23행 오른쪽 Redzone(FA): 스택 프레임 끝을 보호합니다.
  • 37행 에필로그에서 전체 프레임의 Shadow를 해제(unpoison)합니다. 이렇게 하지 않으면 스택 재사용 시 false positive가 발생합니다.

Stack-After-Return 탐지

함수가 반환된 후 스택 변수의 주소를 통해 접근하는 버그(use-after-return)를 탐지하려면 CONFIG_KASAN_STACK=y와 함께 __asan_stack_malloc/__asan_stack_free가 사용됩니다. 이 기능은 스택 프레임을 별도의 가짜 스택(fake stack)에 할당하고, 함수 반환 후 해당 영역을 FB(stack-after-return)로 poisoning합니다.

/* mm/kasan/generic.c - Stack-after-return 개념 */

/* 함수 진입 시: 스택 프레임을 fake stack에 할당 */
void *__asan_stack_malloc(size_t size)
{
    void *fake_stack = kasan_alloc_fake_stack(size);
    if (fake_stack) {
        /* fake stack에 할당 성공: 여기에 스택 변수를 배치 */
        kasan_unpoison(fake_stack, size, false);
        return fake_stack;
    }
    return NULL;  /* 실패 시 실제 스택 사용 */
}

/* 함수 반환 시: fake stack을 poison */
void __asan_stack_free(void *fake_stack, size_t size)
{
    /* FB = Stack use-after-return */
    kasan_poison(fake_stack, size,
                KASAN_STACK_AFTER_RETURN, false);
    /* 일정 시간 후 재사용 허용 */
}
스택 변수 Redzone 레이아웃 (Generic KASAN) 계측된 스택 프레임 Shadow Memory 높은 주소 Right Redzone (32B) FA FA FA FA buf_c[32] (유효) 00 00 00 00 Mid Redzone (28B) F9 F9 F9 val_b (int, 4B) 04 Mid Redzone (16B) F9 F9 buf_a[16] (유효) 00 00 Left Redzone (32B) F8 F8 F8 F8 낮은 주소 유효 데이터 Mid Redzone (F9) Left/Right (F8/FA) buf_a[16] → F9 접근! 그림 12. 스택 변수 Redzone 레이아웃과 Shadow 매핑
스택 계측의 성능 영향: CONFIG_KASAN_STACK=y는 모든 함수의 프롤로그/에필로그에 Shadow 설정 코드를 삽입하므로 커널 이미지 크기가 크게 증가하고(2~5배) 스택 사용량도 증가합니다. 스택 크기 제한(보통 8KB~16KB)에 가까운 함수에서 스택 오버플로(Stack Overflow)가 발생할 수 있으므로 커널 스택 크기를 늘리는 것(THREAD_SIZE)을 고려해야 합니다.

전역 변수 계측 (Global Instrumentation)

전역 변수 Redzone 메커니즘

Generic KASAN은 전역 변수(BSS, Data 섹션)에도 Redzone을 추가합니다. 컴파일러가 각 전역 변수를 __asan_global 구조체(Struct)로 등록하면, 부팅 시 __asan_register_globals()가 호출되어 Shadow를 설정합니다. 전역 변수의 크기가 8바이트 정렬에 맞지 않으면 나머지 바이트를 FE(Global Redzone)로 poisoning합니다.

/* 컴파일러가 생성하는 전역 변수 등록 구조체 */
struct __asan_global {
    unsigned long beg;           /* 전역 변수 시작 주소 */
    unsigned long size;          /* 선언된 크기 */
    unsigned long size_with_rz;  /* Redzone 포함 크기 */
    const char *name;            /* 변수 이름 (디버깅용) */
    const char *module_name;     /* 모듈/파일 이름 */
    unsigned long has_dynamic_init;
};

/* 커널 부팅 시 호출: 전역 변수 Shadow 설정 */
void __asan_register_globals(
    struct __asan_global *globals, size_t n)
{
    for (size_t i = 0; i < n; i++) {
        /* 유효 영역 unpoison */
        kasan_unpoison((void *)globals[i].beg,
                       globals[i].size, false);

        /* Redzone을 FE로 poison */
        unsigned long rz_start = globals[i].beg + globals[i].size;
        unsigned long rz_size = globals[i].size_with_rz - globals[i].size;
        kasan_poison((void *)rz_start, rz_size,
                    KASAN_GLOBAL_REDZONE, false);
    }
}

/* 모듈 언로드 시: Shadow 복원 */
void __asan_unregister_globals(
    struct __asan_global *globals, size_t n)
{
    for (size_t i = 0; i < n; i++) {
        kasan_poison((void *)globals[i].beg,
                    globals[i].size_with_rz, 0, false);
    }
}
코드 설명
  • 2-9행 __asan_global 구조체는 컴파일러가 각 전역 변수마다 생성합니다. 변수의 주소, 크기, Redzone 포함 크기, 이름을 담고 있습니다.
  • 16-18행 전역 변수의 유효 영역을 unpoison합니다. 예: char g[13]이면 13바이트가 접근 가능합니다.
  • 20-24행 유효 크기와 정렬된 크기 사이의 공간(Redzone)을 FE로 poison합니다. 전역 변수 범위를 넘는 접근을 탐지합니다.
  • 28-34행 커널 모듈(Kernel Module) 언로드 시 해당 모듈의 전역 변수 Shadow를 정리합니다.
전역 변수 Redzone 배치 (Generic KASAN) Data/BSS 섹션 (전역 변수들) g_buf[13] 13바이트 RZ 19B (→32B) g_count 4바이트 RZ 28B (→32B) g_data (struct, 48B) 48바이트 (8B 정렬) RZ 16B (→64B) 대응 Shadow Memory 00 05 FE FE g_buf: 8+5=13B 04 FE FE g_count: 4B 00 00 00 00 00 00 FE FE g_data: 48B = 6*8 00 = 전체 접근 가능 01~07 = 부분 접근 FE = Global Redzone 컴파일러가 전역 변수 크기를 다음 32/64바이트 정렬로 확장하고, 나머지를 Redzone으로 채움 그림 13. 전역 변수 Redzone 배치와 Shadow 매핑

vmalloc 영역 KASAN

vmalloc Shadow의 동적 매핑

CONFIG_KASAN_VMALLOC=y이 설정되면 KASAN은 vmalloc 영역에도 적용됩니다. vmalloc 주소 공간은 매우 넓어서(x86_64에서 ~32TB) 전체 Shadow를 사전 할당하는 것은 비효율적입니다. 따라서 vmalloc Shadow는 lazy 매핑으로 동작합니다: 실제 __vmalloc() 호출 시 해당 범위의 Shadow 페이지가 동적으로 할당됩니다.

/* mm/kasan/shadow.c - vmalloc Shadow 동적 매핑 */

int kasan_populate_vmalloc(unsigned long addr, unsigned long size)
{
    unsigned long shadow_start, shadow_end;
    int ret;

    /* vmalloc 주소 범위에 대응하는 Shadow 주소 계산 */
    shadow_start = (unsigned long)kasan_mem_to_shadow(
        (void *)addr);
    shadow_end = (unsigned long)kasan_mem_to_shadow(
        (void *)addr + size);

    /* Shadow 영역에 실제 물리 페이지 매핑 */
    ret = apply_to_page_range(
        &init_mm, shadow_start, shadow_end - shadow_start,
        kasan_populate_vmalloc_pte, NULL);

    if (!ret) {
        /* Shadow를 전체 poison으로 초기화 (접근 불가) */
        kasan_poison((void *)addr, size,
                    KASAN_VMALLOC_INVALID, false);
    }

    return ret;
}

/* vmalloc 해제 시 Shadow 정리 */
void kasan_release_vmalloc(unsigned long addr,
                           unsigned long size)
{
    /* Shadow 영역을 early shadow(zero page)로 되돌림 */
    kasan_depopulate_vmalloc_pte(
        kasan_mem_to_shadow((void *)addr),
        size >> KASAN_SHADOW_SCALE_SHIFT);

    /* 물리 페이지를 Buddy 할당자에 반환 */
    free_shadow_pages(addr, size);
}
코드 설명
  • 3행 kasan_populate_vmalloc()__vmalloc() 내부에서 호출되어 해당 범위의 Shadow 페이지를 동적으로 할당합니다.
  • 14-17행 apply_to_page_range()로 Shadow 영역의 페이지 테이블을 순회하며, early shadow PTE를 실제 물리 페이지로 교체합니다.
  • 19-22행 새로 매핑된 Shadow를 INVALID로 초기화합니다. 이후 vmalloc 사용자가 실제 데이터를 기록할 때 해당 부분만 unpoison됩니다.
  • 28-37행 vfree() 시 Shadow 페이지를 해제하고 PTE를 early shadow로 되돌립니다. Shadow 페이지 누수를 방지합니다.

vmalloc KASAN 지원 범위

vmalloc 사용처KASAN 보호비고
vmalloc() / vzalloc() 지원 Guard 페이지와 결합하여 OOB 탐지
vmap() (페이지 배열 매핑) 지원 매핑 해제 시 Shadow 자동 정리
ioremap() 미지원 MMIO 영역은 KASAN 체크 비활성
커널 모듈 코드/데이터 지원 모듈 로드 시 Shadow 할당
BPF JIT 코드 제한적 BPF 프로그램의 메모리 접근은 별도 검증
vmalloc Shadow 동적 매핑 흐름 __vmalloc(size) alloc_pages() 반복 물리 페이지 확보 map_vm_area() vmalloc VA에 매핑 kasan_populate_vmalloc(addr, size) Shadow 페이지 동적 할당 + PTE 교체 vmalloc 주소 공간 (VMALLOC_START ~ VMALLOC_END) 할당 A G 할당 B G 미할당 (early shadow → zero page) / 8 (Shadow 매핑) Shadow 영역 (vmalloc용) 실제 페이지 실제 페이지 early shadow (zero page 공유) G = Guard 페이지 (접근 불가) — vmalloc 할당 사이의 보호 페이지, Shadow 매핑 불필요 그림 14. vmalloc Shadow 동적 매핑 — 할당 시에만 실제 Shadow 페이지 할당

커널 모듈과 KASAN

모듈 로드 시 KASAN 처리

커널 모듈(.ko)이 로드될 때 KASAN은 다음 작업을 수행합니다:

  1. 모듈의 코드/데이터가 배치되는 vmalloc 영역의 Shadow 페이지를 할당합니다.
  2. 모듈이 -fsanitize=kernel-address로 컴파일되었으면, 모듈의 .kasan_globals 섹션에서 전역 변수 목록을 읽어 __asan_register_globals()를 호출합니다.
  3. 모듈의 함수 내 모든 메모리 접근이 KASAN 체크를 받습니다.
/* kernel/module/main.c - 모듈 로드 시 KASAN 초기화 (간략화) */

static int post_relocation(struct module *mod,
                          const struct load_info *info)
{
    /* 1. 모듈 메모리 영역의 Shadow 매핑 확인 */
    /*    (module_alloc이 vmalloc 기반이므로 이미 매핑됨) */

    /* 2. 모듈의 KASAN 전역 변수 등록 */
    if (mod->kasan_init_globals) {
        mod->kasan_init_globals();
        /* 내부적으로 __asan_register_globals() 호출 */
    }

    /* 3. 모듈 코드/데이터의 Shadow 초기화 */
    kasan_module_alloc(mod->mem[MOD_TEXT].base,
                       mod->mem[MOD_TEXT].size);

    return 0;
}

/* 모듈 언로드 시 */
static void free_module(struct module *mod)
{
    /* 전역 변수 Shadow 해제 */
    if (mod->kasan_init_globals)
        __asan_unregister_globals(mod->globals, mod->num_globals);

    /* Shadow 페이지 해제 (vmalloc 해제 경로에서 자동 처리) */
    module_memfree(mod->mem[MOD_TEXT].base);
}

모듈 KASAN 비활성화

특정 모듈에서 KASAN을 비활성화하려면 해당 모듈의 Makefile에서 설정합니다:

# 특정 파일에서 KASAN 비활성화
KASAN_SANITIZE_problem_file.o := n

# 전체 디렉토리에서 KASAN 비활성화
KASAN_SANITIZE := n

# 사용 예: crypto 모듈에서 성능 코드 제외
# crypto/Makefile
KASAN_SANITIZE_aes_generic.o := n
KASAN_SANITIZE_sha256_generic.o := n
모듈과 KASAN 호환성: KASAN이 활성화된 커널에 KASAN 없이 컴파일된 모듈을 로드하면, 해당 모듈의 코드에는 체크 함수가 삽입되지 않으므로 모듈 내 메모리 접근은 탐지되지 않습니다. 단, Slab 할당자의 KASAN 훅은 여전히 동작하므로 Redzone/Quarantine 수준의 탐지는 유지됩니다.
커널 모듈 로드/언로드 시 KASAN 처리 흐름 모듈 로드 (insmod) load_module() module_alloc() vmalloc + Shadow 할당 __asan_register_globals() 전역 변수 Shadow(FE) 설정 모듈 init 실행 KASAN 보호 활성 모듈 언로드 (rmmod) free_module() __asan_unregister_globals() 전역 변수 Shadow 정리 module_memfree() vmalloc + Shadow 해제 모듈 메모리 구조와 KASAN Shadow .text (코드) .data .bss .kasan_globals .symtab 각 섹션은 vmalloc 영역에 매핑되며, 대응하는 Shadow 페이지가 동적으로 할당됨 그림 15. 커널 모듈 로드/언로드 시 KASAN 처리 흐름과 모듈 메모리 구조

커널 설정

주요 CONFIG 옵션

# KASAN 기본 설정
CONFIG_KASAN=y                    # KASAN 활성화
CONFIG_KASAN_GENERIC=y            # Generic 모드 (x86_64, ARM64)
# CONFIG_KASAN_SW_TAGS=y          # SW-Tag 모드 (ARM64만)
# CONFIG_KASAN_HW_TAGS=y          # HW-Tag 모드 (ARM64 MTE만)

# Generic KASAN 세부 옵션
CONFIG_KASAN_OUTLINE=y            # Outline 모드 (코드 크기 감소, 약간 느림)
# CONFIG_KASAN_INLINE=y           # Inline 모드 (코드 크기 증가, 더 빠름)

# 스택/전역 변수 계측 (Generic만)
CONFIG_KASAN_STACK=y              # 스택 변수 OOB 탐지
CONFIG_KASAN_VMALLOC=y            # vmalloc 영역 KASAN 지원

# 디버깅 보조
CONFIG_STACKTRACE=y               # 스택 트레이스 수집 (리포트에 필요)
CONFIG_SLUB_DEBUG=y               # SLUB 디버깅 활성화

# 부트 파라미터 (runtime 조절)
# kasan.mode=sync               # 동기 모드 (기본값, 즉시 탐지)
# kasan.mode=async              # 비동기 모드 (HW-Tag만, 성능 우선)
# kasan.mode=asymm              # 비대칭 모드 (HW-Tag만, 읽기=비동기, 쓰기=동기)
# kasan.stacktrace=on           # 스택 트레이스 수집 (기본값)
# kasan.fault=report            # 리포트만 출력 (기본값)
# kasan.fault=panic             # 리포트 후 패닉

Kconfig 빌드 예시

# x86_64 Generic KASAN 빌드
make defconfig
./scripts/config -e CONFIG_KASAN
./scripts/config -e CONFIG_KASAN_GENERIC
./scripts/config -e CONFIG_KASAN_INLINE
./scripts/config -e CONFIG_KASAN_STACK
./scripts/config -e CONFIG_KASAN_VMALLOC
./scripts/config -e CONFIG_STACKTRACE
make -j$(nproc)

# QEMU로 KASAN 활성 커널 실행
qemu-system-x86_64 \
    -kernel arch/x86/boot/bzImage \
    -append "console=ttyS0 kasan.fault=report" \
    -initrd rootfs.cpio.gz \
    -nographic

Inline vs Outline 모드 비교

항목Inline 모드Outline 모드
체크 방식 체크 코드가 호출 지점에 인라인 삽입 별도 함수 호출 (__asan_loadN)
성능 더 빠름 (함수 호출 오버헤드 없음) 약간 느림 (함수 호출 1회 추가)
커널 이미지 크기 매우 크게 증가 (3~5배) 약간 증가 (1.5~2배)
권장 용도 성능 민감한 테스트 일반 개발/디버깅 (기본 권장)

성능 오버헤드와 운영 전략

모드별 성능 영향

KASAN 모드별 성능 오버헤드 비교 0% 50% 100% 150% 200% 250% 성능 오버헤드 ~200% Generic (Outline) ~150% Generic (Inline) ~20% SW-Tag (ARM64) ~5% HW-Tag (Sync) ~2% HW-Tag (Async) 메모리 오버헤드: Generic 12.5%, SW-Tag 6.25%, HW-Tag 3% + HW 그림 9. KASAN 모드별 CPU 성능 오버헤드 (개념적 비교)

운영 전략별 권장 설정

운영 목적권장 모드설정근거
개발 중 디버깅 Generic (Outline) CONFIG_KASAN_GENERIC=y 바이트 수준 정밀 탐지, 스택/전역 변수 포함
CI/CD 자동 테스트 Generic (Inline) CONFIG_KASAN_INLINE=y 속도 우선이면서 정밀 탐지 유지
퍼징 (syzkaller) Generic (Inline) CONFIG_KASAN_INLINE=y 최대 커버리지, 결정적 탐지
ARM64 프로덕션 모니터링 HW-Tag (Async) kasan.mode=async 최소 오버헤드(~2%), 실시간(Real-time) 모니터링 가능
ARM64 릴리스 검증 HW-Tag (Sync) kasan.mode=sync 프로덕션급 성능이면서 즉시 탐지
프로덕션 환경 주의: Generic KASAN은 성능 오버헤드가 커서 프로덕션에 적합하지 않습니다. 프로덕션 환경에서는 ARM64 HW-Tag KASAN(비동기 모드)만 권장됩니다. x86_64에서는 프로덕션 KASAN을 사용하지 않는 것이 일반적입니다.

실전 버그 사례 분석

사례 1: 네트워크 서브시스템 UAF (CVE-2021-23134)

NFC 서브시스템의 소켓(Socket) 해제 경합 조건으로 인한 use-after-free입니다. llcp_sock_release()에서 소켓 구조체를 해제한 후에도 llcp_sock_accept()에서 해당 소켓에 접근하는 경로가 존재했습니다.

/* 취약 코드 (간략화) - net/nfc/llcp_sock.c */
static int llcp_sock_release(struct socket *sock)
{
    struct sock *sk = sock->sk;
    struct nfc_llcp_sock *llcp_sock = nfc_llcp_sock(sk);

    /* 소켓 해제 */
    nfc_llcp_accept_unlink(sk);
    sock_orphan(sk);
    sock_put(sk);  /* 마지막 참조 해제 -> 메모리 free */

    return 0;
}

/* 동시 실행 가능한 accept 경로 */
static int llcp_sock_accept(struct socket *sock, ...)
{
    struct nfc_llcp_sock *llcp_sock = nfc_llcp_sock(sk);

    /* BUG: release 후에도 llcp_sock에 접근 가능! */
    if (llcp_sock->ssap == 0)  /* UAF 접근 */
        ...
}
KASAN 탐지 리포트:
BUG: KASAN: use-after-free in llcp_sock_accept+0x1a8/0x340
Read of size 4 at addr ffff888103a5e930 by task nfc-accept/2456

Allocated by task 2456:
 kmem_cache_alloc+0x130/0x270
 sk_prot_alloc+0x35/0x140
 nfc_llcp_sock_create+0x2c/0x120

Freed by task 2457:
 kmem_cache_free+0x90/0x2a0
 __sk_destruct+0x1e0/0x260
 llcp_sock_release+0xc8/0x120

사례 2: 파일시스템(Filesystem) OOB (ext4 확장 속성(Extended Attribute))

/* 취약 코드 (간략화) - fs/ext4/xattr.c */
static int ext4_xattr_set_entry(
    struct ext4_xattr_info *i,
    struct ext4_xattr_search *s)
{
    size_t size = EXT4_XATTR_SIZE(i->value_len);

    /* 경계 검사 누락: value_len이 비정상적으로 크면
       버퍼 끝을 넘어서 쓰기 발생 */
    memcpy((void *)s->here + EXT4_XATTR_SIZE(s->here->e_name_len),
           i->value, size);  /* OOB write! */
}
KASAN과 syzkaller 조합: Google의 syzkaller 퍼저는 KASAN 활성 커널에서 시스템 콜(System Call)을 무작위로 생성하여 커널 버그를 자동으로 발견합니다. Linux 커널의 수천 개 버그가 이 조합으로 발견되었으며, syzbot 대시보드에서 실시간으로 확인할 수 있습니다.

KASAN + KUnit 테스트

/* lib/kasan_test.c - KASAN 자체 테스트 (KUnit) */
#include <kunit/test.h>

static void kasan_test_kmalloc_oob_right(
    struct kunit *test)
{
    char *ptr;
    size_t size = 123;

    ptr = kmalloc(size, GFP_KERNEL);
    KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);

    /* KASAN이 이 OOB 접근을 탐지해야 함 */
    KUNIT_EXPECT_KASAN_FAIL(test,
        ptr[size + OOB_TAG_OFF] = 'x');

    kfree(ptr);
}

static void kasan_test_kmalloc_uaf(
    struct kunit *test)
{
    char *ptr;

    ptr = kmalloc(128, GFP_KERNEL);
    KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ptr);

    kfree(ptr);

    /* KASAN이 이 UAF 접근을 탐지해야 함 */
    KUNIT_EXPECT_KASAN_FAIL(test,
        ((volatile char *)ptr)[0]);
}

static struct kunit_case kasan_kunit_test_cases[] = {
    KUNIT_CASE(kasan_test_kmalloc_oob_right),
    KUNIT_CASE(kasan_test_kmalloc_uaf),
    {},
};

static struct kunit_suite kasan_kunit_test_suite = {
    .name = "kasan",
    .test_cases = kasan_kunit_test_cases,
};
kunit_test_suite(kasan_kunit_test_suite);
코드 설명
  • 14-15행 KUNIT_EXPECT_KASAN_FAIL은 해당 구문 실행 시 KASAN이 버그를 탐지할 것을 기대합니다. 탐지하지 못하면 테스트 실패입니다.
  • 31-32행 UAF 테스트: kfree 후 접근 시 KASAN이 반드시 탐지해야 합니다. volatile은 컴파일러 최적화(Compiler Optimization)를 방지합니다.
  • 35-43행 KUnit 테스트 스위트로 등록하여 kunit.py run으로 실행 가능합니다.

KASAN 메타데이터와 Stack Depot

할당/해제 메타데이터 구조

KASAN은 버그 발견 시 "누가 할당했고 누가 해제했는지"를 보여주기 위해 각 Slab 객체에 메타데이터를 부착합니다. 이 메타데이터에는 할당/해제 시점의 스택 트레이스 핸들이 저장되며, Stack Depot이라는 전역 해시 테이블(Hash Table)에서 실제 스택 프레임을 참조합니다.

/* mm/kasan/kasan.h - KASAN 메타데이터 구조 */

/* 할당 정보 */
struct kasan_alloc_meta {
    depot_stack_handle_t alloc_track;  /* 할당 스택 핸들 */
    depot_stack_handle_t aux_stack[2]; /* 보조 스택 (rcu 등) */
};

/* 해제 정보 */
struct kasan_free_meta {
    depot_stack_handle_t free_track;   /* 해제 스택 핸들 */
    /* Generic: Quarantine 연결 리스트 포인터로 재사용 */
    struct qlist_node quarantine_link;
};

/* mm/kasan/report.c - 리포트 시 메타데이터 출력 */
static void print_track(
    struct kasan_track *track,
    const char *prefix)
{
    pr_err("%s by task %u:\n", prefix, track->pid);

    /* Stack Depot에서 실제 스택 프레임 조회 */
    unsigned long *entries;
    unsigned int nr_entries;
    nr_entries = stack_depot_fetch(
        track->stack, &entries);

    /* 스택 프레임을 사람이 읽을 수 있는 형태로 출력 */
    stack_trace_print(entries, nr_entries, 0);
}
코드 설명
  • 4-7행 kasan_alloc_meta는 객체 할당 시 저장됩니다. depot_stack_handle_t는 32비트 핸들로, Stack Depot의 인덱스입니다.
  • 10-14행 kasan_free_meta는 해제 시 저장됩니다. Generic KASAN에서는 quarantine_link를 Quarantine 연결 리스트(Linked List)로 재사용합니다(메모리 절약).
  • 24-27행 stack_depot_fetch()는 핸들을 통해 Stack Depot에서 실제 스택 프레임 배열을 조회합니다.

Stack Depot의 동작 원리

Stack Depot(lib/stackdepot.c)은 커널 전역적으로 스택 트레이스를 중복 제거(deduplication)하여 저장하는 해시 테이블입니다. 같은 코드 경로에서 반복적으로 할당/해제되는 객체가 많으므로, 동일한 스택 트레이스를 한 번만 저장하고 32비트 핸들로 참조함으로써 메모리를 절약합니다.

/* lib/stackdepot.c - Stack Depot 구조 (간략화) */

#define DEPOT_HASH_BITS  17
#define DEPOT_HASH_SIZE  (1 << DEPOT_HASH_BITS)  /* 131072 슬롯 */

/* 해시 테이블 */
static struct stack_record *depot_hash[DEPOT_HASH_SIZE];

/* 개별 스택 레코드 */
struct stack_record {
    struct stack_record *next;  /* 해시 체인 */
    u32 hash;                    /* 스택 해시값 */
    u32 size;                    /* 프레임 수 */
    unsigned long entries[];     /* 실제 스택 프레임 (PC 배열) */
};

/* 스택 저장 (중복 시 기존 핸들 반환) */
depot_stack_handle_t __stack_depot_save(
    unsigned long *entries, unsigned int nr_entries,
    gfp_t gfp_flags)
{
    u32 hash = hash_stack(entries, nr_entries);
    int bucket = hash & (DEPOT_HASH_SIZE - 1);
    struct stack_record *found;

    /* 1. 해시 테이블에서 기존 레코드 검색 */
    found = find_stack(depot_hash[bucket], entries,
                       nr_entries, hash);
    if (found)
        return record_to_handle(found);  /* 중복: 기존 핸들 */

    /* 2. 새 레코드 할당 및 저장 */
    found = depot_alloc_stack(entries, nr_entries, hash);
    found->next = depot_hash[bucket];
    depot_hash[bucket] = found;

    return record_to_handle(found);  /* 새 핸들 */
}

메타데이터 배치와 메모리 오버헤드

메타데이터크기저장 위치생명주기
kasan_alloc_meta 12바이트 (핸들 3개) Slab 객체 내부 (object_size에 포함) 또는 외부 할당 → 해제
kasan_free_meta 4+16바이트 (핸들 + qlist_node) Slab 객체 내부 (해제 후 재사용) 해제 → Quarantine 완료
Stack Depot 레코드 ~100바이트 (평균 12프레임) 전역 해시 테이블 커널 수명 동안 유지 (해제 안 됨)
Shadow Memory 실제 메모리의 1/8 또는 1/16 전용 가상 주소 영역 커널 수명 동안 유지
KASAN 메타데이터와 Stack Depot 구조 Slab 객체 (Generic KASAN) Left RZ F1 Object Data 00 00 ... (유효) Right RZ F2 alloc_meta alloc_track, aux[2] handle 해제 후 (Quarantine 중) F1 free_meta free_track, qlink F5 F5 F5 poisoned F2 handle Stack Depot (전역 해시 테이블) bucket[0] bucket[h₁] bucket[...] bucket[h₂] bucket[...] bucket[N] stack_record hash=0x1A3F, size=8 entries[]: kmalloc+0x30, ... stack_record hash=0xB72E, size=12 entries[]: kfree+0x40, ... 중복 제거 효과: 같은 코드 경로의 1000번 할당 → Stack Depot에 1개만 저장 32비트 핸들(4바이트)로 참조 → 객체당 메타데이터 오버헤드 최소화 그림 16. KASAN 메타데이터와 Stack Depot 해시 기반 중복 제거 구조

흔한 실수와 디버깅 팁

KASAN 사용 시 자주 발생하는 문제

문제증상원인해결
부팅 실패 커널 패닉 (early boot) Shadow Memory 매핑 실패 (메모리 부족) 물리 메모리 확보 또는 kasan.mode=off
스택 오버플로 커널 패닉 (stack overflow) CONFIG_KASAN_STACK=y 시 스택 사용량 증가 THREAD_SIZE 증가 또는 스택 계측 비활성화
False Positive 정상 코드에서 KASAN 리포트 어셈블리(Assembly) 코드/인라인 asm이 KASAN 체크 우회 __no_sanitize_address 또는 KASAN_SANITIZE := n
성능 극심 저하 커널 10배 이상 느림 Generic Outline + STACK + 큰 Quarantine Inline 모드, 스택 계측 비활성화, Quarantine 축소
OOM 메모리 부족 (OOM killer) Shadow + Quarantine + 메타데이터 오버헤드 물리 메모리 추가, kasan.quarantine_size 축소
모듈 미탐지 특정 모듈 내 버그 놓침 모듈이 KASAN 없이 컴파일됨 모듈도 동일 커널 빌드 트리에서 빌드

KASAN 리포트 디버깅 체크리스트

리포트를 받았을 때 단계별 분석 가이드:
  1. 버그 유형 확인: BUG: KASAN: 뒤의 문자열(use-after-free, out-of-bounds, double-free)을 확인합니다.
  2. 접근 위치 확인: Write/Read of size N at addr에서 접근 크기와 주소를 확인합니다.
  3. 스택 트레이스 분석: Call trace에서 문제를 유발한 함수를 찾습니다. 가장 위의 __asan_ 함수를 건너뛰고 그 아래 함수가 실제 원인입니다.
  4. 할당/해제 이력 확인: UAF 버그라면 Allocated by taskFreed by task를 비교하여 해제 경로를 파악합니다.
  5. Shadow 덤프 해석: Memory state 섹션의 ^ 마커가 실제 문제 위치를 가리킵니다.
  6. Slab 캐시 확인: cache kmalloc-N에서 어떤 크기의 Slab 캐시인지 확인합니다.
  7. addr2line 활용: 오프셋으로 소스 코드 위치를 확인합니다: addr2line -e vmlinux -i ffffffff81234567

KASAN 관련 커널 부트 파라미터 전체 목록

파라미터설명기본값
kasan.mode sync, async, asymm, off KASAN 동작 모드 (HW-Tag만 async/asymm 지원) sync
kasan.stacktrace on, off 할당/해제 스택 트레이스 수집 여부 on
kasan.fault report, panic 버그 발견 시 동작 (리포트만 or 패닉) report
kasan.quarantine_size 바이트 수 전역 Quarantine 최대 크기 RAM / 32
kasan.multi_shot on, off 여러 버그를 연속 리포트할지 여부 off (첫 번째만)

KASAN 디버깅에 유용한 커맨드

# KASAN 리포트에서 함수+오프셋을 소스 위치로 변환
scripts/faddr2line vmlinux trigger_uaf+0x38/0x58

# KASAN 활성 커널의 Shadow 값을 직접 확인 (debugfs)
echo 0xffff888100000000 > /sys/kernel/debug/kasan/shadow_addr
cat /sys/kernel/debug/kasan/shadow_value

# KUnit KASAN 자체 테스트 실행
./tools/testing/kunit/kunit.py run \
    --kconfig_add CONFIG_KASAN=y \
    --kconfig_add CONFIG_KASAN_GENERIC=y \
    kasan

# syzkaller + KASAN 조합 실행
syz-manager -config my.cfg
# my.cfg에서 kernel_config에 CONFIG_KASAN=y 포함

# dmesg에서 KASAN 리포트만 필터링
dmesg | grep -A 50 "BUG: KASAN"

# KASAN 통계 확인 (활성화 시)
cat /proc/meminfo | grep -i kasan
cat /sys/kernel/debug/kasan/report_count
KASAN 리포트 분석 판정 트리 BUG: KASAN 리포트 수신 버그 유형? use-after-free UAF 분석 Freed by task 스택 확인 해제 경로 검토 • 경합 조건? → 락 추가 • 참조 카운트 오류? → refcount 수정 out-of-bounds OOB 분석 cache kmalloc-N 크기 확인 경계 검사 추가 • 배열 인덱스 검증 • size 파라미터 검증 double-free Double-Free 분석 두 해제 경로 모두 추적 소유권 정리 • kfree 호출 위치 통합 • 해제 후 NULL 대입 Shadow Memory 덤프 해석 ^ 마커 위치의 Shadow 값으로 구체적 오류 원인 파악 (F1/F2=RZ, F5=freed, F8/F9/FA=stack) 코드 수정 → KASAN 재테스트 → 리포트 없음 확인 그림 17. KASAN 리포트 분석 판정 트리 — 버그 유형별 분석 전략

관련 도구와 대안

KASAN vs KMSAN vs KCSAN vs KFENCE

도구탐지 대상원리오버헤드프로덕션
KASAN OOB, UAF, double-free Shadow Memory / 태그 높음~낮음 (모드별) HW-Tag만
KMSAN 초기화되지 않은 메모리 사용 Shadow Memory (초기화 추적) 매우 높음 (~3x) 불가
KCSAN 데이터 레이스 (동시 접근) 워치포인트 기반 샘플링 중간 불가
KFENCE OOB, UAF (샘플링) 가드 페이지 매우 낮음 (~1%) 가능
UBSAN 정의되지 않은 동작 컴파일러 삽입 체크 낮음 가능
KFENCE와 병용 전략: KFENCE는 프로덕션 커널에서 매우 낮은 오버헤드로 메모리 버그를 샘플링 탐지합니다. 개발 환경에서는 KASAN, 프로덕션에서는 KFENCE를 사용하는 것이 일반적인 전략입니다. 두 도구는 동시에 활성화할 수 있으며, KFENCE가 KASAN이 놓치는 영역을 보완합니다.

KASAN 비활성화 표시: __no_sanitize_address

/* 특정 함수에서 KASAN 체크를 비활성화하는 속성 */
#define __no_sanitize_address \
    __attribute__((no_sanitize("kernel-address")))

/* 사용 예: KASAN 자체 내부 함수 */
__no_sanitize_address
static void kasan_internal_func(void *addr)
{
    /* 이 함수 내 메모리 접근은 KASAN 체크를 건너뜀 */
    /* Shadow Memory 자체를 조작하는 코드 등에 사용 */
}

KASAN 모드 아키텍처 비교 심층 분석

Generic KASAN vs SW-Tag vs HW-Tag 내부 구조

세 가지 KASAN 모드는 근본적으로 다른 메커니즘을 사용합니다. Generic KASAN은 8:1 Shadow Memory를 기반으로 바이트 수준의 정밀 탐지를 수행하고, SW-Tag KASAN은 ARM64의 TBI(Top Byte Ignore) 기능을 활용하여 포인터 상위 바이트에 랜덤 태그를 삽입하며, HW-Tag KASAN은 ARM64 MTE(Memory Tagging Extension)를 통해 하드웨어가 직접 태그 비교를 수행합니다.

모드별 내부 동작 비교

동작 단계Generic KASANSW-Tag KASANHW-Tag KASAN
할당 시 Shadow = 0x00 설정, Redzone = F1/F2 poison 랜덤 태그 생성, Shadow에 태그 저장, 포인터 상위 바이트에 태그 삽입 MTE 명령어로 메모리 태그 설정, 포인터에 태그 삽입
접근 시 컴파일러 삽입 __asan_loadN/storeN이 Shadow 값 검사 컴파일러 삽입 체크가 포인터 태그와 Shadow 태그 비교 CPU가 자동으로 포인터 태그와 메모리 태그 비교 (소프트웨어 개입 없음)
해제 시 Shadow = F5 poison, Quarantine에 격리 새 태그로 변경 (기존 포인터 태그와 불일치 유도) MTE 태그 변경 (기존 포인터 태그와 하드웨어 불일치)
에러 감지 Shadow 값이 접근 불가 → kasan_report() 포인터 태그 ≠ Shadow 태그 → kasan_report() CPU 예외 (동기: 즉시 / 비동기: TFSR_EL1 레지스터)
탐지 보장 결정적 100% (접근 가능 범위 내) 확률적 ~99.6% (255/256) 확률적 ~93.75% (15/16)
KASAN 3가지 모드 아키텍처 비교 Generic KASAN x86_64, ARM64 컴파일러 계측 -fsanitize=kernel-address Shadow Memory (8:1) 주소 >> 3 + OFFSET 바이트 수준 검사 shadow == 0 또는 부분 접근 Quarantine (격리 큐) 스택 계측 전역 변수 계측 오버헤드 CPU: 1.5x ~ 3x 메모리: 12.5% 개발/테스트 전용 SW-Tag KASAN ARM64 (TBI) 컴파일러 계측 태그 비교 코드 삽입 Shadow Memory (16:1) 1바이트에 태그(0~FF) 저장 포인터 태그 비교 ptr[63:56] vs *shadow Quarantine 선택적 Slab 객체만 (스택/전역 미지원) 오버헤드 CPU: 0.9x ~ 1.2x 메모리: 6.25% 제한적 프로덕션 가능 HW-Tag KASAN ARM64 MTE 하드웨어 MTE 컴파일러 계측 불필요 물리 메모리 태그 16B당 4비트 태그 (HW 내장) CPU 자동 비교 ptr[59:56] vs MTE 태그 태그 변경으로 UAF 탐지 Sync 모드 Async 모드 오버헤드 CPU: ~5% (Sync) / ~2% (Async) 메모리: HW 관리 프로덕션 권장 그림 18. KASAN 3가지 모드의 아키텍처 비교 — 계측 방식, Shadow/태그 구조, 오버헤드

MTE(Memory Tagging Extension) 하드웨어 동작

ARM64 MTE는 물리 메모리의 16바이트(granule) 단위마다 4비트 태그를 하드웨어 전용 메모리에 저장합니다. 포인터의 bit[59:56]에 4비트 태그가 삽입되며, CPU는 매 메모리 접근 시 자동으로 두 태그를 비교합니다. 불일치 시 동기 모드에서는 즉시 예외(Tag Check Fault)가 발생하고, 비동기 모드에서는 TFSR_EL1 레지스터에 기록되어 나중에 확인됩니다.

/* arch/arm64/include/asm/mte.h - MTE 태그 설정 (HW-Tag KASAN) */

/* IRG: Insert Random Tag - 랜덤 태그가 설정된 포인터 반환 */
static inline void *mte_set_mem_tag_range(
    void *addr, size_t size, u8 tag, bool init)
{
    u64 curr = (u64)addr;
    u64 end = curr + size;

    /* STG/STZG 명령으로 16바이트 granule마다 태그 설정 */
    while (curr < end) {
        if (init)
            /* STZG: 메모리를 0으로 초기화하면서 태그 설정 */
            asm volatile("stzg %0, [%0]"
                         : "+r"(curr));
        else
            /* STG: 태그만 설정 (데이터 유지) */
            asm volatile("stg %0, [%0]"
                         : "+r"(curr));
        curr += MTE_GRANULE_SIZE;  /* 16바이트 */
    }

    /* 포인터에 태그 삽입 (bit 59:56) */
    return (void *)((u64)addr | ((u64)tag << 56));
}

/* HW-Tag KASAN 비동기 모드에서 TFSR 확인 */
void kasan_check_tfsr(void)
{
    u64 tfsr;

    /* TFSR_EL1: Tag Fault Status Register */
    tfsr = read_sysreg(tfsr_el1);
    if (unlikely(tfsr & SYS_TFSR_EL1_TF1)) {
        /* 비동기 태그 체크 실패 감지 */
        write_sysreg(0, tfsr_el1);
        kasan_report_async();
    }
}
코드 설명
  • 12-21행 16바이트 granule 단위로 STG(태그만 설정) 또는 STZG(태그 설정 + 제로화) 명령을 실행합니다. MTE 태그는 물리 메모리에 연결된 별도 저장소에 기록됩니다.
  • 30-38행 비동기 모드에서는 TFSR_EL1(Tag Fault Status Register)을 주기적으로 확인합니다. 컨텍스트 스위치(Context Switch) 시점이나 시스템 콜 반환 시에 체크하여 비동기 태그 오류를 리포트합니다.

Shadow 메모리 매핑 상세

가상 주소에서 Shadow 주소 변환 공식

KASAN의 Shadow Memory 매핑은 단순한 수학적 공식으로 동작합니다. 아키텍처별로 오프셋과 스케일이 다르지만, 기본 원리는 동일합니다.

아키텍처변환 공식SHADOW_OFFSETShadow 크기
x86_64 (Generic) shadow = (addr >> 3) + 0xdffffc0000000000 0xdffffc0000000000 커널 VA / 8
ARM64 (Generic) shadow = (addr >> 3) + SHADOW_OFFSET VA_BITS에 따라 계산 커널 VA / 8
ARM64 (SW-Tag) shadow = (addr >> 4) + SHADOW_OFFSET VA_BITS에 따라 계산 커널 VA / 16
ARM64 (HW-Tag) Shadow Memory 사용하지 않음 N/A HW 내장 (4bit/16B)

Shadow 바이트 인코딩 상세

Generic KASAN에서 Shadow 바이트는 해당 8바이트 영역의 접근 가능 상태를 인코딩합니다. 값이 0이면 8바이트 전체 접근 가능, 1~7이면 앞쪽 N바이트만 접근 가능, 음수(0x80 이상)이면 전체 접근 불가입니다.

/* mm/kasan/kasan.h - Shadow 바이트 값 정의 전체 */

/* === 접근 가능 상태 === */
#define KASAN_SHADOW_INIT        0x00  /* 8바이트 전체 접근 가능 */
/* 0x01 ~ 0x07: 앞쪽 N바이트만 접근 가능 */

/* === Slab 관련 poison 값 === */
#define KASAN_SLAB_REDZONE       0xF1  /* Slab 왼쪽 Redzone */
#define KASAN_SLAB_PADDING       0xF2  /* Slab 오른쪽 Redzone (외부) */
#define KASAN_SLAB_PADDING_INT   0xF3  /* Slab 내부 패딩 Redzone */
#define KASAN_KMALLOC_REDZONE    0xF4  /* kmalloc Redzone */
#define KASAN_KMALLOC_FREE       0xF5  /* kfree된 객체 */
#define KASAN_KMALLOC_FREETRACK  0xFA  /* 해제 추적 가능한 객체 */

/* === 스택 관련 poison 값 (Generic 전용) === */
#define KASAN_STACK_LEFT         0xF8  /* 스택 프레임 왼쪽 Redzone */
#define KASAN_STACK_MID          0xF9  /* 스택 변수 사이 Redzone */
#define KASAN_STACK_RIGHT        0xFA  /* 스택 프레임 오른쪽 Redzone */
#define KASAN_STACK_AFTER_RETURN 0xFB  /* 반환 후 스택 프레임 */
#define KASAN_USE_AFTER_SCOPE    0xFC  /* 스코프 종료 후 지역 변수 */

/* === 전역 변수 (Generic 전용) === */
#define KASAN_GLOBAL_REDZONE     0xFE  /* 전역 변수 Redzone */

/* === 페이지/vmalloc === */
#define KASAN_PAGE_FREE          0xFF  /* 해제된 페이지 */
#define KASAN_VMALLOC_INVALID    0xF6  /* vmalloc 미할당 영역 */
Shadow 바이트 인코딩과 접근 판정 Shadow 바이트 값 범위와 의미 0x00 8바이트 전체 접근 가능 0x01~07 앞쪽 N바이트만 접근 가능 (Slab 객체 끝) 0xF1~F4 Slab Redzone (왼쪽/오른쪽/내부/kmalloc) 0xF5 kfree()로 해제된 객체 (Quarantine 중) 0xF6 vmalloc 미할당 영역 0xF8~FC 스택 Redzone / after-return / after-scope 0xFE 전역 변수 Redzone 접근 판정 로직 1. shadow == 0 → 접근 허용 2. shadow > 0 && shadow < 8: (addr & 7) + size <= shadow → 허용 (addr & 7) + size > shadow → 거부 3. shadow < 0 (0x80+) → 무조건 거부 구체적 예시 kmalloc(13) → Slab 크기 16B Shadow: [00] [05] (8B+5B=13B 유효) ptr[0] → shadow=00, 접근 OK ptr[12] → shadow=05, (12&7)+1=5 ≤ 5, OK ptr[13] → shadow=05, (13&7)+1=6 > 5, OOB! ptr[16] → shadow=F2 (Redzone), 거부! kfree후 ptr[0] → shadow=F5, UAF! 그림 19. Shadow 바이트 인코딩 범위와 접근 판정 로직

Quarantine 메커니즘 심층 분석

Quarantine의 필요성

UAF(Use-After-Free) 버그는 해제된 메모리가 즉시 재할당되면 탐지하기 어렵습니다. 재할당된 객체는 새로운 유효 데이터를 포함하므로 Shadow가 접근 가능(0x00) 상태로 돌아가기 때문입니다. Quarantine은 해제된 객체를 일정 기간 격리하여 Shadow의 poison 상태(0xF5)를 유지함으로써 UAF 탐지 확률을 극대화합니다.

Quarantine 메모리 오버헤드 관리

Quarantine은 해제된 객체가 실제로 Slab에 반환되지 않고 메모리를 계속 점유하므로 상당한 메모리 오버헤드를 발생시킵니다. 이를 관리하기 위해 두 가지 메커니즘이 동작합니다.

관리 메커니즘트리거 조건동작효과
quarantine_reduce() 전역 Quarantine > 전체 RAM / 32 가장 오래된 배치(batch) 해제 정상 상황에서의 크기 제한
quarantine_shrinker 메모리 압력(memory pressure) 발생 shrinker 콜백으로 강제 해제 OOM 상황 완화
per-CPU 배칭 per-CPU 큐 > 1MB 전역 큐로 일괄 이동 spinlock 경합 최소화
kasan.quarantine_size 부트 파라미터 설정 시 전역 Quarantine 최대 크기 조절 메모리 제한 환경 대응

Quarantine 객체의 수명 주기

/* Quarantine 객체 수명 주기 의사 코드 */

/* 1단계: kfree() 호출 */
kfree(ptr);
  → __kasan_slab_free(cache, ptr)
    → kasan_save_free_info(cache, ptr)  /* 해제 스택 저장 */
    → Shadow를 0xF5로 poison
    → kasan_quarantine_put(cache, ptr)

/* 2단계: per-CPU 큐에 삽입 (락 불필요) */
quarantine_put():
  cpu_quarantine[smp_processor_id()].bytes += size
  qlist_put(&cpu_quarantine[..], ptr, size)

/* 3단계: per-CPU 임계치 초과 시 전역 큐로 이동 */
if (cpu_quarantine.bytes > QUARANTINE_PERCPU_SIZE) {
    spin_lock(&quarantine_lock);
    qlist_move_all(&cpu_quarantine,
                   &global_quarantine[quarantine_head]);
    spin_unlock(&quarantine_lock);
}

/* 4단계: 전역 큐 임계치 초과 시 가장 오래된 배치 해제 */
if (global_quarantine_bytes > QUARANTINE_FRACTION) {
    qlist_free_all(&global_quarantine[quarantine_tail]);
    /* 각 객체에 대해: */
    /*   Shadow를 0x00으로 unpoison */
    /*   실제 Slab freelist에 반환 */
}
Quarantine 객체 수명 주기와 UAF 탐지 창 시간 kmalloc() 활성 사용 중 Shadow: 00 00 ... (접근 가능) kfree() per-CPU Q 락 없이 삽입 Shadow: F5 F5 ... (접근 불가) 전역 Quarantine spinlock 보호 순환 큐 Shadow: F5 F5 ... (UAF 탐지 가능) quarantine_reduce() Slab 반환 재할당 가능 Shadow: 00 (unpoison → 재사용) UAF 탐지 창 (Quarantine 유지 기간) 메모리 오버헤드 시각화 전체 Slab 메모리 활성 객체 (사용 중) Quarantine (격리 중) 실제 Free (재할당 가능) Quarantine 크기 튜닝 가이드 기본값: RAM / 32 (예: 8GB RAM → 256MB Quarantine) 축소: kasan.quarantine_size=64M (메모리 제한 환경) / 확대: 512M (UAF 탐지율 우선) 그림 20. Quarantine 객체 수명 주기 — 할당부터 Slab 반환까지

KASAN 리포트 심층 해석

Out-of-Bounds 리포트 상세 해석

==================================================================
BUG: KASAN: slab-out-of-bounds in test_oob+0x4c/0x80
Write of size 4 at addr ffff888012345678 by task test/5678

CPU: 3 PID: 5678 Comm: test Not tainted 6.8.0-kasan #1
Hardware name: QEMU Virtual Machine
Call trace:
 dump_backtrace+0x0/0x1e0
 show_stack+0x18/0x24
 dump_stack_lvl+0x48/0x60
 print_report+0xf0/0x538
 kasan_report+0xac/0xe0
 __asan_store4+0x6c/0x80
 test_oob+0x4c/0x80           ← 실제 버그 발생 함수
 test_module_init+0x20/0x48

Allocated by task 5678:
 kasan_save_stack+0x24/0x50
 __kasan_kmalloc+0x88/0xa0
 kmalloc_trace+0x2c/0x40
 test_oob+0x24/0x80            ← 할당 위치
 test_module_init+0x14/0x48

The buggy address belongs to the object at ffff888012345660
 which belongs to the cache kmalloc-32 of size 32
The buggy address is located 24 bytes inside of
 allocated 20-byte region [ffff888012345660, ffff888012345674)

Memory state around the buggy address:
 ffff888012345500: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff888012345580: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff888012345600: fc fc fc fc fc fc fc fc fc fc fc fc 00 00 04 fc
                                                              ^
 ffff888012345680: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff888012345700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
==================================================================

리포트 심층 해석 가이드

리포트 필드값 (예시)의미분석 포인트
slab-out-of-bounds 버그 유형 Slab 할당 객체의 범위를 초과한 접근 heap-out-of-bounds와 구분: Slab 캐시 내부 OOB
Write of size 4 접근 크기 4바이트(int) 크기의 쓰기 시도 접근 크기로 어떤 타입의 데이터인지 추론
24 bytes inside of allocated 20-byte 오프셋 정보 20바이트 할당에서 24바이트 위치 접근 → 4바이트 초과 정확히 몇 바이트 초과했는지 산출 가능
cache kmalloc-32 of size 32 Slab 정보 kmalloc-32 캐시(Slab 크기 32) 소속 객체 요청 20B → 32B 캐시, 나머지 12B가 Redzone
00 00 04 fc Shadow 덤프 00=유효8B, 00=유효8B, 04=유효4B, fc=Redzone 8+8+4=20바이트 유효, 나머지 12B는 Redzone(fc)
^ 위치 fc (Redzone) Redzone 영역에 대한 접근 → OOB 확정 Shadow 값이 fc(Redzone)이므로 Slab 객체 범위 초과

Shadow Memory 덤프 해석 실전 예제

Shadow Memory 덤프 해석 방법:

행 형식: >주소: S1 S2 S3 S4 S5 S6 S7 S8 S9 SA SB SC SD SE SF S10
                                ^  ← 문제 발생 위치

각 Shadow 바이트 = 실제 메모리 8바이트에 대응

예제 1: OOB
>ffff888012345600: 00 00 04 fc fc fc fc fc
                         ^
  → 00 00 = 16바이트 유효
  → 04 = 4바이트만 유효 (총 20바이트)
  → fc = Slab Redzone (접근 불가)
  → ^ 가 fc를 가리킴: Redzone 접근 = OOB

예제 2: UAF
>ffff888012345600: fb fb fb fb fc fc fc fc
                   ^
  → fb = KASAN_KMALLOC_FREE (kfree된 객체)
  → ^ 가 fb를 가리킴: 해제된 메모리 접근 = UAF

예제 3: Stack OOB
>ffff888012345600: 00 00 f9 f9 00 00 00 00
                         ^
  → 00 00 = 스택 변수 A (16바이트)
  → f9 f9 = Stack Mid Redzone (변수 사이)
  → ^ 가 f9를 가리킴: 스택 변수 범위 초과 = Stack OOB

KFENCE (Kernel Electric-Fence) 상세

KFENCE의 설계 철학

KFENCE는 프로덕션 커널에서 사용할 수 있도록 설계된 저오버헤드 메모리 에러 탐지 도구입니다. KASAN이 모든 메모리 접근을 검사하는 반면, KFENCE는 샘플링 기반으로 일부 할당만 특수한 가드 페이지(Guard Page) 풀(Pool)에서 수행합니다. 기본 샘플 간격은 100ms이며, 이 간격마다 하나의 할당이 KFENCE 풀에서 이루어집니다.

KFENCE 동작 원리

동작 단계설명핵심 메커니즘
풀 초기화 부팅 시 KFENCE 전용 메모리 풀 할당 (기본 255개 객체) 연속 물리 페이지 할당, 가드 페이지로 구분
샘플링 할당 sample_interval(100ms)마다 다음 1회 할당을 KFENCE 풀에서 수행 타이머 기반 toggle, kmalloc 경로에서 체크
가드 페이지 배치 할당 객체 양쪽에 접근 불가 가드 페이지 배치 PTE를 no-access로 설정, 접근 시 page fault
OOB 탐지 객체를 페이지 끝에 정렬하여 1바이트 초과도 탐지 오른쪽 가드 = 다음 페이지(접근 불가)
UAF 탐지 해제 시 객체 페이지를 접근 불가로 설정 PTE를 no-access로 변경, 이후 접근 시 fault
에러 리포트 page fault 핸들러에서 KFENCE 영역인지 확인 후 리포트 kfence_handle_page_fault()
/* mm/kfence/core.c - KFENCE 핵심 구현 (간략화) */

/* KFENCE 풀 구조: [Guard][Object][Guard][Object][Guard]... */
static char *__kfence_pool;
#define KFENCE_POOL_SIZE  \
    ((CONFIG_KFENCE_NUM_OBJECTS + 1) * 2 * PAGE_SIZE)

/* 객체 메타데이터 */
struct kfence_metadata {
    enum kfence_object_state state;  /* UNUSED, ALLOCATED, FREED */
    unsigned long addr;               /* 객체 시작 주소 */
    size_t size;                      /* 요청된 크기 */
    struct kmem_cache *cache;         /* 원래 Slab 캐시 */
    depot_stack_handle_t alloc_stack; /* 할당 스택 */
    depot_stack_handle_t free_stack;  /* 해제 스택 */
};

/* 샘플링 할당 체크 */
static __always_inline bool kfence_alloc_should_sample(void)
{
    /* 타이머 기반: sample_interval마다 한 번 true */
    if (!READ_ONCE(kfence_allocation_gate))
        return false;
    WRITE_ONCE(kfence_allocation_gate, false);
    return true;
}

/* KFENCE 풀에서 할당 */
void *__kfence_alloc(struct kmem_cache *s,
                     size_t size, gfp_t flags)
{
    int idx = kfence_find_free_slot();
    if (idx < 0)
        return NULL;  /* 풀 소진 */

    unsigned long obj_page = kfence_object_page(idx);

    /* 객체를 페이지 오른쪽 끝에 정렬 (우측 OOB 탐지 최적화) */
    unsigned long addr = obj_page + PAGE_SIZE - size;
    addr = ALIGN_DOWN(addr, max(s->align, (size_t)1));

    /* 객체 페이지를 접근 가능으로 설정 */
    kfence_protect(obj_page, false);  /* PTE 쓰기 가능 */

    /* 가드 페이지는 접근 불가 유지 */
    /* [Guard: no-access][Object: read-write][Guard: no-access] */

    return (void *)addr;
}
코드 설명
  • 4-6행 KFENCE 풀은 [Guard][Object][Guard][Object]... 패턴으로 배치됩니다. 각 객체는 독립된 페이지를 사용하며 양쪽이 접근 불가 가드 페이지로 보호됩니다.
  • 38-39행 객체를 페이지 오른쪽 끝에 정렬하면, 1바이트만 초과해도 다음 페이지(가드)에 접근하게 되어 page fault로 즉시 탐지됩니다.
  • 22-25행 샘플링 게이트는 타이머(sample_interval)에 의해 주기적으로 열립니다. 게이트가 열린 상태에서 다음 kmalloc 호출이 KFENCE 풀에서 할당됩니다.
KFENCE 가드 페이지 기반 탐지 구조 KFENCE 메모리 풀 레이아웃 Guard no-access 4KB Object A read-write 4KB 페이지 Guard no-access Object B read-write Guard Object C (Freed) no-access Guard ... Guard OOB 탐지: 객체를 페이지 끝에 정렬 페이지 내부 (4KB) 사용 안 함 obj (20B) Guard 1바이트 초과 → page fault! UAF 탐지: 해제 시 no-access 해제된 Object C PTE → no-access로 변경 접근 시도 → page fault! KASAN vs KFENCE 적용 전략 개발/테스트: KASAN (Generic) → 모든 접근 검사, 바이트 수준 정밀 탐지 프로덕션: KFENCE → 샘플링 기반 ~1% 오버헤드, 가드 페이지로 확실한 탐지 ARM64 프로덕션: HW-Tag KASAN (Async) + KFENCE → 최대 커버리지 그림 21. KFENCE 가드 페이지 기반 탐지 구조 — OOB와 UAF 탐지 원리

KMSAN (Kernel Memory Sanitizer) 상세

초기화되지 않은 메모리 사용 문제

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

KMSAN의 Shadow와 Origin 추적

KMSAN은 KASAN과 유사하게 Shadow Memory를 사용하지만, 목적이 다릅니다. 각 메모리 바이트에 대해 두 종류의 메타데이터를 유지합니다:

메타데이터비율값의 의미용도
Shadow Memory 1:1 0 = 초기화됨, 비트별 1 = 해당 비트 미초기화 초기화 상태 추적
Origin Memory 4:1 (4바이트당 1개 origin) Stack Depot 핸들 (초기화되지 않은 데이터의 출처) 미초기화 데이터가 어디서 왔는지 추적
/* 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() 시 커널 데이터가 초기화되었는지 검증합니다. 미초기화 데이터가 사용자 공간으로 복사되면 정보 누출(information disclosure) 취약점이 됩니다.

KMSAN 리포트 예시

=====================================================
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()로 사용자 공간에 복사 → 정보 누출

UBSAN (Undefined Behavior Sanitizer) 상세

UBSAN이 탐지하는 정의되지 않은 동작

UBSAN은 C 언어 표준에서 정의되지 않은 동작(Undefined Behavior, UB)을 런타임에 탐지합니다. UB는 컴파일러가 임의로 최적화할 수 있는 영역이므로 예측 불가능한 버그와 보안 취약점의 원인이 됩니다.

탐지 유형CONFIG 옵션예시위험도
부호 있는 정수 오버플로 CONFIG_UBSAN_SIGNED_WRAP INT_MAX + 1 높음 (보안 취약점)
배열 인덱스 범위 초과 CONFIG_UBSAN_BOUNDS arr[size] (고정 크기 배열) 높음
정렬 오류 CONFIG_UBSAN_ALIGNMENT 4바이트 정렬 필요 타입의 비정렬 접근 중간 (아키텍처 의존)
부호 없는 정수 오버플로 CONFIG_UBSAN_UNSIGNED_WRAP 0u - 1 (의도적 wrap 제외) 중간
NULL 포인터 역참조 CONFIG_UBSAN_OBJECT_SIZE ((struct foo *)NULL)->member 높음
시프트 오류 CONFIG_UBSAN_SHIFT 1 << 33 (32비트 타입) 중간
bool 잘못된 값 CONFIG_UBSAN_BOOL 0/1 외의 값이 bool로 로드 낮음
enum 잘못된 값 CONFIG_UBSAN_ENUM 정의 범위 밖의 enum 값 낮음
/* UBSAN 정수 오버플로 탐지 예시 */

/* 취약 코드: 부호 있는 정수 오버플로 */
int vulnerable_calc(int a, int b)
{
    return a + b;  /* a=INT_MAX, b=1이면 UB! */
}

/* UBSAN이 삽입하는 체크 (개념적) */
int vulnerable_calc(int a, int b)
{
    if (__builtin_add_overflow(a, b, &result))
        __ubsan_handle_add_overflow(a, b);  /* 리포트! */
    return a + b;
}

/* UBSAN 배열 범위 탐지 예시 */
struct example {
    int data[10];
};

void test_bounds(struct example *e, int idx)
{
    /* idx=10이면 UBSAN이 탐지: array index 10 out of bounds */
    e->data[idx] = 42;
}

/* UBSAN 커널 설정 */
/*
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y            # 배열 범위 (가장 유용)
CONFIG_UBSAN_SIGNED_WRAP=y       # 부호 있는 정수 오버플로
CONFIG_UBSAN_SHIFT=y             # 시프트 오류
CONFIG_UBSAN_TRAP=n              # 리포트만 (y이면 즉시 트랩)
*/

UBSAN 리포트 예시

================================================================================
UBSAN: array-index-out-of-bounds in drivers/net/example.c:42:15
index 10 is out of range for type 'int [10]'
CPU: 1 PID: 1234 Comm: test Not tainted 6.8.0 #1
Call trace:
 dump_backtrace+0x0/0x1e0
 ubsan_epilogue+0x10/0x50
 __ubsan_handle_out_of_bounds+0x68/0x80
 test_bounds+0x38/0x50
 example_ioctl+0x20/0x40
================================================================================

UBSAN: signed-integer-overflow in fs/ext4/resize.c:123:30
2147483647 + 1 cannot be represented in type 'int'
================================================================================

KCSAN (Kernel Concurrency Sanitizer) 상세

데이터 레이스 탐지 원리

KCSAN은 커널의 데이터 레이스(data race)를 탐지합니다. 데이터 레이스는 두 스레드가 동시에 같은 메모리 위치에 접근하고, 그중 적어도 하나가 쓰기이며, 적절한 동기화(synchronization)가 없는 상황을 말합니다. C11 메모리 모델에서 데이터 레이스는 정의되지 않은 동작(UB)입니다.

KCSAN의 워치포인트 기반 샘플링

KCSAN은 컴파일러 계측으로 모든 메모리 접근에 체크 코드를 삽입하되, 실제로는 확률적 샘플링으로 동작합니다. 하나의 접근에 "워치포인트(watchpoint)"를 설정하고, 짧은 딜레이(delay) 동안 다른 CPU에서 같은 주소에 접근하는지 감시합니다.

단계동작비고
1. 접근 선택 확률적으로 메모리 접근 하나를 선택 모든 접근을 감시하면 오버헤드가 너무 큼
2. 워치포인트 설정 해당 주소에 워치포인트 등록 per-CPU 워치포인트 슬롯 사용
3. 딜레이 짧은 시간(~수 마이크로초) 대기 다른 CPU의 접근이 발생할 시간 확보
4. 충돌 감지 다른 CPU에서 같은 주소 접근 시 워치포인트 히트 적어도 하나가 쓰기이면 데이터 레이스
5. 리포트 두 접근의 스택 트레이스 출력 어떤 동기화가 필요한지 안내
/* kernel/kcsan/core.c - KCSAN 워치포인트 체크 (간략화) */

static __always_inline void check_access(
    const volatile void *ptr,
    size_t size, int type)
{
    /* 1. 기존 워치포인트와 충돌하는지 체크 */
    long encoded_watchpoint;
    if (find_matching_watchpoint(
            (unsigned long)ptr, size,
            type & KCSAN_ACCESS_WRITE,
            &encoded_watchpoint)) {
        /* 충돌! 데이터 레이스 발견 */
        kcsan_report_known_origin(
            ptr, size, type, encoded_watchpoint);
        return;
    }

    /* 2. 확률적으로 이 접근에 워치포인트 설정 */
    if (!kcsan_should_sample())
        return;

    /* 3. 워치포인트 설정 */
    insert_watchpoint((unsigned long)ptr, size, type);

    /* 4. 짧은 딜레이 (다른 CPU의 접근 기회 제공) */
    udelay(KCSAN_UDELAY_TASK);

    /* 5. 워치포인트 해제 */
    remove_watchpoint((unsigned long)ptr);
}

KCSAN 리포트 해석

==================================================================
BUG: KCSAN: data-race in task_struct_update / task_struct_read

write to 0xffff888103a5e000 of 4 bytes by task 1234 on cpu 0:
 task_struct_update+0x30/0x60
 worker_thread+0x1a0/0x340

read to 0xffff888103a5e000 of 4 bytes by task 5678 on cpu 2:
 task_struct_read+0x18/0x40
 another_worker+0x80/0x120

value changed: 0x00000001 → 0x00000002

Reported by Kernel Concurrency Sanitizer on:
CPU: 0 PID: 1234 Comm: worker Not tainted 6.8.0 #1
==================================================================

분석:
  - task 1234 (CPU 0)가 주소 0xffff888103a5e000에 4바이트 쓰기
  - task 5678 (CPU 2)가 같은 주소에서 4바이트 읽기
  - 동기화 없이 동시 접근 → 데이터 레이스
  - 해결: 적절한 락, READ_ONCE/WRITE_ONCE, 또는 atomic 연산 사용
KCSAN 구성 옵션: CONFIG_KCSAN=y로 활성화합니다. CONFIG_KCSAN_STRICT=yREAD_ONCE/WRITE_ONCE 없이 일반 C 접근으로 공유 변수에 접근하는 것도 데이터 레이스로 리포트합니다. KCSAN_SANITIZE := n으로 특정 파일/디렉토리를 제외할 수 있습니다.

실전 버그 사례 확장

사례 3: Double-Free in 네트워크 필터

/* 취약 코드 (간략화) - netfilter conntrack */
static void nf_ct_destroy(struct nf_conntrack *ct)
{
    /* 참조 카운트 감소 후 해제 */
    if (atomic_dec_and_test(&ct->use)) {
        nf_ct_remove(ct);
        kfree(ct);  /* 첫 번째 해제 */
    }
}

/* 경합 조건: 다른 CPU에서 같은 ct 참조 */
static void nf_ct_timer_expire(struct timer_list *t)
{
    struct nf_conntrack *ct = from_timer(ct, t, timeout);

    /* BUG: ct가 이미 해제된 후 이 타이머가 실행될 수 있음 */
    if (atomic_dec_and_test(&ct->use)) {
        nf_ct_remove(ct);
        kfree(ct);  /* 두 번째 해제 → double-free! */
    }
}
KASAN 탐지 리포트:
BUG: KASAN: double-free in kfree+0x94/0x130

Free of addr ffff888100ab4200 by task ksoftirqd/3:
 kasan_save_free_info+0x30/0x48
 __kasan_slab_free+0x40/0x58
 kfree+0x94/0x130
 nf_ct_timer_expire+0x58/0x80     ← 두 번째 해제

Previously freed by task test/4567:
 kasan_save_free_info+0x30/0x48
 __kasan_slab_free+0x40/0x58
 kfree+0x94/0x130
 nf_ct_destroy+0x48/0x60          ← 첫 번째 해제

해결: del_timer_sync() 호출로 타이머 만료 전 안전하게 제거

사례 4: 스택 버퍼 오버플로 (Stack Buffer Overflow)

/* 취약 코드 (간략화) - 드라이버 IOCTL 핸들러 */
static long driver_ioctl(struct file *f,
    unsigned int cmd, unsigned long arg)
{
    char buf[64];
    size_t len;

    if (copy_from_user(&len, (void __user *)arg,
                       sizeof(len)))
        return -EFAULT;

    /* BUG: len 검증 없이 buf에 복사 → 스택 OOB! */
    if (copy_from_user(buf,
            (void __user *)(arg + sizeof(len)), len))
        return -EFAULT;

    /* 해결: if (len > sizeof(buf)) return -EINVAL; */
}
Generic KASAN (CONFIG_KASAN_STACK=y) 탐지:
BUG: KASAN: stack-out-of-bounds in driver_ioctl+0x78/0xc0
Write of size 128 at addr ffff888012345e00 by task exploit/9999

Memory state:
  ffff888012345d00: 00 00 00 00 00 00 00 00 f9 f9 f9 f9 00 00 00 00
  ffff888012345d80: 00 00 00 00 fa fa fa fa f8 f8 f8 f8 f8 f8 f8 f8
→ 스택 변수 buf[64] = 8개의 00 (64바이트)
  128바이트 복사 시도 → Mid Redzone(f9) 침범 → 탐지

사례 5: KMSAN으로 발견된 정보 누출

/* 취약 코드 - 사용자에게 구조체 반환 시 패딩 미초기화 */
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)); 후 필드 설정 */
}
커널 Sanitizer 도구 비교 아키텍처 KASAN 메모리 접근 오류 OOB / UAF / double-free Shadow Memory / 태그 오버헤드: 5%~200% HW-Tag: 프로덕션 가능 KFENCE 메모리 접근 오류 (샘플링) OOB / UAF (일부 할당만) 가드 페이지 (page fault) 오버헤드: ~1% 프로덕션 권장 KMSAN 미초기화 메모리 사용 uninit-value / info-leak Shadow(1:1) + Origin(4:1) 오버헤드: ~300% 개발/테스트 전용 UBSAN 정의되지 않은 동작 정수 오버플로/배열 범위 KCSAN 데이터 레이스 워치포인트 기반 샘플링 공통 기반: GCC / Clang 컴파일러 계측 (-fsanitize=...) 탐지 영역 커버리지 매트릭스 버그 유형 KASAN KFENCE KMSAN UBSAN KCSAN Heap OOB Use-After-Free 미초기화 사용 정수 오버플로 데이터 레이스 ● = 주요 탐지 대상 ◐ = 샘플링/부분 탐지 ○ = 미지원 그림 22. 커널 Sanitizer 도구 비교 — 탐지 영역, 원리, 오버헤드, 프로덕션 적합성

CI/CD 통합과 자동화된 Sanitizer 테스트

syzkaller + KASAN 연동 아키텍처

syzkaller는 Google이 개발한 커널 퍼저(fuzzer)로, KASAN 활성 커널에서 무작위 시스템 콜 시퀀스를 생성하여 버그를 자동 발견합니다. syzkaller는 커버리지 기반(Coverage-guided) 퍼징으로 새로운 코드 경로를 탐색하며, KASAN이 버그를 탐지하면 해당 시스템 콜 시퀀스를 최소화하여 재현 가능한 테스트 케이스를 생성합니다.

구성 요소역할설정
syz-manager 퍼저 매니저, VM 관리, 크래시 수집 호스트 머신에서 실행
syz-fuzzer 각 VM 내에서 시스템 콜 생성 KASAN 활성 커널 VM 내부
syz-executor 생성된 시스템 콜 프로그램 실행 샌드박스(Sandbox)에서 실행
KASAN 커널 버그 탐지 및 리포트 출력 CONFIG_KASAN=y, KCOV=y
syz-repro 크래시 재현 시퀀스 최소화 자동 바이너리(binary) 서치

syzkaller 설정 예시

// syzkaller 설정 파일 (syz-manager.cfg)
{
    "name": "linux-kasan",
    "target": "linux/amd64",
    "http": ":56741",
    "workdir": "/root/syzkaller/workdir",
    "kernel_obj": "/root/linux",
    "image": "/root/image/bullseye.img",
    "syzkaller": "/root/syzkaller",
    "type": "qemu",
    "vm": {
        "count": 4,
        "cpu": 2,
        "mem": 4096,
        "kernel": "/root/linux/arch/x86/boot/bzImage",
        "cmdline": "kasan.fault=report kasan.multi_shot=on"
    },
    "enable_syscalls": [
        "open", "read", "write", "close",
        "mmap", "ioctl", "socket", "connect",
        "sendmsg", "recvmsg"
    ]
}

CI/CD 파이프라인에서 Sanitizer 활용

#!/bin/bash
# CI/CD 파이프라인: Sanitizer 활성 커널 빌드 + 테스트

# 1단계: KASAN 활성 커널 빌드
make defconfig
./scripts/config -e CONFIG_KASAN -e CONFIG_KASAN_GENERIC
./scripts/config -e CONFIG_KASAN_INLINE -e CONFIG_KASAN_STACK
./scripts/config -e CONFIG_KASAN_VMALLOC
./scripts/config -e CONFIG_KFENCE
./scripts/config -e CONFIG_UBSAN -e CONFIG_UBSAN_BOUNDS
./scripts/config -e CONFIG_KCSAN
./scripts/config -e CONFIG_KMSAN
./scripts/config -e CONFIG_KCOV  # syzkaller 커버리지
make olddefconfig
make -j$(nproc) 2>&1 | tee build.log

# 2단계: KUnit 자체 테스트 실행
./tools/testing/kunit/kunit.py run \
    --kconfig_add CONFIG_KASAN=y \
    --kconfig_add CONFIG_KASAN_GENERIC=y \
    --kconfig_add CONFIG_KFENCE=y \
    kasan kfence 2>&1 | tee kunit.log

# 3단계: QEMU에서 부팅 테스트
timeout 300 qemu-system-x86_64 \
    -kernel arch/x86/boot/bzImage \
    -append "console=ttyS0 kasan.fault=panic" \
    -initrd rootfs.cpio.gz \
    -nographic -m 4G \
    2>&1 | tee boot.log

# 4단계: KASAN 리포트 체크
if grep -q "BUG: KASAN" boot.log; then
    echo "FAIL: KASAN bug detected!"
    exit 1
fi

# 5단계: syzkaller 단기 퍼징 (선택)
timeout 3600 syz-manager -config syz-ci.cfg 2>&1 | tee fuzz.log
if grep -q "crashes:" fuzz.log; then
    echo "WARNING: Fuzzer found crashes"
    exit 1
fi

echo "PASS: All sanitizer tests passed"
CI/CD Sanitizer 파이프라인 git push 커밋/PR 빌드 KASAN+UBSAN+KCSAN KUnit 자체 테스트 부팅 테스트 QEMU + fault=panic Fuzzing syzkaller Sanitizer 리포트? No PASS → 머지 허용 Yes FAIL → 리포트 수집 + 리그레션 등록 syzkaller + KASAN 퍼징 아키텍처 syz-manager 호스트: VM 관리 크래시 수집/분류 VM 0 (KASAN) VM 1 (KASAN) VM N (KASAN) syz-fuzzer syz-executor syscall BUG: KASAN 리포트 → syz-manager로 전달 → 재현 프로그램 자동 생성 그림 23. CI/CD Sanitizer 파이프라인과 syzkaller 퍼징 아키텍처

syzbot 대시보드 활용

Google은 syzkaller를 24/7 운영하여 리눅스 커널 upstream에서 발견한 버그를 syzbot 대시보드에 자동으로 보고합니다. 각 버그에는 다음 정보가 포함됩니다:

Sanitizer 병용 전략 정리:
  • 개발 중: KASAN (Generic) + UBSAN + KCSAN → 최대 커버리지
  • CI/CD: KASAN (Inline) + UBSAN + KUnit → 빠른 피드백
  • 퍼징: KASAN + KMSAN + syzkaller → 심층 버그 탐색
  • 프로덕션: KFENCE + UBSAN (bounds) + HW-Tag KASAN (ARM64) → 최소 오버헤드

참고자료

커널 공식 문서

소스 코드 위치

소스 코드 위치 (추가)

LWN.net 기사

Google Sanitizer 프로젝트

컨퍼런스 발표 및 학술 논문

ARM MTE 및 하드웨어 지원

다음 학습: