동시성 디버깅(Debugging) 종합 가이드
리눅스 커널에서 동시성 버그(데이터 레이스, 데드락, 라이브락, 우선순위 역전(Priority Inversion), use-after-free race)를 탐지하고 해결하는 모든 도구와 기법을 통합 정리합니다. lockdep 아키텍처, KCSAN 계측 원리, KASAN/KFENCE 메모리 감지, sparse/Coccinelle 정적 분석, ftrace/perf 런타임 추적, CONFIG 옵션 종합, 실제 CVE 사례 분석, 테스트 전략, 그리고 발견에서 검증까지의 체계적인 디버깅 워크플로를 다룹니다.
핵심 요약
- lockdep — 락 획득 순서를 런타임에 추적하여 데드락 가능성을 발생 전에 경고합니다.
- KCSAN — 컴파일러 계측 기반으로 데이터 레이스를 탐지합니다.
data_race()로 의도적 레이스를 표시합니다. - KASAN/KFENCE — 메모리 접근 오류(UAF, OOB)를 감지하며, 동시성 관련 use-after-free race를 포착합니다.
- sparse/Coccinelle — 빌드 시 정적 분석으로 동기화 어노테이션 위반과 패턴 기반 버그를 찾습니다.
- ftrace/perf — 런타임 lock contention, IRQ latency, preempt-off 구간을 프로파일링(Profiling)합니다.
단계별 이해
- 1단계 — 버그 분류: 증상을 데이터 레이스, 데드락, 라이브락, 우선순위 역전 중 어느 유형인지 식별합니다.
- 2단계 — 도구 선택: 유형에 따라 lockdep(데드락), KCSAN(데이터 레이스), KASAN(UAF) 등 적절한 도구를 활성화합니다.
- 3단계 — 재현: stress-ng, locktorture, rcutorture 등으로 부하를 가해 버그를 안정적으로 재현합니다.
- 4단계 — 분석: 경고 메시지, 스택 트레이스, lock 의존성 그래프를 해석하여 근본 원인을 파악합니다.
- 5단계 — 수정/검증: 패치(Patch) 적용 후 동일 도구로 경고가 사라졌는지, 새로운 문제가 없는지 확인합니다.
동시성 버그 분류
커널 동시성 버그는 크게 5가지 유형으로 분류됩니다. 각 유형은 발생 조건, 증상, 탐지 도구가 다르므로 정확한 분류가 디버깅의 첫걸음입니다.
| 유형 | 발생 조건 | 증상 | 1차 탐지 도구 | 2차 보조 도구 |
|---|---|---|---|---|
| Data Race | 비보호 공유 변수 동시 접근 (최소 1회 쓰기) | 값 손상, 비결정적 오동작 | KCSAN | sparse, TSAN (유저스페이스 테스트) |
| Deadlock | 순환 락 의존성 (ABBA) | 시스템/태스크(Task) 행(hang) | lockdep | /proc/lockdep, ftrace |
| Livelock | 무한 재시도 (cmpxchg loop 등) | CPU 100%, soft lockup | ftrace/perf | lockdep (간접) |
| Priority Inversion | 저우선 태스크가 고우선 태스크의 락 점유 | RT 태스크 지연(Latency) | rt_mutex PI chain | ftrace sched, perf sched |
| UAF Race | 객체 해제/접근 간 경합(Contention) | 메모리 손상, 커널 패닉(Kernel Panic) | KASAN | KFENCE (프로덕션), lockdep |
데이터 레이스 상세
두 용어는 자주 혼동됩니다. 데이터 레이스(data race)는 C11/C++11 메모리 모델에서 정의한 개념으로, 두 스레드가 동일 메모리 위치에 비원자적으로 동시 접근하고 최소 하나가 쓰기인 경우입니다. 레이스 컨디션(race condition)은 타이밍에 따라 결과가 달라지는 논리적 버그를 포괄하는 상위 개념입니다.
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);
}
- 두 메모리 접근이 동일 주소 범위에 대해 발생
- 최소 하나가 쓰기(write) 접근
- 최소 하나가 plain(비마킹) 접근 —
READ_ONCE()/WRITE_ONCE()/atomic_*()가 아닌 접근 - 두 접근 사이에 적절한 동기화(락, 배리어)가 없음
/* 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);
}
}
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 지원 */
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);
}
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();
}
smp_store_release()/smp_load_acquire()는 x86에서 컴파일러 배리어만 삽입하고 ARM에서 하드웨어 배리어를 삽입하여 두 아키텍처 모두에서 최적의 성능을 제공합니다.
lockdep 요약
lockdep(Lock Dependency Validator)은 커널의 런타임 락 의존성 검증 인프라입니다. CONFIG_PROVE_LOCKING을 활성화하면 매 락 획득/해제 시 의존성 그래프를 갱신하고 순환을 검사합니다.
| 주제 | 핵심 내용 | 상세 참조 |
|---|---|---|
| 아키텍처 | 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);
}
- lockdep_assert_held()
CONFIG_PROVE_LOCKING활성화 시에만 검증을 수행합니다. 프로덕션 빌드에서는 오버헤드가 없으므로, 락 보호가 필요한 모든 내부 함수에 적극적으로 추가하는 것이 권장됩니다. - 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_SLEEP과 CONFIG_PROVE_LOCKING이 활성화된 개발 커널에서만 비용이 발생하므로 과도하게 사용해도 프로덕션 성능에 영향이 없습니다.
KCSAN (Kernel Concurrency Sanitizer)
KCSAN은 컴파일러 계측(compiler instrumentation) 기반의 데이터 레이스 탐지 도구입니다. GCC 11+ 또는 Clang 12+에서 -fsanitize=thread 유사한 계측을 삽입하여 런타임에 동시 접근을 감시합니다.
KCSAN 동작 원리
- 컴파일러가 모든 메모리 접근에
__tsan_readN()/__tsan_writeN()콜백을 삽입합니다. - KCSAN은 확률적으로 일부 접근에 watchpoint를 설정합니다 (샘플링).
- watchpoint 설정 후 짧은 지연 윈도우(기본 ~80us)를 둡니다.
- 이 윈도우 동안 다른 CPU가 같은 주소에 접근하면 watchpoint 테이블 매치가 발생합니다.
- 양쪽 접근 중 최소 하나가 쓰기이고, 적절한 동기화 없이 수행되었으면 data race로 리포트합니다.
KCSAN CONFIG 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_KCSAN | n | KCSAN 활성화 |
CONFIG_KCSAN_STRICT | n | 모든 비표시 레이스를 경고 (data_race() 없이) |
CONFIG_KCSAN_WEAK_MEMORY | y | 약한 메모리 모델 접근도 탐지 |
CONFIG_KCSAN_NUM_WATCHPOINTS | 64 | 동시 watchpoint 슬롯 수 |
CONFIG_KCSAN_UDELAY_TASK | 80 | 태스크 컨텍스트 지연 (us) |
CONFIG_KCSAN_UDELAY_INTERRUPT | 20 | 인터럽트(Interrupt) 컨텍스트 지연 (us) |
CONFIG_KCSAN_REPORT_ONCE_IN_MS | 3000 | 동일 리포트 반복 억제 간격 |
CONFIG_KCSAN_SKIP_WATCH | 5000 | N번 접근마다 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_SKIP_WATCH=100과 KCSAN_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); /* 원자적 비트 설정 */
- 보안 관련 레이스: 권한 검사, 참조 카운트(Reference Count) → 즉시 수정 (READ_ONCE/atomic 또는 락)
- 데이터 무결성(Integrity) 레이스: 자료구조 필드, 링크 리스트 → READ_ONCE/WRITE_ONCE 또는 락
- 성능 카운터/통계: 근사값 허용 →
data_race()매크로(Macro) - 진단/로깅: 디버그 출력 용도 →
data_race()매크로
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 KASAN | CONFIG_KASAN_GENERIC | 컴파일러 계측 + 섀도 메모리 | ~2-3x 메모리, ~2x CPU | 모든 접근 검사 |
| SW Tag KASAN | CONFIG_KASAN_SW_TAGS | 소프트웨어 태그 기반 | ~1.5x 메모리, ~1.5x CPU | 확률적 (태그 충돌) |
| HW Tag KASAN | CONFIG_KASAN_HW_TAGS | ARM 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 후 해제 */
}
CONFIG_KASAN_QUARANTINE_SIZE로 조절합니다.
KASAN 섀도 메모리 레이아웃
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 동작 원리
- 부팅 시 고정 크기의 KFENCE 풀(기본 255개 객체)을 할당합니다.
- 각 객체는 양쪽에 가드 페이지(Page)로 둘러싸여 있습니다. 가드 페이지는 접근 불가로 매핑(Mapping)됩니다.
- 일정 간격(
CONFIG_KFENCE_SAMPLE_INTERVAL)마다 slab 할당을 가로채서 KFENCE 풀에서 할당합니다. - OOB 접근: 가드 페이지 접근 시 page fault로 즉시 탐지됩니다.
- 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
<1%의 CPU 오버헤드와 ~1MB 메모리만 사용하므로 프로덕션 커널에서도 안전하게 활성화할 수 있습니다. Google은 자사 프로덕션 서버에서 KFENCE를 상시 활성화하여 실제 UAF/OOB 버그를 조기에 발견합니다.
KFENCE 설정과 튜닝
| CONFIG 옵션 | 기본값 | 설명 | 프로덕션 권장 |
|---|---|---|---|
CONFIG_KFENCE | n | KFENCE 활성화 | y |
CONFIG_KFENCE_NUM_OBJECTS | 255 | KFENCE 풀 객체 수 | 255 (기본값 유지) |
CONFIG_KFENCE_SAMPLE_INTERVAL | 100 | slab 할당 가로채기 간격 (ms) | 100-500ms |
CONFIG_KFENCE_STATIC_KEYS | y | static key 기반 활성화/비활성화 | y |
CONFIG_KFENCE_STRESS_TEST_FAULTS | 0 | 테스트용 인위적 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 KASAN | HW Tag KASAN | KFENCE |
|---|---|---|---|
| 메커니즘 | 컴파일러 계측 + 섀도 | 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+ | 모든 아키텍처 |
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"
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. CI | KASAN + syzkaller | 퍼징 기반 자동 탐지 | syzkaller 인스턴스 24시간 실행 | N/A (병렬) |
| 3. QA | KMSAN | 미초기화 메모리 사용 | 별도 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 사용법
# 변경된 파일만 검사
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/
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.cocci | irqsave/irqrestore 불일치 | flags 변수 재사용 오류 |
free/kfree.cocci | 이중 해제(Double Free) | 동일 포인터 중복 kfree |
api/atomic_as_refcounter.cocci | refcount 패턴 | 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();
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)으로 추적합니다.
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 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
perf lock / perf sched 활용
perf의 lock/sched 서브커맨드를 사용하면 시스템 전체의 락 경합과 스케줄링 지연을 프로파일링할 수 있습니다.
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
/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 옵션 | 기능 | 오버헤드 | 프로덕션 |
|---|---|---|---|---|
| lockdep | CONFIG_PROVE_LOCKING | 락 의존성 검증 (핵심 스위치) | 높음 | 불가 |
CONFIG_DEBUG_LOCK_ALLOC | 락 할당/해제 추적 | 중간 | 불가 | |
CONFIG_LOCK_STAT | /proc/lock_stat 통계 | 중간 | 조건부 | |
CONFIG_DEBUG_LOCKDEP | lockdep 자체 디버깅 | 높음 | 불가 | |
CONFIG_PROVE_RCU | RCU 의존성 검증 | 중간 | 불가 | |
CONFIG_DEBUG_WW_MUTEX_SLOWPATH | ww_mutex 슬로우패스 디버깅 | 낮음 | 불가 | |
| 데이터 레이스 | CONFIG_KCSAN | KCSAN 활성화 | 중간 | 불가 |
CONFIG_KCSAN_STRICT | 엄격 모드 (모든 비표시 레이스) | 중간 | 불가 | |
CONFIG_KCSAN_KUNIT_TEST | KCSAN 기능 테스트 | 낮음 | 불가 | |
CONFIG_KCSAN_WEAK_MEMORY | 약한 메모리 모델 검사 | 낮음 | 불가 | |
| 메모리 안전 | CONFIG_KASAN | 커널 주소 새니타이저 | 높음 | 불가 |
CONFIG_KASAN_GENERIC | 컴파일러 계측 모드 | 매우 높음 | 불가 | |
CONFIG_KFENCE | 경량 메모리 감지 | 매우 낮음 | 가능 | |
CONFIG_KFENCE_SAMPLE_INTERVAL | 샘플링 간격 (기본 100ms) | 설정 의존 | 가능 | |
| 컨텍스트 | CONFIG_DEBUG_ATOMIC_SLEEP | 원자 컨텍스트에서 슬립 탐지 | 낮음 | 불가 |
CONFIG_DEBUG_PREEMPT | preempt count 검증 | 낮음 | 불가 | |
CONFIG_DEBUG_RT_MUTEXES | RT mutex 디버깅 | 중간 | 불가 | |
CONFIG_DETECT_HUNG_TASK | 행 태스크 탐지 | 매우 낮음 | 가능 | |
| 스트레스 테스트 | CONFIG_LOCK_TORTURE_TEST | 락 스트레스 테스트 모듈 | N/A | 불가 |
CONFIG_RCU_TORTURE_TEST | RCU 스트레스 테스트 | N/A | 불가 | |
CONFIG_DEBUG_OBJECTS | 객체 수명 추적 | 중간 | 불가 |
환경별 CONFIG 옵션 매트릭스
| CONFIG 옵션 | 개발 PC | CI/CD | QA 테스트 | 스테이징 | 프로덕션 |
|---|---|---|---|---|---|
PROVE_LOCKING | Y | Y | Y | N | N |
LOCK_STAT | Y | N | Y | 선택 | N |
KCSAN | Y | Y (별도빌드) | Y | N | N |
KASAN_GENERIC | Y | Y (별도빌드) | Y | N | N |
KASAN_HW_TAGS | N/A | N/A | ARM MTE | ARM MTE | ARM MTE |
KFENCE | Y | Y | Y | Y | Y |
DEBUG_ATOMIC_SLEEP | Y | Y | Y | 선택 | N |
DETECT_HUNG_TASK | Y | Y | Y | Y | Y |
SOFTLOCKUP_DETECTOR | Y | Y | Y | Y | Y |
LOCK_TORTURE_TEST | M | M | M | N | N |
DEBUG_OBJECTS | Y | 선택 | Y | N | N |
DEBUG_PREEMPT | Y | Y | Y | N | N |
권장 디버깅 커널 설정
# .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
실제 커널 동시성 버그 사례 분석
실제 CVE로 보고된 커널 동시성 버그를 분석하여 어떤 유형의 버그가 발생하고, 어떤 도구로 탐지할 수 있었는지 살펴봅니다.
CVE-2022-0847 (Dirty Pipe) 분석
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 */
공통 레이스 패턴 분석
커널 CVE에서 반복적으로 나타나는 레이스 패턴을 정리합니다.
| 패턴 | 설명 | 대표 CVE | 수정 방법 | 탐지 도구 |
|---|---|---|---|---|
| fd install race | fd_install() 전후 레이스 윈도우 | CVE-2022-1729 | fd_install 순서 보장(Ordering), refcount | syzkaller + KASAN |
| TOCTOU (mmap/write) | 메모리 매핑 + 접근 사이 레이스 | CVE-2016-5195 | 검사-사용 원자화, PTE 보호 | 코드 리뷰, fuzzing |
| netfilter hook race | hook 등록/해제와 패킷(Packet) 처리 레이스 | CVE-2024-1086 | RCU 보호, synchronize_rcu() | KASAN + syzkaller |
| TC filter UAF | 트래픽 컨트롤 필터 삭제/사용 레이스 | CVE-2022-2588 | RCU + refcount 조합 | KASAN + syzbot |
| refcount overflow | 참조 카운터 정수 오버플로(Integer Overflow)우 | CVE-2021-4154 | refcount_t 전환 | Coccinelle + 코드 리뷰 |
| sleep-in-atomic | 원자 컨텍스트에서 슬립 호출 | 다수 | 작업 위임 (work_struct) | lockdep, DEBUG_ATOMIC_SLEEP |
CVE에서 배우는 교훈
- 레이스 윈도우는 항상 존재한다: "이 간격은 너무 짧아서 레이스가 불가능하다"는 가정은 위험합니다. 공격자는 CPU 스케줄링, userfaultfd, FUSE 등으로 윈도우를 확장할 수 있습니다.
- syzkaller/syzbot이 게임 체인저: 최근 커널 동시성 CVE의 상당수(60%+)는 syzkaller 퍼저가 최초 발견했습니다.
- KASAN + lockdep 조합이 핵심: UAF race는 KASAN으로, 데드락/순서 문제는 lockdep으로 탐지합니다.
- RCU는 만능이 아니다: RCU를 사용해도 grace period 내 참조 카운팅을 올바르게 하지 않으면 UAF가 발생합니다.
- 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,
};
- 빌드 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: 파일 잠금 동시성 검증
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: 동시성 테스트 통과"
동시성 테스트 커버리지 분석
동시성 테스트의 효과를 측정하려면 락 커버리지와 경로 커버리지를 함께 분석해야 합니다.
# 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 코드 커버리지 확인
lock-classes가 전체 커널의 10% 이상이면 기본 커버리지로 볼 수 있습니다. syzkaller의 커버리지가 40% 이상이면 양호한 수준입니다. KCSAN의 total_watchpoints가 100만 이상이면 충분한 샘플링입니다. 이 지표들을 CI 대시보드에 표시하여 테스트 품질을 지속적으로 모니터링하세요.
디버깅 워크플로 체계화
동시성 버그의 발견부터 검증까지 5단계 체계적 워크플로를 정리합니다.
실전 디버깅 시나리오: 발견과 재현
시나리오: 서버에서 간헐적 행(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
도구 선택 결정 트리
lockdep 내부 해시 테이블(Hash Table)
lockdep은 성능을 위해 여러 해시 테이블을 사용합니다. classhash_table은 lock_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_lock은 raw_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
- 코드 리팩토링: 내부 함수에서 락을 다시 잡지 않도록
__func_locked()패턴 사용 - 의도적 중첩이면:
spin_lock_nested(&dev->lock, SINGLE_DEPTH_NESTING) - 서로 다른 인스턴스면:
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/16 | 1~16바이트 읽기 | addr |
__tsan_write1/2/4/8/16 | 1~16바이트 쓰기 | addr |
__tsan_unaligned_read* | 비정렬 읽기 | addr |
__tsan_unaligned_write* | 비정렬 쓰기 | addr |
__tsan_read_range | memcpy 등 범위 읽기 | addr, size |
__tsan_write_range | memset 등 범위 쓰기 | addr, size |
__tsan_func_entry/exit | 함수 진입/종료 | caller |
__tsan_atomic*_load/store | atomic 접근 | 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 의심
____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 | 기타 | 합계 | 주요 서브시스템 |
|---|---|---|---|---|---|---|---|
| 2020 | 12 | 8 | 18 | 3 | 5 | 46 | net, mm, fs |
| 2021 | 15 | 11 | 24 | 5 | 7 | 62 | net, bpf, io_uring |
| 2022 | 18 | 9 | 31 | 4 | 8 | 70 | net, mm, bpf |
| 2023 | 22 | 13 | 28 | 6 | 9 | 78 | net, bpf, drivers |
| 2024 | 19 | 10 | 25 | 5 | 11 | 70 | net, fs, crypto |
| 2025 (Q1) | 8 | 4 | 12 | 2 | 3 | 29 | net, bpf, mm |
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_t | UAF 방지 참조 카운팅 |
| 순서 변경 | ~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);
}
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_type | spin_lock | 테스트 대상 락 유형 (spin_lock, spin_lock_irq, rw_lock, mutex_lock, rwsem_lock 등) |
nwriters_stress | online CPUs | 쓰기 스트레스 스레드 수 |
nreaders_stress | online CPUs | 읽기 스트레스 스레드 수 (rw_lock/rwsem용) |
torture_runnable | 1 | 1=즉시 시작, 0=수동 시작 |
stat_interval | 60 | 통계 출력 간격 (초) |
stutter | 5 | 주기적 정지/재시작(Reboot)으로 코너 케이스 노출 |
shuffle_interval | 3 | 스레드 CPU 이동 간격 (초) |
verbose | 1 | 상세 로그 수준 |
# 강도 높은 종합 테스트
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 locking | ABBA 데드락 가능성 | 락 순서 통일 또는 trylock |
possible recursive locking | 자기 참조 데드락 | __locked 패턴 또는 nested 사용 |
inconsistent lock state | IRQ safe/unsafe 혼용 | spin_lock_irqsave()로 교체 |
Invalid wait context | wait_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 의존
커널 버전별 동시성 디버깅 도구 진화
도구별 버그 탐지 능력 매트릭스
| 버그 유형 | lockdep | KCSAN | KASAN | KFENCE | sparse | Coccinelle | perf lock | syzkaller |
|---|---|---|---|---|---|---|---|---|
| ABBA 데드락 | O | - | - | - | - | - | - | O (간접) |
| IRQ 데드락 | O | - | - | - | - | - | - | O (간접) |
| 데이터 레이스 | - | O | - | - | O (부분) | O (패턴) | - | O (간접) |
| UAF 레이스 | - | O (부분) | O | O (샘플) | - | - | - | O |
| OOB 접근 | - | - | O | O (페이지) | - | - | - | O |
| Sleep-in-atomic | O | - | - | - | - | O (패턴) | - | O (간접) |
| wait_type 위반 | O | - | - | - | - | - | - | - |
| RCU 위반 | O | - | - | - | O (__rcu) | - | - | O |
| 락 경합 | - | - | - | - | - | - | O | - |
| False Sharing | - | - | - | - | - | - | O (c2c) | - |
| 우선순위 역전 | - | - | - | - | - | - | O (sched) | - |
| TOCTOU | - | 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 누락 불가능
* }
*/
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_TIMERS | struct timer_list | 미초기화 사용, 활성 중 해제, 이중 초기화 |
CONFIG_DEBUG_OBJECTS_WORK | struct work_struct | 큐잉 중 해제, 미초기화 큐잉 |
CONFIG_DEBUG_OBJECTS_RCU_HEAD | struct rcu_head | 이중 call_rcu, 미초기화 사용 |
CONFIG_DEBUG_OBJECTS_PERCPU_COUNTER | struct 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 환경 |
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);
"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() | N | preemption (non-RT) | 최고 | 일반 데이터 구조 읽기 |
rcu_read_lock_bh() | N | BH 비활성화 | 높음 | softirq 경로와 공유 |
rcu_read_lock_sched() | N | preemption 비활성화 | 높음 | preempt-off 보호 필요 |
srcu_read_lock() | Y | SRCU 도메인 | 보통 | 슬립 필요한 읽기 경로 |
rcu_read_lock_trace() | Y | Tasks 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+) */
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
herd7은 형식 검증으로 모든 가능한 실행 순서를 탐색하므로, 타이밍에 의존하지 않는 완전한 검증이 가능합니다. 다만 작은 코드 조각(litmus test)에만 적용 가능하며, 전체 서브시스템 검증에는 부적합합니다.
서브시스템별 락 패턴 모범 사례
| 서브시스템 | 주요 락 | 보호 대상 | 일반적 패턴 | 주의사항 |
|---|---|---|---|---|
| VFS | inode->i_rwsem | 파일 메타데이터 | rwsem (읽기 우선) | rename 시 2개 디렉토리 inode 순서 주의 |
| MM | mm->mmap_lock | VMA 트리 | rwsem (v6.1+: maple tree RCU) | page fault 경로에서 읽기 경합 심각 |
| Network | sk->sk_lock | 소켓 상태 | spinlock + BH disable | softirq 경로와의 교차 주의 |
| Block I/O | q->queue_lock | 요청 큐 | spinlock (v5.0+: tag-based) | blk-mq는 per-CPU hw queue 사용 |
| Device Model | driver_data | 드라이버 데이터 | mutex + RCU | probe/remove와 I/O 경로 직렬화(Serialization) |
| Scheduler | rq->lock | 런큐 | raw_spinlock | 중첩 불가, IRQ-safe 필수 |
| RCU tree | rnp->lock | RCU 노드 | raw_spinlock | 계층적 락 순서 엄격 |
| Workqueue | pool->lock | worker pool | raw_spinlock | flush_work + cancel_work 순서 |
Documentation/locking/ 디렉토리의 해당 서브시스템 문서를 확인하고, (2) grep -r "lockdep_assert_held\|lockdep_is_held"로 기존 코드의 락 보유 가정(assertion)을 검색하며, (3) /proc/lockdep에서 해당 lock class의 의존성 체인을 확인합니다. 커널 메일링 리스트에서 "locking" 태그가 붙은 패치를 추적하는 것도 좋은 방법입니다.
동시성 디버깅 자주 묻는 질문 (FAQ)
A: lockdep 경고는 잠재적 데드락 경로를 보여줍니다. 실제로 데드락이 발생하지 않았더라도 두 CPU가 특정 타이밍으로 실행되면 데드락이 가능하다는 의미입니다. lockdep의 false positive 비율은 매우 낮으므로, 경고가 나오면 반드시 조사해야 합니다.
A: 기술적으로 가능하지만 권장하지 않습니다. 두 도구 모두 컴파일러 계측을 사용하여 메모리 접근을 가로채므로, 동시 활성화 시 오버헤드가 10배 이상 증가합니다. CI에서는 별도 빌드로 분리하세요.
A:
KFENCE(경량 메모리 감지), DETECT_HUNG_TASK(행 탐지), SOFTLOCKUP_DETECTOR, perf lock contention(on-demand), ftrace(on-demand)가 프로덕션에서 안전합니다. lockdep, KCSAN, Generic KASAN은 개발/테스트 전용입니다.
A:
data_race()는 레이스가 존재하지만 의도적이고 무해한 경우에만 사용합니다. 대표 사례: 통계 카운터(정확성 불필요), 진단 로그용 읽기, 조건 없는 단순 최적화 힌트. 값의 정확성이 로직에 영향을 미치면 READ_ONCE()/WRITE_ONCE() 또는 적절한 동기화를 사용하세요.
A: lock class 수가 한계를 초과했습니다.
.config에서 CONFIG_LOCKDEP_BITS를 13→14로 증가시키면 최대 클래스 수가 8192→16384로 늘어납니다. 대규모 모듈 로드 환경(예: 다수의 네트워크 드라이버)에서 자주 발생합니다.
A: RT 커널에서는
spinlock_t가 rt_mutex로 변환되어 슬립이 가능해집니다. 따라서 일반 커널에서는 문제없던 raw_spinlock 아래의 spinlock 획득이 RT에서는 wait_type 위반("Invalid wait context")으로 보고됩니다. RT 전환 시 모든 raw_spinlock 구간에서 슬립 가능 경로가 없는지 확인해야 합니다.
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이 모두 활성화된 상태에서 실행해야 감지 확률이 높아집니다.
A:
cat /proc/lockdep_stats에서 debug_locks: 0이면 lockdep이 비활성화된 상태입니다. dmesg | grep "lockdep"으로 비활성화 원인을 확인할 수 있습니다. 재활성화는 재부팅만 가능합니다. debug_locks_silent가 1이면 추가 경고 출력도 억제됩니다.
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로 자동 후보 탐지가 가능합니다.
종합 디버깅 체크리스트
- 커널 로그에서 WARNING/BUG 패턴 검색 (
dmesg | grep -E "BUG:|WARNING:|DEADLOCK|KCSAN|KASAN|KFENCE|hung_task|soft lockup") - 경고 유형에 따라 적절한 도구 활성화 (lockdep/KCSAN/KASAN)
- 재현기 작성 또는 stress-ng/torture test로 안정적 재현
- 스택 트레이스와 의존성 체인으로 근본 원인 파악
- 수정 패턴 적용 (READ_ONCE, 락 순서, irqsave, RCU 등)
- 동일 도구로 경고 해소 확인 + 새 경고 없는지 확인
- locktorture/rcutorture로 스트레스 안정성 검증
- sparse
make C=1로 어노테이션 일관성 확인 - 패치를 LKML 또는 서브시스템 메일링 리스트에 제출
- Fixes: 태그와 Cc: stable@ 태그 포함 (CVE급이면)
커널 메일링 리스트 참고 자료
| 자료 | 내용 | 출처 |
|---|---|---|
| Documentation/locking/lockdep-design.rst | lockdep 설계 문서 (공식) | 커널 소스 트리 |
| Documentation/dev-tools/kcsan.rst | KCSAN 사용 가이드 | 커널 소스 트리 |
| Documentation/dev-tools/kasan.rst | KASAN 상세 가이드 | 커널 소스 트리 |
| Documentation/dev-tools/kfence.rst | KFENCE 설정 및 활용 | 커널 소스 트리 |
| 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 dashboard | syzkaller가 발견한 커널 버그 현황 | 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 Class | lockdep이 동일 타입의 락 인스턴스를 그룹화하는 단위 | lockdep |
| Chain Key | 현재 태스크가 보유한 락들의 조합을 해시한 고유 식별자 | lockdep |
| Watchpoint | KCSAN이 특정 메모리 주소를 감시하기 위해 설정하는 마커 | KCSAN |
| Shadow Memory | KASAN이 실제 메모리의 접근 가능 여부를 추적하는 메타데이터 영역 | KASAN |
| Guard Page | KFENCE가 객체 양쪽에 배치하여 OOB 접근을 탐지하는 비매핑 페이지 | KFENCE |
| Grace Period | RCU에서 모든 기존 읽기 측이 완료되는 것이 보장되는 시간 구간 | rcutorture |
| Quarantine | KASAN이 해제된 메모리를 일정 기간 재할당하지 않는 메커니즘 (UAF 탐지용) | KASAN |
| Contention | 여러 스레드가 동일 락을 동시에 획득하려고 경합하는 상태 | perf lock, lock_stat |
| False Sharing | 서로 다른 데이터가 같은 캐시 라인에 있어 불필요한 캐시 무효화가 발생 | perf c2c |
| Memory Barrier | CPU/컴파일러의 메모리 접근 재배치를 방지하는 명령 | LKMM herd7 |
| Acquire/Release | 락 획득(acquire)과 해제(release) 시 암묵적으로 적용되는 메모리 순서 보장 | lockdep |
PREEMPT_RT 환경에서의 동시성 디버깅 특수 사항
PREEMPT_RT 커널에서는 동시성 디버깅에 추가적인 고려사항이 있습니다. RT 커널은 spinlock_t를 rt_mutex로 변환하므로 일반 커널과 다른 동작을 보입니다.
| 항목 | 일반 커널 (PREEMPT) | RT 커널 (PREEMPT_RT) |
|---|---|---|
| spinlock_t | 실제 스핀 (비선점(Non-preemptive)) | rt_mutex (슬립 가능, PI 지원) |
| raw_spinlock_t | 실제 스핀 | 실제 스핀 (변경 없음) |
| local_lock_t | preempt_disable() | per-CPU rt_mutex |
| lockdep wait_type | LD_WAIT_SPIN | LD_WAIT_SLEEP (spinlock이 슬립) |
| softirq | ksoftirqd 또는 inline | 항상 스레드화 |
| IRQ 핸들러 | 하드 IRQ 컨텍스트 | 스레드화 (forced threaded) |
/* RT 커널에서의 lockdep wait_type 위반 예시 */
void rt_problematic(void) {
raw_spin_lock(&raw_lock); /* LD_WAIT_FREE */
spin_lock(®ular_lock); /* RT에서는 LD_WAIT_SLEEP!
* → wait_type 위반 경고 */
spin_unlock(®ular_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);
}
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_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=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) |
참고 자료
커널 공식 문서
- Runtime locking correctness validator — lockdep 설계와 동작 원리
- The Kernel Concurrency Sanitizer (KCSAN) — KCSAN 설정, 사용법, 보고 해석
- The Kernel Address Sanitizer (KASAN) — 메모리 안전성 검사기 가이드
- Kernel Electric-Fence (KFENCE) — 저오버헤드 메모리 오류 탐지기
- Lock Statistics — /proc/lock_stat 기반 contention 분석
- Linux Kernel Memory Model — LKMM 정형 검증 도구
- ftrace — Function Tracer — 동시성 문제 추적을 위한 ftrace 가이드
LWN.net 심층 기사
- Lockdep: how to read its cryptic output (2013) — lockdep 출력 해석 완전 튜토리얼
- Concurrency bugs should fear the big bad data-race detector (2019) — KCSAN 소개와 원리
- The kernel lock validator (2006) — lockdep 최초 소개 (Ingo Molnár)
- Kernel address sanitizer (2014) — KASAN 도입 배경과 설계
- KFENCE: A low-overhead memory safety error detector (2020) — KFENCE 설계와 프로덕션 활용
- A formal kernel memory-ordering model (part 1) (2017) — LKMM으로 동시성 버그 정형 검증
학술 자료 및 외부 참고
- Paul McKenney — "Is Parallel Programming Hard?" — 동시성 버그 분류와 디버깅 방법론
- syzbot dashboard — syzkaller가 발견한 커널 동시성 버그 현황
- 커널 소스:
kernel/locking/lockdep.c,kernel/kcsan/,mm/kasan/,mm/kfence/ - perf lock:
perf lock record/report/contention— 런타임 잠금 경합 분석 - LKMM 도구:
tools/memory-model/— herd7, klitmus7 litmus test 프레임워크
관련 문서
| 문서 | 관련 내용 |
|---|---|
| lockdep | lockdep 아키텍처, 소스 분석, 경고 해석, 어노테이션, /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 규칙