동시성 디버깅(Debugging) 종합 가이드

리눅스 커널에서 동시성 버그(데이터 레이스, 데드락, 라이브락, 우선순위 역전(Priority Inversion), use-after-free race)를 탐지하고 해결하는 모든 도구와 기법을 통합 정리합니다. lockdep 아키텍처, KCSAN 계측 원리, KASAN/KFENCE 메모리 감지, sparse/Coccinelle 정적 분석, ftrace/perf 런타임 추적, CONFIG 옵션 종합, 실제 CVE 사례 분석, 테스트 전략, 그리고 발견에서 검증까지의 체계적인 디버깅 워크플로를 다룹니다.

전제 조건: 동기화 기법메모리 배리어(Memory Barrier) 문서를 먼저 읽으세요. 동시성 디버깅 도구는 각 동기화 프리미티브의 의미론을 이해한 상태에서만 올바르게 사용할 수 있습니다.
일상 비유: 이 주제는 교통사고 조사관에 해당합니다. 도로(코드)에서 차량(스레드(Thread))이 충돌(레이스/데드락)했을 때, CCTV(lockdep/KCSAN)로 원인을 찾고, 블랙박스(ftrace/perf)로 재현하며, 도로 설계(코드 수정)를 개선하는 전체 과정입니다.

핵심 요약

  • lockdep — 락 획득 순서를 런타임에 추적하여 데드락 가능성을 발생 전에 경고합니다.
  • KCSAN — 컴파일러 계측 기반으로 데이터 레이스를 탐지합니다. data_race()로 의도적 레이스를 표시합니다.
  • KASAN/KFENCE — 메모리 접근 오류(UAF, OOB)를 감지하며, 동시성 관련 use-after-free race를 포착합니다.
  • sparse/Coccinelle — 빌드 시 정적 분석으로 동기화 어노테이션 위반과 패턴 기반 버그를 찾습니다.
  • ftrace/perf — 런타임 lock contention, IRQ latency, preempt-off 구간을 프로파일링(Profiling)합니다.

단계별 이해

  1. 1단계 — 버그 분류: 증상을 데이터 레이스, 데드락, 라이브락, 우선순위 역전 중 어느 유형인지 식별합니다.
  2. 2단계 — 도구 선택: 유형에 따라 lockdep(데드락), KCSAN(데이터 레이스), KASAN(UAF) 등 적절한 도구를 활성화합니다.
  3. 3단계 — 재현: stress-ng, locktorture, rcutorture 등으로 부하를 가해 버그를 안정적으로 재현합니다.
  4. 4단계 — 분석: 경고 메시지, 스택 트레이스, lock 의존성 그래프를 해석하여 근본 원인을 파악합니다.
  5. 5단계 — 수정/검증: 패치(Patch) 적용 후 동일 도구로 경고가 사라졌는지, 새로운 문제가 없는지 확인합니다.

동시성 버그 분류

커널 동시성 버그는 크게 5가지 유형으로 분류됩니다. 각 유형은 발생 조건, 증상, 탐지 도구가 다르므로 정확한 분류가 디버깅의 첫걸음입니다.

동시성 버그 분류 체계 Concurrency Bug Data Race - 비보호 공유 변수 접근 - KCSAN 탐지 - data_race() 예외 표시 - 증상: 값 손상/오동작 Deadlock - ABBA 락 순서 역전 - lockdep 탐지 - 자기 참조 데드락 - 증상: 행(hang) Livelock - 무한 재시도 루프 - CPU 100% 스핀 - ftrace/perf 프로파일 - 증상: soft lockup Priority Inversion - 저우선 태스크가 락 점유 - 고우선 태스크 대기 - rt_mutex/PI futex - 증상: RT 지연 UAF Race - 해제 후 접근 경합 - KASAN/KFENCE 탐지 - RCU grace period 미준수 - 증상: 메모리 손상 탐지 도구 매핑 KCSAN lockdep ftrace/perf rt_mutex PI KASAN/KFENCE sparse + Coccinelle (모든 유형의 정적 사전 탐지) 런타임 동적 탐지: lockdep, KCSAN, KASAN, KFENCE, ftrace, perf 빌드 시 정적 탐지: sparse (__acquires/__releases/__rcu), Coccinelle (SmPL 패턴)
유형발생 조건증상1차 탐지 도구2차 보조 도구
Data Race비보호 공유 변수 동시 접근 (최소 1회 쓰기)값 손상, 비결정적 오동작KCSANsparse, TSAN (유저스페이스 테스트)
Deadlock순환 락 의존성 (ABBA)시스템/태스크(Task) 행(hang)lockdep/proc/lockdep, ftrace
Livelock무한 재시도 (cmpxchg loop 등)CPU 100%, soft lockupftrace/perflockdep (간접)
Priority Inversion저우선 태스크가 고우선 태스크의 락 점유RT 태스크 지연(Latency)rt_mutex PI chainftrace sched, perf sched
UAF Race객체 해제/접근 간 경합(Contention)메모리 손상, 커널 패닉(Kernel Panic)KASANKFENCE (프로덕션), lockdep

데이터 레이스 상세

두 용어는 자주 혼동됩니다. 데이터 레이스(data race)는 C11/C++11 메모리 모델에서 정의한 개념으로, 두 스레드가 동일 메모리 위치에 비원자적으로 동시 접근하고 최소 하나가 쓰기인 경우입니다. 레이스 컨디션(race condition)은 타이밍에 따라 결과가 달라지는 논리적 버그를 포괄하는 상위 개념입니다.

LKMM 참고: 리눅스 커널은 C11 표준이 아닌 자체 메모리 모델(LKMM — Linux Kernel Memory Model)을 사용합니다. tools/memory-model/ 디렉토리에 litmus test 도구가 포함되어 있습니다. LKMM에서 data race는 "두 메모리 접근이 동일 위치에 대해 발생하고, 최소 하나가 쓰기이며, 최소 하나가 plain 접근(non-atomic)이고, happens-before 관계가 없는 경우"로 정의됩니다.
/* 데이터 레이스 예시 — KCSAN이 탐지 */
static int shared_counter;

void thread_a(void) {
    shared_counter++;   /* 비보호 쓰기 */
}

void thread_b(void) {
    int val = shared_counter;  /* 비보호 읽기 — data race! */
    pr_info("counter = %d\n", val);
}

/* 수정: atomic 또는 락 사용 */
static atomic_t safe_counter = ATOMIC_INIT(0);

void thread_a_fixed(void) {
    atomic_inc(&safe_counter);
}

void thread_b_fixed(void) {
    int val = atomic_read(&safe_counter);
    pr_info("counter = %d\n", val);
}
KCSAN이 정의하는 data race 조건:
  1. 두 메모리 접근이 동일 주소 범위에 대해 발생
  2. 최소 하나가 쓰기(write) 접근
  3. 최소 하나가 plain(비마킹) 접근 — READ_ONCE()/WRITE_ONCE()/atomic_*()가 아닌 접근
  4. 두 접근 사이에 적절한 동기화(락, 배리어)가 없음
/* KCSAN이 data race로 보지 않는 경우들 */

/* 1. 양쪽 모두 READ_ONCE/WRITE_ONCE 사용 */
WRITE_ONCE(shared_var, 42);    /* 마킹된 쓰기 */
int val = READ_ONCE(shared_var); /* 마킹된 읽기 — data race 아님 */

/* 2. atomic 연산 사용 */
atomic_set(&counter, 0);
atomic_inc(&counter);

/* 3. 적절한 락으로 보호 */
spin_lock(&lock);
shared_var = 42;    /* 락으로 보호 — data race 아님 */
spin_unlock(&lock);

/* 4. data_race() 매크로로 명시적 허용 */
data_race(stats->count++);  /* 의도적 레이스 선언 */

/* 5. 읽기만 하는 두 접근 (쓰기 없음) */
int a = shared_var;  /* 읽기 */
int b = shared_var;  /* 읽기 — 두 접근 모두 읽기이므로 OK */

데드락 유형

데드락은 발생 패턴에 따라 여러 유형으로 세분화됩니다. 각 유형의 특성을 이해하면 lockdep 경고를 더 빠르게 해석할 수 있습니다.

유형패턴lockdep 탐지발생 빈도수정 방법
ABBA 데드락CPU0: lock(A)→lock(B)
CPU1: lock(B)→lock(A)
circular dependency가장 빈번전역 락 순서 규칙 정립
Self-deadlock동일 락을 재귀 획득
spin_lock(&L); spin_lock(&L);
recursive locking빈번__locked 패턴, nested 사용
IRQ 데드락프로세스(Process): lock(L)
IRQ: lock(L) (같은 CPU)
inconsistent lock state빈번spin_lock_irqsave() 사용
Cross-subsystem서브시스템 A→B 호출 시
각 서브시스템의 락 순서 충돌
circular dependency간헐적인터페이스 재설계, trylock
AB-BA-BC 체인3개 이상 락의 순환
A→B, B→C, C→A
circular dependency드물지만 치명적락 계층 전면 재설계
RCU/completion 간접synchronize_rcu() + lock
간접적 대기 순환
PROVE_RCU (일부)드묾RCU 콜백(Callback)으로 전환
/* Cross-subsystem 데드락 예시 — 네트워크 + 파일시스템 */
/* 경로 A: 네트워크 수신 → NFS write-back */
rtnl_lock();           /* 네트워크 서브시스템 */
  inode_lock(inode);   /* 파일시스템 서브시스템 */
  inode_unlock(inode);
rtnl_unlock();

/* 경로 B: NFS 마운트 → 네트워크 설정 */
inode_lock(inode);     /* 파일시스템 서브시스템 */
  rtnl_lock();         /* 네트워크 서브시스템 → ABBA! */
  rtnl_unlock();
inode_unlock(inode);

/* lockdep: possible circular locking dependency
 * rtnl_mutex → &inode->i_rwsem → rtnl_mutex */

라이브락 상세

라이브락은 데드락과 달리 태스크가 완전히 멈추지 않고 계속 실행되지만, 유용한 작업을 진행하지 못하는 상태입니다. CPU 사용률이 100%이지만 처리량(Throughput)은 0에 가까워집니다.

/* 라이브락 예시 1: cmpxchg 무한 재시도 */
void livelock_cmpxchg(atomic_t *counter) {
    int old, new;
    do {
        old = atomic_read(counter);
        new = old + 1;
        /* 다른 CPU도 동시에 이 루프를 실행하면
         * 양쪽 모두 cmpxchg 실패를 반복 → 라이브락 */
    } while (atomic_cmpxchg(counter, old, new) != old);
}

/* 수정: exponential backoff 추가 */
void safe_cmpxchg(atomic_t *counter) {
    int old, new;
    int backoff = 1;
    do {
        old = atomic_read(counter);
        new = old + 1;
        if (atomic_cmpxchg(counter, old, new) == old)
            return;
        /* 실패 시 backoff */
        cpu_relax();
        if (backoff < 1024)
            backoff <<= 1;
        for (int i = 0; i < backoff; i++)
            cpu_relax();
    } while (true);
}

/* 라이브락 예시 2: trylock 무한 재시도 */
void livelock_trylock(spinlock_t *a, spinlock_t *b) {
    while (true) {
        spin_lock(a);
        if (spin_trylock(b))
            break;
        spin_unlock(a);
        /* 다른 CPU가 b→a 순서로 동일 패턴 실행 시
         * 양쪽 모두 영원히 trylock 실패 → 라이브락 */
    }
}

/* 수정: 랜덤 backoff + 순서 보장 */
void safe_trylock(spinlock_t *a, spinlock_t *b) {
    while (true) {
        spin_lock(a);
        if (spin_trylock(b))
            break;
        spin_unlock(a);
        /* 랜덤 지연으로 라이브락 깨뜨리기 */
        udelay(get_random_u32() & 0xf);
    }
}
라이브락 탐지의 어려움: lockdep은 라이브락을 직접 탐지하지 못합니다. soft lockup 감지기(SOFTLOCKUP_DETECTOR)가 CPU가 특정 시간 이상 스케줄러(Scheduler)를 호출하지 않으면 경고를 출력합니다. ftrace의 function_graph tracer나 perf top으로 CPU를 소모하는 함수를 식별하여 라이브락을 진단합니다.

우선순위 역전 상세

우선순위 역전(priority inversion)은 저우선순위 태스크가 고우선순위 태스크가 필요한 자원을 점유하고, 중간우선순위 태스크에 의해 선점(Preemption)되어 고우선순위 태스크가 무한 대기하는 현상입니다.

/* 우선순위 역전 시나리오 */
/* Task L (저우선순위): mutex 보유 */
/* Task M (중우선순위): CPU 집약적 작업 실행 */
/* Task H (고우선순위): mutex 대기 */
/*
 * 1. Task L이 mutex 획득
 * 2. Task H가 실행되어 mutex 대기 시작
 * 3. Task M이 깨어나 Task L을 선점 (L < M)
 * 4. Task M이 오래 실행 → Task L은 실행 못함 → mutex 해제 못함
 * 5. Task H는 Task M이 끝날 때까지 간접적으로 대기 (unbounded!)
 */

/* 해결 1: Priority Inheritance (PI) — rt_mutex */
#include <linux/rtmutex.h>
static DEFINE_RT_MUTEX(pi_mutex);

void high_prio_task(void) {
    rt_mutex_lock(&pi_mutex);
    /* rt_mutex는 PI 지원:
     * Task H가 대기하면 Task L의 우선순위를 H로 부스트
     * → Task M이 Task L을 선점할 수 없음
     * → 역전 해소!
     */
    rt_mutex_unlock(&pi_mutex);
}

/* 해결 2: Priority Ceiling — PREEMPT_RT에서 사용 */
/* spinlock_t가 rt_mutex로 변환되어 자동 PI 지원 */
Mars Pathfinder 사건: 1997년 NASA Mars Pathfinder 미션에서 우선순위 역전이 발생하여 시스템이 반복 리셋되었습니다. VxWorks RTOS에서 저우선순위 ASI/MET 태스크가 공유 메모리 버스(Bus)를 점유하여 고우선순위 bc_sched 태스크가 실행되지 못했습니다. 우선순위 상속(PI) 활성화로 원격 수정되었습니다.
기술 문서: 우선순위 역전의 PI 프로토콜 해결 메커니즘은 PREEMPT_RT: PI chain를, 사용자 공간(User Space) PI futex는 Futex: PI Futex를 참고하세요.

Use-After-Free 레이스

UAF 레이스는 동시성과 메모리 안전성의 교차점에 있는 버그로, 한 스레드가 객체를 해제하는 동안 다른 스레드가 접근하는 패턴입니다. RCU grace period 미준수가 대표적 원인입니다.

/* UAF 레이스의 3가지 패턴 */

/* 패턴 1: RCU 미사용 — 가장 기본적 UAF */
struct entry *e = lookup_entry(key);  /* e 참조 획득 */
/* 다른 CPU: delete_entry(key) → kfree(e) */
pr_info("value = %d\n", e->value);   /* UAF! */

/* 패턴 2: SLAB reuse — 해제 후 같은 캐시에서 재할당 */
/* CPU0: kfree(obj_A)
 * CPU1: obj_B = kmalloc(same_size, GFP_KERNEL)
 *        → obj_B가 obj_A의 메모리를 재사용!
 * CPU0: obj_A->data 접근 → obj_B의 데이터를 읽게 됨
 *        → 잠재적 정보 유출 또는 타입 혼동 공격
 */

/* 패턴 3: refcount underflow → 조기 해제 */
struct my_obj {
    refcount_t ref;
    int data;
};
/* 공격자가 close()를 빠르게 반복 호출하여
 * refcount를 0 이하로 만들면 → 객체 조기 해제 → UAF */

/* 수정: RCU + refcount 조합 */
struct safe_entry {
    struct rcu_head rcu;
    refcount_t ref;
    int value;
};

struct safe_entry *safe_lookup(int key) {
    struct safe_entry *e;
    rcu_read_lock();
    e = rcu_dereference(hashtable[key]);
    if (e && !refcount_inc_not_zero(&e->ref))
        e = NULL;  /* 이미 해제 진행 중 */
    rcu_read_unlock();
    return e;
}

TOCTOU 레이스

TOCTOU(Time-of-Check to Time-of-Use) 레이스는 검사 시점과 사용 시점 사이에 상태가 변경되는 논리적 레이스입니다. 파일시스템(Filesystem) 권한 검사, capabilities 확인 등에서 자주 발생합니다.

/* TOCTOU 예시: 파일 권한 검사 */
/* 위험한 패턴 */
if (inode_permission(inode, MAY_WRITE) == 0) {
    /* 여기서 다른 프로세스가 chmod로 권한을 변경하면? */
    vfs_write(file, buf, len, &pos);  /* 권한 변경 후 쓰기! */
}

/* TOCTOU 예시: 사용자 공간 포인터 검사 */
if (access_ok(user_ptr, size)) {
    /* 여기서 다른 스레드가 munmap()으로 매핑을 해제하면? */
    copy_from_user(kernel_buf, user_ptr, size);  /* page fault! */
}
/* copy_from_user()는 내부적으로 page fault를 처리하므로 안전
 * 하지만 access_ok() 결과를 캐시하여 나중에 직접 접근하면 위험 */

/* 수정 원칙: 검사와 사용을 원자적으로 수행 */
/* 1. 락으로 검사-사용 구간 보호 */
/* 2. 검사-사용 결합 API 사용 (copy_from_user 등) */
/* 3. atomic cmpxchg로 검사+변경 원자화 */

비트필드 레이스

C 언어의 비트필드(bit field)는 동시 접근 시 특히 위험합니다. 인접 비트필드의 개별 필드를 서로 다른 스레드가 수정하면, 컴파일러가 전체 word를 read-modify-write하므로 데이터가 손실될 수 있습니다.

/* 비트필드 레이스: 컴파일러가 전체 word를 RMW */
struct flags {
    unsigned int active : 1;     /* 비트 0 */
    unsigned int pending : 1;    /* 비트 1 */
    unsigned int error : 1;      /* 비트 2 */
    unsigned int count : 29;     /* 비트 3-31 */
};

/* CPU0: active를 설정 */
void set_active(struct flags *f) {
    f->active = 1;
    /* 컴파일러 생성 코드:
     * tmp = *(u32 *)f;          // 전체 32비트 읽기
     * tmp |= 0x1;              // 비트 0 설정
     * *(u32 *)f = tmp;          // 전체 32비트 쓰기 ← RMW!
     */
}

/* CPU1: pending을 설정 (동시 실행 시 active 손실 가능!) */
void set_pending(struct flags *f) {
    f->pending = 1;
    /* 컴파일러 생성 코드:
     * tmp = *(u32 *)f;          // CPU0의 쓰기 전 값 읽을 수 있음
     * tmp |= 0x2;              // 비트 1 설정
     * *(u32 *)f = tmp;          // CPU0의 active=1 덮어씀!
     */
}

/* 해결 1: 별도의 atomic 변수 사용 */
struct safe_flags {
    atomic_t bits;  /* set_bit/clear_bit/test_bit 사용 */
};

void safe_set_active(struct safe_flags *f) {
    set_bit(0, (unsigned long *)&f->bits);  /* 원자적 비트 설정 */
}

/* 해결 2: 동일 락으로 보호 */
void safe_set_active_v2(struct flags *f, spinlock_t *lock) {
    spin_lock(lock);
    f->active = 1;  /* 락으로 보호 → 안전 */
    spin_unlock(lock);
}
KCSAN과 비트필드: KCSAN은 비트필드 레이스를 탐지할 수 있습니다. CONFIG_KCSAN_ASSUME_PLAIN_WRITES_ATOMIC=n으로 설정하면 비트필드 RMW도 레이스로 보고합니다. 커널 코드에서 struct의 비트필드를 여러 컨텍스트에서 동시에 수정하는 패턴은 반드시 검토해야 합니다.

순서 위반 (Ordering Violation)

순서 위반은 프로그래머가 가정한 실행 순서가 하드웨어/컴파일러 최적화(Compiler Optimization)에 의해 깨지는 버그입니다. 메모리 배리어 누락이 대표적인 원인입니다.

/* 초기화 순서 위반 — 전형적 패턴 */
struct device {
    int data;
    bool initialized;
};

/* CPU0: 디바이스 초기화 */
void init_device(struct device *dev) {
    dev->data = 42;           /* Store A */
    dev->initialized = true;   /* Store B */
    /* CPU/컴파일러가 Store B를 Store A 앞으로 재배치 가능! */
}

/* CPU1: 디바이스 사용 */
void use_device(struct device *dev) {
    if (dev->initialized) {    /* Load B: true를 봄 */
        use(dev->data);        /* Load A: 아직 0일 수 있음! */
    }
}

/* 수정: WRITE_ONCE/READ_ONCE + 배리어 */
void init_device_fixed(struct device *dev) {
    WRITE_ONCE(dev->data, 42);
    smp_store_release(&dev->initialized, true);  /* 배리어 포함 */
}

void use_device_fixed(struct device *dev) {
    if (smp_load_acquire(&dev->initialized)) {   /* 배리어 포함 */
        use(READ_ONCE(dev->data));  /* 반드시 42 */
    }
}

/* 더 안전한 패턴: RCU publish-subscribe */
struct device __rcu *global_dev;

void publish_device(struct device *dev) {
    dev->data = 42;            /* 초기화 완료 */
    rcu_assign_pointer(global_dev, dev);  /* 내부에 smp_store_release */
}

void consume_device(void) {
    struct device *dev;
    rcu_read_lock();
    dev = rcu_dereference(global_dev);  /* 내부에 smp_load_acquire */
    if (dev)
        use(dev->data);    /* 반드시 초기화된 값 */
    rcu_read_unlock();
}
ARM vs x86 차이: x86은 TSO(Total Store Order) 메모리 모델로 Store-Store 재배치(Relocation)가 발생하지 않으므로 위 초기화 패턴이 실제로는 동작합니다. 그러나 ARM/RISC-V 등 약한 메모리 모델(Weak Memory Model) 아키텍처에서는 재배치가 실제로 발생합니다. 커널 코드는 모든 아키텍처에서 동작해야 하므로 항상 배리어를 명시해야 합니다. smp_store_release()/smp_load_acquire()는 x86에서 컴파일러 배리어만 삽입하고 ARM에서 하드웨어 배리어를 삽입하여 두 아키텍처 모두에서 최적의 성능을 제공합니다.
동시성 버그 세부 분류 트리 Data Race Plain read/write Compound r-m-w Bitfield race KCSAN KCSAN KCSAN Deadlock ABBA Self IRQ Cross-sub lockdep Memory Race UAF race Double-free SLAB reuse KASAN / KFENCE Logic Race TOCTOU Refcount Ordering 코드 리뷰 / fuzzing Livelock CAS loop trylock ftrace / perf 심각도 순위 (보안 관점) UAF Race (LPE) > TOCTOU (LPE) > Data Race (손상) > Deadlock (DoS) > Livelock (DoS)

