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 할당자 통합, 리포트 분석 방법, 성능 전략까지 전 영역을 심층적으로 다룹니다.

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

핵심 요약

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

단계별 이해

  1. 메모리 안전성 문제 인식
    커널 메모리 버그(OOB, UAF, double-free)가 왜 치명적인지 이해합니다.
  2. Shadow Memory 개념 파악
    8:1 매핑으로 메모리 상태를 추적하는 원리를 학습합니다.
  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 전용
메모리 오버헤드 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 (왼쪽) 할당 객체의 시작 전 패딩 영역
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 함수 반환 후 스택 프레임 영역
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 + 오프셋입니다.
  • 5행 KASAN_SHADOW_OFFSET은 아키텍처별로 다르며, Shadow Memory가 배치되는 가상 주소 시작점입니다.
  • 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에서 도입된 하드웨어 메모리 태깅 확장입니다. 물리 메모리의 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 명령은 메모리에 저장된 태그를 포인터에 로드합니다. 디버깅 시 현재 태그 값을 확인하는 데 사용합니다.
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행 매크로로 각 크기별 load/store 체크 함수를 생성합니다. memory_is_poisoned()가 true이면 kasan_report()를 호출합니다.
  • 6행 kasan_arch_is_ready()는 부팅 초기 KASAN이 아직 초기화되지 않은 시점에서 체크를 건너뜁니다.
  • 8행 unlikely()로 분기 예측 힌트를 제공하여 정상 경로(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 큐를 사용하여 락 경합을 최소화합니다.
  • 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 배칭과 메모리 회수

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 접근은 락 없이, 전역 큐 삽입만 스핀락으로 보호합니다.
  • 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 캐시 이름과 크기 어떤 슬랩 캐시의 객체인지 파악
Memory state Shadow Memory 덤프 주변 메모리 상태를 시각적으로 확인 (^가 문제 위치)

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 영역의 페이지 테이블을 구축해야 하므로, 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합니다. 이를 통해 스택 버퍼 오버플로를 탐지합니다. 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)에 가까운 함수에서 스택 오버플로가 발생할 수 있으므로 커널 스택 크기를 늘리는 것(THREAD_SIZE)을 고려해야 합니다.

전역 변수 계측 (Global Instrumentation)

전역 변수 Redzone 메커니즘

Generic KASAN은 전역 변수(BSS, Data 섹션)에도 Redzone을 추가합니다. 컴파일러가 각 전역 변수를 __asan_global 구조체로 등록하면, 부팅 시 __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행 커널 모듈 언로드 시 해당 모듈의 전역 변수 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%), 실시간 모니터링 가능
ARM64 릴리스 검증 HW-Tag (Sync) kasan.mode=sync 프로덕션급 성능이면서 즉시 탐지
프로덕션 환경 주의: Generic KASAN은 성능 오버헤드가 커서 프로덕션에 적합하지 않습니다. 프로덕션 환경에서는 ARM64 HW-Tag KASAN(비동기 모드)만 권장됩니다. x86_64에서는 프로덕션 KASAN을 사용하지 않는 것이 일반적입니다.

실전 버그 사례 분석

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

NFC 서브시스템의 소켓 해제 경합 조건으로 인한 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: 파일시스템 OOB (ext4 확장 속성)

/* 취약 코드 (간략화) - 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 활성 커널에서 시스템 콜을 무작위로 생성하여 커널 버그를 자동으로 발견합니다. 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은 컴파일러 최적화를 방지합니다.
  • 35-43행 KUnit 테스트 스위트로 등록하여 kunit.py run으로 실행 가능합니다.

KASAN 메타데이터와 Stack Depot

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

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

/* 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 연결 리스트로 재사용합니다(메모리 절약).
  • 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 리포트 어셈블리 코드/인라인 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 자체를 조작하는 코드 등에 사용 */
}

참고자료

커널 공식 문서

소스 코드 위치

외부 자료

다음 학습: