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 -- 커널 주소 공간의 메모리 접근 오류를 런타임에 탐지하는 동적 분석 도구입니다.
- Shadow Memory -- 실제 메모리 8바이트당 1바이트의 메타데이터를 유지하여 접근 가능 여부를 추적합니다.
- Generic KASAN -- 컴파일러가 모든 메모리 접근에 체크 코드를 삽입하는 소프트웨어 방식입니다.
- Tag-Based KASAN -- 포인터와 메모리에 태그를 부여하여 불일치를 탐지합니다(SW-Tag/HW-Tag).
- Quarantine -- 해제된 객체를 일정 기간 격리하여 use-after-free 탐지 확률을 높입니다.
단계별 이해
- 메모리 안전성 문제 인식
커널 메모리 버그(OOB, UAF, double-free)가 왜 치명적인지 이해합니다. - Shadow Memory 개념 파악
8:1 매핑으로 메모리 상태를 추적하는 원리를 학습합니다. - 컴파일러 삽입 체크 이해
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 전용 |
| 메모리 오버헤드 | 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 (왼쪽) | 할당 객체의 시작 전 패딩 영역 |
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 곱하기)하면 원본 주소입니다.
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에서 도입된 하드웨어 메모리 태깅 확장입니다. 물리 메모리의 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명령은 메모리에 저장된 태그를 포인터에 로드합니다. 디버깅 시 현재 태그 값을 확인하는 데 사용합니다.
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행
매크로로 각 크기별 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바이트 접근에 대해 각각 체크 함수가 생성됩니다.
버그 탐지 유형
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 큐를 사용하여 락 경합을 최소화합니다.
-
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 배칭과 메모리 회수
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 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 캐시 이름과 크기 | 어떤 슬랩 캐시의 객체인지 파악 |
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_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합니다. 이를 통해 스택 버퍼 오버플로를 탐지합니다.
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)에 가까운 함수에서 스택 오버플로가 발생할 수 있으므로
커널 스택 크기를 늘리는 것(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를 정리합니다.
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%), 실시간 모니터링 가능 |
| ARM64 릴리스 검증 | HW-Tag (Sync) | kasan.mode=sync |
프로덕션급 성능이면서 즉시 탐지 |
실전 버그 사례 분석
사례 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 + 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 사용 시 자주 발생하는 문제
| 문제 | 증상 | 원인 | 해결 |
|---|---|---|---|
| 부팅 실패 | 커널 패닉 (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 리포트 디버깅 체크리스트
- 버그 유형 확인:
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 자체를 조작하는 코드 등에 사용 */
}
참고자료
커널 공식 문서
- Kernel Address Sanitizer (KASAN) -- Linux Kernel Documentation
- KFENCE -- Kernel Electric-Fence
- KMSAN -- Kernel Memory Sanitizer
소스 코드 위치
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 지원
외부 자료
- AddressSanitizer (ASan) -- Google Sanitizers Wiki
- syzbot -- Linux Kernel Bug Tracker
- ARM Memory Tagging Extension (MTE) -- ARM Developer
- Serebryany, K. et al. "AddressSanitizer: A Fast Address Sanity Checker" (USENIX ATC 2012)
- 메모리 관리 개요 -- 커널 메모리 할당 기초
- Slab Allocator (SLUB/SLOB) -- KASAN이 통합되는 할당자 구조
- 디버깅 & 트러블슈팅 -- 커널 디버깅 종합 가이드
- 커널 보안 -- 메모리 안전성과 보안 취약점