lockdep 요약

lockdep(Lock Dependency Validator)은 커널의 런타임 락 의존성 검증 인프라입니다. CONFIG_PROVE_LOCKING을 활성화하면 매 락 획득/해제 시 의존성 그래프를 갱신하고 순환을 검사합니다.

상세 문서: lockdep의 아키텍처, 소스 분석, 경고 해석, 어노테이션, /proc 인터페이스 등 전체 내용은 lockdep 기술 문서를 참고하세요.
주제핵심 내용상세 참조
아키텍처 lock_acquire()__lock_acquire()validate_chain() 파이프라인(Pipeline)으로 매 락 획득 시 의존성 검증. 핵심 구조체(Struct): lock_class, lock_chain, held_lock lockdep 아키텍처
경고 해석 circular dependency, inconsistent lock state, possible recursive locking, BUG: sleeping function called 4가지 주요 유형. 경고 메시지에서 dependency chain과 unsafe locking scenario를 읽는 것이 핵심 경고 메시지 해석, 주요 경고 유형
lock_class_key 동일 구조체의 락이라도 맥락이 다르면 lockdep_set_class()로 별도 클래스 지정. mutex_lock_nested()로 서브클래스 구분하여 false positive 방지 정적 키, 어노테이션
Cross-release 한 태스크가 획득하고 다른 태스크가 해제하는 패턴(completion, RCU, page lock, workqueue). v4.14 실험 패치 후 오버헤드(Overhead)/false positive로 제거됨 Cross-release, 심층 분석
wait_type 검사 v5.8+ LD_WAIT_FREE/SPIN/CONFIG/SLEEP 4단계로 잘못된 컨텍스트 슬립(Sleep) 탐지. PREEMPT_RT에서 spinlock_t → rt_mutex 변환 시 특히 중요 wait_type 검사
/proc 인터페이스 /proc/lockdep(클래스 목록), /proc/lockdep_stats(통계), /proc/lock_stat(경합 통계), /proc/lockdep_chains(체인 목록)으로 런타임 상태 확인 /proc 인터페이스, LOCK_STAT

방어적 어서션 패턴

락 의존성 검증(lockdep)과 더불어, 커널은 코드 작성 시점에 잠재적 동시성 버그를 사전 탐지하는 어서션(Assertion) 매크로를 제공합니다. 이들은 런타임에 조건이 위반되면 즉시 경고를 출력하여 버그의 원인을 조기에 파악할 수 있게 합니다.

lockdep_assert_held — 락 보유 검증

매크로검증 내용사용 시나리오
lockdep_assert_held(&lock)현재 태스크가 해당 락을 보유 중인지 확인락 보호 필수인 함수 진입점
lockdep_assert_held_write(&rwsem)쓰기 락을 보유 중인지 확인rwsem 보호 수정 연산
lockdep_assert_held_read(&rwsem)읽기 또는 쓰기 락 보유 확인rwsem 보호 읽기 연산
lockdep_assert_not_held(&lock)해당 락을 보유하지 않은 상태 확인데드락 방지 (락 순서 강제)
/* lockdep_assert_held — 함수 진입 시 락 보유 검증 */
void update_stats(struct my_device *dev)
{
    lockdep_assert_held(&dev->lock);  /* 위반 시 WARNING 출력 */
    dev->stats.tx_count++;
    dev->stats.last_update = jiffies;
}

/* rwsem 쓰기 보유 검증 */
void tree_insert(struct rb_root *root, struct rb_node *node)
{
    lockdep_assert_held_write(&tree_rwsem);
    rb_insert_color(node, root);
}
  1. lockdep_assert_held()CONFIG_PROVE_LOCKING 활성화 시에만 검증을 수행합니다. 프로덕션 빌드에서는 오버헤드가 없으므로, 락 보호가 필요한 모든 내부 함수에 적극적으로 추가하는 것이 권장됩니다.
  2. lockdep_assert_held_write()rwsem에서 읽기 락만 보유한 채 수정 연산을 호출하는 실수를 탐지합니다. 이는 데이터 레이스로 이어질 수 있는 위험한 버그입니다.

might_sleep — 슬립 컨텍스트 검증

매크로검증 내용사용 시나리오
might_sleep()현재 컨텍스트가 sleep 가능한지 확인kmalloc(GFP_KERNEL), mutex_lock 등 호출 전
might_fault()copy_to/from_user 가능한 컨텍스트인지 확인사용자 메모리 접근 전
cant_sleep()현재 컨텍스트가 sleep 불가인지 확인atomic 섹션 내부 검증
cant_migrate()CPU 마이그레이션이 비활성화되었는지 확인per-CPU 데이터 접근 시
/* might_sleep — spinlock 내부에서 sleep 시도 탐지 */
void my_alloc_buffer(struct my_device *dev, size_t size)
{
    might_sleep();  /* kmalloc(GFP_KERNEL)이 sleep 가능하므로 사전 검증 */
    dev->buf = kmalloc(size, GFP_KERNEL);
}

/*
 * spinlock 보유 상태에서 my_alloc_buffer()를 호출하면:
 * BUG: sleeping function called from invalid context at ...
 * in_atomic(): 1, irqs_disabled(): 0, ...
 * → spinlock 내부에서 GFP_KERNEL 할당 시도를 즉시 탐지
 */

WARN_ON / BUG_ON 사용 가이드

매크로동작사용 권장도
WARN_ON(cond)조건 참이면 스택 트레이스 + 실행 계속권장 — 복구 가능한 상태 위반
WARN_ON_ONCE(cond)최초 1회만 경고 출력적극 권장 — 로그 폭주 방지
WARN(cond, fmt, ...)조건 참이면 포맷 메시지 + 스택 트레이스권장 — 디버깅 정보 추가 시
BUG_ON(cond)조건 참이면 커널 패닉지양 — 데이터 손상이 확실할 때만
/* WARN_ON_ONCE — 반복 경고 방지 패턴 (권장) */
if (WARN_ON_ONCE(refcount < 0))
    return -EINVAL;

/* WARN — 디버깅 정보를 포함한 경고 */
if (WARN(size > MAX_BUFFER,
         "buffer overflow attempt: size=%zu max=%zu\n",
         size, (size_t)MAX_BUFFER))
    return -EOVERFLOW;
실전 어서션 배치 전략: lockdep_assert_held()는 외부에서 호출 가능한 모든 내부 함수(static helper)에 넣고, might_sleep()은 GFP_KERNEL 할당이나 mutex 사용이 있는 함수의 진입점에 넣는 것이 좋습니다. 이 매크로들은 CONFIG_DEBUG_ATOMIC_SLEEPCONFIG_PROVE_LOCKING이 활성화된 개발 커널에서만 비용이 발생하므로 과도하게 사용해도 프로덕션 성능에 영향이 없습니다.

KCSAN (Kernel Concurrency Sanitizer)

KCSAN은 컴파일러 계측(compiler instrumentation) 기반의 데이터 레이스 탐지 도구입니다. GCC 11+ 또는 Clang 12+에서 -fsanitize=thread 유사한 계측을 삽입하여 런타임에 동시 접근을 감시합니다.

KCSAN Watchpoint 메커니즘 CPU 0: 메모리 접근 __tsan_write4(addr) watchpoint 설정 addr + size 범위 등록 지연 윈도우 udelay(~80us) 값 비교 변경 여부 확인 CPU 1: 동일 addr 접근 watchpoint 테이블에서 매치 발견! Data Race 탐지! 리포트 출력 Watchpoint 테이블 (슬롯 배열) slot[0]: addr slot[1]: free slot[2]: free ... slot[N-1]: free 기본 N = CONFIG_KCSAN_NUM_WATCHPOINTS (기본 64)

KCSAN 동작 원리

  1. 컴파일러가 모든 메모리 접근에 __tsan_readN()/__tsan_writeN() 콜백을 삽입합니다.
  2. KCSAN은 확률적으로 일부 접근에 watchpoint를 설정합니다 (샘플링).
  3. watchpoint 설정 후 짧은 지연 윈도우(기본 ~80us)를 둡니다.
  4. 이 윈도우 동안 다른 CPU가 같은 주소에 접근하면 watchpoint 테이블 매치가 발생합니다.
  5. 양쪽 접근 중 최소 하나가 쓰기이고, 적절한 동기화 없이 수행되었으면 data race로 리포트합니다.

KCSAN CONFIG 옵션

옵션기본값설명
CONFIG_KCSANnKCSAN 활성화
CONFIG_KCSAN_STRICTn모든 비표시 레이스를 경고 (data_race() 없이)
CONFIG_KCSAN_WEAK_MEMORYy약한 메모리 모델 접근도 탐지
CONFIG_KCSAN_NUM_WATCHPOINTS64동시 watchpoint 슬롯 수
CONFIG_KCSAN_UDELAY_TASK80태스크 컨텍스트 지연 (us)
CONFIG_KCSAN_UDELAY_INTERRUPT20인터럽트(Interrupt) 컨텍스트 지연 (us)
CONFIG_KCSAN_REPORT_ONCE_IN_MS3000동일 리포트 반복 억제 간격
CONFIG_KCSAN_SKIP_WATCH5000N번 접근마다 watchpoint 설정 (샘플링 비율)

KCSAN 성능 영향과 최소화

KCSAN은 모든 메모리 접근에 콜백을 삽입하므로 커널 전체 성능에 영향을 줍니다. 성능 영향을 최소화하면서 효과적으로 사용하는 방법을 정리합니다.

설정성능 영향탐지율권장 용도
KCSAN_SKIP_WATCH=5000 (기본)~1.5-2x 느림보통일반 CI 빌드
KCSAN_SKIP_WATCH=1000~2-3x 느림높음집중 테스트
KCSAN_SKIP_WATCH=500~3-5x 느림매우 높음버그 재현 시도
KCSAN_NUM_WATCHPOINTS=128약간 증가높음다중 CPU 시스템
KCSAN_UDELAY_TASK=200증가 (긴 윈도우)매우 높음희귀 레이스 탐지
# KCSAN 런타임 제어 (일부 설정)
# 특정 모듈만 KCSAN 비활성화 (컴파일 시)
# 해당 모듈의 Makefile에:
KCSAN_SANITIZE_module_name.o := n

# 특정 파일 제외
# KCSAN_SANITIZE := n  ← 해당 디렉토리의 Makefile에 추가

# kcsan-not 파일 (v6.1+)
# 특정 함수를 런타임에 제외
echo "function_name" >> /sys/kernel/debug/kcsan/skip
KCSAN 디버깅 모드: 디버깅이 어려운 희귀 레이스를 탐지할 때는 KCSAN_SKIP_WATCH=100KCSAN_UDELAY_TASK=500으로 설정하세요. 성능은 5-10x 느려지지만 watchpoint 윈도우가 넓어져 짧은 레이스도 포착할 수 있습니다. 이 설정은 QEMU/KVM 가상 머신에서 사용하고, 호스트에서는 기본값을 유지하는 것을 권장합니다.

KCSAN 서브시스템별 데이터 레이스 통계

KCSAN 도입 이후 커널 서브시스템별 데이터 레이스 탐지 현황과 수정 패턴을 정리합니다.

서브시스템탐지 건수 (2020-2025)주요 패턴대표 수정 방법
네트워킹 (net/)~450+sk_buff 필드 레이스, 소켓(Socket) 상태READ_ONCE/WRITE_ONCE, RCU
스케줄러 (kernel/sched/)~200+task_struct 필드, 런큐(Runqueue) 상태WRITE_ONCE, data_race()
메모리 관리 (mm/)~180+VMA 필드, page 플래그READ_ONCE, 배리어
파일시스템 (fs/)~120+inode 상태, 캐시(Cache) 플래그smp_load_acquire, 락
블록 I/O (block/)~60+요청 큐 상태, bio 필드READ_ONCE, atomic
드라이버 (drivers/)~300+디바이스 상태, 레지스터(Register) 캐시spinlock, READ_ONCE
BPF (kernel/bpf/)~80+맵 상태, prog 참조RCU, atomic
/* 서브시스템별 KCSAN 수정 패턴 대표 사례 */

/* 네트워킹: sk->sk_state 레이스 수정 (대표적) */
/* Before: plain read in fast path */
if (sk->sk_state == TCP_ESTABLISHED)
    tcp_send_data(sk);

/* After: READ_ONCE for safe lockless check */
if (READ_ONCE(sk->sk_state) == TCP_ESTABLISHED)
    tcp_send_data(sk);

/* 스케줄러: task->flags 레이스 (의도적 레이스) */
/* data_race()로 마킹: 성능 카운터/힌트 용도 */
if (data_race(task->flags & PF_WQ_WORKER))
    wq_worker_running(task);

/* MM: VMA 필드 접근 (RCU 보호) */
/* Before: 락 없이 VMA 접근 */
unsigned long start = vma->vm_start;

/* After: RCU read-side에서 접근 */
rcu_read_lock();
vma = vma_lookup(mm, addr);
if (vma)
    start = READ_ONCE(vma->vm_start);
rcu_read_unlock();

/* 드라이버: 디바이스 상태 플래그 */
/* Before: bitfield race */
dev->flags |= DEV_ACTIVE;  /* RMW on shared bitfield */

/* After: atomic bit ops */
set_bit(DEV_ACTIVE_BIT, &dev->flags);  /* 원자적 비트 설정 */
KCSAN 수정 우선순위(Priority):
  1. 보안 관련 레이스: 권한 검사, 참조 카운트(Reference Count) → 즉시 수정 (READ_ONCE/atomic 또는 락)
  2. 데이터 무결성(Integrity) 레이스: 자료구조 필드, 링크 리스트 → READ_ONCE/WRITE_ONCE 또는 락
  3. 성능 카운터/통계: 근사값 허용 → data_race() 매크로(Macro)
  4. 진단/로깅: 디버그 출력 용도 → data_race() 매크로
보안 관련 레이스는 CVE로 이어질 수 있으므로 가장 높은 우선순위로 수정해야 합니다.

KCSAN 리포트 해석과 false positive 처리

KCSAN 리포트는 두 접근 지점(racing pair)의 스택 트레이스를 함께 보여줍니다.

실제 KCSAN 리포트 예시

==================================================================
BUG: KCSAN: data-race in el1_irq / scheduler_tick

write to 0xffff000812345678 of 4 bytes by interrupt on cpu 2:
 scheduler_tick+0x124/0x300
 update_process_times+0x3c/0x60
 tick_sched_handle+0x38/0x50
 tick_sched_timer+0x4c/0x98
 __hrtimer_run_queues+0x110/0x1c8

read to 0xffff000812345678 of 4 bytes by task 1234 on cpu 0:
 task_cputime+0x58/0xa0
 thread_group_cputime+0x60/0x140
 do_sys_times+0x50/0x100
 __arm64_sys_times+0x28/0x38

value changed: 0x00001234 -> 0x00001235

Reported by Kernel Concurrency Sanitizer on:
CPU: 2 PID: 0 Comm: swapper/2 Not tainted 6.8.0-rc1 #1
==================================================================
리포트 구조:
  • 접근 1: interrupt 컨텍스트(cpu 2)에서 4바이트 write — scheduler_tick
  • 접근 2: task 1234(cpu 0)에서 4바이트 read — task_cputime
  • value changed: watchpoint 설정 후 실제 값 변경이 관찰됨
  • 두 접근 사이에 적절한 동기화(락, atomic, READ_ONCE/WRITE_ONCE)가 없으면 data race

False Positive 처리 방법

/* 방법 1: data_race() — 의도적 레이스 표시 (성능 카운터 등) */
void update_stats(struct stats *s) {
    data_race(s->count++);  /* 정확성 불필요, 근사값이면 충분 */
}

/* 방법 2: READ_ONCE / WRITE_ONCE — 단일 접근 원자성 보장 */
void safe_update(struct shared *s) {
    WRITE_ONCE(s->flag, 1);
}
int safe_read(struct shared *s) {
    return READ_ONCE(s->flag);
}

/* 방법 3: ASSERT_EXCLUSIVE_ACCESS — 배타적 접근 주장 */
void init_phase(struct obj *o) {
    ASSERT_EXCLUSIVE_ACCESS(o->field);  /* 다른 접근이 있으면 KCSAN 경고 */
    o->field = initial_value;
}

/* 방법 4: ASSERT_EXCLUSIVE_WRITER — 단일 작성자 주장 */
void single_writer(struct obj *o) {
    ASSERT_EXCLUSIVE_WRITER(o->data);
    o->data = new_value;  /* 다른 writer가 있으면 경고, reader는 허용 */
}

/* 방법 5: __no_kcsan 함수 어트리뷰트 — 전체 함수 제외 (최후의 수단) */
static __no_kcsan void perf_critical_path(void) {
    /* KCSAN 계측 완전히 제외 */
}
주의: data_race()는 "이 레이스가 존재하지만 안전하다"는 명시적 선언입니다. 남용하면 실제 버그를 숨기게 됩니다. 사용 전에 반드시 해당 레이스가 진정으로 무해한지 검증하세요.

ASSERT_EXCLUSIVE_ACCESS/WRITER 활용

KCSAN의 assertion 매크로는 코드의 동시성 계약(contract)을 명시적으로 문서화하고 검증합니다.

/* ASSERT_EXCLUSIVE_ACCESS: 이 시점에서 다른 접근이 없어야 함 */
void init_object(struct my_obj *obj) {
    /* 초기화 중이므로 다른 스레드가 접근하면 안 됨 */
    ASSERT_EXCLUSIVE_ACCESS(obj->state);
    obj->state = OBJ_INITIALIZED;
    ASSERT_EXCLUSIVE_ACCESS(obj->data);
    obj->data = default_value;
}

/* ASSERT_EXCLUSIVE_WRITER: 다른 writer가 없어야 함 (reader는 허용) */
void update_config(struct config *cfg) {
    /* 구성 업데이트는 단일 스레드만 수행
     * reader는 RCU로 보호되어 동시 읽기 허용 */
    ASSERT_EXCLUSIVE_WRITER(cfg->value);
    cfg->value = new_value;
}

/* KCSAN 경고 예시 (assertion 위반 시):
 * BUG: KCSAN: assert: race in init_object / concurrent_user
 *
 * assert no accesses to 0xffff888100abc000:
 *  init_object+0x20/0x60
 *
 * racing access to 0xffff888100abc000:
 *  concurrent_user+0x30/0x40
 */

KCSAN 제외 설정

정당한 이유로 KCSAN 검사를 제외해야 하는 경우의 방법을 정리합니다.

방법범위적용 시점용도
data_race(expr)단일 표현식코드 수정의도적 레이스 허용
__no_kcsan전체 함수코드 수정성능 핫패스 제외
KCSAN_SANITIZE := n전체 파일/디렉토리Makefile모듈 단위 제외
KCSAN_SANITIZE_file.o := n단일 파일Makefile특정 파일 제외
kcsan-not (디버그fs)함수 이름런타임동적 제외
/* __no_kcsan: 성능 크리티컬 함수 전체 제외 */
static __no_kcsan void scheduler_hotpath(void) {
    /* 이 함수 내 모든 메모리 접근이 KCSAN 계측에서 제외됨 */
    /* 주의: 실제 data race가 있어도 탐지 못함 */
}

/* noinstr: 계측 자체를 금지 (KCSAN + ftrace + kprobes 모두) */
static noinstr void nmi_handler(void) {
    /* NMI 핸들러 등 재진입 불가 영역 */
}

KASAN과 동시성

KASAN(Kernel Address Sanitizer)은 주로 메모리 안전성 도구이지만, use-after-free(UAF) race를 탐지하는 데 강력합니다. 동시성 버그 중 "한 스레드가 객체를 해제하는 동안 다른 스레드가 아직 접근 중"인 패턴을 포착합니다.

KASAN 모드 비교

모드CONFIG 옵션메커니즘오버헤드정확도
Generic KASANCONFIG_KASAN_GENERIC컴파일러 계측 + 섀도 메모리~2-3x 메모리, ~2x CPU모든 접근 검사
SW Tag KASANCONFIG_KASAN_SW_TAGS소프트웨어 태그 기반~1.5x 메모리, ~1.5x CPU확률적 (태그 충돌)
HW Tag KASANCONFIG_KASAN_HW_TAGSARM MTE 하드웨어 태그최소확률적 (4비트 태그)

UAF race 탐지 예시

/* UAF race: 해제 시점의 경합 */
struct my_obj {
    struct rcu_head rcu;
    int data;
    spinlock_t lock;
};

/* 버그 있는 코드: RCU 없이 직접 해제 */
void buggy_remove(struct my_obj *obj) {
    spin_lock(&obj->lock);
    list_del(&obj->node);
    spin_unlock(&obj->lock);
    kfree(obj);  /* 다른 CPU가 아직 obj->data를 읽고 있을 수 있음! */
}

void concurrent_reader(struct my_obj *obj) {
    /* KASAN: use-after-free read detected! */
    pr_info("data = %d\n", obj->data);  /* UAF! */
}

/* KASAN 리포트:
 * BUG: KASAN: slab-use-after-free in concurrent_reader+0x18/0x30
 * Read of size 4 at addr ffff888100abcde8 by task reader/567
 *
 * Freed by task remover/456:
 *  kfree+0xb0/0x110
 *  buggy_remove+0x48/0x60
 */

/* 수정: RCU로 보호 */
void safe_remove(struct my_obj *obj) {
    spin_lock(&obj->lock);
    list_del_rcu(&obj->node);
    spin_unlock(&obj->lock);
    kfree_rcu(obj, rcu);  /* grace period 후 해제 */
}
quarantine: KASAN은 해제된 객체를 즉시 재사용하지 않고 격리(Isolation) 큐(quarantine)에 유지합니다. 이 덕분에 UAF 접근 시 섀도 메모리에 "freed" 상태가 남아있어 탐지 가능합니다. 격리 크기는 CONFIG_KASAN_QUARANTINE_SIZE로 조절합니다.

KASAN 섀도 메모리 레이아웃

KASAN Shadow Memory 레이아웃 (Generic) 커널 가상 주소 공간 (64-bit) 0xffff800000000000 — 0xffffffffffffffff (128TB) 1/8 매핑 (>> 3) KASAN Shadow Memory (16TB) shadow_addr = (addr >> 3) + KASAN_SHADOW_OFFSET 실제 메모리 → 섀도 매핑 8B 유효 5B 유효 해제됨 레드존 0x00 0x05 0xFD 0xFE 0x00 = 8B 모두 유효 0x01-0x07 = 처음 N바이트만 유효 0xFD = 해제됨 (kfree) 0xFE = KASAN 내부 레드존 0xF1/F2 = slab 레드존 (좌/우) Generic vs Tag-Based 비교 Generic KASAN - 컴파일러 계측 (모든 접근 검사) - 1/8 섀도 비율 → 메모리 2-3x - 결정적 탐지 (모든 접근) - 아키텍처 무관 (x86, ARM, RISC-V) HW Tag KASAN (ARM MTE) - 하드웨어 태그 비교 (4비트 태그) - 1/32 메모리 오버헤드 (태그 메모리) - 확률적 탐지 (16분의 1 태그 충돌) - ARM64 MTE 전용 (v8.5-A 이상) - 프로덕션 사용 가능 (<5% 오버헤드)

