KFENCE (Kernel Electric-Fence)

KFENCE는 프로덕션 커널에서 사용할 수 있도록 설계된 저오버헤드(Overhead) 메모리 에러 탐지 도구입니다. KASAN이 모든 메모리 접근을 검사하는 것과 달리, KFENCE는 샘플링 기반으로 일부 할당만 가드 페이지(Guard Page) 풀에서 수행해 Out-of-Bounds와 Use-After-Free를 확실하게 탐지합니다. 커널 디버깅(Debugging) 전 범위에서 활용 가능합니다.

전제 조건: KASAN의 동작 원리(Shadow Memory, OOB/UAF 탐지)와 SLUB 할당자의 kmalloc 경로, 페이지 할당자(Page Allocator)의 PTE 권한 조작을 알면 이해가 빠릅니다.
일상 비유: KFENCE는 박물관의 "만지지 마세요" 스탠드 로프와 같습니다. 모든 전시물에 보디가드(KASAN)를 붙이면 비용이 너무 크니, 대신 일부 전시물 주위에만 접근 금지 로프(가드 페이지)를 둘러 두고, 누가 넘으면 즉시 알람이 울립니다(page fault). 검사 대상은 적지만 "걸리면 확실"합니다.

핵심 요약

  • 샘플링 할당 — 기본 100ms 간격으로 하나의 kmalloc만 KFENCE 풀에서 수행
  • 가드 페이지 — 객체 양쪽에 접근 불가(PTE no-access) 페이지 배치. 경계 초과 시 즉시 page fault
  • 프로덕션 가능 — 오버헤드 ~1%. KASAN(고오버헤드) 대신 장시간 운영 환경에 투입
  • KASAN 보완 — 둘 다 켤 수 있음. KASAN이 놓치는 영역을 샘플링으로 확률 탐지
  • 런타임 튜닝/sys/module/kfence/parameters/sample_interval로 간격 즉시 조정

단계별 이해

  1. 부팅 시 풀 할당
    KFENCE 전용 메모리 풀(기본 255개 객체, 객체 1개당 2페이지: Object+Guard)을 연속 할당합니다.
  2. 샘플링 게이트
    타이머가 매 sample_interval마다 kfence_allocation_gate를 true로 셋. 다음 kmalloc 한 번이 KFENCE 풀에서 수행됩니다.
  3. 객체 배치
    객체를 페이지 오른쪽 끝에 정렬하고, 좌우 가드 페이지의 PTE를 no-access로 설정합니다.
  4. 경계 접근
    OOB/UAF가 가드 페이지에 닿으면 CPU가 page fault를 발생시키고, kfence_handle_page_fault()가 리포트를 출력합니다.
  5. 해제
    kfree 시 객체 페이지 PTE를 no-access로 전환. 이후 UAF 접근도 fault로 잡힙니다.

설계 철학

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

핵심 아이디어: "모든 할당을 검사하는 대신, 일부 할당을 확실하게 잡는다." 프로덕션 커널에 항상 켜두어도 오버헤드(Overhead)가 약 1% 수준이며, 시간이 쌓이면 드물게 발생하는 버그도 확률적으로 포착됩니다.

동작 원리

동작 단계설명핵심 메커니즘
풀 초기화 부팅 시 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 풀에서 할당됩니다.

가드 페이지 탐지 구조

KFENCE 가드 페이지 기반 탐지 구조 KFENCE 메모리 풀 레이아웃 (각 박스 = 4KB 페이지) Guard no-access 4KB Object A UNUSED (슬롯 대기) Guard 4KB Object B ALLOCATED read-write Guard 4KB Object C FREED no-access Guard 4KB Guard 4KB OOB 탐지: 객체를 페이지 끝에 정렬 페이지 내부 (4KB, 4096바이트) 사용 안 함 객체 20바이트 Guard 1바이트만 초과해도 Guard 페이지에 닿아 즉시 page fault → kfence_handle_page_fault() addr = obj_page + PAGE_SIZE − size UAF 탐지: 해제 시 PTE를 no-access로 전환 ALLOCATED PTE: read-write kfree() FREED PTE: no-access use-after-free 접근 → page fault __kfence_alloc() (실선 = 정상 전이, 점선 = 금지 접근) 핵심 원리 요약 Slab 객체는 풀 내 한 페이지의 오른쪽 끝에 정렬 → 좌우 Guard가 OOB를, 해제 후 no-access가 UAF를 잡음 그림 1. KFENCE 가드 페이지 기반 탐지 구조 — 풀 레이아웃·OOB·UAF 원리

커널 내부 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 풀에서 수행되어 가드 페이지로 별도 보호됩니다.

권장 조합:
  • 개발/CICONFIG_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 수준까지 낮추는 것도 일반적입니다.

운영 튜닝과 최신 동향

# 실시간 통계 확인
$ 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
   ...

흔한 실수와 디버깅 팁

커널 설정

# 필수
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 파일에 값을 쓰면 런타임 조정도 가능합니다.

참고자료

다음 학습: