KFENCE (Kernel Electric-Fence)
KFENCE는 프로덕션 커널에서 사용할 수 있도록 설계된 저오버헤드(Overhead) 메모리 에러 탐지 도구입니다. KASAN이 모든 메모리 접근을 검사하는 것과 달리, KFENCE는 샘플링 기반으로 일부 할당만 가드 페이지(Guard Page) 풀에서 수행해 Out-of-Bounds와 Use-After-Free를 확실하게 탐지합니다. 커널 디버깅(Debugging) 전 범위에서 활용 가능합니다.
핵심 요약
- 샘플링 할당 — 기본 100ms 간격으로 하나의 kmalloc만 KFENCE 풀에서 수행
- 가드 페이지 — 객체 양쪽에 접근 불가(PTE no-access) 페이지 배치. 경계 초과 시 즉시 page fault
- 프로덕션 가능 — 오버헤드 ~1%. KASAN(고오버헤드) 대신 장시간 운영 환경에 투입
- KASAN 보완 — 둘 다 켤 수 있음. KASAN이 놓치는 영역을 샘플링으로 확률 탐지
- 런타임 튜닝 —
/sys/module/kfence/parameters/sample_interval로 간격 즉시 조정
단계별 이해
- 부팅 시 풀 할당
KFENCE 전용 메모리 풀(기본 255개 객체, 객체 1개당 2페이지: Object+Guard)을 연속 할당합니다. - 샘플링 게이트
타이머가 매sample_interval마다kfence_allocation_gate를 true로 셋. 다음 kmalloc 한 번이 KFENCE 풀에서 수행됩니다. - 객체 배치
객체를 페이지 오른쪽 끝에 정렬하고, 좌우 가드 페이지의 PTE를 no-access로 설정합니다. - 경계 접근
OOB/UAF가 가드 페이지에 닿으면 CPU가 page fault를 발생시키고,kfence_handle_page_fault()가 리포트를 출력합니다. - 해제
kfree 시 객체 페이지 PTE를 no-access로 전환. 이후 UAF 접근도 fault로 잡힙니다.
설계 철학
KFENCE는 프로덕션 커널에서 사용할 수 있도록 설계된 저오버헤드(Overhead) 메모리 에러 탐지 도구입니다. KASAN(Kernel Address Sanitizer)이 모든 메모리 접근을 검사하는 반면, KFENCE는 샘플링 기반으로 일부 할당만 특수한 가드 페이지(Guard Page) 풀(Pool)에서 수행합니다. 기본 샘플 간격은 100ms이며, 이 간격마다 하나의 할당이 KFENCE 풀에서 이루어집니다.
동작 원리
| 동작 단계 | 설명 | 핵심 메커니즘 |
|---|---|---|
| 풀 초기화 | 부팅 시 KFENCE 전용 메모리 풀 할당 (기본 255개 객체) | 연속 물리 페이지 할당, 가드 페이지로 구분 |
| 샘플링 할당 | sample_interval(100ms)마다 다음 1회 할당을 KFENCE 풀에서 수행 | 타이머(Timer) 기반 toggle, kmalloc 경로에서 체크 |
| 가드 페이지 배치 | 할당 객체 양쪽에 접근 불가 가드 페이지 배치 | PTE를 no-access로 설정, 접근 시 page fault |
| OOB 탐지 | 객체를 페이지 끝에 정렬하여 1바이트 초과도 탐지 | 오른쪽 가드 = 다음 페이지(접근 불가) |
| UAF 탐지 | 해제 시 객체 페이지를 접근 불가로 설정 | PTE를 no-access로 변경, 이후 접근 시 fault |
| 에러 리포트 | page fault 핸들러(Handler)에서 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)
/* 객체 메타데이터 (mm/kfence/kfence.h, 실제 필드 요약) */
struct kfence_track {
pid_t pid; /* 할당/해제한 태스크 PID */
int cpu; /* CPU 번호 */
u64 ts_nsec; /* 타임스탬프 */
int num_stack_entries;
unsigned long stack_entries[KFENCE_STACK_DEPTH];
};
struct kfence_metadata {
struct list_head list; /* 사용/자유 리스트 연결 */
struct rcu_head rcu_head; /* RCU 지연 해제 */
raw_spinlock_t lock;
enum kfence_object_state state; /* UNUSED, ALLOCATED, FREED */
unsigned long addr; /* 객체 시작 주소 */
size_t size; /* 요청된 크기 */
struct kmem_cache *cache; /* 원래 Slab 캐시 */
unsigned long unprotected_page; /* 일시적 보호 해제 페이지 */
struct kfence_track alloc_track; /* 할당 스택+PID+시간 */
struct kfence_track free_track; /* 해제 스택+PID+시간 */
u32 alloc_stack_hash; /* 스택 해시 (covered counter용) */
/* CONFIG_MEMCG: obj_exts — 메모리 cgroup 정보 */
};
/* 샘플링 할당 체크 */
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 풀에서 할당됩니다.
가드 페이지 탐지 구조
커널 내부 API
KFENCE는 외부로 몇 개의 작은 API만 노출합니다. 대부분의 커널 코드는 직접 호출하지 않지만, 페이지 폴트(Page Fault) 핸들러와 Slab 핵심 코드는 이들 API를 경유해 KFENCE 영역을 식별합니다.
| API | 역할 | 호출 위치 |
|---|---|---|
is_kfence_address(addr) |
주소가 KFENCE 풀에 속하는지 O(1) 확인 | Slab free 경로, page fault 핸들러 |
kfence_handle_page_fault(addr, is_write, regs) |
page fault가 KFENCE 가드에 닿았는지 판단하고 리포트 출력 | 각 아키텍처 fault 경로 (do_kern_addr_fault) |
__kfence_alloc(s, size, flags) |
KFENCE 풀에서 객체 할당 (샘플링 게이트 true일 때만 호출) | __slab_alloc, kmem_cache_alloc |
__kfence_free(obj) |
KFENCE 객체 해제, PTE를 no-access로 전환 | Slab free 경로 |
kfence_shutdown_cache(s) |
kmem_cache 파괴 시 KFENCE 풀 내 객체 무효화(Invalidation) | kmem_cache_destroy |
KASAN과의 병용 전략
KFENCE와 KASAN은 상호 배타적이지 않습니다. 동시에 활성화하면 KASAN이 계측 가능한 할당을 먼저 검사하고, 샘플링 시점의 할당은 KFENCE 풀에서 수행되어 가드 페이지로 별도 보호됩니다.
- 개발/CI —
CONFIG_KASAN=y+CONFIG_KFENCE=y: 정밀 탐지와 프로덕션 유사 환경을 동시 검증 - ARM64 프로덕션 — HW-Tag KASAN(Async) + KFENCE: 낮은 오버헤드로 최대 커버리지
- x86_64 프로덕션 — KFENCE 단독: 오버헤드 ~1%
실전 사례와 리포트 해석
KFENCE 리포트 구조
==================================================================
BUG: KFENCE: out-of-bounds read in some_parser+0x1a4/0x2e0
Out-of-bounds read at 0xffff888012340fff (1B right of kfence-#42):
some_parser+0x1a4/0x2e0
process_frame+0x88/0x200
worker_thread+0x120/0x340
kfence-#42: 0xffff888012340fa0-0xffff888012340ffe, size=95, cache=kmalloc-96
allocated by task 1234 on cpu 2 at 1234.567890s:
__kmem_cache_alloc_lru+0x180/0x1f0
kmalloc_trace+0x30/0x80
driver_alloc_buf+0x28/0x60
driver_ioctl+0xc0/0x200
CPU: 2 PID: 1234 Comm: app Not tainted 6.11.0 #1
==================================================================
핵심 읽는 법:
① "1B right of kfence-#42" → 95바이트 객체의 96번째 바이트 접근 (OOB)
② allocated by → 해당 객체의 원본 kmalloc 호출 스택
③ kfence-#42 → 풀 내 슬롯 번호, /sys/kernel/debug/kfence/objects에서 상세 조회 가능
사례 1: 드물게 재현되는 드라이버 OOB를 프로덕션 플릿이 포착
어떤 네트워크 드라이버가 특정 패킷(Packet) 시퀀스에서만 재현되는 1바이트 OOB를 가지고 있었지만,
개발 환경 부하 테스트에서는 재현율이 낮아 발견되지 않았습니다. KFENCE를 활성화한 프로덕션 플릿에서
1주일간 /sys/kernel/debug/kfence/stats를 수집한 결과 같은 객체에 14회 히트가 발생했고,
동일 스택 트레이스의 out-of-bounds read 리포트로 문제가 특정되었습니다.
핵심은 "드문 경로를 오랜 시간 낮은 비용으로 감시"한다는 프로덕션 운영 모델입니다.
sample_interval=100ms, 애플리케이션 kmalloc 호출이 초당 10,000건이라면
KFENCE 풀에 들어가는 비율은 약 1/1,000,000입니다. 따라서 드물게 재현되는 버그도
플릿 규모(수천~수만 호스트)에서 장기간(며칠~몇 주) 관측하면 누적 확률이 1에 수렴합니다.
사례 2: UAF가 샘플링에 걸려 재현 조건이 밝혀진 경우
use-after-free read/write는 해제 직후 접근을 안정적으로 잡습니다.
리포트에 포함된 alloc/free 스택 덤프(Dump)로 원본 해제 경로를 즉시 역추적(Backtrace)할 수 있고, 샘플링 풀에
들어간 객체는 오랫동안 Freed 상태로 남아 있어 늦게 발생한 UAF도 포착합니다.
실제 syzbot 대시보드에서
"KFENCE" 필터로 검색하면 메인라인에 패치(Patch)된 CVE 다수를 확인할 수 있습니다.
사례 3: 새로 도입한 kmem_cache의 경계 버그
새 자료구조를 위해 kmem_cache_create()로 만든 커스텀 슬랩이 내부 정렬 버그로 인해
실제 객체보다 1바이트 짧은 영역만 기록 가능한 상태였던 사례가 있습니다. 일반 부하 테스트는 해제 시점의
쓰레기 값만 보여주었지만, KFENCE가 샘플링한 객체는 오른쪽 가드 페이지와 맞닿아 있어
쓰기 시도 즉시 page fault로 잡혔습니다. 이처럼 페이지 정렬 객체를 직접 다루는 서브시스템에서
KFENCE는 특히 효과적입니다.
syzkaller 연동
syzkaller 기본 설정은 CONFIG_KASAN만 켜지만, 코드 경로가 계측되지 않은 영역은 KASAN이 보지 못합니다.
KFENCE를 추가로 켜면 다음 조합이 만들어집니다:
{
"target": "linux/amd64",
"kernel_obj": "/root/linux",
"vm": {
"cmdline": "kasan.fault=report kfence.sample_interval=50 kcov=1"
}
}
kfence.sample_interval=50으로 100ms → 50ms 단축 시 탐지 확률이 올라가지만 오버헤드도 같이 올라갑니다.
퍼징 전용 환경에서는 10ms 수준까지 낮추는 것도 일반적입니다.
운영 튜닝과 최신 동향
- 런타임 샘플 조정:
echo 50 > /sys/module/kfence/parameters/sample_interval로 100ms → 50ms 단축 (탐지율↑, 오버헤드↑). 부팅 파라미터kfence.sample_interval=<ms>로도 지정 가능하며 0을 넣으면 동적으로 완전 비활성화됩니다. - Deferrable 타이머:
CONFIG_KFENCE_DEFERRABLE=y이거나 부팅 파라미터kfence.deferrable=1을 주면 샘플링 타이머가 idle CPU를 깨우지 않는 deferrable 모드로 동작합니다. 장시간 idle이 많은 시스템에서 전력 소모를 줄이되 탐지 지연(Latency)이 커질 수 있습니다. - 풀 크기는 컴파일 타임 고정:
CONFIG_KFENCE_NUM_OBJECTS로 결정되며(mm/kfence/core.c가static_assert(CONFIG_KFENCE_NUM_OBJECTS > 0)으로 강제) 런타임에 동적으로 늘릴 수 없습니다. 운영 중 풀 소진이 빈번하면 재빌드가 필요합니다. - 연속 분석:
/sys/kernel/debug/kfence/stats와/sys/kernel/debug/kfence/objects로 할당·샘플·히트 카운터를 수집할 수 있어, Prometheus/Grafana 익스포터를 연결해 플릿 전체를 장기 관측하는 운영 모델이 일반적입니다. - Stack Depot 공유:
stack_depot_save_flags()·stack_depot_put()API로 KASAN·KMSAN·page_owner와 Stack 정보를 공유합니다. 이 덕분에 같은 스택 트레이스에 해시(Hash)를 중복 저장하지 않고, KFENCE 객체가 해제될 때 참조 카운트(Reference Count)를 즉시 반납할 수 있습니다. - CPU hotplug·NUMA: KFENCE 풀은 부팅 시 단일 연속 물리 메모리(Physical Memory)로 확보되어 CPU hotplug에 영향받지 않습니다. NUMA 시스템에서는 풀이 특정 노드에 상주하므로 원격 노드 접근 비용이 추가되지만, 샘플링이 드물기 때문에 전체 오버헤드 관점에서는 무시할 수 있습니다.
# 실시간 통계 확인
$ cat /sys/kernel/debug/kfence/stats
enabled: 1
currently allocated: 87
total allocations: 21843
total frees: 21756
zombie allocations: 0
total bugs: 3
skipped allocations (incompatible): 0
skipped allocations (capacity): 142
skipped allocations (covered): 8801
# 히트 이력
$ cat /sys/kernel/debug/kfence/objects
Object #42: ALLOCATED, size=72 cache=kmalloc-96
allocated by task 1234 (drv_probe):
__kmem_cache_alloc+0x30/0x100
...
흔한 실수와 디버깅 팁
- "아무것도 안 잡힌다" —
sample_interval이 너무 크거나 풀 크기가 너무 작을 수 있습니다. 먼저stats의total allocations가 증가하는지 확인하세요. - 풀 소진 — 장시간 가동 시 freed 객체가 쌓여 풀이 소진됩니다.
CONFIG_KFENCE_NUM_OBJECTS를 늘리거나(커널 ≥6.14에서 런타임 조정) 재부팅합니다. - False negative 가능성 — 샘플링 특성상 빠르게 반복되는 OOB/UAF는 놓칠 수 있습니다. 개발 단계에선 KASAN 병용이 필수입니다.
- 특정 할당자만 대상 — KFENCE는
kmalloc/kmem_cache_alloc경로에만 관여합니다.alloc_pages,vmalloc은 대상이 아닙니다. - ARM64 프로덕션 조합 — HW-Tag KASAN(Async) + KFENCE가 가장 효율적인 설정입니다. 둘 다 ~1% 오버헤드로 커버리지가 겹치지 않습니다.
커널 설정
# 필수
CONFIG_KFENCE=y
# 샘플 간격 (밀리초). 0이면 완전 비활성화
CONFIG_KFENCE_SAMPLE_INTERVAL=100
# 풀에 유지할 객체 수 (컴파일 타임 고정, 기본 255)
CONFIG_KFENCE_NUM_OBJECTS=255
# static_key 기반 빠른 경로 (EXPERT 전용, 기본 n — 권장 기본값 유지)
CONFIG_KFENCE_STATIC_KEYS=n
# Deferrable 타이머 (idle CPU 깨우지 않음, 전력 우선 환경)
CONFIG_KFENCE_DEFERRABLE=y
# KUnit 자체 테스트 (선택)
CONFIG_KFENCE_KUNIT_TEST=m
런타임에는 부팅 파라미터 kfence.sample_interval=<ms>로 샘플 간격을 조정합니다.
0을 주면 KFENCE를 동적으로 꺼 오버헤드를 0으로 만들 수 있습니다.
/sys/module/kfence/parameters/sample_interval 파일에 값을 쓰면 런타임 조정도 가능합니다.
참고자료
- Kernel Electric-Fence (KFENCE) — 커널 공식 문서
- LWN: KFENCE — Low-overhead, sampling-based memory error detector
- 소스 코드:
mm/kfence/core.c,mm/kfence/report.c,include/linux/kfence.h
- KASAN — 메모리 주소 오류 전수 탐지 (개발 환경 파트너)
- KMSAN — 초기화되지 않은 메모리 탐지
- UBSAN — C 표준 UB 탐지
- KCSAN — 데이터 레이스 탐지
- Slab Allocator (SLUB) — KFENCE가 연동하는 kmalloc 경로
- 디버깅 & 트러블슈팅 — 증상별 탐지 도구 선택