quarantine 메커니즘과 레이스 감지

KASAN quarantine은 해제된 객체를 일정 기간 격리하여 UAF를 더 확실하게 탐지합니다. 동시성 UAF에서 핵심적인 역할을 합니다.

/* quarantine이 없을 때 UAF를 놓치는 시나리오:
 *
 * CPU0:                    CPU1:
 * ----                     ----
 * kfree(obj)
 *                          obj_new = kmalloc(same_size)
 *                          // obj_new == obj (같은 주소 재사용!)
 *                          // 섀도가 0x00으로 리셋됨
 * obj->data 접근
 * // 섀도가 0x00이므로 KASAN이 탐지 못함!
 *
 * quarantine이 있으면:
 * CPU0:                    CPU1:
 * ----                     ----
 * kfree(obj)
 * // obj가 quarantine에 들어감
 * // 섀도 = 0xFD (freed)
 *                          obj_new = kmalloc(same_size)
 *                          // quarantine에서 다른 객체 할당
 *                          // obj는 아직 quarantine에 있음
 * obj->data 접근
 * // 섀도 = 0xFD → KASAN: use-after-free!
 */

/* quarantine 크기 설정 */
/* CONFIG_KASAN_QUARANTINE_SIZE (MB 단위)
 * 크게 설정할수록 UAF 탐지 확률 증가
 * 하지만 메모리 사용량도 증가
 * 기본값: 시스템 메모리의 3% */

KFENCE (Kernel Electric Fence)

KFENCE는 프로덕션 환경에서도 사용 가능한 경량 메모리 오류 감지 도구입니다. KASAN의 높은 오버헤드 문제를 해결하기 위해 설계되었습니다.

KFENCE 가드 페이지 레이아웃 GUARD PAGE (no access) Object Page object redzone GUARD PAGE (no access) Object Page GUARD PAGE OOB left → page fault OOB right → page fault KFENCE 특성 풀 크기: CONFIG_KFENCE_NUM_OBJECTS (기본 255) | 샘플 간격: CONFIG_KFENCE_SAMPLE_INTERVAL (기본 100ms) 탐지: OOB (가드 페이지), UAF (페이지 언맵), double-free (메타데이터 체크) | 오버헤드: <1% CPU, ~1MB 메모리

KFENCE 동작 원리

  1. 부팅 시 고정 크기의 KFENCE 풀(기본 255개 객체)을 할당합니다.
  2. 각 객체는 양쪽에 가드 페이지(Page)로 둘러싸여 있습니다. 가드 페이지는 접근 불가로 매핑(Mapping)됩니다.
  3. 일정 간격(CONFIG_KFENCE_SAMPLE_INTERVAL)마다 slab 할당을 가로채서 KFENCE 풀에서 할당합니다.
  4. OOB 접근: 가드 페이지 접근 시 page fault로 즉시 탐지됩니다.
  5. UAF: 해제 시 객체 페이지를 언맵하여 이후 접근 시 fault가 발생합니다.
# KFENCE 활성화 확인
cat /sys/kernel/debug/kfence/stats
# enabled: 1
# allocated: 234
# freed: 189
# currently allocated: 45
# total faults: 3

# KFENCE 리포트 예시 (dmesg)
# BUG: KFENCE: out-of-bounds read in buggy_func+0x20/0x40
# Out-of-bounds read at 0xffff888100xyz100 (4 bytes right of
#  kfence-#123 [0xffff888100xyz000-0xffff888100xyz0ff, 256 bytes]):
#  buggy_func+0x20/0x40
#  caller_func+0x30/0x50
프로덕션 배포 팁: KFENCE는 <1%의 CPU 오버헤드와 ~1MB 메모리만 사용하므로 프로덕션 커널에서도 안전하게 활성화할 수 있습니다. Google은 자사 프로덕션 서버에서 KFENCE를 상시 활성화하여 실제 UAF/OOB 버그를 조기에 발견합니다.

KFENCE 설정과 튜닝

CONFIG 옵션기본값설명프로덕션 권장
CONFIG_KFENCEnKFENCE 활성화y
CONFIG_KFENCE_NUM_OBJECTS255KFENCE 풀 객체 수255 (기본값 유지)
CONFIG_KFENCE_SAMPLE_INTERVAL100slab 할당 가로채기 간격 (ms)100-500ms
CONFIG_KFENCE_STATIC_KEYSystatic key 기반 활성화/비활성화y
CONFIG_KFENCE_STRESS_TEST_FAULTS0테스트용 인위적 fault 빈도0 (비활성화)
# KFENCE 런타임 제어
# 샘플 간격 동적 변경 (v6.0+)
echo 200 > /sys/module/kfence/parameters/sample_interval

# KFENCE 통계 확인
cat /sys/kernel/debug/kfence/stats
# enabled: 1
# allocated: 1234
# freed: 1100
# currently allocated: 134
# total faults: 5
# total bugs: 3

# 개별 객체 상태 확인
cat /sys/kernel/debug/kfence/objects | head -20

KFENCE vs KASAN 비교

특성Generic KASANHW Tag KASANKFENCE
메커니즘컴파일러 계측 + 섀도ARM MTE 하드웨어가드 페이지 + 샘플링
메모리 오버헤드2-3x~3%~1MB
CPU 오버헤드2-3x<5%<1%
탐지 확률100% (모든 접근)~93.75% (태그 기반)낮음 (샘플링)
OOB 탐지1바이트 단위16바이트 단위페이지 경계 + canary
UAF 탐지quarantine 기간재할당까지영구 (페이지 언맵)
프로덕션불가가능 (ARM64)가능
아키텍처모든 아키텍처ARM64 v8.5-A+모든 아키텍처
상호 보완 전략: KASAN과 KFENCE는 경쟁 관계가 아닌 보완 관계입니다. 개발/테스트에서는 KASAN으로 결정적 탐지를 수행하고, 프로덕션에서는 KFENCE로 낮은 오버헤드의 확률적 탐지를 수행합니다. KFENCE가 프로덕션에서 발견한 버그를 KASAN 빌드로 재현하여 정밀 분석하는 것이 효과적인 워크플로입니다.

KFENCE sysfs 인터페이스와 런타임 제어

KFENCE는 /sys/kernel/debug/kfence//sys/module/kfence/를 통해 런타임에 모니터링 및 제어할 수 있습니다.

# KFENCE 상태 확인
cat /sys/kernel/debug/kfence/stats
# Enabled: 1
# Allocated: 234
# Freed: 189
# Faults: 3
# Objects: 256 (pool size)

# 샘플링 간격 동적 변경 (부팅 후)
echo 50 > /sys/module/kfence/parameters/sample_interval
# 50ms마다 하나의 slab 할당을 KFENCE 풀로 리디렉션

# KFENCE 일시 비활성화/재활성화 (static key)
echo 0 > /sys/module/kfence/parameters/sample_interval  # 비활성화
echo 100 > /sys/module/kfence/parameters/sample_interval  # 재활성화

# 현재 KFENCE 보호 중인 객체 목록
cat /sys/kernel/debug/kfence/objects
# 0xfffffc0000040000-0xfffffc0000040fff  size=128  cache=kmalloc-128  alloc:
#   my_driver_alloc+0x34/0x80
#   kmalloc+0x123/0x200
#   ...

# KFENCE에서 탐지된 오류 이력
dmesg | grep "KFENCE"
프로덕션 KFENCE 운영 팁: 프로덕션에서 KFENCE를 운영할 때는 (1) sample_interval=500 이상으로 설정하여 오버헤드를 최소화하고, (2) 크래시 덤프(Dump)에 KFENCE 메타데이터가 포함되도록 CONFIG_KFENCE_NUM_OBJECTS=512 이상으로 설정하며, (3) dmesg를 주기적으로 모니터링하여 KFENCE 리포트를 자동 수집하는 스크립트를 배치합니다. 서버 10,000대에서 sample_interval=100으로 운영하면 하루에 수십 개의 메모리 안전 버그를 탐지할 수 있습니다.

메모리 새니타이저 통합 워크플로

KASAN, KFENCE, KMSAN(Kernel Memory Sanitizer)을 포함한 메모리 새니타이저의 효과적인 통합 사용법입니다.

단계도구목적실행 방법소요 시간 (상대)
1. 개발KASAN (Generic)UAF/OOB 결정적 탐지개발 커널에서 수동 테스트2-3x
2. CIKASAN + syzkaller퍼징 기반 자동 탐지syzkaller 인스턴스 24시간 실행N/A (병렬)
3. QAKMSAN미초기화 메모리 사용별도 KMSAN 빌드로 기능 테스트3-5x
4. 스테이징KASAN (HW Tags)낮은 오버헤드 검증ARM64 MTE 지원 환경1.05x
5. 프로덕션KFENCE확률적 장기 모니터링기본 커널에 포함1.01x
6. 사후 분석KASAN 재현KFENCE 리포트 정밀 분석KASAN 빌드에서 재현2-3x

sparse 정적 분석

sparse는 커널 전용 C 정적 분석 도구로, 타입 어노테이션을 통해 컴파일 타임에 동기화 규칙 위반을 탐지합니다.

sparse 어노테이션 기반 정적 분석 소스 코드 __acquires(lock) __releases(lock) __must_hold(lock) __rcu, __iomem, __user sparse make C=1 또는 C=2 어노테이션 검증 경고 출력 context imbalance: +1 expected 0 incompatible types: __rcu vs plain incorrect type in argument should be with __releases annotation 주요 어노테이션 __acquires(x) 함수가 락 x를 획득한 채 리턴 __releases(x) 함수가 락 x를 해제한 채 리턴 __must_hold(x) 호출자가 락 x를 보유해야 함 __rcu RCU 보호 포인터 (rcu_dereference() 필요) __iomem I/O 매핑 메모리 (ioread/iowrite 필요) __user 유저 공간 포인터 (copy_from_user 필요)

sparse 사용법

# 변경된 파일만 검사
make C=1 drivers/net/ethernet/intel/e1000e/

# 전체 트리 검사 (시간 소요)
make C=2 M=drivers/net/

# 특정 파일만
make C=2 CHECK=sparse drivers/block/loop.o

동기화 어노테이션 활용

/* __acquires / __releases 예시 */
static void my_lock_func(struct my_dev *dev)
    __acquires(&dev->lock)
{
    spin_lock(&dev->lock);
}

static void my_unlock_func(struct my_dev *dev)
    __releases(&dev->lock)
{
    spin_unlock(&dev->lock);
}

/* __must_hold 예시 — 호출자가 반드시 락 보유해야 함 */
static void update_state(struct my_dev *dev)
    __must_hold(&dev->lock)
{
    dev->state = NEW_STATE;
}

/* __rcu 어노테이션 — RCU 보호 포인터 */
struct my_config {
    struct rcu_head rcu;
    int value;
};

struct my_subsystem {
    struct my_config __rcu *config;  /* __rcu 표시 */
};

void read_config(struct my_subsystem *sys) {
    struct my_config *cfg;
    rcu_read_lock();
    cfg = rcu_dereference(sys->config);  /* __rcu 접근은 반드시 이것으로 */
    pr_info("value = %d\n", cfg->value);
    rcu_read_unlock();
}

/* sparse 경고: sys->config를 직접 역참조하면 */
/* warning: incorrect type in assignment (different address spaces)
 *    expected struct my_config *cfg
 *    got struct my_config [noderef] __rcu *config */

엔디안(Endianness) 검사

sparse는 __le16, __be32 등의 엔디안 어노테이션을 통해 바이트 순서(Byte Order) 변환 누락을 탐지합니다. 네트워크 및 디바이스 드라이버에서 특히 중요합니다.

/* 엔디안 어노테이션 예시 */
struct packet_header {
    __be16 src_port;    /* 네트워크 바이트 순서 (big-endian) */
    __be16 dst_port;
    __be32 seq_num;
};

/* 올바른 사용 */
void process_packet(struct packet_header *hdr) {
    u16 port = ntohs(hdr->src_port);  /* be16 → host 변환 */
    pr_info("src port: %u\n", port);
}

/* sparse 경고: 변환 없이 직접 사용 */
void buggy_process(struct packet_header *hdr) {
    u16 port = hdr->src_port;  /* sparse: restricted __be16 */
    pr_info("src port: %u\n", port);
}
/* warning: incorrect type in assignment
 *    expected unsigned short [usertype] port
 *    got restricted __be16 [usertype] src_port */

커스텀 sparse 검사 활용

# sparse에 추가 검사 플래그 전달
# 엔디안 검사 활성화
make C=2 CF="-D__CHECK_ENDIAN__" drivers/net/

# 비트 필드 검사
make C=2 CF="-Wbitwise" kernel/

# 모든 경고를 에러로 처리
make C=2 CF="-Werror" drivers/block/

# 특정 sparse 플래그 조합 (CI용)
make C=2 CF="-D__CHECK_ENDIAN__ -Wbitwise -Wno-return-void" \
  drivers/net/ethernet/
정적 분석 파이프라인 (빌드 시) 소스 코드 *.c + 어노테이션 sparse (make C=1) Coccinelle (SmPL) 타입 불일치, 락 균형 위반 __rcu/__user/__iomem 검사 패턴 기반 버그 탐지 이중 잠금, sleep-in-atomic GCC/Clang 경고 -Wthread-safety (Clang) 일반 경고 (-Wall -Wextra) CI 빌드 실패/경고 도구별 강점 비교 sparse 타입 시스템 기반 — __rcu, __user, __iomem, __acquires, __releases, 엔디안 Coccinelle 패턴 매칭 — SmPL로 코드 구조 검색, 커스텀 규칙 작성 가능 GCC/Clang 일반 경고 — 초기화되지 않은 변수, 형변환, NULL 역참조 등 권장: 세 도구를 모두 CI에 통합하여 상호 보완적으로 사용

Coccinelle 패턴 매칭으로 동기화 버그 탐지

Coccinelle은 SmPL(Semantic Patch Language)을 사용하여 C 코드의 구조적 패턴을 매칭/변환하는 도구입니다. 커널 트리에 scripts/coccinelle/에 다수의 시맨틱 패치가 포함되어 있습니다.

Coccinelle 실행

# 전체 Coccinelle 검사
make coccicheck MODE=report

# 특정 규칙만 실행
make coccicheck COCCI=scripts/coccinelle/locks/double_lock.cocci MODE=report

# 특정 디렉토리만
make coccicheck MODE=report M=drivers/net/

동기화 관련 Coccinelle 규칙

규칙 파일탐지 대상설명
locks/double_lock.cocci이중 락 획득같은 락을 두 번 잡는 패턴
locks/mini_lock.cocci불필요한 락 구간임계구역이 너무 짧은 패턴
locks/flags.cocciirqsave/irqrestore 불일치flags 변수 재사용 오류
free/kfree.cocci이중 해제(Double Free)동일 포인터 중복 kfree
api/atomic_as_refcounter.coccirefcount 패턴atomic_t를 refcount로 사용하는 위험

커스텀 SmPL 규칙 작성

// spin_lock 후 GFP_KERNEL 할당 탐지 (sleep-in-atomic)
@rule@
expression E, lock;
@@

spin_lock(&lock);
...
* kmalloc(E,
*   GFP_KERNEL
  )
...
spin_unlock(&lock);

// 실행: spatch --sp-file my_rule.cocci --dir drivers/ --mode report
// RCU read-side에서 슬립 가능 함수 호출 탐지
@rcu_sleep@
@@

rcu_read_lock();
...
* mutex_lock(...)
...
rcu_read_unlock();
CI 통합: Coccinelle 검사를 CI 파이프라인에 통합하면 코드 리뷰 전에 기계적으로 탐지 가능한 동기화 버그를 미리 걸러낼 수 있습니다.

SmPL 기초 문법

SmPL(Semantic Patch Language)의 기본 구문을 이해하면 커스텀 패턴을 작성할 수 있습니다.

// SmPL 기본 구조
@rule_name@                    // 규칙 이름
type T;                        // 타입 메타변수
expression E, E1;              // 표현식 메타변수
identifier func, lock;         // 식별자 메타변수
@@                             // 메타변수 선언 종료

// 패턴 매칭 (- = 삭제할 코드, + = 추가할 코드, * = 보고)
// '...' = 임의의 코드 (C 문법 구조 내)

// 예: spin_lock 후 에러 경로에서 unlock 누락 탐지
@missing_unlock@
expression lock, E;
@@

spin_lock(&lock);
...
* if (E) {
*   return ...;  // spin_unlock 없이 리턴!
  }
...
spin_unlock(&lock);

// 실행: spatch --sp-file missing_unlock.cocci --dir drivers/ --mode report

레이스 탐지 SmPL 패턴

// 패턴 1: kmalloc + memset 사이에 레이스 가능 (kzalloc 사용 권장)
@kzalloc_pattern@
expression E, size, flags;
@@

- E = kmalloc(size, flags);
- ... when != E == NULL
- memset(E, 0, size);
+ E = kzalloc(size, flags);

// 패턴 2: 락 보유 중 usleep_range 호출 (sleep-in-atomic)
@usleep_in_lock@
expression lock, min, max;
@@

spin_lock(&lock);
...
* usleep_range(min, max)
...
spin_unlock(&lock);

// 패턴 3: rcu_dereference 없이 __rcu 포인터 접근
@rcu_deref_missing@
expression ptr;
identifier member;
@@

rcu_read_lock();
...
* ptr->member    // rcu_dereference(ptr) 없이 직접 접근
...
rcu_read_unlock();

// 패턴 4: refcount underflow 가능 (dec_and_test 후 재사용)
@refcount_uaf@
expression obj, ref;
@@

* if (refcount_dec_and_test(&ref)) {
*   kfree(obj);
  }
  ...
* obj->...    // kfree 이후 접근 가능!

ftrace를 이용한 동시성 이벤트 추적

ftrace는 커널의 내장 트레이싱 프레임워크로, lock contention, IRQ-off 구간, preempt-off 구간 등 동시성 관련 이벤트를 실시간(Real-time)으로 추적합니다.

ftrace 동시성 추적 아키텍처 특수 Tracer irqsoff preemptoff preemptirqsoff wakeup / wakeup_rt Lock Events lock:lock_acquire lock:lock_release lock:lock_contended lock:lock_acquired Ring Buffer per-CPU timestamp + data trace_pipe 출력 출력 /sys/kernel/debug/ tracing/trace tracing/trace_pipe 주요 사용 시나리오 irqsoff tracer IRQ 비활성화 최대 구간 추적 → RT 성능 저하 원인 파악 lock events 락 contention 핫스팟 식별 → 병목 락 최적화 preemptoff preempt 비활성화 구간 추적 → 스케줄링 지연 원인

irqsoff tracer

# IRQ 비활성화 최대 시간 추적
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 워크로드 실행 후
cat /sys/kernel/debug/tracing/trace
# irqsoff latency trace v1.1.5 on 6.8.0-rc1
# latency: 1234 us, #4/4, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0)
#    -----------------
#    | task: kworker/2:1-567 (uid:0 nice:0 policy:0 rt_prio:0)
#    -----------------
# => started at: driver_func
# => ended at:   driver_func_done
#
#                    _------=> CPU#
#                   / _-----=> irqs-off
#                  | / _----=> need-resched
#                  || / _---=> hardirq/softirq
#                  ||| / _--=> preempt-depth
#                  ||||/
#     cmd     pid  ||||| time  |   caller
#        \   /     |||||  \    |    /
#  kworker-567   2d...  0us : spin_lock_irqsave <- driver_func
#  kworker-567   2d...  1234us : spin_unlock_irqrestore <- driver_func_done

Lock event 트레이싱

# lock 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/lock/enable

# 또는 개별 이벤트
echo 1 > /sys/kernel/debug/tracing/events/lock/lock_contended/enable

# 실시간 모니터링
cat /sys/kernel/debug/tracing/trace_pipe
# kworker/0:1-25  [000] d... 12345.678: lock_contended: &dev->lock
# kworker/0:1-25  [000] d... 12345.679: lock_acquired: &dev->lock (1234 ns)

# 히스토그램으로 contention 분포 확인 (hist trigger)
echo 'hist:key=name:val=hitcount:sort=hitcount.descending' \
  > /sys/kernel/debug/tracing/events/lock/lock_contended/trigger
cat /sys/kernel/debug/tracing/events/lock/lock_contended/hist
function_graph와 조합: function_graph tracer를 lock event와 함께 사용하면 어떤 함수 호출 체인에서 락 contention이 발생하는지 시각적으로 파악할 수 있습니다.

trace-cmd로 잠금(Lock) 분석

trace-cmd는 ftrace의 사용자 공간 프론트엔드로, 복잡한 debugfs 조작 없이 트레이싱을 수행할 수 있습니다.

# trace-cmd로 lock 이벤트 수집
trace-cmd record -e lock -e irq -- sleep 10

# 결과 분석
trace-cmd report | head -50
# trace-cmd-1234 [002] 12345.678: lock_acquire: &dev->lock read=0
# trace-cmd-1234 [002] 12345.679: lock_acquired: &dev->lock (wait: 45 ns)
# trace-cmd-1234 [002] 12345.680: lock_release: &dev->lock

# 특정 이벤트만 필터링
trace-cmd report -F 'lock_contended' | sort -k5 -rn | head -20

# 프로파일 모드 (통계 요약)
trace-cmd record -e lock --profile -- sleep 30
trace-cmd profile
# Lock contention report:
#   rtnl_mutex:    567 contentions,  avg 45us,  max 890us
#   mmap_lock-W:  2345 contentions,  avg 12us,  max 456us

# KernelShark GUI로 시각화 (X11 필요)
trace-cmd record -e lock -e sched -- workload_command
kernelshark trace.dat
trace-cmd의 장점: debugfs 직접 조작 대비 (1) 명령어가 직관적이고 (2) 바이너리 포맷으로 저장하여 후처리가 빠르며 (3) 원격 수집(trace-cmd listen/record -N)이 가능합니다. 프로덕션 서버에서 on-demand 분석 시 특히 유용합니다.

perf lock / perf sched 활용

perf의 lock/sched 서브커맨드를 사용하면 시스템 전체의 락 경합과 스케줄링 지연을 프로파일링할 수 있습니다.

perf lock 분석 워크플로 perf lock record 이벤트 수집 perf lock report 통계 분석 perf lock contention BPF 기반 실시간 분석 병목 식별 최적화 대상 perf lock report 출력 Name acquired contended avg wait total wait rtnl_mutex 1234 567 45 us 25515 us &mm->mmap_lock 5678 2345 12 us 28140 us &sb->s_lock 890 123 8 us 984 us → total wait이 큰 락이 병목 perf lock contention 출력 contended total wait max wait avg wait type caller 567 25515 us 890 us 45 us mutex func_a 2345 28140 us 456 us 12 us rwsem func_b 123 984 us 34 us 8 us spin func_c → caller 스택으로 contention 원인 함수 특정

