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 할당자 통합, 리포트 분석 방법, 성능 전략까지 전 영역을 심층적으로 다룹니다.
핵심 요약
- KASAN -- 커널 주소 공간(Address Space)의 메모리 접근 오류를 런타임에 탐지하는 동적 분석 도구입니다.
- Shadow Memory -- 실제 메모리 8바이트당 1바이트의 메타데이터를 유지하여 접근 가능 여부를 추적합니다.
- Generic KASAN -- 컴파일러가 모든 메모리 접근에 체크 코드를 삽입하는 소프트웨어 방식입니다.
- Tag-Based KASAN -- 포인터와 메모리에 태그를 부여하여 불일치를 탐지합니다(SW-Tag/HW-Tag).
- Quarantine -- 해제된 객체를 일정 기간 격리(Isolation)하여 use-after-free 탐지 확률을 높입니다.
단계별 이해
- 메모리 안전성 문제 인식
커널 메모리 버그(OOB, UAF, double-free)가 왜 치명적인지 이해합니다. - Shadow Memory 개념 파악
8:1 매핑(Mapping)으로 메모리 상태를 추적하는 원리를 학습합니다. - 컴파일러 삽입 체크 이해
GCC/Clang이 모든 메모리 접근 전에 삽입하는 체크 코드의 동작을 파악합니다. - KASAN 리포트 읽기
버그 발생 시 출력되는 리포트의 각 필드를 해석하는 방법을 익힙니다. - 커널 빌드 및 실전 적용
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 KASAN | SW-Tag KASAN | HW-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 동작 원리 -- 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 곱하기)하면 원본 주소입니다.
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이 아닌 값이 있으면 에러입니다.
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 태그를 비교합니다.
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) 시 현재 태그 값을 확인하는 데 사용합니다.
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
메모리 접근 체크 흐름
__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바이트 접근에 대해 각각 체크 함수가 생성됩니다.
버그 탐지 유형
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 */
}
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 큐가 한계를 초과하면 전역 큐로 이동하고, 가장 오래된 배치를 실제 해제합니다.
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 KASAN | SW-Tag / HW-Tag KASAN |
|---|---|---|
| Quarantine 사용 | 필수 (UAF 탐지의 핵심) | 선택적 (태그 변경으로 UAF 탐지 가능) |
| UAF 탐지 원리 | Shadow를 F5로 poisoning | 해제 시 태그를 변경 → 구 포인터 태그 불일치 |
| 재할당 시 동작 | Quarantine 해제 후 재할당 시 UAF 놓칠 수 있음 | 재할당 시 새 태그 부여 → 구 포인터로 접근 시 탐지 |
| 메모리 오버헤드 | 높음 (격리된 객체가 메모리 점유) | 낮음 (Quarantine 없이도 기본 탐지 가능) |
| 탐지 확률 | 결정적 (Quarantine 중 100%) | 확률적 (SW: 255/256, HW: 15/16) |
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_64 | ARM64 (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 내장) |
스택 변수 계측 (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);
/* 일정 시간 후 재사용 허용 */
}
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를 정리합니다.
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 프로그램의 메모리 접근은 별도 검증 |
커널 모듈과 KASAN
모듈 로드 시 KASAN 처리
커널 모듈(.ko)이 로드될 때 KASAN은 다음 작업을 수행합니다:
- 모듈의 코드/데이터가 배치되는 vmalloc 영역의 Shadow 페이지를 할당합니다.
- 모듈이
-fsanitize=kernel-address로 컴파일되었으면, 모듈의.kasan_globals섹션에서 전역 변수 목록을 읽어__asan_register_globals()를 호출합니다. - 모듈의 함수 내 모든 메모리 접근이 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
커널 설정
주요 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배) |
| 권장 용도 | 성능 민감한 테스트 | 일반 개발/디버깅 (기본 권장) |
성능 오버헤드와 운영 전략
모드별 성능 영향
운영 전략별 권장 설정
| 운영 목적 | 권장 모드 | 설정 | 근거 |
|---|---|---|---|
| 개발 중 디버깅 | 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 |
프로덕션급 성능이면서 즉시 탐지 |
실전 버그 사례 분석
사례 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 + 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 사용 시 자주 발생하는 문제
| 문제 | 증상 | 원인 | 해결 |
|---|---|---|---|
| 부팅 실패 | 커널 패닉 (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 리포트 디버깅 체크리스트
- 버그 유형 확인:
BUG: KASAN:뒤의 문자열(use-after-free, out-of-bounds, double-free)을 확인합니다. - 접근 위치 확인:
Write/Read of size N at addr에서 접근 크기와 주소를 확인합니다. - 스택 트레이스 분석:
Call trace에서 문제를 유발한 함수를 찾습니다. 가장 위의__asan_함수를 건너뛰고 그 아래 함수가 실제 원인입니다. - 할당/해제 이력 확인: UAF 버그라면
Allocated by task와Freed by task를 비교하여 해제 경로를 파악합니다. - Shadow 덤프 해석:
Memory state섹션의^마커가 실제 문제 위치를 가리킵니다. - Slab 캐시 확인:
cache kmalloc-N에서 어떤 크기의 Slab 캐시인지 확인합니다. - 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 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 | 정의되지 않은 동작 | 컴파일러 삽입 체크 | 낮음 | 가능 |
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 KASAN | SW-Tag KASAN | HW-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) |
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_OFFSET | Shadow 크기 |
|---|---|---|---|
| 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 미할당 영역 */
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에 반환 */
}
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 풀에서 할당됩니다.
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 연산 사용
CONFIG_KCSAN=y로 활성화합니다.
CONFIG_KCSAN_STRICT=y는 READ_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)); 후 필드 설정 */
}
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"
syzbot 대시보드 활용
Google은 syzkaller를 24/7 운영하여 리눅스 커널 upstream에서 발견한 버그를 syzbot 대시보드에 자동으로 보고합니다. 각 버그에는 다음 정보가 포함됩니다:
- 크래시 리포트: KASAN/KMSAN/KCSAN/UBSAN 등의 전체 리포트
- 재현 프로그램: C 또는 syz 형식의 최소 재현 코드
- 커밋 범위: 버그가 도입된 커밋 범위 (bisect 결과)
- 패치 테스트: 수정 패치를 제출하면 자동으로 테스트하여 수정 여부 확인
- 개발 중: KASAN (Generic) + UBSAN + KCSAN → 최대 커버리지
- CI/CD: KASAN (Inline) + UBSAN + KUnit → 빠른 피드백
- 퍼징: KASAN + KMSAN + syzkaller → 심층 버그 탐색
- 프로덕션: KFENCE + UBSAN (bounds) + HW-Tag KASAN (ARM64) → 최소 오버헤드
참고자료
커널 공식 문서
- Kernel Address Sanitizer (KASAN) -- Linux Kernel Documentation
- KFENCE -- Kernel Electric-Fence
- KMSAN -- Kernel Memory Sanitizer
- KCSAN -- Kernel Concurrency Sanitizer Documentation
- UBSAN -- Undefined Behavior Sanitizer Documentation
- Kernel Testing Overview -- 커널 테스트 및 디버깅 도구 종합 안내
소스 코드 위치
mm/kasan/-- KASAN 핵심 구현 (generic.c, sw_tags.c, hw_tags.c, common.c, quarantine.c, report.c)include/linux/kasan.h-- KASAN 공개 API 헤더mm/kasan/kasan.h-- KASAN 내부 헤더 (Shadow 값 정의)lib/kasan_test.c-- KASAN KUnit 테스트arch/x86/include/asm/kasan.h-- x86_64 Shadow 레이아웃arch/arm64/include/asm/kasan.h-- ARM64 Shadow 레이아웃arch/arm64/include/asm/mte.h-- ARM MTE 지원
소스 코드 위치 (추가)
mm/kfence/-- KFENCE 핵심 구현 (core.c, report.c, kfence.h)mm/kmsan/-- KMSAN 핵심 구현 (core.c, hooks.c, shadow.c)kernel/kcsan/-- KCSAN 핵심 구현 (core.c, report.c, kcsan.h)lib/ubsan.c-- UBSAN 런타임 핸들러lib/stackdepot.c-- Stack Depot (KASAN/KFENCE/KMSAN 공통)arch/arm64/include/asm/mte.h-- ARM64 MTE 지원 (HW-Tag KASAN)
LWN.net 기사
- Kernel address sanitizer (LWN, 2014) -- KASAN의 초기 소개 및 커널 통합 과정을 다룬 기사입니다
- Tag-based KASAN (LWN, 2019) -- SW-Tag 모드의 설계 배경과 ARM64 Top-Byte Ignore 활용 방식을 설명합니다
- Hardware tag-based KASAN with MTE (LWN, 2020) -- ARM MTE 기반 HW-Tag KASAN의 구현 및 성능 이점을 분석합니다
- Detecting memory errors with KFENCE (LWN, 2020) -- 프로덕션 환경에서 사용 가능한 경량 메모리 오류 탐지 도구 KFENCE를 소개합니다
- The Kernel Concurrency Sanitizer (LWN, 2020) -- KCSAN의 동작 원리와 데이터 레이스 탐지 메커니즘을 설명합니다
- The Undefined Behavior Sanitizer - UBSAN (LWN, 2012) -- 정의되지 않은 동작(Undefined Behavior) 탐지 도구의 원리와 활용법을 다룹니다
- Finding bugs with sanitizers (LWN, 2018) -- 커널 Sanitizer 도구들의 종합 비교 및 실전 활용 가이드입니다
Google Sanitizer 프로젝트
- AddressSanitizer (ASan) -- Google Sanitizers Wiki
- AddressSanitizer Algorithm -- Shadow Memory 매핑 알고리즘 상세 설명
- MemorySanitizer (MSan) -- 초기화되지 않은 메모리 읽기 탐지 도구
- ThreadSanitizer (TSan) -- 데이터 레이스 탐지 도구 (KCSAN의 사용자 공간 대응)
- Clang AddressSanitizer -- LLVM/Clang ASan 컴파일러 지원 문서
- Clang UndefinedBehaviorSanitizer -- LLVM/Clang UBSan 문서
- syzkaller -- Kernel Fuzzer (GitHub) -- KASAN과 결합하여 커널 버그를 자동 탐지하는 퍼저입니다
- syzbot -- Linux Kernel Bug Tracker -- KASAN이 탐지한 실제 커널 버그 리포트를 확인할 수 있습니다
컨퍼런스 발표 및 학술 논문
- Serebryany, K. et al. "AddressSanitizer: A Fast Address Sanity Checker" (USENIX ATC 2012) -- KASAN의 이론적 기반이 되는 핵심 논문입니다
- Stepanov, E. et al. "MemorySanitizer: fast detector of uninitialized memory use in C++" (CGO 2015) -- KMSAN의 사용자 공간 대응 도구에 관한 논문입니다
- KASAN: How to Find Bugs in the Linux Kernel -- Andrey Ryabinin (Linux Foundation) -- KASAN 개발자가 직접 발표한 설계 및 구현 상세입니다
- Finding races and memory errors with compiler instrumentation (Linux Plumbers 2019) -- 커널 Sanitizer 도구 개발 현황과 향후 계획을 발표한 세션입니다
- KMSAN: Kernel Memory Sanitizer (Linux Plumbers 2022) -- KMSAN의 커널 통합 과정과 탐지 사례를 다룬 발표입니다
ARM MTE 및 하드웨어 지원
- ARM Memory Tagging Extension (MTE) -- ARM Developer
- Enhancing Memory Safety -- ARM Community Blog -- MTE를 활용한 메모리 안전성 향상 전략을 설명합니다
- ARM MTE on Android -- Android Open Source Project -- Android에서의 MTE 활용 사례로, HW-Tag KASAN과 동일한 하드웨어 기반입니다
- 메모리 관리 개요 -- 커널 메모리 할당 기초
- Slab Allocator (SLUB/SLOB) -- KASAN이 통합되는 할당자 구조
- 디버깅 & 트러블슈팅 -- 커널 디버깅 종합 가이드
- 커널 보안 -- 메모리 안전성과 보안 취약점(Vulnerability)