perf lock 실전 사용법

# 1. 락 이벤트 수집 (30초간)
perf lock record -- sleep 30

# 2. 통계 리포트
perf lock report
# Name                    acquired contended  avg wait (ns)  total wait (ns)
# rtnl_mutex                  1234       567          45000         25515000
# &mm->mmap_lock (W)         5678      2345          12000         28140000
# ...

# 3. BPF 기반 실시간 contention 분석 (v6.2+)
perf lock contention -ab -- sleep 10
# contended  total wait   max wait  avg wait  type   caller
#      567     25.5 ms     890 us    45 us   mutex  rtnl_lock+0x18
#     2345     28.1 ms     456 us    12 us   rwsem  mmap_read_lock+0x20

# 4. 특정 락만 필터
perf lock contention -L rtnl_mutex -- sleep 10

# 5. 콜스택 포함
perf lock contention -abcs -- sleep 10

perf sched: 스케줄링 지연 분석

# 스케줄링 이벤트 수집
perf sched record -- sleep 10

# 스케줄링 지연 통계
perf sched latency
# Task                  | Runtime   | Switches | Avg delay | Max delay |
# kworker/0:1           |   5.234 ms|       123|    0.045 ms|    1.234 ms|
# migration/2           |   0.123 ms|        45|    0.012 ms|    0.089 ms|

# 타임라인 보기
perf sched timehist
# time    cpu  task name          wait time  sch delay
# 12345.001 [002] kworker/0:1       0.000 ms   0.045 ms
# 12345.002 [002] bash              0.123 ms   0.012 ms

# 시각적 맵
perf sched map
perf lock contention의 장점: v6.2 이후 BPF를 사용하여 tracepoint 없이도 락 contention을 분석할 수 있습니다. /proc/lock_stat보다 오버헤드가 적고, 콜스택 정보를 함께 수집할 수 있어 원인 분석이 더 쉽습니다.

bpftrace 기반 잠금 분석

bpftrace를 사용하면 커스텀 잠금 분석 스크립트를 빠르게 작성하여 특정 시나리오를 조사할 수 있습니다.

# 특정 락의 holdtime 분포 측정
bpftrace -e '
kprobe:mutex_lock {
    @lock_start[tid] = nsecs;
    @lock_name[tid] = str(arg0);
}
kprobe:mutex_unlock /@lock_start[tid]/ {
    $holdtime = nsecs - @lock_start[tid];
    @hold_hist = hist($holdtime);
    if ($holdtime > 10000000) {  /* 10ms 이상 */
        printf("long hold: %d ns, comm=%s\n", $holdtime, comm);
        print(kstack(8));
    }
    delete(@lock_start[tid]);
    delete(@lock_name[tid]);
}
END { print(@hold_hist); }
'

# 락 경합(contention)이 가장 많은 스택 Top-10
bpftrace -e '
tracepoint:lock:lock_contended {
    @contention[kstack(5)] = count();
}
interval:s:30 {
    print(@contention, 10);
    exit();
}
'

# 특정 함수에서 락을 얼마나 오래 보유하는지 측정
bpftrace -e '
kprobe:my_driver_xmit {
    @func_entry[tid] = nsecs;
}
kretprobe:my_driver_xmit /@func_entry[tid]/ {
    $dur = nsecs - @func_entry[tid];
    @xmit_latency = hist($dur);
    delete(@func_entry[tid]);
}
interval:s:10 { print(@xmit_latency); clear(@xmit_latency); }
'

CONFIG 옵션 종합

동시성 디버깅에 사용되는 커널 CONFIG 옵션을 체계적으로 정리합니다.

카테고리CONFIG 옵션기능오버헤드프로덕션
lockdepCONFIG_PROVE_LOCKING락 의존성 검증 (핵심 스위치)높음불가
CONFIG_DEBUG_LOCK_ALLOC락 할당/해제 추적중간불가
CONFIG_LOCK_STAT/proc/lock_stat 통계중간조건부
CONFIG_DEBUG_LOCKDEPlockdep 자체 디버깅높음불가
CONFIG_PROVE_RCURCU 의존성 검증중간불가
CONFIG_DEBUG_WW_MUTEX_SLOWPATHww_mutex 슬로우패스 디버깅낮음불가
데이터 레이스CONFIG_KCSANKCSAN 활성화중간불가
CONFIG_KCSAN_STRICT엄격 모드 (모든 비표시 레이스)중간불가
CONFIG_KCSAN_KUNIT_TESTKCSAN 기능 테스트낮음불가
CONFIG_KCSAN_WEAK_MEMORY약한 메모리 모델 검사낮음불가
메모리 안전CONFIG_KASAN커널 주소 새니타이저높음불가
CONFIG_KASAN_GENERIC컴파일러 계측 모드매우 높음불가
CONFIG_KFENCE경량 메모리 감지매우 낮음가능
CONFIG_KFENCE_SAMPLE_INTERVAL샘플링 간격 (기본 100ms)설정 의존가능
컨텍스트CONFIG_DEBUG_ATOMIC_SLEEP원자 컨텍스트에서 슬립 탐지낮음불가
CONFIG_DEBUG_PREEMPTpreempt count 검증낮음불가
CONFIG_DEBUG_RT_MUTEXESRT mutex 디버깅중간불가
CONFIG_DETECT_HUNG_TASK행 태스크 탐지매우 낮음가능
스트레스 테스트CONFIG_LOCK_TORTURE_TEST락 스트레스 테스트 모듈N/A불가
CONFIG_RCU_TORTURE_TESTRCU 스트레스 테스트N/A불가
CONFIG_DEBUG_OBJECTS객체 수명 추적중간불가

환경별 CONFIG 옵션 매트릭스

CONFIG 옵션개발 PCCI/CDQA 테스트스테이징프로덕션
PROVE_LOCKINGYYYNN
LOCK_STATYNY선택N
KCSANYY (별도빌드)YNN
KASAN_GENERICYY (별도빌드)YNN
KASAN_HW_TAGSN/AN/AARM MTEARM MTEARM MTE
KFENCEYYYYY
DEBUG_ATOMIC_SLEEPYYY선택N
DETECT_HUNG_TASKYYYYY
SOFTLOCKUP_DETECTORYYYYY
LOCK_TORTURE_TESTMMMNN
DEBUG_OBJECTSY선택YNN
DEBUG_PREEMPTYYYNN
CI 빌드 분리 전략: KASAN과 KCSAN은 동시 활성화 시 10x 이상의 오버헤드가 발생하므로, CI에서는 최소 3개의 커널 빌드를 운영하세요: (1) lockdep + KCSAN + DEBUG_ATOMIC_SLEEP, (2) KASAN + KFENCE + DEBUG_OBJECTS, (3) locktorture + rcutorture 스트레스 전용. 각 빌드에서 동일한 테스트 스위트를 실행하면 90% 이상의 동시성 버그를 탐지할 수 있습니다.
# .config 발췌 — 동시성 디버깅 종합 설정
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_LOCK_STAT=y
CONFIG_PROVE_RCU=y
CONFIG_DEBUG_ATOMIC_SLEEP=y
CONFIG_KCSAN=y
CONFIG_KCSAN_STRICT=y
CONFIG_KCSAN_WEAK_MEMORY=y
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KFENCE=y
CONFIG_DEBUG_PREEMPT=y
CONFIG_DETECT_HUNG_TASK=y
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT=120
CONFIG_LOCK_TORTURE_TEST=m
CONFIG_RCU_TORTURE_TEST=m
동시 활성화 주의: KASAN과 KCSAN을 동시에 활성화하면 오버헤드가 크게 증가합니다(10x 이상). 개발 초기에는 별도로 빌드하여 테스트하고, CI에서는 각각 별도 커널 이미지로 분리하는 것을 권장합니다.

실제 커널 동시성 버그 사례 분석

실제 CVE로 보고된 커널 동시성 버그를 분석하여 어떤 유형의 버그가 발생하고, 어떤 도구로 탐지할 수 있었는지 살펴봅니다.

동시성 CVE 유형별 분류 Data Race / UAF CVE-2022-1729: perf race → LPE CVE-2022-2588: route4 UAF race CVE-2023-3090: ipvlan UAF race CVE-2022-0847: pipe race (dirty pipe) 탐지: KASAN, KCSAN, syzkaller 수정: 락 추가, RCU 적용, refcount Deadlock / Livelock CVE-2019-11085: i915 deadlock CVE-2021-28972: USB deadlock CVE-2023-52447: bpf map deadlock ABBA 패턴이 가장 빈번 탐지: lockdep, hung_task, syzkaller 수정: 락 순서 재정의, trylock TOCTOU / Logic Race CVE-2016-5195: dirty COW race CVE-2021-4154: cgroup1 refcount CVE-2024-1086: nf_tables race 윈도우 기반 논리적 경합 탐지: 코드 리뷰, fuzzing, KASAN 수정: atomic ops, 검사-사용 통합

CVE-2022-0847 (Dirty Pipe) 분석

유형: Data Race / TOCTOU — pipe 페이지 캐시(Page Cache) 플래그 레이스

splice()write()의 경합으로 파이프 버퍼(Buffer)의 PIPE_BUF_FLAG_CAN_MERGE 플래그가 부적절하게 설정되어, 읽기 전용(Read-Only) 파일에 쓰기가 가능해지는 권한 상승 취약점(Vulnerability)입니다.

/* 취약 경로 (간소화) */
/* 1. splice()로 읽기 전용 파일의 페이지 캐시를 파이프에 매핑 */
/* 2. 이전 write()에서 PIPE_BUF_FLAG_CAN_MERGE가 남아있음 */
/* 3. 새 write()가 페이지 캐시에 직접 쓰기 수행 */

/* 수정: splice 후 무조건 PIPE_BUF_FLAG_CAN_MERGE 클리어 */
buf->flags &= ~PIPE_BUF_FLAG_CAN_MERGE;  /* commit 9d2231c5d74e */

CVE-2022-1729 (perf_event race) 분석

perf_event의 perf_event_open()에서 event->owner 설정과 fd_install() 사이의 레이스 윈도우를 악용한 권한 상승입니다.

/* 레이스 윈도우 */
fd = get_unused_fd_flags(O_RDWR | O_CLOEXEC);
/* ... event 설정 ... */
event->owner = current;  /* (A) owner 설정 */
/* 여기서 다른 스레드가 fd를 close하면? */
fd_install(fd, event->filp);  /* (B) fd 설치 */
/* A와 B 사이에 close() 경합 → UAF */

/* 수정: fd_install() 전에 적절한 참조 카운팅과 순서 보장 */

CVE-2016-5195 (Dirty COW) 분석

역사적으로 가장 유명한 커널 레이스 취약점입니다. madvise(MADV_DONTNEED)와 COW(Copy-On-Write) 페이지 폴트(Page Fault) 처리 사이의 레이스로 읽기 전용 매핑에 쓰기가 가능해집니다.

/* 공격 흐름 (간소화) */
/* Thread A: 반복 write() 시도 (COW 트리거) */
/* Thread B: 반복 madvise(MADV_DONTNEED) (페이지 해제) */
/*
 * 타이밍:
 * 1. Thread A: COW 처리 시작 → 새 페이지 할당
 * 2. Thread B: MADV_DONTNEED → 페이지 테이블 엔트리 제거
 * 3. Thread A: COW 완료 → 원본 페이지에 직접 쓰기 (!)
 *
 * 레이스 윈도우에서 pte_dirty 검사를 우회
 */

/* 수정: can_follow_write_pte() 검사 강화 — commit 19be0eaffa3a */
교훈: 커널 동시성 버그의 상당수는 수년간 존재한 레이스 윈도우가 뒤늦게 발견된 사례입니다. Dirty COW는 2007년부터 존재했으며 2016년에야 발견되었습니다. 이는 동시성 테스트 인프라(KCSAN, fuzzing)의 중요성을 보여줍니다.
CVE-2016-5195 Dirty COW 레이스 시퀀스 Thread A (write) Thread B (madvise) Kernel (mm) write(/proc/self/mem) COW: 새 페이지 할당 PTE 갱신 (새 페이지) 레이스 윈도우 madvise(MADV_DONTNEED) PTE 제거 (페이지 해제) COW 재시도 (PTE 없음) 원본 페이지에 직접 쓰기! 읽기전용 파일 변조! 수정 (commit 19be0eaffa3a): can_follow_write_pte() 강화 — COW 완료 전 PTE dirty 플래그를 검사하여 원본 페이지 쓰기를 차단

공통 레이스 패턴 분석

커널 CVE에서 반복적으로 나타나는 레이스 패턴을 정리합니다.

패턴설명대표 CVE수정 방법탐지 도구
fd install racefd_install() 전후 레이스 윈도우CVE-2022-1729fd_install 순서 보장(Ordering), refcountsyzkaller + KASAN
TOCTOU (mmap/write)메모리 매핑 + 접근 사이 레이스CVE-2016-5195검사-사용 원자화, PTE 보호코드 리뷰, fuzzing
netfilter hook racehook 등록/해제와 패킷(Packet) 처리 레이스CVE-2024-1086RCU 보호, synchronize_rcu()KASAN + syzkaller
TC filter UAF트래픽 컨트롤 필터 삭제/사용 레이스CVE-2022-2588RCU + refcount 조합KASAN + syzbot
refcount overflow참조 카운터 정수 오버플로(Integer Overflow)우CVE-2021-4154refcount_t 전환Coccinelle + 코드 리뷰
sleep-in-atomic원자 컨텍스트에서 슬립 호출다수작업 위임 (work_struct)lockdep, DEBUG_ATOMIC_SLEEP

CVE에서 배우는 교훈

커널 동시성 CVE의 공통 교훈:
  1. 레이스 윈도우는 항상 존재한다: "이 간격은 너무 짧아서 레이스가 불가능하다"는 가정은 위험합니다. 공격자는 CPU 스케줄링, userfaultfd, FUSE 등으로 윈도우를 확장할 수 있습니다.
  2. syzkaller/syzbot이 게임 체인저: 최근 커널 동시성 CVE의 상당수(60%+)는 syzkaller 퍼저가 최초 발견했습니다.
  3. KASAN + lockdep 조합이 핵심: UAF race는 KASAN으로, 데드락/순서 문제는 lockdep으로 탐지합니다.
  4. RCU는 만능이 아니다: RCU를 사용해도 grace period 내 참조 카운팅을 올바르게 하지 않으면 UAF가 발생합니다.
  5. refcount_t 전환은 필수: atomic_t 기반 참조 카운팅은 오버플로우 공격에 취약합니다.

동시성 테스트 전략

동시성 버그는 비결정적이므로 일반적인 단위 테스트로는 재현이 어렵습니다. 체계적인 스트레스 테스트와 퍼징이 필수적입니다.

locktorture (Lock Torture Test)

# 모듈 로드 — 기본 테스트 (spin_lock)
modprobe locktorture torture_type=spin_lock nwriters_stress=8

# 지정 시간 후 결과 확인
cat /proc/lock_torture_stats
# spin_lock-torture: Writes:  Total: 12345678  Max/Min: 1234567/1234567
# spin_lock-torture: Writes:  Coverage: 0
# spin_lock-torture: Writes:  0 rtmutex_chain_walk:0

# mutex 테스트
modprobe locktorture torture_type=mutex_lock nwriters_stress=8

# rwsem 테스트
modprobe locktorture torture_type=rwsem_lock nwriters_stress=4 nreaders_stress=8

# 종료
rmmod locktorture

rcutorture (RCU Torture Test)

# RCU 스트레스 테스트
modprobe rcutorture torture_type=rcu nreaders=8 nfakewriters=4 stat_interval=60

# 실시간 통계
cat /proc/rcudata
# 0 c=12345 g=12346 pq=1 qp=1 dt=567/890 ...

# SRCU 테스트
modprobe rcutorture torture_type=srcu nreaders=8

# 종료
rmmod rcutorture

syzkaller (커널 퍼저)

# syzkaller 설정 예시 (syz-manager.cfg)
{
    "target": "linux/amd64",
    "http": "127.0.0.1:56741",
    "workdir": "/root/syzkaller/workdir",
    "kernel_obj": "/root/linux/",
    "image": "/root/image/bullseye.img",
    "syzkaller": "/root/syzkaller",
    "procs": 8,
    "type": "qemu",
    "vm": {
        "count": 4,
        "kernel": "/root/linux/arch/x86/boot/bzImage",
        "cpu": 2,
        "mem": 2048
    },
    "enable_syscalls": [
        "open", "close", "read", "write",
        "mmap", "munmap", "madvise",
        "futex", "clone", "epoll*"
    ]
}

stress-ng

# 락 경합 스트레스
stress-ng --lock 8 --timeout 60s --metrics-brief

# futex 스트레스
stress-ng --futex 8 --timeout 60s

# mmap 레이스 스트레스
stress-ng --mmap 4 --mmap-bytes 256M --timeout 60s

# 종합 동시성 스트레스
stress-ng --lock 4 --futex 4 --mmap 4 --sem 4 --timeout 120s

KUnit 동시성 테스트

/* KUnit을 활용한 동시성 단위 테스트 */
#include <kunit/test.h>

static void test_atomic_counter(struct kunit *test) {
    atomic_t counter = ATOMIC_INIT(0);
    int i;

    /* 단일 스레드 기본 검증 */
    for (i = 0; i < 10000; i++)
        atomic_inc(&counter);

    KUNIT_EXPECT_EQ(test, atomic_read(&counter), 10000);
}

/* KCSAN과 함께 사용하면 멀티스레드 테스트에서 레이스 탐지 */
static struct kunit_case concurrency_test_cases[] = {
    KUNIT_CASE(test_atomic_counter),
    {}
};

static struct kunit_suite concurrency_test_suite = {
    .name = "concurrency",
    .test_cases = concurrency_test_cases,
};
CI 구성 권장:
  • 빌드 1: PROVE_LOCKING + DEBUG_ATOMIC_SLEEP → locktorture 1시간
  • 빌드 2: KCSAN_STRICT → rcutorture + 일반 테스트 스위트
  • 빌드 3: KASAN_GENERIC → syzkaller 24시간
  • 프로덕션: KFENCE + DETECT_HUNG_TASK 상시 활성화

LTP 동시성 테스트

LTP(Linux Test Project)는 커널의 동시성 관련 시스템 콜(System Call)을 체계적으로 테스트하는 대규모 테스트 스위트입니다.

# LTP 설치 (Git에서 빌드)
git clone https://github.com/linux-test-project/ltp.git
cd ltp && make autotools && ./configure && make -j$(nproc)
sudo make install

# 동시성 관련 테스트 실행
# futex 테스트 (우선순위 역전, 데드락 탐지)
sudo /opt/ltp/runltp -f syscalls -s futex

# pthread 레이스 테스트
sudo /opt/ltp/runltp -f threads

# mmap 레이스 테스트
sudo /opt/ltp/runltp -f mm -s mmap

# 전체 동시성 관련 테스트 (시간 소요)
sudo /opt/ltp/runltp -f syscalls,mm,threads,ipc

# 주요 동시성 테스트 케이스
# futex_wake04: PI futex 우선순위 상속 검증
# madvise06: MADV_DONTNEED + mmap 레이스 검증
# clone08: clone + exec 레이스 검증
# fcntl_lock: 파일 잠금 동시성 검증
LTP vs syzkaller: LTP는 정의된 시나리오를 반복 테스트하는 결정적 접근이고, syzkaller는 무작위 시스템 콜 시퀀스를 생성하는 확률적 접근입니다. 두 접근을 병행하면 커버리지를 극대화할 수 있습니다. LTP는 회귀 테스트에, syzkaller는 새로운 버그 발견에 더 효과적입니다.

CI/CD 동시성 테스트 통합 가이드

동시성 디버깅 도구를 CI/CD 파이프라인에 통합하면 커밋 단위로 동시성 버그를 조기에 탐지할 수 있습니다.

#!/bin/bash
# CI용 동시성 테스트 스크립트 예시
# ci-concurrency-test.sh

set -euo pipefail
KERNEL_DIR="${1:-.}"
RESULT_DIR="test-results/concurrency"
mkdir -p "$RESULT_DIR"

echo "=== Phase 1: 정적 분석 ==="
# sparse 검사 (변경된 파일만)
git diff --name-only HEAD~1 | grep '\.c$' | while read f; do
    make -C "$KERNEL_DIR" C=2 "${f%.c}.o" 2>>"$RESULT_DIR/sparse.log" || true
done
echo "sparse 경고: $(grep -c 'warning:' "$RESULT_DIR/sparse.log" || echo 0)건"

echo "=== Phase 2: lockdep 커널 부팅 테스트 ==="
# QEMU에서 lockdep 커널 부팅 (5분 제한)
timeout 300 qemu-system-x86_64 \
    -kernel "$KERNEL_DIR/arch/x86/boot/bzImage" \
    -initrd initramfs.cpio.gz \
    -append "console=ttyS0 lockdep_test=1" \
    -nographic -no-reboot \
    -serial file:"$RESULT_DIR/boot.log" || true

echo "=== Phase 3: locktorture ==="
# 모듈 로드 후 5분 실행
echo "modprobe locktorture torture_type=spin_lock nwriters_stress=4" \
    | timeout 300 qemu-system-x86_64 ... 2>>"$RESULT_DIR/torture.log" || true

echo "=== Phase 4: 결과 집계 ==="
# BUG/WARNING 검색
if grep -qE "BUG:|WARNING:|DEADLOCK|KASAN|KCSAN" "$RESULT_DIR"/*.log; then
    echo "FAIL: 동시성 문제 감지됨!"
    grep -E "BUG:|WARNING:|DEADLOCK|KASAN|KCSAN" "$RESULT_DIR"/*.log
    exit 1
fi
echo "PASS: 동시성 테스트 통과"
CI 시간 예산 가이드: lockdep 커널은 부팅만으로 기존 데드락 경로를 검증합니다 (~2분). locktorture 5분 실행으로 기본 락 안전성을 검증합니다. syzkaller는 24시간 이상 실행해야 충분한 커버리지를 확보할 수 있으므로, 별도의 nightly CI 작업으로 분리하세요. 총 CI 시간이 제한적이라면 "lockdep 부팅 + sparse + locktorture 5분" 조합이 투자 대비 효과가 가장 높습니다.

동시성 테스트 커버리지 분석

동시성 테스트의 효과를 측정하려면 락 커버리지와 경로 커버리지를 함께 분석해야 합니다.

# lockdep 커버리지: 발견된 lock class 수
grep "lock-classes" /proc/lockdep_stats
# lock-classes:     2345

# 의존성 커버리지: 탐색된 의존성 수
grep "dependency" /proc/lockdep_stats
# dependency chains:                  8901
# dependency chain hlocks used:      34567
# direct dependencies:               12345

# KCSAN 커버리지: 감시된 메모리 접근 수
cat /sys/kernel/debug/kcsan
# total_watchpoints: 1234567
# data_races_detected: 23
# report_count: 23

# gcov 기반 코드 커버리지 (CONFIG_GCOV_KERNEL)
# 동시성 관련 코드 경로가 실행되었는지 확인
lcov --capture --directory kernel/ --output-file coverage.info
genhtml coverage.info --output-directory coverage-html
# coverage-html/kernel/locking/ 디렉토리에서 lockdep 코드 커버리지 확인
커버리지 목표: lockdep의 lock-classes가 전체 커널의 10% 이상이면 기본 커버리지로 볼 수 있습니다. syzkaller의 커버리지가 40% 이상이면 양호한 수준입니다. KCSAN의 total_watchpoints가 100만 이상이면 충분한 샘플링입니다. 이 지표들을 CI 대시보드에 표시하여 테스트 품질을 지속적으로 모니터링하세요.

디버깅 워크플로 체계화

동시성 버그의 발견부터 검증까지 5단계 체계적 워크플로를 정리합니다.

동시성 디버깅 5단계 워크플로 1. 발견 lockdep/KCSAN 경고 crash/hang 리포트 2. 재현 stress-ng/torture test syzkaller reproducer 3. 분석 스택 트레이스 해석 lock 의존성 추적 4. 수정 패치 작성 코드 리뷰 5. 검증 회귀 테스트 CI 통과 실패 시 2단계(재현)부터 반복 1단계: 발견 체크리스트 a. dmesg에서 WARNING/BUG 패턴 검색 b. lockdep: "possible circular", "inconsistent" c. KCSAN: "BUG: KCSAN: data-race in" d. KASAN: "BUG: KASAN: slab-use-after-free" e. soft lockup: "BUG: soft lockup - CPU#X stuck" f. hung_task: "INFO: task X blocked for more than" 3단계: 분석 도구 선택 데이터 레이스 → KCSAN 리포트의 racing pair 분석 데드락 → lockdep 의존 체인 역추적 UAF → KASAN freed-by/allocated-by 스택 비교 성능 저하 → perf lock contention + ftrace soft lockup → ftrace irqsoff + perf sched hung_task → /proc/<pid>/stack + lockdep 4단계: 대표 수정 패턴 Data Race → READ_ONCE/WRITE_ONCE, atomic_t 변환, 적절한 락 추가 ABBA Deadlock → 전역 락 순서 규칙, lock_nested(), trylock + 재시도 IRQ Deadlock → spin_lock_irqsave()로 교체, 락 분리 (process vs IRQ) UAF Race → RCU 보호 (kfree_rcu), refcount 도입, 수명 관리 강화 Sleep-in-Atomic → 락 밖으로 슬립 코드 이동, GFP_ATOMIC 사용, work_struct 분리

실전 디버깅 시나리오: 발견과 재현

시나리오: 서버에서 간헐적 행(hang) 발생

# 1단계: 발견 — dmesg 확인
dmesg | grep -E "lockdep|DEADLOCK|hung_task|soft lockup"
# [12345.678] INFO: task kworker/0:1:25 blocked for more than 120 seconds.

# 2단계: 재현 — 행 발생 시 태스크 스택 덤프
echo t > /proc/sysrq-trigger  # 전체 태스크 스택 덤프
# 또는 특정 프로세스
cat /proc/25/stack
# [<0>] __mutex_lock+0x200/0x580
# [<0>] rtnl_lock+0x18/0x20
# [<0>] dev_ioctl+0x3c/0x240

# 3단계: 분석 — lockdep 활성화 재현
# CONFIG_PROVE_LOCKING=y로 재빌드 후 재현 시도
# lockdep이 데드락 경로를 보여줄 것

# 4단계: 수정 — 원인에 따라
# - ABBA: 락 순서 통일
# - 자기 데드락: trylock + 재시도 로직
# - sleep-in-atomic: 임계구역 밖으로 이동

# 5단계: 검증
modprobe locktorture torture_type=mutex_lock nwriters_stress=16
# 1시간 후 stats 확인, lockdep 경고 없는지 확인
cat /proc/lock_torture_stats

도구 선택 결정 트리

도구 선택 결정 트리 증상이 무엇인가? 행(hang)/응답 없음 lockdep PROVE_LOCKING hung_task sysrq-t 덤프 데이터 손상/crash KCSAN data race 탐지 KASAN UAF/OOB 탐지 성능 저하/지연 perf lock contention 분석 ftrace irqsoff latency 추적 환경에 따른 도구 선택 개발/테스트 환경 lockdep (PROVE_LOCKING) KCSAN (STRICT 모드) KASAN (GENERIC 모드) DEBUG_ATOMIC_SLEEP locktorture / rcutorture syzkaller fuzzing 프로덕션 환경 KFENCE (상시 활성화, <1% 오버헤드) DETECT_HUNG_TASK (행 탐지) perf lock contention (on-demand) ftrace (on-demand, 동적 활성화) /proc/lock_stat (on-demand) HW Tag KASAN (ARM MTE, 있을 경우) KCSAN 탐지 시퀀스 상세 CPU 0 (Watchpoint 설정자) CPU 1 (레이스 접근자) __tsan_write4(0xffff...100) 계측된 메모리 접근 should_watch() == true SKIP_WATCH 카운터 도달 old_value = *(u32*)addr 현재 값 저장 (0x1234) set_watchpoint(slot[N]) addr+size를 watchpoint 테이블에 등록 udelay(80us) 지연 윈도우 (레이스 대기) __tsan_read4(0xffff...100) 같은 주소 접근! find_matching_watchpoint() watchpoint 테이블 매치! kcsan_report_unknown_origin() new_value = *(u32*)addr 0x1234 → 0x1235 (변경됨!) kcsan_report_known_origin() remove_watchpoint(slot[N]) BUG: KCSAN: data-race 리포트 출력 같은 주소 접근 탐지 KFENCE 동작 상태 전이 UNUSED 풀에서 대기 ALLOCATED 사용 중 FREED 해제됨 (보호) kmalloc kfree 재사용 (canary 검증 후) ALLOCATED 상태 객체 페이지: PROT_READ | PROT_WRITE 가드 페이지: PROT_NONE (접근 불가) canary: 패딩 영역 0xAA 채움 할당 스택 트레이스 저장 FREED 상태 객체 페이지: PROT_NONE (접근 불가!) 가드 페이지: PROT_NONE (그대로) UAF 접근 → page fault → 리포트 해제 스택 트레이스 저장 탐지 메커니즘 OOB left → 좌측 가드 페이지 fault OOB right → 우측 가드 페이지 fault UAF → FREED 객체 페이지 fault Invalid free → 메타데이터 불일치 샘플링 메커니즘 1. 타이머 인터럽트 (CONFIG_KFENCE_SAMPLE_INTERVAL=100ms) → kfence_guarded_alloc() 활성화 2. 다음 slab 할당 요청 → KFENCE 풀에서 할당 (1회) → 타이머 리셋 3. 100ms마다 1개 할당만 가로채므로 전체 할당의 극소 비율만 KFENCE로 보호 → 오버헤드 <1%

lockdep 내부 해시 테이블(Hash Table)

lockdep은 성능을 위해 여러 해시 테이블을 사용합니다. classhash_tablelock_class_key에서 lock_class로의 매핑을, chainhash_table은 락 획득 체인의 빠른 조회를 담당합니다.

/* kernel/locking/lockdep_internals.h 에서 발췌 */
#define CLASSHASH_BITS      (MAX_LOCKDEP_KEYS_BITS - 1)
#define CLASSHASH_SIZE      (1UL << CLASSHASH_BITS)
#define CHAINHASH_BITS      (MAX_LOCKDEP_CHAINS_BITS - 1)
#define CHAINHASH_SIZE      (1UL << CHAINHASH_BITS)

/* lock_class 조회 경로 */
static struct hlist_head classhash_table[CLASSHASH_SIZE];
static struct hlist_head chainhash_table[CHAINHASH_SIZE];

/* 해시 키 생성:
 * classhash: lock_class_key 주소 기반
 * chainhash: 현재 보유 중인 락들의 클래스 ID 체인을 해시
 */

스택 트레이스 캐시

lockdep은 매 락 획득 시 스택 트레이스를 기록하여 경고 메시지에 포함합니다. 이 스택 트레이스는 별도의 전역 배열(stack_trace)에 저장되며, MAX_STACK_TRACE_ENTRIES(기본 524288)개까지 캐시합니다.

/* 스택 트레이스 저장 구조 */
static unsigned long stack_trace[MAX_STACK_TRACE_ENTRIES];
static unsigned int nr_stack_trace_entries;

struct lock_trace {
    unsigned int        nr_entries;
    unsigned int        offset;  /* stack_trace[] 내 시작 오프셋 */
};

/* 스택 트레이스 저장은 spin_lock(&lockdep_lock)으로 보호
 * → lockdep 자체의 오버헤드 원인 중 하나 */
lockdep 자체 데드락 방지: lockdep 내부의 lockdep_lockraw_spinlock으로 구현됩니다. lockdep이 자기 자신의 락을 추적하면 재귀가 발생하므로, lockdep_recursion 변수로 재진입을 방지합니다. debug_locks 플래그가 0이면 lockdep 전체가 비활성화됩니다.

lockdep 비활성화 조건

조건메시지해결 방법
MAX_LOCKDEP_KEYS 초과"BUG: MAX_LOCKDEP_KEYS too low!"CONFIG_LOCKDEP_BITS 증가
MAX_LOCKDEP_CHAINS 초과"BUG: MAX_LOCKDEP_CHAINS too low!"CONFIG_LOCKDEP_CHAINS_BITS 증가
MAX_STACK_TRACE_ENTRIES 초과"BUG: MAX_STACK_TRACE_ENTRIES too low!"CONFIG_LOCKDEP_STACK_TRACE_BITS 증가
내부 오류 (BUG)"BUG: lockdep internal error"lockdep 코드 버그 보고
lockdep_recursion 재진입조용히 비활성화lockdep 호출 경로 분석

self-deadlock (자기 참조 데드락)

=============================================
WARNING: possible recursive locking detected
6.8.0-rc1 #1 Not tainted
---------------------------------------------
modprobe/1234 is trying to acquire lock:
ffff888100xyz000 (&dev->lock){+.+.}-{2:2}, at: nested_func+0x20/0x40

but task is already holding lock:
ffff888100xyz000 (&dev->lock){+.+.}-{2:2}, at: outer_func+0x18/0x60

other info that might help us debug this:
 Possible unsafe locking scenario:
       CPU0
       ----
  lock(&dev->lock);
  lock(&dev->lock);

 *** DEADLOCK ***
 May be due to missing lock nesting notation
해결법: 자기 참조 데드락은 크게 3가지로 해결합니다.
  1. 코드 리팩토링: 내부 함수에서 락을 다시 잡지 않도록 __func_locked() 패턴 사용
  2. 의도적 중첩이면: spin_lock_nested(&dev->lock, SINGLE_DEPTH_NESTING)
  3. 서로 다른 인스턴스면: lockdep_set_class()로 별도 클래스 할당
/* __locked 패턴: 락 보유 여부에 따른 진입점 분리 */
static void __do_work_locked(struct my_dev *dev)
    __must_hold(&dev->lock)
{
    /* 실제 작업 수행 — 락이 이미 잡혀있다고 가정 */
    dev->state = BUSY;
}

void do_work(struct my_dev *dev) {
    spin_lock(&dev->lock);
    __do_work_locked(dev);
    spin_unlock(&dev->lock);
}

void do_work_in_irq(struct my_dev *dev) {
    /* 이미 IRQ 핸들러가 락을 잡은 상태에서 호출 */
    __do_work_locked(dev);  /* 재귀 락 없음 */
}

KCSAN 컴파일러 계측 상세

KCSAN은 GCC의 -fsanitize=thread와 유사하지만 커널 전용으로 재구현되었습니다. 컴파일러가 삽입하는 콜백 함수의 전체 목록은 다음과 같습니다.

콜백 함수트리거 조건매개변수
__tsan_read1/2/4/8/161~16바이트 읽기addr
__tsan_write1/2/4/8/161~16바이트 쓰기addr
__tsan_unaligned_read*비정렬 읽기addr
__tsan_unaligned_write*비정렬 쓰기addr
__tsan_read_rangememcpy 등 범위 읽기addr, size
__tsan_write_rangememset 등 범위 쓰기addr, size
__tsan_func_entry/exit함수 진입/종료caller
__tsan_atomic*_load/storeatomic 접근addr, memory_order

KCSAN은 이 콜백들을 kernel/kcsan/core.c에서 구현합니다. 핵심은 check_access() 함수입니다.

/* kernel/kcsan/core.c — 핵심 로직 (간소화) */
static __always_inline void check_access(const volatile void *ptr,
                                          size_t size, int type)
{
    struct kcsan_ctx *ctx = get_ctx();
    unsigned long addr = (unsigned long)ptr;
    long encoded_watchpoint;

    if (unlikely(should_watch(ctx))) {
        /* 이 접근에 watchpoint 설정 */
        encoded_watchpoint = encode_watchpoint(addr, size, type & KCSAN_ACCESS_WRITE);
        if (set_watchpoint(encoded_watchpoint)) {
            /* 지연 윈도우 — 다른 CPU의 접근을 기다림 */
            kcsan_delay(type);
            /* 지연 후 값 변경 여부 확인 */
            if (remove_watchpoint_and_check(encoded_watchpoint))
                kcsan_report_known_origin(ptr, size, type, ...);
        }
    } else {
        /* 다른 CPU의 watchpoint와 매치하는지 확인 */
        if (find_matching_watchpoint(addr, size, type))
            kcsan_report_unknown_origin(ptr, size, type, ...);
    }
}

/* 콜백 구현 */
void __tsan_write4(void *ptr) {
    check_access(ptr, 4, KCSAN_ACCESS_WRITE);
}
EXPORT_SYMBOL(__tsan_write4);

KCSAN 약한 메모리 모델 검사

CONFIG_KCSAN_WEAK_MEMORY=y일 때 KCSAN은 추가로 메모리 순서 위반도 탐지합니다. 예를 들어 smp_store_release()/smp_load_acquire() 없이 producer-consumer 패턴을 구현한 경우를 잡습니다.

/* 약한 메모리 모델 위반 예시 */
int data;
int flag;

/* Producer — smp_wmb/store_release 없음 */
void producer(void) {
    data = 42;        /* (1) 데이터 쓰기 */
    WRITE_ONCE(flag, 1);  /* (2) 플래그 쓰기 */
    /* ARM/POWER에서 (1)과 (2)가 재배열될 수 있음! */
}

/* Consumer — smp_rmb/load_acquire 없음 */
void consumer(void) {
    if (READ_ONCE(flag)) {
        int val = data;   /* data가 아직 0일 수 있음! */
        pr_info("data = %d\n", val);
    }
}

/* KCSAN 약한 메모리 경고:
 * BUG: KCSAN: data-race in producer / consumer
 * ... with possible missing memory barrier ...
 */

/* 수정: 적절한 배리어 추가 */
void producer_fixed(void) {
    data = 42;
    smp_store_release(&flag, 1);  /* 배리어 + 쓰기 */
}

void consumer_fixed(void) {
    if (smp_load_acquire(&flag)) {
        int val = data;  /* 이제 42가 보장됨 */
        pr_info("data = %d\n", val);
    }
}

KASAN 섀도 메모리 구조

Generic KASAN은 커널 가상 주소 공간(Address Space)의 1/8을 섀도 메모리로 매핑합니다. 각 8바이트의 실제 메모리가 1바이트의 섀도 메모리로 표현됩니다.

섀도 메모리 값 해석:
  0x00       : 8바이트 전체 접근 가능
  0x01-0x07  : 처음 N바이트만 접근 가능 (slab 객체 끝 패딩)
  0xFE       : KASAN 내부 레드존 (slub redzone)
  0xFD       : 해제됨 (freed by kfree/kmem_cache_free)
  0xFB       : 해제됨 - 스택 변수 (stack after scope)
  0xFA       : 좌측 레드존 (stack left redzone)
  0xFC       : 우측 레드존 (stack right redzone)
  0xF1       : slab 레드존 좌측
  0xF2       : slab 레드존 우측
  0xF3       : slab 해제 레드존
  0xF5       : 전역 변수 레드존
  0xF8       : KFENCE 가드 페이지
/* 섀도 메모리 변환 매크로 */
#define KASAN_SHADOW_SCALE_SHIFT  3  /* 8바이트당 1바이트 */
#define KASAN_SHADOW_OFFSET  ...     /* 아키텍처별 상수 */

static inline void *kasan_mem_to_shadow(const void *addr)
{
    return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
           + KASAN_SHADOW_OFFSET;
}

/* 접근 검사 — 컴파일러가 삽입 */
static __always_inline bool memory_is_poisoned(unsigned long addr, size_t size)
{
    s8 *shadow = (s8 *)kasan_mem_to_shadow((void *)addr);
    s8 shadow_value = *shadow;

    if (shadow_value == 0)
        return false;  /* 완전 접근 가능 */
    if (shadow_value > 0)
        return (addr & 7) + size > shadow_value;  /* 부분 접근 검사 */
    return true;  /* 음수 = poisoned */
}

KASAN quarantine 상세

/* 격리 큐 구조 — 해제된 객체를 즉시 재사용하지 않음 */
struct kasan_quarantine {
    struct list_head batches;
    unsigned long bytes;
};

/* 해제 흐름:
 * 1. kfree(obj)
 * 2. KASAN이 obj를 quarantine에 넣음
 * 3. obj의 섀도를 0xFD(freed)로 설정
 * 4. quarantine 크기가 한계 초과 시 가장 오래된 객체를 실제 해제
 *
 * 이 덕분에 UAF 접근 시 섀도에서 0xFD를 읽어 탐지 가능
 * quarantine 크기 조절: CONFIG_KASAN_QUARANTINE_SIZE
 */

/* 동시성 UAF 탐지 시나리오:
 * CPU0: kfree(obj) → quarantine → 섀도 = 0xFD
 * CPU1: obj->data 접근 → 섀도 0xFD 확인 → BUG: use-after-free!
 *
 * quarantine 없으면 obj가 즉시 재할당되어 섀도가 0x00이 되고
 * UAF를 놓칠 수 있음 → quarantine이 핵심
 */

KFENCE 메타데이터 구조

/* include/linux/kfence.h */
struct kfence_metadata {
    struct list_head list;
    struct rcu_head rcu_head;
    unsigned long addr;         /* 객체 시작 주소 */
    size_t size;                /* 객체 크기 */
    struct kmem_cache *cache;   /* slab 캐시 */
    unsigned long unprotected_page;
    enum kfence_object_state state;  /* UNUSED, ALLOCATED, FREED */
    /* 할당/해제 스택 트레이스 */
    unsigned long alloc_stack[KFENCE_STACK_DEPTH];
    unsigned long free_stack[KFENCE_STACK_DEPTH];
    int alloc_stack_entries;
    int free_stack_entries;
    u32 alloc_track_checksum;
};

/* 상태 전이:
 * UNUSED → ALLOCATED (kfence_alloc)
 *   - 객체 페이지를 PROT_READ|PROT_WRITE로 매핑
 *   - 할당 스택 트레이스 저장
 * ALLOCATED → FREED (kfence_free)
 *   - 객체 페이지를 PROT_NONE으로 매핑 (접근 불가)
 *   - 해제 스택 트레이스 저장
 *   - 이후 접근 시 page fault → UAF 리포트
 * FREED → ALLOCATED (재사용)
 *   - canary 값 검증으로 이전 사용 중 corruption 탐지
 */

KFENCE canary 기반 OOB 탐지

가드 페이지는 전체 페이지 크기(보통 4KB)의 OOB만 탐지합니다. 객체 크기보다 작은 오프셋(Offset)의 OOB는 canary 패턴으로 탐지합니다.

/* KFENCE canary 검증 */
/* 객체 뒤쪽 패딩 영역을 특정 패턴(0xAA)으로 채움 */
/* 해제 시 패턴이 변경되었으면 OOB write가 있었다는 증거 */

/* 예: 64바이트 객체가 4096바이트 페이지에 할당됨
 * [object 64B][canary 4032B][GUARD PAGE]
 *
 * 65바이트 쓰기 시:
 * - 가드 페이지 전이므로 page fault 미발생
 * - 하지만 canary가 파괴됨
 * - kfence_free() 시 canary 검증에서 탐지!
 */

ftrace preemptirqsoff tracer 상세

preemptirqsoff는 preempt 비활성화와 IRQ 비활성화가 동시에 발생하는 최대 구간을 추적합니다. RT 시스템에서 worst-case latency를 측정하는 데 핵심적입니다.

# preemptirqsoff 최대 레이턴시 추적
echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency  # 리셋
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 워크로드 실행 (예: 네트워크 부하)
iperf3 -c 192.168.1.1 -t 60

# 최대 레이턴시 확인
cat /sys/kernel/debug/tracing/tracing_max_latency
# 2345  (단위: us)

# 상세 트레이스
cat /sys/kernel/debug/tracing/trace
# preemptirqsoff latency trace v1.1.5 on 6.8.0-rc1
# latency: 2345 us, #8/8, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0)
#    -----------------
#    | task: iperf3-12345 (uid:0 nice:0 policy:0 rt_prio:0)
#    -----------------
# => started at: driver_xmit
# => ended at:   driver_xmit_complete
#
#                    _------=> CPU#
#                   / _-----=> irqs-off/BH-disabled
#                  | / _----=> need-resched
#                  || / _---=> hardirq/softirq
#                  ||| / _--=> preempt-depth
#                  ||||/     delay
#     cmd     pid  ||||| time  |   caller
#        \   /     |||||  \    |    /
#  iperf3-12345  3d...    0us+: trace_preempt_off <- driver_xmit
#  iperf3-12345  3d...  100us : dma_map_single <- driver_xmit
#  iperf3-12345  3d...  800us : memcpy <- driver_build_desc
#  iperf3-12345  3d... 2000us : writel <- driver_kick_hw
#  iperf3-12345  3d... 2345us+: trace_preempt_on <- driver_xmit_complete

function_graph tracer로 락 보유 구간 분석

# function_graph 설정 — 특정 함수만 필터
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'spin_lock*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'spin_unlock*' >> /sys/kernel/debug/tracing/set_ftrace_filter

# 또는 특정 드라이버의 함수만 추적
echo ':mod:e1000e' > /sys/kernel/debug/tracing/set_ftrace_filter

echo 1 > /sys/kernel/debug/tracing/tracing_on
# 워크로드 실행
echo 0 > /sys/kernel/debug/tracing/tracing_on

cat /sys/kernel/debug/tracing/trace
#  3)               |  spin_lock_irqsave() {
#  3)   0.234 us    |    _raw_spin_lock_irqsave();
#  3)   0.567 us    |  }
#  ...
#  3)   0.123 us    |  spin_unlock_irqrestore();

trace event 필터와 트리거 고급 사용

# 필터: 특정 PID의 락 이벤트만
echo 'common_pid == 1234' > /sys/kernel/debug/tracing/events/lock/lock_contended/filter

# 트리거: contention 발생 시 스택 트레이스 기록
echo 'stacktrace' > /sys/kernel/debug/tracing/events/lock/lock_contended/trigger

# 트리거: 10회 contention 후 tracing 중지
echo 'traceoff:10' > /sys/kernel/debug/tracing/events/lock/lock_contended/trigger

# 히스토그램: 락 이름별 contention 횟수
echo 'hist:key=name:val=hitcount:sort=hitcount.descending' \
  > /sys/kernel/debug/tracing/events/lock/lock_contended/trigger

# 히스토그램 확인
cat /sys/kernel/debug/tracing/events/lock/lock_contended/hist
# { name: rtnl_mutex } hitcount:  567
# { name: &mm->mmap_lock } hitcount:  2345
# { name: &sb->s_lock } hitcount:  123
# Totals: Hits: 3035 Entries: 3 Dropped: 0

perf lock contention BPF 모드 상세

v6.2부터 도입된 BPF 기반 perf lock contention은 tracepoint가 아닌 BPF 프로그램을 커널에 직접 부착하여 락 contention을 측정합니다.

# 기본 contention 분석 (BPF 사용)
perf lock contention -- sleep 10
# contended  total wait   max wait   avg wait  type     caller
#      1234    125.4 ms    23.4 ms    101 us   mutex    rtnl_lock
#       567     45.6 ms     5.6 ms     80 us   rwsem    mmap_read_lock
#       890     12.3 ms     1.2 ms     13 us   spinlock __queue_work

# -a: 시스템 전체, -b: BPF 사용, -s: 스택 트레이스
perf lock contention -abs -- sleep 10

# 특정 프로세스만
perf lock contention -p 1234 -- sleep 10

# --lock-filter: 특정 락만 필터
perf lock contention --lock-filter rtnl_mutex -- sleep 10

# --type-filter: 락 유형별 필터
perf lock contention --type-filter mutex,rwsem -- sleep 10

# flame graph 생성
perf lock contention -absg -- sleep 30 2> contention.log
# Flamescope나 speedscope에서 시각화

perf c2c: 캐시 라인(Cache Line) False Sharing 탐지

동시성 성능 문제의 또 다른 원인인 false sharing을 탐지합니다. 서로 다른 CPU의 서로 다른 변수가 같은 캐시 라인에 있어 불필요한 캐시 무효화(Invalidation)가 발생하는 현상입니다.

# cache-to-cache (c2c) 이벤트 수집
perf c2c record -- sleep 30

# 분석
perf c2c report
# Shared Data Cache Line Table
# Index   Offset  Pid   Tid    Total   LclHitm  RmtHitm  ...  Symbol
#     0   0x0040  1234  1234    5678      2345      890  ...  shared_data+0x40
#     1   0x0080  5678  5678    3456      1234      567  ...  stats_struct+0x0

# LclHitm: 로컬 캐시 히트 Modified (같은 소켓 내)
# RmtHitm: 리모트 캐시 히트 Modified (다른 소켓) ← 비용 큼
# 높은 RmtHitm = false sharing 의심
False Sharing 해결:
  • ____cacheline_aligned_in_smp: 구조체 필드를 캐시 라인 경계에 정렬
  • per-CPU 변수: DEFINE_PER_CPU()로 CPU별 분리
  • 구조체 패딩(Padding): 자주 접근되는 읽기/쓰기 필드를 다른 캐시 라인에 배치
/* False sharing 해결 예시 */
struct my_stats {
    atomic_long_t reads ____cacheline_aligned_in_smp;
    atomic_long_t writes ____cacheline_aligned_in_smp;
    /* 각 카운터가 별도 캐시 라인에 위치 → false sharing 방지 */
};

/* 또는 per-CPU 변수 사용 */
static DEFINE_PER_CPU(unsigned long, per_cpu_reads);
static DEFINE_PER_CPU(unsigned long, per_cpu_writes);

void increment_reads(void) {
    this_cpu_inc(per_cpu_reads);  /* 다른 CPU의 캐시 라인 무효화 없음 */
}

unsigned long total_reads(void) {
    unsigned long sum = 0;
    int cpu;
    for_each_possible_cpu(cpu)
        sum += per_cpu(per_cpu_reads, cpu);
    return sum;
}

CVE-2023-3090 (ipvlan UAF race) 상세 분석

ipvlan 드라이버에서 IPVLAN_MODE_L3S 모드 변경과 패킷 수신 경로 사이의 레이스로 use-after-free가 발생합니다.

/* 레이스 시나리오:
 * CPU0: ipvlan_set_port_mode() → 모드를 L3S로 변경
 *        → ipvlan_register_nf_hook() 호출
 *        → 기존 hook 해제 + 새 hook 등록 (사이에 윈도우 존재)
 *
 * CPU1: ipvlan_handle_frame() → 수신 패킷 처리
 *        → 해제된 hook 구조체 참조 → UAF!
 *
 * 탐지: KASAN (slab-use-after-free)
 * 수정: rtnl_lock()으로 모드 변경과 패킷 경로 직렬화
 *        + RCU 보호 추가 (commit: 5c7af26e3e2b)
 */

CVE-2023-52447 (bpf map deadlock) 상세 분석

/* 데드락 시나리오:
 * Thread A:
 *   bpf_map_update_elem()
 *     → lock(map->lock)              [1]
 *     → bpf_prog_run() (콜백)
 *       → bpf_map_lookup_elem()
 *         → rcu_read_lock()           [2]
 *
 * Thread B:
 *   bpf_map_free()
 *     → synchronize_rcu()            [3] — RCU GP 대기
 *     → lock(map->lock)              [4]
 *
 * 데드락: A는 map->lock[1] 보유 + RCU read-side[2]
 *         B는 synchronize_rcu()[3]에서 A의 RCU 해제 대기
 *         하지만 A는 작업 완료 후 map->lock 해제 예정
 *         B가 map->lock[4]을 잡으려면 A가 먼저 끝나야 함
 *         → 간접 순환!
 *
 * 탐지: lockdep (PROVE_RCU + PROVE_LOCKING)
 * 수정: map->lock 보유 중 bpf_prog_run() 호출 금지
 *        → rcu_read_lock() 밖에서 map->lock 획득
 */

연도별 동시성 CVE 통계 분석

연도데이터 레이스데드락UAF 레이스TOCTOU기타합계주요 서브시스템
2020128183546net, mm, fs
20211511245762net, bpf, io_uring
2022189314870net, mm, bpf
20232213286978net, bpf, drivers
202419102551170net, fs, crypto
2025 (Q1)84122329net, bpf, mm
추세 분석: UAF 레이스가 지속적으로 가장 높은 비중을 차지하며, 네트워킹 서브시스템이 동시성 CVE의 40% 이상을 차지합니다. KCSAN 도입(v5.8, 2020) 이후 데이터 레이스 발견이 크게 증가했으며, 이는 도구 성숙에 따른 자연스러운 현상입니다. BPF 서브시스템은 2021년부터 급증하여 현재 두 번째로 많은 동시성 버그 소스입니다.

CVE 수정 패턴 분류

수정 패턴적용 비율대표 기법적용 예시
락 추가/변경~35%mutex, spinlock, rwsem 추가경합 데이터 구조에 락 도입
RCU 전환~20%rcu_read_lock + rcu_dereference읽기 빈번 경로의 락 제거
atomic 연산~15%READ_ONCE/WRITE_ONCE, atomic_t플래그/카운터 원자적(Atomic) 접근
refcount 강화~12%atomic_t → refcount_tUAF 방지 참조 카운팅
순서 변경~10%락 획득 순서 통일ABBA 데드락 수정
동기화 대기~5%synchronize_rcu, completion리소스 해제 전 대기 보장
기타~3%코드 구조 변경, 경합 제거per-CPU 데이터 분리
/* 대표적 CVE 수정 패턴: refcount_t 전환 */

/* Before (취약): atomic_t는 0 이하로 감소 가능 → UAF */
struct my_obj {
    atomic_t refcnt;
    /* ... */
};

void put_obj(struct my_obj *obj) {
    if (atomic_dec_and_test(&obj->refcnt))
        kfree(obj);  /* 이미 0인 상태에서 재진입 → double-free */
}

/* After (안전): refcount_t는 0→(0-1) 전환 시 WARN + saturation */
struct my_obj {
    refcount_t refcnt;
    /* ... */
};

void put_obj(struct my_obj *obj) {
    if (refcount_dec_and_test(&obj->refcnt))
        kfree(obj);  /* 이미 0이면 WARN_ONCE + 감소 거부 */
}

/* refcount_t 사용 시 추가 안전장치 */
bool get_obj(struct my_obj *obj) {
    /* refcount가 0이면 false 반환 → UAF 방지 */
    return refcount_inc_not_zero(&obj->refcnt);
}
refcount_t 마이그레이션 시 주의: atomic_t에서 refcount_t로 전환할 때, atomic_read()refcount_read()로 변경하되, atomic_set()refcount_set()으로 1:1 대체할 수 없는 경우가 있습니다. 특히 초기화 시점에서 refcount_set(&obj->refcnt, 1)만 허용되며, 임의의 값으로 설정하면 CONFIG_REFCOUNT_FULL 활성화 시 경고가 발생합니다.

syzkaller 재현기(reproducer) 활용법

syzkaller가 버그를 발견하면 자동으로 최소 재현기(syz reproducer)와 C reproducer를 생성합니다.

/* syzkaller가 생성한 C reproducer 예시 (간소화) */
#define _GNU_SOURCE
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/if.h>

static void *thread_a(void *arg) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    struct ifreq ifr = {};
    strncpy(ifr.ifr_name, "lo", IFNAMSIZ);
    for (int i = 0; i < 100000; i++)
        ioctl(fd, SIOCSIFFLAGS, &ifr);
    close(fd);
    return NULL;
}

static void *thread_b(void *arg) {
    int fd = socket(AF_NETLINK, SOCK_RAW, 0);
    for (int i = 0; i < 100000; i++) {
        /* 동시에 네트워크 설정 변경 시도 → 레이스 트리거 */
        char buf[4096];
        recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
    }
    close(fd);
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    for (int i = 0; i < 100; i++) {
        pthread_create(&t1, NULL, thread_a, NULL);
        pthread_create(&t2, NULL, thread_b, NULL);
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
    }
    return 0;
}
# syzkaller reproducer 실행
gcc -o repro -lpthread syzkaller_repro.c
# KASAN/lockdep 활성화된 커널에서 실행
./repro
# dmesg에서 경고 확인
dmesg | grep -E "BUG:|WARNING:"

locktorture 고급 옵션

매개변수기본값설명
torture_typespin_lock테스트 대상 락 유형 (spin_lock, spin_lock_irq, rw_lock, mutex_lock, rwsem_lock 등)
nwriters_stressonline CPUs쓰기 스트레스 스레드 수
nreaders_stressonline CPUs읽기 스트레스 스레드 수 (rw_lock/rwsem용)
torture_runnable11=즉시 시작, 0=수동 시작
stat_interval60통계 출력 간격 (초)
stutter5주기적 정지/재시작(Reboot)으로 코너 케이스 노출
shuffle_interval3스레드 CPU 이동 간격 (초)
verbose1상세 로그 수준
# 강도 높은 종합 테스트
modprobe locktorture torture_type=mutex_lock \
  nwriters_stress=16 stat_interval=30 stutter=2 shuffle_interval=1

# RW lock 테스트 — 읽기/쓰기 경합
modprobe locktorture torture_type=rwsem_lock \
  nwriters_stress=4 nreaders_stress=12

# 결과 확인
dmesg | grep "lock_torture"
# lock_torture: --- End of test: SUCCESS [or FAILURE]

실전 디버깅: 데이터 레이스 해결 과정

시나리오: KCSAN이 네트워크 드라이버에서 data race를 보고

# 1단계: KCSAN 리포트 확인
dmesg | grep "KCSAN"
# BUG: KCSAN: data-race in e1000_watchdog / e1000_xmit_frame
#
# write to 0xffff8881234567a0 of 4 bytes by task 567 on cpu 2:
#  e1000_watchdog+0x180/0x300 [e1000e]
#
# read to 0xffff8881234567a0 of 4 bytes by task 890 on cpu 0:
#  e1000_xmit_frame+0x60/0x200 [e1000e]

# 2단계: 어떤 변수인지 확인
# addr2line으로 소스 코드 위치 확인
addr2line -e vmlinux -f 0xffff8881234567a0
# 또는 objdump로 오프셋 분석
# adapter->tx_ring->count 변수로 확인됨

# 3단계: 코드 분석
# e1000_watchdog: adapter->tx_ring->count 갱신 (리사이즈)
# e1000_xmit_frame: adapter->tx_ring->count 참조 (전송)
# → 두 경로 사이에 동기화 없음

# 4단계: 수정 방안 결정
# 옵션 A: READ_ONCE/WRITE_ONCE (성능 우선, 근사값 허용 시)
# 옵션 B: 기존 adapter->lock으로 보호 (정확성 필요 시)
# → 이 경우 tx_ring->count는 정확해야 하므로 옵션 B

# 5단계: 패치 작성 및 검증
# KCSAN 활성화 상태로 테스트 재실행, 경고 해소 확인

실전 디버깅: IRQ 데드락 해결 과정

# 1단계: lockdep 경고 확인
dmesg | grep "inconsistent lock state"
# WARNING: inconsistent lock state
# inconsistent {HARDIRQ-ON-W} -> {IN-HARDIRQ-W} usage
# my_driver_irq_handler is trying to acquire lock:
# ffff8881234567b0 (&priv->data_lock)

# 2단계: 사용 패턴 분석
# 프로세스 컨텍스트: spin_lock(&priv->data_lock)
# IRQ 핸들러: spin_lock(&priv->data_lock)  ← 위험!
# → 같은 CPU에서 IRQ가 프로세스를 중단하면 self-deadlock

# 3단계: 수정
# 모든 프로세스 컨텍스트 사용을 spin_lock_irqsave()로 교체

# 4단계: 검증
# lockdep 경고 해소 + locktorture로 안정성 확인
modprobe locktorture torture_type=spin_lock_irq nwriters_stress=8
sleep 300
cat /proc/lock_torture_stats

lockdep 경고 분석 치트 시트

경고 메시지 키워드의미즉시 조치
possible circular lockingABBA 데드락 가능성락 순서 통일 또는 trylock
possible recursive locking자기 참조 데드락__locked 패턴 또는 nested 사용
inconsistent lock stateIRQ safe/unsafe 혼용spin_lock_irqsave()로 교체
Invalid wait contextwait_type 위반 (RT)raw_spinlock 아래 슬립 락 금지
held lock freed락 보유 중 해제해제 전 unlock 확인
lock held when returning to user space시스템 콜 리턴 시 락 미해제에러 경로에서 unlock 누락 확인
BUG: sleeping function called from invalid context원자 컨텍스트에서 슬립GFP_ATOMIC 또는 코드 이동

동시성 버그 패치 작성 및 제출 가이드

동시성 버그를 수정한 패치를 커널 메일링 리스트에 제출할 때는 특정 형식과 정보를 포함해야 합니다.

# 패치 형식 예시 (git format-patch)

# Subject: [PATCH] net: e1000e: fix data race in tx_ring->count access
#
# e1000_watchdog() modifies tx_ring->count during ring resize while
# e1000_xmit_frame() reads it concurrently without synchronization.
# This data race was detected by KCSAN:
#
# BUG: KCSAN: data-race in e1000_watchdog / e1000_xmit_frame
#  write to 0xffff8881234567a0 of 4 bytes by task 567 on cpu 2
#  read to 0xffff8881234567a0 of 4 bytes by task 890 on cpu 0
#
# Fix by holding adapter->tx_lock during ring resize to serialize
# with the transmit path.
#
# Reported-by: syzbot+abc123def456@syzkaller.appspotmail.com
# Fixes: a1b2c3d4e5f6 ("e1000e: add ring resize support")
# Cc: stable@vger.kernel.org
# Signed-off-by: Author Name <email@example.com>

# 패치 제출
git format-patch -1 HEAD
./scripts/checkpatch.pl 0001-net-*.patch
git send-email --to netdev@vger.kernel.org \
    --cc stable@vger.kernel.org \
    0001-net-*.patch
동시성 패치 필수 요소:
  • Fixes: 태그 — 버그를 도입한 원래 커밋 해시 (git bisect로 찾기)
  • Reported-by: — syzbot, 사용자, 또는 도구 리포트 출처
  • Cc: stable@vger.kernel.org — 보안 영향이 있으면 안정 커널 백포트 요청
  • 커밋 메시지에 KCSAN/lockdep/KASAN 리포트 원문 포함
  • checkpatch.pl 통과 확인 (코딩 스타일(Coding Style))
  • 수정 사유와 선택한 동기화 기법의 근거 설명
# git bisect로 버그 도입 커밋 찾기
git bisect start
git bisect bad HEAD          # 현재 커밋은 버그 있음
git bisect good v6.1         # v6.1에서는 버그 없었음

# 각 커밋에서 테스트 (자동화)
git bisect run ./test_repro.sh
# test_repro.sh: reproducer 실행 후 dmesg 검사
# exit 0 = good, exit 1 = bad

# 결과
# abc123def456 is the first bad commit
# commit abc123def456
# Author: ...
# Date: ...
#
#     e1000e: add ring resize support
# → 이 커밋이 Fixes: 태그에 들어감

동시성 버그 수정 레시피

자주 발생하는 동시성 버그 유형별 수정 패턴을 정리합니다.

/* 레시피 1: ABBA 데드락 → 글로벌 락 순서 정의 */
/* Problem: func_A()는 lock_a → lock_b, func_B()는 lock_b → lock_a */
/* Solution: 항상 주소 순서로 락 획득 */
void lock_two(spinlock_t *a, spinlock_t *b) {
    if (a < b) {
        spin_lock(a);
        spin_lock_nested(b, SINGLE_DEPTH_NESTING);
    } else {
        spin_lock(b);
        spin_lock_nested(a, SINGLE_DEPTH_NESTING);
    }
}

/* 레시피 2: IRQ 데드락 → irqsave/irqrestore */
/* Problem: 프로세스와 IRQ 핸들러가 같은 락 사용 */
/* Solution: 프로세스 컨텍스트에서 IRQ 비활성화 */
void process_context_func(struct dev *d) {
    unsigned long flags;
    spin_lock_irqsave(&d->lock, flags);  /* IRQ off */
    do_work(d);
    spin_unlock_irqrestore(&d->lock, flags);  /* IRQ restore */
}

irqreturn_t irq_handler(int irq, void *data) {
    struct dev *d = data;
    spin_lock(&d->lock);  /* IRQ 내에서는 irqsave 불필요 */
    handle_irq(d);
    spin_unlock(&d->lock);
    return IRQ_HANDLED;
}

/* 레시피 3: UAF 레이스 → RCU + grace period */
/* Problem: 한 스레드가 객체를 해제하는 동안 다른 스레드가 접근 */
/* Solution: RCU로 접근 보호, grace period 후 해제 */
void safe_remove(struct my_obj __rcu **ptr) {
    struct my_obj *old = rcu_dereference_protected(*ptr,
                            lockdep_is_held(&my_lock));
    rcu_assign_pointer(*ptr, NULL);
    synchronize_rcu();  /* 모든 읽기 측 완료 대기 */
    kfree(old);         /* 안전하게 해제 */
}

/* 레시피 4: 데이터 레이스 → READ_ONCE/WRITE_ONCE */
/* Problem: plain read/write에서 torn read/write 가능 */
/* Solution: 마킹된 접근으로 컴파일러 최적화 방지 */
void update_flag(struct shared *s) {
    WRITE_ONCE(s->flag, 1);  /* 컴파일러가 제거/분할하지 않음 */
}

bool check_flag(struct shared *s) {
    return READ_ONCE(s->flag);  /* 캐시된 값 재사용 방지 */
}

/* 레시피 5: Sleep-in-atomic → work_struct 위임 */
/* Problem: spinlock 아래에서 슬립 가능 함수 호출 */
/* Solution: 슬립 작업을 work_struct로 위임 */
static DECLARE_WORK(deferred_work, deferred_work_fn);

void atomic_path(struct dev *d) {
    spin_lock(&d->lock);
    d->needs_firmware = true;
    schedule_work(&deferred_work);  /* 비블로킹 */
    spin_unlock(&d->lock);
}

static void deferred_work_fn(struct work_struct *work) {
    /* 프로세스 컨텍스트: 슬립 가능 */
    request_firmware(&fw, "device.bin", dev);
}

/* 레시피 6: False sharing → 캐시 라인 정렬 */
/* Problem: 서로 다른 CPU가 인접 변수 수정 → 캐시 라인 바운싱 */
struct per_cpu_data {
    atomic_t counter;
    /* 패딩으로 다른 CPU의 변수와 캐시 라인 분리 */
} ____cacheline_aligned_in_smp;

/* 또는 per-CPU 변수 사용 */
DEFINE_PER_CPU(unsigned long, my_counter);
수정 패턴 선택 가이드:
  • 성능이 중요하고 읽기 빈번 → RCU (읽기 측 오버헤드 거의 0)
  • 단순 플래그/상태 → READ_ONCE/WRITE_ONCE (최소 오버헤드)
  • 복합 데이터 구조 보호 → spinlock 또는 mutex
  • 참조 카운팅 → refcount_t (오버플로우/언더플로우 보호)
  • 초기화/해체 구간 → completion 또는 synchronize_rcu()
  • 카운터만 증감 → atomic_t 또는 per-CPU 변수

디버깅 도구 빠른 참조 (치트 시트)

# === lockdep 빠른 참조 ===
# 활성화: CONFIG_PROVE_LOCKING=y
dmesg | grep -E "BUG:|WARNING:|DEADLOCK"  # 경고 확인
cat /proc/lockdep_stats                    # 통계 확인
cat /proc/lock_stat                        # 경합 통계 (LOCK_STAT)
echo 0 > /proc/lock_stat                   # 통계 리셋

# === KCSAN 빠른 참조 ===
# 활성화: CONFIG_KCSAN=y
dmesg | grep "KCSAN"                       # 레이스 리포트 확인
# 서브시스템 제외: Makefile에 KCSAN_SANITIZE := n

# === KASAN 빠른 참조 ===
# 활성화: CONFIG_KASAN=y + CONFIG_KASAN_GENERIC=y
dmesg | grep "KASAN"                       # UAF/OOB 리포트 확인
# KASAN 비활성화 부팅: kasan=off

# === KFENCE 빠른 참조 ===
# 활성화: CONFIG_KFENCE=y
cat /sys/kernel/debug/kfence/stats         # 상태 확인
dmesg | grep "KFENCE"                      # 오류 리포트 확인
echo 100 > /sys/module/kfence/parameters/sample_interval  # 간격 조정

# === ftrace 빠른 참조 ===
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace        # 결과 확인
echo nop > /sys/kernel/debug/tracing/current_tracer  # 비활성화

# === perf lock 빠른 참조 ===
perf lock record -- sleep 30               # 수집
perf lock report                           # 보고
perf lock contention -abs -- sleep 10      # BPF 분석 (v6.2+)
perf c2c record -- workload                # false sharing
perf c2c report                            # false sharing 보고

# === sparse 빠른 참조 ===
make C=1 drivers/net/foo.o                 # 단일 파일
make C=2 M=drivers/net/                    # 디렉토리
make C=2 CF="-D__CHECK_ENDIAN__"           # 엔디안 검사 포함

# === 스트레스 테스트 빠른 참조 ===
modprobe locktorture torture_type=spin_lock nwriters_stress=8
modprobe rcutorture                        # RCU 스트레스
stress-ng --lock 8 --futex 8 --timeout 60s # 유저 스페이스 스트레스

sparse 실전 활용 패턴

# 전체 커널에서 __rcu 위반 검색
make C=2 2>&1 | grep "different address spaces"
# drivers/net/foo.c:123:45: warning: incorrect type in assignment
#    expected struct net_device *dev
#    got struct net_device [noderef] __rcu *__rcu_dev

# endianness 검사도 동시에 수행
make C=2 CF="-D__CHECK_ENDIAN__" 2>&1 | grep "restricted"

# sparse 어노테이션 통계
# __acquires: 커널 전체 약 1,500개 함수
# __releases: 커널 전체 약 1,500개 함수
# __must_hold: 커널 전체 약 800개 함수
# __rcu: 커널 전체 약 3,000개 포인터

sparse와 lockdep의 보완 관계

검사 항목sparse (빌드 시)lockdep (런타임)
락 획득/해제 균형__acquires/__releases 어노테이션실제 lock/unlock 추적
RCU 포인터 접근__rcu 타입 검사rcu_read_lock() 범위 검증
락 순서 검증불가능의존성 그래프 + BFS
IRQ 안전성불가능usage_mask 추적
커버리지빌드하는 모든 코드실행된 경로만
false positive어노테이션 누락 시 높음낮음 (실제 실행 기반)

Coccinelle 고급 SmPL 규칙

// 락 보유 중 copy_to_user 호출 탐지 (sleep-in-atomic 가능)
@lock_copy@
expression E1, E2, E3, lock;
@@

spin_lock(&lock);
...
* copy_to_user(E1, E2, E3)
...
spin_unlock(&lock);

// 실행
// spatch --sp-file lock_copy.cocci --dir drivers/ --mode report
// atomic_t를 refcount_t로 변환 제안
@atomic_to_refcount@
identifier x;
expression E;
@@

- atomic_inc(&x)
+ refcount_inc(&x)

// 또는

- if (atomic_dec_and_test(&x))
+ if (refcount_dec_and_test(&x))

// atomic_t로 참조 카운팅하면 오버플로우 공격에 취약
// refcount_t는 오버플로우/언더플로우 시 경고 + 포화 처리

개발 커널 빌드 자동화 스크립트

#!/bin/bash
# debug-kernel-config.sh — 동시성 디버깅 최적 설정 적용

# 기본 설정 로드
make defconfig

# lockdep 활성화
scripts/config -e PROVE_LOCKING
scripts/config -e DEBUG_LOCK_ALLOC
scripts/config -e LOCK_STAT
scripts/config -e PROVE_RCU

# KCSAN 활성화 (GCC 11+ 또는 Clang 12+ 필요)
scripts/config -e KCSAN
scripts/config -e KCSAN_STRICT
scripts/config -e KCSAN_WEAK_MEMORY

# KASAN 활성화
scripts/config -e KASAN
scripts/config -e KASAN_GENERIC

# 컨텍스트 디버깅
scripts/config -e DEBUG_ATOMIC_SLEEP
scripts/config -e DEBUG_PREEMPT
scripts/config -e DETECT_HUNG_TASK
scripts/config -e DEBUG_RT_MUTEXES

# 토쳐 테스트 모듈
scripts/config -m LOCK_TORTURE_TEST
scripts/config -m RCU_TORTURE_TEST

# 객체 추적
scripts/config -e DEBUG_OBJECTS
scripts/config -e DEBUG_OBJECTS_FREE
scripts/config -e DEBUG_OBJECTS_TIMERS
scripts/config -e DEBUG_OBJECTS_WORK

# 최종 .config 생성
make olddefconfig

echo "=== 동시성 디버깅 커널 설정 완료 ==="
echo "주의: KASAN + KCSAN 동시 활성화 — 오버헤드 높음"
echo "빌드: make -j\$(nproc)"

프로덕션 커널 최소 디버깅 설정

# 프로덕션에서 안전하게 활성화 가능한 옵션
scripts/config -e KFENCE                    # <1% 오버헤드
scripts/config --set-val KFENCE_SAMPLE_INTERVAL 500  # 500ms 간격
scripts/config -e DETECT_HUNG_TASK          # 행 탐지
scripts/config --set-val DEFAULT_HUNG_TASK_TIMEOUT 120
scripts/config -e SOFTLOCKUP_DETECTOR       # soft lockup 탐지
scripts/config -e HARDLOCKUP_DETECTOR       # hard lockup (NMI watchdog)
scripts/config -e WQ_WATCHDOG              # workqueue stall 탐지

# 절대 프로덕션에서 활성화하면 안 되는 옵션
# CONFIG_PROVE_LOCKING — 심각한 성능 저하
# CONFIG_KASAN_GENERIC — 2-3x 메모리/CPU 오버헤드
# CONFIG_KCSAN — 컴파일러 계측으로 전체 성능 저하
# CONFIG_DEBUG_LOCK_ALLOC — lockdep 의존

커널 버전별 동시성 디버깅 도구 진화

커널 동시성 디버깅 도구 타임라인 v2.6.18 lockdep (2006) v4.0 KASAN (2015) v5.8 KCSAN (2020) v5.12 KFENCE (2021) v5.14 KASAN HW Tags (2021) v6.2 perf lock BPF (2023) 핵심 이정표 v2.6.18: Ingo Molnar의 lockdep → 커널 데드락 탐지 혁신 v4.0: Andrey Ryabinin의 KASAN → UAF/OOB 체계적 탐지 시작 v5.8: Marco Elver의 KCSAN → 데이터 레이스 최초 체계적 탐지 v5.8: Peter Zijlstra의 wait_type → RT 슬립 컨텍스트 검증 v5.12: Alexander Potapenko의 KFENCE → 프로덕션 메모리 감지 최신 동향 v5.14: ARM MTE 기반 HW Tag KASAN → 최소 오버헤드 메모리 검사 v6.2: BPF 기반 perf lock contention → tracepoint 없는 분석 v6.5+: KCSAN report 개선, lockdep chain 최적화 진행 중: Rust for Linux → 소유권/수명 기반 컴파일 타임 보장 진행 중: eBPF 기반 동적 락 프로파일링 도구 확대

도구별 버그 탐지 능력 매트릭스

버그 유형lockdepKCSANKASANKFENCEsparseCoccinelleperf locksyzkaller
ABBA 데드락O------O (간접)
IRQ 데드락O------O (간접)
데이터 레이스-O--O (부분)O (패턴)-O (간접)
UAF 레이스-O (부분)OO (샘플)---O
OOB 접근--OO (페이지)---O
Sleep-in-atomicO----O (패턴)-O (간접)
wait_type 위반O-------
RCU 위반O---O (__rcu)--O
락 경합------O-
False Sharing------O (c2c)-
우선순위 역전------O (sched)-
TOCTOU-O (부분)---O (패턴)-O
O = 직접 탐지, O (부분/간접/샘플/패턴) = 부분적 또는 간접 탐지, - = 탐지 불가
각 도구는 고유한 강점이 있으므로, 종합적인 동시성 버그 탐지를 위해서는 여러 도구를 조합하는 것이 필수적입니다. lockdep + KCSAN + KASAN + syzkaller의 4중 조합이 가장 높은 탐지율을 보입니다.

Rust for Linux와 동시성 안전성

Rust for Linux 프로젝트는 소유권(ownership)과 수명(lifetime) 시스템을 통해 컴파일 타임에 데이터 레이스를 방지합니다. 이는 lockdep/KCSAN과 같은 런타임 도구를 보완하는 근본적인 접근입니다. Rust의 Send/Sync 트레이트 시스템은 데이터 공유의 안전성을 타입 시스템 수준에서 강제하며, Mutex<T>/RwLock<T>는 RAII 가드 패턴을 통해 unlock 누락을 원천적으로 방지합니다.

/* C: 컴파일러가 동시성 버그를 감지하지 못함 */
struct my_data {
    int value;            /* 누구나 접근 가능 */
};
void thread_func(struct my_data *d) {
    d->value++;           /* 레이스 가능 — 컴파일러는 모름 */
}

/* Rust: 소유권 시스템이 컴파일 타임에 방지 */
/* struct MyData {
 *     value: Mutex<i32>,   // Mutex로 감싸야만 접근 가능
 * }
 * fn thread_func(d: &MyData) {
 *     let mut guard = d.value.lock();  // 락 획득 필수
 *     *guard += 1;                      // 가드 통해서만 접근
 *     // guard drop 시 자동 해제 — unlock 누락 불가능
 * }
 */
현실적 한계: Rust for Linux는 아직 커널의 일부 서브시스템에만 적용되고 있으며, 기존 C 코드와의 FFI 경계에서는 여전히 런타임 검증 도구가 필요합니다. 장기적으로 Rust와 런타임 도구의 조합이 최적의 안전성을 제공할 것입니다.

eBPF 기반 커스텀 동시성 모니터링

bpftrace와 libbpf를 사용하면 커널 재빌드 없이 동적으로 동시성 이벤트를 모니터링할 수 있습니다. 특히 프로덕션 환경에서 lockdep을 활성화할 수 없을 때, eBPF 프로그램으로 특정 락의 contention을 실시간 분석하는 것이 효과적입니다. BCC(BPF Compiler Collection) 도구 모음의 deadlock 도구는 유저스페이스 프로세스의 mutex 데드락도 탐지할 수 있습니다.

# bpftrace: mutex contention 실시간 모니터링
bpftrace -e '
kprobe:mutex_lock {
    @start[tid] = nsecs;
}
kretprobe:mutex_lock /@start[tid]/ {
    $dur = nsecs - @start[tid];
    @lock_duration = hist($dur);
    if ($dur > 1000000) {  /* 1ms 이상 */
        printf("long mutex wait: %d ns, comm=%s, stack:\n", $dur, comm);
        print(kstack);
    }
    delete(@start[tid]);
}
'

# bpftrace: spin_lock contention 핫스팟
bpftrace -e '
kprobe:_raw_spin_lock {
    @[kstack(5)] = count();
}
interval:s:10 { exit(); }
'

# bpftrace: IRQ-off 구간 측정
bpftrace -e '
tracepoint:irq:irq_handler_entry {
    @irq_start[cpu] = nsecs;
}
tracepoint:irq:irq_handler_exit /@irq_start[cpu]/ {
    $dur = nsecs - @irq_start[cpu];
    @irq_duration = hist($dur);
    delete(@irq_start[cpu]);
}
interval:s:10 {
    print(@irq_duration);
    clear(@irq_duration);
}
'

DEBUG_OBJECTS를 이용한 객체 수명 추적

CONFIG_DEBUG_OBJECTS는 커널 객체(timer, work_struct 등)의 수명을 추적하여 동시성 관련 수명 오류를 탐지합니다.

/* DEBUG_OBJECTS가 탐지하는 패턴 */

/* 1. 초기화되지 않은 타이머 사용 */
struct timer_list my_timer;
/* timer_setup() 없이 바로 사용 */
mod_timer(&my_timer, jiffies + HZ);
/* DEBUG_OBJECTS: ODEBUG: activate not initialized timer */

/* 2. 활성 상태의 work 구조체 해제 */
struct work_struct *w = kmalloc(sizeof(*w), GFP_KERNEL);
INIT_WORK(w, my_work_func);
queue_work(wq, w);
kfree(w);  /* work가 아직 큐에 있는데 해제! */
/* DEBUG_OBJECTS: ODEBUG: free active work */

/* 3. 이미 해제된 타이머 삭제 시도 */
del_timer_sync(&expired_timer);
/* 이미 만료되어 콜백이 실행된 타이머를 다시 삭제 */
/* DEBUG_OBJECTS: ODEBUG: deactivate not active timer */
CONFIG 옵션추적 대상탐지 버그
CONFIG_DEBUG_OBJECTS_TIMERSstruct timer_list미초기화 사용, 활성 중 해제, 이중 초기화
CONFIG_DEBUG_OBJECTS_WORKstruct work_struct큐잉 중 해제, 미초기화 큐잉
CONFIG_DEBUG_OBJECTS_RCU_HEADstruct rcu_head이중 call_rcu, 미초기화 사용
CONFIG_DEBUG_OBJECTS_PERCPU_COUNTERstruct percpu_counter미초기화 접근
CONFIG_DEBUG_OBJECTS_FREE모든 추적 객체slab 해제 시 활성 객체 포함 여부

refcount_t와 atomic_t의 차이

참조 카운팅에 atomic_t 대신 refcount_t를 사용하면 오버플로우/언더플로우 공격을 방지할 수 있습니다. 이는 CVE-2017-5753 등의 취약점에서 교훈을 얻은 것입니다.

/* atomic_t 기반 참조 카운팅 — 취약 */
struct my_obj {
    atomic_t refcnt;
};

void get_obj(struct my_obj *obj) {
    atomic_inc(&obj->refcnt);
    /* 오버플로우 가능: INT_MAX+1 → 0
     * 공격자가 충분히 많이 호출하면 카운터를 0으로 만들 수 있음
     * → 다른 경로에서 dec_and_test가 true → UAF!
     */
}

/* refcount_t 기반 — 안전 */
struct my_obj_safe {
    refcount_t refcnt;
};

void get_obj_safe(struct my_obj_safe *obj) {
    refcount_inc(&obj->refcnt);
    /* 오버플로우 시:
     * - REFCOUNT_SATURATED로 포화 (값 고정)
     * - WARN_ONCE() 경고 출력
     * - 이후 dec_and_test는 절대 true 반환하지 않음
     * → UAF 방지!
     */
}

void put_obj_safe(struct my_obj_safe *obj) {
    if (refcount_dec_and_test(&obj->refcnt)) {
        /* 언더플로우 보호: 이미 0이면 dec 거부 + 경고 */
        kfree(obj);
    }
}

/* Coccinelle로 atomic_t → refcount_t 전환 후보 찾기:
 * scripts/coccinelle/api/atomic_as_refcounter.cocci
 */

lockdep 성능 최적화 팁

lockdep의 오버헤드가 테스트 시나리오에 영향을 줄 때 최적화하는 방법입니다.

최적화방법효과주의
체인 캐시 활용이미 검증된 락 순서는 캐시 히트반복 검증 생략자동 (커널 내장)
MAX 상수 축소LOCKDEP_BITS/CHAINS_BITS 감소메모리 절약복잡한 워크로드에서 한계 초과 위험
모듈별 테스트특정 서브시스템만 빌드lock class 수 감소교차 서브시스템 버그 놓칠 수 있음
LOCK_STAT 분리lockdep + lock_stat 별도 빌드통계 수집 오버헤드 분리두 번 테스트 필요
boot 시 비활성화lockdep=off 부트 파라미터필요 시에만 활성화부팅 중 발생하는 버그 놓침

per-CPU 데이터와 동시성 안전성

per-CPU 변수는 CPU별로 독립적인 복사본을 유지하여 락 없이 데이터를 관리하는 핵심 기법입니다. 그러나 잘못 사용하면 미묘한 동시성 버그가 발생합니다.

/* per-CPU 올바른 사용 패턴 */
DEFINE_PER_CPU(unsigned long, my_counter);

void increment_counter(void) {
    /* preempt 비활성화: CPU 마이그레이션 방지 */
    preempt_disable();
    this_cpu_inc(my_counter);
    preempt_enable();
}

/* 또는 local_lock 사용 (RT-safe) */
static DEFINE_PER_CPU(local_lock_t, my_local_lock);
DEFINE_PER_CPU(struct my_data, my_data);

void update_data_rt_safe(void) {
    local_lock(&my_local_lock);
    /* RT 커널에서도 안전 (per-CPU rt_mutex 사용) */
    struct my_data *d = this_cpu_ptr(&my_data);
    d->value++;
    local_unlock(&my_local_lock);
}

/* 위험한 패턴: preempt_disable 없이 per-CPU 접근 */
void buggy_percpu_access(void) {
    unsigned long *ptr = this_cpu_ptr(&my_counter);
    /* 여기서 preemption → 다른 CPU로 마이그레이션 → 잘못된 CPU의 데이터 수정! */
    (*ptr)++;  /* DATA RACE! (KCSAN이 탐지) */
}
per-CPU 접근 매크로preemption원자성RT-safe권장 용도
this_cpu_read/write비활성화 필요단일 연산조건부단순 읽기/쓰기
this_cpu_inc/dec비활성화 필요RMW 원자조건부카운터 증감
this_cpu_add/sub비활성화 필요RMW 원자조건부가중 카운터
raw_cpu_read/write불필요비원자N통계 전용 (부정확 허용)
local_lock + this_cpu_ptr자동임계구역Y복합 연산, RT 환경
per-CPU + IRQ 주의: per-CPU 변수를 IRQ 핸들러(Handler)에서도 접근하는 경우, 단순 preempt_disable()이 아닌 local_irq_save() 또는 this_cpu_inc()의 IRQ-safe 변형을 사용해야 합니다. IRQ가 동일 CPU에서 per-CPU 변수를 동시에 수정하면 RMW 연산 중간에 인터럽트되어 값이 손실될 수 있습니다.

RCU 디버깅 고급 패턴

RCU(Read-Copy-Update)는 커널에서 가장 널리 사용되는 동기화 메커니즘 중 하나이며, CONFIG_PROVE_RCU를 통해 다양한 위반을 탐지합니다.

/* RCU 관련 lockdep 경고 유형 */

/* 1. RCU read-side 밖에서 rcu_dereference */
struct my_obj *obj = rcu_dereference(ptr);
/* lockdep: suspicious rcu_dereference_check() usage!
 * ... not in RCU read-side critical section!
 */

/* 2. RCU read-side에서 슬립 (classic RCU) */
rcu_read_lock();
mutex_lock(&my_mutex);  /* 슬립! */
/* RCU read-side는 비선점 → 슬립 불가
 * (SRCU 사용 시 슬립 가능)
 */

/* 3. 올바른 패턴: rcu_dereference 변형 */
/* RCU read-side에서 */
rcu_read_lock();
p = rcu_dereference(global_ptr);
rcu_read_unlock();

/* 락 보유 시 (업데이트 측) */
spin_lock(&my_lock);
p = rcu_dereference_protected(global_ptr,
        lockdep_is_held(&my_lock));
spin_unlock(&my_lock);

/* 초기화 시 (단일 스레드) */
p = rcu_dereference_raw(global_ptr);  /* lockdep 검사 생략 */

/* SRCU: 슬립 가능한 RCU */
DEFINE_STATIC_SRCU(my_srcu);
int idx = srcu_read_lock(&my_srcu);
/* mutex_lock() 등 슬립 가능 */
srcu_read_unlock(&my_srcu, idx);
CONFIG_PROVE_RCU 경고 해결 가이드:
  • "suspicious rcu_dereference"rcu_read_lock() 추가 또는 rcu_dereference_protected() + lockdep_is_held() 사용
  • "Illegal context switch in RCU read-side" → 슬립 가능 코드를 RCU 밖으로 이동 또는 SRCU 전환
  • "RCU used illegally from idle CPU!" → idle 컨텍스트에서 rcu_read_lock_bh() 또는 RCU_NONIDLE() 사용
  • "Voluntary context switch within RCU read-side"cond_resched() 호출을 RCU 밖으로 이동

RCU API 선택 매트릭스

RCU 변형슬립 가능배타적 차단성능사용 사례
rcu_read_lock()Npreemption (non-RT)최고일반 데이터 구조 읽기
rcu_read_lock_bh()NBH 비활성화높음softirq 경로와 공유
rcu_read_lock_sched()Npreemption 비활성화높음preempt-off 보호 필요
srcu_read_lock()YSRCU 도메인보통슬립 필요한 읽기 경로
rcu_read_lock_trace()YTasks Trace RCU보통BPF 프로그램 보호
/* RCU 변형별 사용 예시 비교 */

/* Classic RCU: 가장 빠르지만 슬립 불가 */
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, node) {
    process(entry);  /* 슬립 불가! */
}
rcu_read_unlock();

/* SRCU: 슬립 가능하지만 약간 느림 */
idx = srcu_read_lock(&my_srcu);
list_for_each_entry_rcu(entry, &my_list, node) {
    err = mutex_lock_interruptible(&entry->mutex);  /* 슬립 OK */
    if (!err) {
        process_with_lock(entry);
        mutex_unlock(&entry->mutex);
    }
}
srcu_read_unlock(&my_srcu, idx);

/* 해제 측: 변형에 따라 다른 synchronize 호출 */
synchronize_rcu();          /* Classic RCU */
synchronize_rcu_bh();       /* BH RCU (deprecated, 대부분 synchronize_rcu로 통합) */
synchronize_srcu(&my_srcu);  /* SRCU */

/* 비동기 해제 (grace period 대기하지 않음) */
call_rcu(&obj->rcu_head, my_rcu_callback);  /* 콜백에서 kfree */
kfree_rcu(obj, rcu_head);                   /* 간편 매크로 */
kfree_rcu_mightsleep(obj);                  /* 헤드 없는 버전 (v6.3+) */
RCU grace period 주의사항: synchronize_rcu()는 블로킹 함수이므로 spinlock이나 RCU read-side에서 호출하면 안 됩니다. IRQ 핸들러에서 객체를 제거해야 할 때는 call_rcu() 또는 kfree_rcu()를 사용하세요. 또한 SRCU의 synchronize_srcu()는 해당 SRCU 도메인의 읽기 측만 기다리므로, 여러 SRCU 도메인을 사용하면 각각 별도로 동기화해야 합니다.

메모리 배리어 디버깅

커널의 메모리 배리어(smp_mb, smp_wmb, smp_rmb)는 KCSAN으로 직접 검증할 수 없지만, LKMM(Linux Kernel Memory Model)의 herd7 도구로 형식 검증이 가능합니다.

/* 메모리 배리어 누락 패턴 (KCSAN이 감지하지 못하는 미묘한 버그) */

/* Producer-Consumer: 배리어 없는 위험한 패턴 */
/* CPU0 (Producer) */
data = new_value;          /* Store A */
flag = 1;                  /* Store B */

/* CPU1 (Consumer) */
if (flag) {                /* Load B */
    use(data);             /* Load A — 재배치로 old_value 사용 가능! */
}

/* 올바른 패턴: smp_store_release / smp_load_acquire */
/* CPU0 (Producer) */
WRITE_ONCE(data, new_value);     /* Store A */
smp_store_release(&flag, 1);     /* Store B + release 배리어 */

/* CPU1 (Consumer) */
if (smp_load_acquire(&flag)) {   /* Load B + acquire 배리어 */
    use(READ_ONCE(data));         /* Load A — 반드시 new_value */
}
# LKMM herd7으로 메모리 모델 검증
cd tools/memory-model/
# litmus test 작성 (litmus7 형식)
cat > my_test.litmus <<'EOF'
C my_test
{ int x = 0; int y = 0; }

P0(int *x, int *y) {
    WRITE_ONCE(*x, 1);
    smp_store_release(y, 1);
}

P1(int *x, int *y) {
    int r0 = smp_load_acquire(y);
    int r1 = READ_ONCE(*x);
}

exists (1:r0=1 /\ 1:r1=0)
EOF

# 검증 실행
herd7 -conf linux-kernel.cfg my_test.litmus
# Test my_test Allowed: No  ← 이 결과가 나와야 안전

# 커널 트리 내 기존 litmus 테스트 실행
make -C tools/memory-model litmus-test
LKMM과 KCSAN의 보완 관계: KCSAN은 실행 시간에 동시 접근을 탐지하지만, 메모리 순서 관련 미묘한 버그(Store-Store/Load-Load 재배치)는 감지하기 어렵습니다. herd7은 형식 검증으로 모든 가능한 실행 순서를 탐색하므로, 타이밍에 의존하지 않는 완전한 검증이 가능합니다. 다만 작은 코드 조각(litmus test)에만 적용 가능하며, 전체 서브시스템 검증에는 부적합합니다.

서브시스템별 락 패턴 모범 사례

서브시스템주요 락보호 대상일반적 패턴주의사항
VFSinode->i_rwsem파일 메타데이터rwsem (읽기 우선)rename 시 2개 디렉토리 inode 순서 주의
MMmm->mmap_lockVMA 트리rwsem (v6.1+: maple tree RCU)page fault 경로에서 읽기 경합 심각
Networksk->sk_lock소켓 상태spinlock + BH disablesoftirq 경로와의 교차 주의
Block I/Oq->queue_lock요청 큐spinlock (v5.0+: tag-based)blk-mq는 per-CPU hw queue 사용
Device Modeldriver_data드라이버 데이터mutex + RCUprobe/remove와 I/O 경로 직렬화(Serialization)
Schedulerrq->lock런큐raw_spinlock중첩 불가, IRQ-safe 필수
RCU treernp->lockRCU 노드raw_spinlock계층적 락 순서 엄격
Workqueuepool->lockworker poolraw_spinlockflush_work + cancel_work 순서
서브시스템 분석 팁: 새로운 서브시스템의 락 규칙을 파악할 때는 (1) Documentation/locking/ 디렉토리의 해당 서브시스템 문서를 확인하고, (2) grep -r "lockdep_assert_held\|lockdep_is_held"로 기존 코드의 락 보유 가정(assertion)을 검색하며, (3) /proc/lockdep에서 해당 lock class의 의존성 체인을 확인합니다. 커널 메일링 리스트에서 "locking" 태그가 붙은 패치를 추적하는 것도 좋은 방법입니다.

동시성 디버깅 자주 묻는 질문 (FAQ)

Q: lockdep 경고가 실제 데드락을 의미하나요?
A: lockdep 경고는 잠재적 데드락 경로를 보여줍니다. 실제로 데드락이 발생하지 않았더라도 두 CPU가 특정 타이밍으로 실행되면 데드락이 가능하다는 의미입니다. lockdep의 false positive 비율은 매우 낮으므로, 경고가 나오면 반드시 조사해야 합니다.
Q: KCSAN과 KASAN을 동시에 활성화할 수 있나요?
A: 기술적으로 가능하지만 권장하지 않습니다. 두 도구 모두 컴파일러 계측을 사용하여 메모리 접근을 가로채므로, 동시 활성화 시 오버헤드가 10배 이상 증가합니다. CI에서는 별도 빌드로 분리하세요.
Q: 프로덕션에서 사용할 수 있는 동시성 디버깅 도구는?
A: KFENCE(경량 메모리 감지), DETECT_HUNG_TASK(행 탐지), SOFTLOCKUP_DETECTOR, perf lock contention(on-demand), ftrace(on-demand)가 프로덕션에서 안전합니다. lockdep, KCSAN, Generic KASAN은 개발/테스트 전용입니다.
Q: data_race() 매크로를 언제 사용해야 하나요?
A: data_race()는 레이스가 존재하지만 의도적이고 무해한 경우에만 사용합니다. 대표 사례: 통계 카운터(정확성 불필요), 진단 로그용 읽기, 조건 없는 단순 최적화 힌트. 값의 정확성이 로직에 영향을 미치면 READ_ONCE()/WRITE_ONCE() 또는 적절한 동기화를 사용하세요.
Q: lockdep이 "BUG: MAX_LOCKDEP_KEYS too low!" 메시지를 출력하면?
A: lock class 수가 한계를 초과했습니다. .config에서 CONFIG_LOCKDEP_BITS를 13→14로 증가시키면 최대 클래스 수가 8192→16384로 늘어납니다. 대규모 모듈 로드 환경(예: 다수의 네트워크 드라이버)에서 자주 발생합니다.
Q: PREEMPT_RT 커널에서 lockdep 경고가 크게 증가하는 이유는?
A: RT 커널에서는 spinlock_trt_mutex로 변환되어 슬립이 가능해집니다. 따라서 일반 커널에서는 문제없던 raw_spinlock 아래의 spinlock 획득이 RT에서는 wait_type 위반("Invalid wait context")으로 보고됩니다. RT 전환 시 모든 raw_spinlock 구간에서 슬립 가능 경로가 없는지 확인해야 합니다.
Q: syzbot에서 보고된 버그를 재현하려면 어떻게 하나요?
A: syzbot 리포트에는 C reproducer와 .config가 포함됩니다. (1) 해당 .config로 커널을 빌드하고, (2) C reproducer를 컴파일하여 (gcc -pthread -o repro repro.c) 실행합니다. (3) 재현이 안 되면 stress-ng --cpu $(nproc)으로 CPU 부하를 주면서 실행하거나, QEMU에서 CPU 수를 줄여(2-4개) 레이스 윈도우를 넓히세요. (4) KASAN/KCSAN/lockdep이 모두 활성화된 상태에서 실행해야 감지 확률이 높아집니다.
Q: lockdep이 비활성화된 것을 어떻게 확인하나요?
A: cat /proc/lockdep_stats에서 debug_locks: 0이면 lockdep이 비활성화된 상태입니다. dmesg | grep "lockdep"으로 비활성화 원인을 확인할 수 있습니다. 재활성화는 재부팅만 가능합니다. debug_locks_silent가 1이면 추가 경고 출력도 억제됩니다.
Q: atomic_t에서 refcount_t로 전환할 때 주의할 점은?
A: (1) atomic_set(&ref, N)에서 N>1인 경우 refcount_set()으로 그대로 전환 가능하지만, 0으로 설정하면 이후 refcount_inc()가 경고를 발생시킵니다. (2) atomic_add_unless(&ref, 1, 0)refcount_inc_not_zero()로 대체합니다. (3) atomic_dec_return()은 직접 대응이 없으므로 refcount_dec_and_test()로 변경하고 로직을 조정해야 합니다. Coccinelle 규칙 scripts/coccinelle/api/atomic_as_refcounter.cocci로 자동 후보 탐지가 가능합니다.

종합 디버깅 체크리스트

동시성 버그 대응 체크리스트:
  1. 커널 로그에서 WARNING/BUG 패턴 검색 (dmesg | grep -E "BUG:|WARNING:|DEADLOCK|KCSAN|KASAN|KFENCE|hung_task|soft lockup")
  2. 경고 유형에 따라 적절한 도구 활성화 (lockdep/KCSAN/KASAN)
  3. 재현기 작성 또는 stress-ng/torture test로 안정적 재현
  4. 스택 트레이스와 의존성 체인으로 근본 원인 파악
  5. 수정 패턴 적용 (READ_ONCE, 락 순서, irqsave, RCU 등)
  6. 동일 도구로 경고 해소 확인 + 새 경고 없는지 확인
  7. locktorture/rcutorture로 스트레스 안정성 검증
  8. sparse make C=1로 어노테이션 일관성 확인
  9. 패치를 LKML 또는 서브시스템 메일링 리스트에 제출
  10. Fixes: 태그와 Cc: stable@ 태그 포함 (CVE급이면)

커널 메일링 리스트 참고 자료

자료내용출처
Documentation/locking/lockdep-design.rstlockdep 설계 문서 (공식)커널 소스 트리
Documentation/dev-tools/kcsan.rstKCSAN 사용 가이드커널 소스 트리
Documentation/dev-tools/kasan.rstKASAN 상세 가이드커널 소스 트리
Documentation/dev-tools/kfence.rstKFENCE 설정 및 활용커널 소스 트리
tools/memory-model/LKMM (Linux Kernel Memory Model)커널 소스 트리
LWN: "Lockdep: How to read its cryptic output"lockdep 출력 해석 튜토리얼lwn.net
LWN: "Concurrency bugs should fear the big bad data-race detector"KCSAN 소개 및 원리lwn.net
LWN: "KFENCE: A low-overhead memory safety error detector"KFENCE 설계 배경lwn.net
syzbot dashboardsyzkaller가 발견한 커널 버그 현황syzkaller.appspot.com
LWN: "An introduction to lockless algorithms"락 없는 알고리즘 기초lwn.net
Documentation/RCU/RCU 전체 문서 (20+ 파일)커널 소스 트리
LWN: "Wait/wound mutexes"ww_mutex 데드락 회피 메커니즘lwn.net
Documentation/memory-barriers.txt리눅스 메모리 배리어 완전 가이드커널 소스 트리

동시성 디버깅 용어 사전

용어정의관련 도구
Data Race두 스레드가 동일 메모리에 비원자적으로 동시 접근하고 최소 하나가 쓰기인 상태KCSAN
Race Condition타이밍에 따라 프로그램 결과가 달라지는 상위 개념 (data race를 포함)-
Deadlock두 이상의 스레드가 서로의 자원을 대기하며 영원히 진행하지 못하는 상태lockdep
Livelock스레드들이 활성 상태이지만 유용한 진행을 하지 못하는 상태ftrace, perf
Priority Inversion낮은 우선순위 태스크가 높은 우선순위 태스크의 자원을 점유하는 현상perf sched
Lock Classlockdep이 동일 타입의 락 인스턴스를 그룹화하는 단위lockdep
Chain Key현재 태스크가 보유한 락들의 조합을 해시한 고유 식별자lockdep
WatchpointKCSAN이 특정 메모리 주소를 감시하기 위해 설정하는 마커KCSAN
Shadow MemoryKASAN이 실제 메모리의 접근 가능 여부를 추적하는 메타데이터 영역KASAN
Guard PageKFENCE가 객체 양쪽에 배치하여 OOB 접근을 탐지하는 비매핑 페이지KFENCE
Grace PeriodRCU에서 모든 기존 읽기 측이 완료되는 것이 보장되는 시간 구간rcutorture
QuarantineKASAN이 해제된 메모리를 일정 기간 재할당하지 않는 메커니즘 (UAF 탐지용)KASAN
Contention여러 스레드가 동일 락을 동시에 획득하려고 경합하는 상태perf lock, lock_stat
False Sharing서로 다른 데이터가 같은 캐시 라인에 있어 불필요한 캐시 무효화가 발생perf c2c
Memory BarrierCPU/컴파일러의 메모리 접근 재배치를 방지하는 명령LKMM herd7
Acquire/Release락 획득(acquire)과 해제(release) 시 암묵적으로 적용되는 메모리 순서 보장lockdep

PREEMPT_RT 환경에서의 동시성 디버깅 특수 사항

PREEMPT_RT 커널에서는 동시성 디버깅에 추가적인 고려사항이 있습니다. RT 커널은 spinlock_trt_mutex로 변환하므로 일반 커널과 다른 동작을 보입니다.

항목일반 커널 (PREEMPT)RT 커널 (PREEMPT_RT)
spinlock_t실제 스핀 (비선점(Non-preemptive))rt_mutex (슬립 가능, PI 지원)
raw_spinlock_t실제 스핀실제 스핀 (변경 없음)
local_lock_tpreempt_disable()per-CPU rt_mutex
lockdep wait_typeLD_WAIT_SPINLD_WAIT_SLEEP (spinlock이 슬립)
softirqksoftirqd 또는 inline항상 스레드화
IRQ 핸들러하드 IRQ 컨텍스트스레드화 (forced threaded)
/* RT 커널에서의 lockdep wait_type 위반 예시 */
void rt_problematic(void) {
    raw_spin_lock(&raw_lock);       /* LD_WAIT_FREE */
    spin_lock(&regular_lock);       /* RT에서는 LD_WAIT_SLEEP!
                                     * → wait_type 위반 경고 */
    spin_unlock(&regular_lock);
    raw_spin_unlock(&raw_lock);
}

/* RT-safe 패턴:
 * raw_spinlock 구간은 최소화하고, 슬립 가능 작업은 밖으로 이동
 * 또는 regular spinlock만 사용 (중첩 시 RT-safe)
 */
void rt_safe(void) {
    spin_lock(&lock_a);     /* RT: rt_mutex (슬립 가능) */
    spin_lock(&lock_b);     /* RT: rt_mutex (슬립 가능, PI 체인) */
    /* 두 rt_mutex 사이의 PI 상속이 자동으로 작동 */
    spin_unlock(&lock_b);
    spin_unlock(&lock_a);
}
RT 커널 디버깅 시 주의: RT 커널에서 lockdep을 활성화하면 일반 커널에서는 발생하지 않는 wait_type 경고가 대량 출력될 수 있습니다. 이는 일반 커널에서 숨겨져 있던 raw_spinlock 아래의 슬립 가능 경로가 RT에서 드러나기 때문입니다. RT 전환 시 이러한 경고를 모두 해결해야 합니다.

주요 커널 CONFIG 의존성 트리

동시성 디버깅 CONFIG 옵션들은 서로 의존 관계가 있습니다. 주요 의존성을 정리합니다.

CONFIG_PROVE_LOCKING
  ├── CONFIG_DEBUG_LOCK_ALLOC (자동 선택)
  │     ├── CONFIG_TRACE_IRQFLAGS (자동 선택)
  │     └── CONFIG_DEBUG_SPINLOCK (자동 선택)
  ├── CONFIG_PROVE_RCU (자동 선택)
  └── CONFIG_LOCK_STAT (선택적, 추가 통계)

CONFIG_KCSAN
  ├── CC_HAS_TSAN_COMPOUND_READ_BEFORE_WRITE (컴파일러 능력 검사)
  ├── CONFIG_KCSAN_STRICT (선택적, 엄격 모드)
  ├── CONFIG_KCSAN_WEAK_MEMORY (선택적)
  └── CONFIG_KCSAN_KUNIT_TEST (선택적, 기능 테스트)

CONFIG_KASAN
  ├── CONFIG_KASAN_GENERIC (선택 1: 컴파일러 계측)
  │     └── CONFIG_KASAN_OUTLINE 또는 CONFIG_KASAN_INLINE
  ├── CONFIG_KASAN_SW_TAGS (선택 2: 소프트웨어 태그)
  └── CONFIG_KASAN_HW_TAGS (선택 3: ARM MTE)
        └── CONFIG_ARM64_MTE (하드웨어 요구)

CONFIG_KFENCE
  ├── CONFIG_KFENCE_NUM_OBJECTS (객체 풀 크기)
  ├── CONFIG_KFENCE_SAMPLE_INTERVAL (샘플링 간격)
  └── CONFIG_KFENCE_STATIC_KEYS (동적 활성화/비활성화)
CONFIG 의존성 해결 팁: CONFIG_PROVE_LOCKING을 활성화하면 CONFIG_DEBUG_LOCK_ALLOC, CONFIG_TRACE_IRQFLAGS, CONFIG_DEBUG_SPINLOCK이 자동으로 선택됩니다. 하지만 CONFIG_LOCK_STAT은 별도로 활성화해야 합니다. make menuconfig에서 심볼을 검색하려면 / 키를 누르고 "PROVE_LOCKING"을 입력하면 의존성 트리를 확인할 수 있습니다. scripts/config -e PROVE_LOCKING으로 커맨드라인에서 직접 활성화하면 의존성이 자동으로 해결되지 않으므로, 이후 make olddefconfig를 실행하여 의존성을 채워야 합니다.

동시성 디버깅 관련 부트 파라미터

# lockdep 관련
lockdep=off             # lockdep 비활성화 (오버헤드 제거)
lockdep_test=1          # 부팅 시 lockdep 셀프 테스트 실행

# KASAN 관련
kasan=off               # KASAN 비활성화 (빌드에 포함되어 있어도)
kasan.multi_shot=1      # 첫 오류 후에도 계속 보고 (기본: 첫 오류에서 멈춤)
kasan.mode=prod         # HW Tag KASAN의 프로덕션 모드 (v5.14+)
kasan.stacktrace=on     # KASAN 스택 트레이스 활성화

# KFENCE 관련
kfence.sample_interval=100  # 부트 시 샘플링 간격 설정 (ms)

# 일반 디버깅
detect_hung_task=180    # hung task 탐지 타임아웃 (초)
softlockup_panic=1      # soft lockup 발생 시 패닉 (크래시 덤프 생성)
hung_task_panic=1        # hung task 발생 시 패닉
panic_on_warn=1          # WARNING 발생 시 패닉 (CI에서 유용)
panic_on_warn 주의: panic_on_warn=1은 CI 환경에서 lockdep/KCSAN 경고를 놓치지 않기 위해 유용하지만, 프로덕션에서는 절대 사용하지 마세요. 무해한 경고에도 시스템이 재부팅됩니다. CI에서만 사용하고, kdump를 설정하여 크래시 덤프를 자동 수집하세요.

디버깅 도구별 메모리/CPU 오버헤드 비교

도구메모리 오버헤드CPU 오버헤드커널 이미지 크기 증가프로덕션 사용
lockdep~40MB (해시 테이블, 스택)2-5x (락 경로)~1MB불가
KCSAN~1MB (watchpoint 테이블)1.5-3x (계측 콜백)~20% 증가불가
KASAN (Generic)2-3x (섀도 메모리)2-3x (검사 콜백)~30% 증가불가
KASAN (HW Tags)~3% (태그 메모리)<5%최소가능 (ARM MTE)
KFENCE~1MB (풀)<1%최소가능
DEBUG_ATOMIC_SLEEP무시할 수준<1%최소조건부 가능
DETECT_HUNG_TASK무시할 수준무시할 수준최소가능
ftrace (동적)설정에 따라 가변비활성 시 0, 활성 시 가변~5% (mcount/fentry)가능 (on-demand)
perf lock (BPF)BPF 맵 크기<2% (활성 시)N/A (도구)가능 (on-demand)
동시성 디버깅 도구 종합 선택 가이드 단계 코딩 빌드 테스트 프로덕션 정적 분석 sparse 어노테이션 Coccinelle SmPL Clang -Wthread-safety 코드 리뷰 체크리스트 계측 빌드 PROVE_LOCKING KCSAN (STRICT) KASAN (GENERIC) DEBUG_ATOMIC_SLEEP 스트레스/퍼징 locktorture rcutorture syzkaller LTP / KUnit 경량 모니터링 KFENCE (<1% 오버헤드) DETECT_HUNG_TASK perf lock contention (on-demand) ftrace / bpftrace (on-demand) 버그 유형 → 도구 매핑 (우선순위 순) Data Race: KCSAN → sparse(__rcu) → Coccinelle → 코드 리뷰 Deadlock: lockdep(PROVE_LOCKING) → locktorture → hung_task → /proc/PID/stack UAF Race: KASAN(Generic) → KFENCE(프로덕션) → syzkaller → refcount_t 전환 Livelock: perf top → ftrace function_graph → softlockup_detector → bpftrace Priority Inversion: rt_mutex(PI) → perf sched latency → ftrace wakeup_rt Sleep-in-Atomic: DEBUG_ATOMIC_SLEEP → lockdep wait_type → Coccinelle(SmPL) False Sharing: perf c2c → ____cacheline_aligned_in_smp → DEFINE_PER_CPU

참고 자료

동시성 디버깅 도구(lockdep, KCSAN, KASAN 등)의 설계, 사용법, 사례에 대한 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

계속 학습하기: 동시성 디버깅은 동기화 프리미티브, 메모리 모델, 커널 아키텍처에 대한 깊은 이해를 바탕으로 합니다. 아래 문서를 함께 참고하세요.
문서관련 내용
lockdeplockdep 아키텍처, 소스 분석, 경고 해석, 어노테이션, /proc 인터페이스, 서브시스템별 활용
동기화 기법 (Synchronization)spinlock, mutex, rwsem, seqlock, wait queue, completion 프리미티브 상세
메모리 배리어 (Memory Barriers)메모리 순서, smp_mb/wmb/rmb, 컴파일러 배리어, LKMM
RCU (Read-Copy-Update)RCU 패턴, grace period, SRCU, RCU stall 디버깅
원자적 연산 (Atomic Operations)atomic_t, atomic64_t, refcount_t, 비트 연산
Futex사용자 공간 동기화, PI futex, 우선순위 역전
Lock-Free 프로그래밍CAS, lock-free 큐/스택, ABA 문제
PREEMPT_RT실시간 커널, rt_mutex, threaded IRQ, RT 스케줄링
추가 커널 문서 경로:
  • Documentation/locking/ — 락킹 관련 전체 문서 (lockdep, mutex, spinlock, ww-mutex 등)
  • Documentation/dev-tools/ — 개발 도구 가이드 (KASAN, KCSAN, KFENCE, sparse 등)
  • Documentation/RCU/ — RCU 전용 문서 (rcu.txt, RTFP.txt, Design/ 등)
  • Documentation/trace/ — ftrace, tracepoint, histogram trigger 문서
  • tools/testing/selftests/locking/ — 락킹 셀프 테스트
  • tools/perf/ — perf 도구 소스와 문서
  • scripts/coccinelle/locks/ — 락 관련 Coccinelle 규칙
최종 주의사항: 동시성 버그는 비결정적이며 재현이 어렵습니다. 예방이 탐지보다 중요합니다. 코드 작성 시 처음부터 올바른 동기화 패턴을 적용하고, lockdep/KCSAN/sparse를 개발 초기부터 활성화하여 버그가 축적되기 전에 잡으세요. 프로덕션에서는 KFENCE와 DETECT_HUNG_TASK를 상시 활성화하여 현장에서 발생하는 메모리 버그와 행을 조기에 포착하는 것이 핵심입니다.