KCSAN (Kernel Concurrency Sanitizer)

KCSAN은 커널의 데이터 레이스(Data Race)를 탐지하는 Sanitizer입니다. 컴파일러 계측으로 메모리 접근에 워치포인트(Watchpoint)를 설정하고 짧은 지연(Latency) 동안 다른 CPU의 경합(Contention) 접근을 관측해 동기화가 누락된 동시 접근을 확률적으로 포착합니다. 메모리 배리어(Memory Barrier) 규약 기반으로 매크로(Macro) 주석(READ_ONCE/data_race)을 통한 의도 표기를 권장하며, 오버헤드(Overhead)는 중간 수준이고 페이지(Page) 레벨 공유 변수까지 포함합니다.

전제 조건: Atomic 연산메모리 배리어 / LKMM, RCU의 lockless 읽기 패턴을 먼저 이해하세요. KCSAN은 이들이 제공하는 "동기화 표기"를 기준으로 레이스를 판정합니다.
일상 비유: KCSAN은 교차로의 스피드 건과 같습니다. 모든 차량을 추적하는 대신, 한 차량에 잠깐 레이더를 겨누고 그 사이 다른 차량이 동일 지점을 지나가는지 봅니다. 레이더가 겨눈 짧은 시간(워치포인트 딜레이)에 충돌이 관측되면 "동시 접근"이라고 기록합니다. 확률적이지만 시간이 쌓이면 반복 발생하는 레이스는 반드시 잡힙니다.

핵심 요약

  • 워치포인트 샘플링 — 접근 일부에만 워치포인트 설정 → 짧은 딜레이 동안 다른 CPU 접근 감시
  • LKMM 기준 판정 — 커널은 C11이 아닌 LKMM(Linux Kernel Memory Model)을 따름. marked/unmarked 구분이 핵심
  • marked 접근READ_ONCE/WRITE_ONCE/atomic_*/data_race()는 레이스로 간주하지 않음
  • strict 모드CONFIG_KCSAN_STRICT=y는 unmarked 접근도 경고 (가장 엄격)
  • 주석 매크로ASSERT_EXCLUSIVE_WRITER/_SCOPED, ASSERT_EXCLUSIVE_ACCESS/_SCOPED, ASSERT_EXCLUSIVE_BITS로 불변식 명시

단계별 이해

  1. 계측
    Clang/GCC가 모든 메모리 접근에 __tsan_readN/__tsan_writeN 호출을 삽입합니다.
  2. 샘플링
    kcsan_should_sample()이 true를 반환하면 이번 접근에 워치포인트를 설정합니다.
  3. 딜레이
    udelay(KCSAN_UDELAY_TASK) 또는 KCSAN_UDELAY_INTERRUPT 동안 다른 CPU 접근을 대기합니다.
  4. 충돌 감지
    다른 CPU가 동일 주소에 접근하면 워치포인트 히트. 한 쪽이라도 쓰기이면 데이터 레이스로 분류.
  5. 값 기반 검증
    충돌이 없어도 워치포인트 해제 시 값이 달라져 있으면 "놓친 레이스"로 리포트합니다.

데이터 레이스 개요

KCSAN은 커널의 데이터 레이스(data race)를 탐지합니다. 데이터 레이스는 두 스레드(Thread)가 동시에 같은 메모리 위치에 접근하고, 그중 적어도 하나가 쓰기이며, 적절한 동기화(synchronization)가 없는 상황을 말합니다. C11 메모리 모델에서 데이터 레이스는 정의되지 않은 동작(UB)입니다.

LKMM과의 관계: 리눅스 커널은 C11 모델을 그대로 따르지 않고 Linux Kernel Memory Model (LKMM)을 사용합니다. KCSAN은 LKMM 규약을 기준으로 marked/unmarked 접근을 구분해, READ_ONCE/WRITE_ONCE/atomic_* 없이 공유 변수에 접근하는 코드를 경고합니다.
KCSAN 워치포인트 샘플링 타임라인 CPU 0 CPU 1 write x 계측 호출 should_sample? insert watchpoint udelay(KCSAN_UDELAY_TASK) remove watchpoint read x delay 구간에 히트 → 레이스 확정 BUG: KCSAN: data-race in writer / reader write(4B) by CPU 0 · read(4B) by CPU 1 · value changed: 0x01 → 0x02 두 접근의 스택 트레이스와 변경된 값이 함께 출력됨 read x (샘플링 안 됨) = 계측 접근 = 워치포인트 슬롯 점유 구간 = 딜레이 (다른 CPU 충돌 기회) = 워치포인트 히트 (레이스)

워치포인트 기반 샘플링

KCSAN은 컴파일러 계측으로 모든 메모리 접근에 체크 코드를 삽입하되, 실제로는 확률적 샘플링으로 동작합니다. 하나의 접근에 "워치포인트(watchpoint)"를 설정하고, 짧은 딜레이(delay) 동안 다른 CPU에서 같은 주소에 접근하는지 감시합니다.

단계동작비고
1. 접근 선택 확률적으로 메모리 접근 하나를 선택 모든 접근을 감시하면 오버헤드가 너무 큼
2. 워치포인트 설정 해당 주소에 워치포인트 등록 per-CPU 워치포인트 슬롯 사용
3. 딜레이 짧은 시간(~수 마이크로초) 대기 다른 CPU의 접근이 발생할 시간 확보
4. 충돌 감지 다른 CPU에서 같은 주소 접근 시 워치포인트 히트 적어도 하나가 쓰기이면 데이터 레이스
5. 값 기반 검증 히트가 없어도 해제 시 값 변경되었으면 "놓친 레이스"로 리포트 샘플링이 못 본 경합도 후행 검증

핵심 체크 로직

/* kernel/kcsan/core.c - KCSAN 워치포인트 체크 (간략화) */

static __always_inline void check_access(
    const volatile void *ptr,
    size_t size, int type)
{
    /* 1. 기존 워치포인트와 충돌하는지 체크 */
    long encoded_watchpoint;
    if (find_matching_watchpoint(
            (unsigned long)ptr, size,
            type & KCSAN_ACCESS_WRITE,
            &encoded_watchpoint)) {
        /* 충돌! 데이터 레이스 발견 */
        kcsan_report_known_origin(
            ptr, size, type, encoded_watchpoint);
        return;
    }

    /* 2. 확률적으로 이 접근에 워치포인트 설정 */
    if (!kcsan_should_sample())
        return;

    /* 3. 워치포인트 설정 */
    insert_watchpoint((unsigned long)ptr, size, type);

    /* 4. 짧은 딜레이 (다른 CPU의 접근 기회 제공) */
    udelay(KCSAN_UDELAY_TASK);

    /* 5. 워치포인트 해제. 값이 바뀌었으면 "놓친 레이스" 리포트 */
    remove_watchpoint((unsigned long)ptr);
}
코드 설명
  • 7-15행 이미 설치된 워치포인트와 겹치는지 find_matching_watchpoint()로 확인합니다. 주소·크기·쓰기 플래그가 일치하고 최소 한 쪽이 쓰기이면 즉시 레이스로 리포트합니다.
  • 19-20행 kcsan_should_sample()CONFIG_KCSAN_SKIP_WATCH 주기마다 true를 반환합니다. 기본값 4000 접근당 1번.
  • 24행 per-CPU 워치포인트 슬롯(NUM_SLOTS=64)에 주소·크기·쓰기 여부를 인코딩해 저장합니다. 슬롯이 모두 차 있으면 이번 샘플은 포기합니다.
  • 27행 태스크(Task) 컨텍스트는 KCSAN_UDELAY_TASK(기본 80μs), 인터럽트(Interrupt) 컨텍스트는 KCSAN_UDELAY_INTERRUPT(기본 20μs) 대기합니다.

marked vs unmarked 접근

LKMM에서 공유 변수 접근은 "marked"와 "unmarked"로 구분됩니다. KCSAN은 이 구분을 기준으로 판정합니다. CONFIG_KCSAN_STRICT=y는 unmarked 접근 자체를 레이스로 간주하는 가장 엄격한 모드입니다.

접근 형태예시KCSAN 판정LKMM 해석
plain 읽기/쓰기 x = 1;, if (x) unmarked — strict 모드에서 레이스 컴파일러가 재배열·분할·합성 가능. 공유 변수에 금지
READ_ONCE READ_ONCE(x) marked — 레이스 아님 단일 load, 컴파일러 재배열 금지
WRITE_ONCE WRITE_ONCE(x, 1) marked — 레이스 아님 단일 store, 컴파일러 재배열 금지
atomic_* atomic_read(), atomic_inc() marked — 레이스 아님 원자성 보장. 메모리 순서는 API별로 다름
smp_load_acquire/smp_store_release smp_load_acquire(&x) marked — 레이스 아님 release-acquire 순서 보장(Ordering)
rcu_dereference p = rcu_dereference(head); marked — 레이스 아님 RCU 읽기 측. rcu_read_lock 필수
data_race() data_race(x) 의도적 marked — 레이스 아님 "알고 있지만 허용된 레이스" 명시

ASSERT 계열 매크로로 불변식 표기

때로는 "이 자료구조는 특정 문맥에서 배타적 접근이어야 한다"는 불변식을 코드로 표기해 두면, KCSAN이 자동으로 해당 구간에 워치포인트를 설정하여 침범을 검증해 줍니다.

매크로의미사용 예
ASSERT_EXCLUSIVE_WRITER(var) 이 시점에 다른 writer가 없어야 함 락 잡은 후 쓰기 직전
ASSERT_EXCLUSIVE_ACCESS(var) 이 시점에 모든 접근(R/W)이 배타적이어야 함 free 직전, 초기화/파괴 구간
ASSERT_EXCLUSIVE_BITS(var, mask) 지정 비트만 배타적 접근이어야 함 (나머지 비트는 허용) 비트필드 락 보호 범위가 일부인 경우
ASSERT_EXCLUSIVE_WRITER_SCOPED(var) 블록 범위 동안 writer 배타 함수 진입~반환 구간
ASSERT_EXCLUSIVE_ACCESS_SCOPED(var) 블록 범위 동안 모든 접근(R/W) 배타 초기화/파괴 스코프 전체
/* 사용 예 */
void update(struct foo *f)
{
    spin_lock(&f->lock);

    /* 이 시점에 다른 writer가 있으면 KCSAN 레이스 리포트 */
    ASSERT_EXCLUSIVE_WRITER(f->counter);
    f->counter++;

    spin_unlock(&f->lock);
}

void destroy(struct foo *f)
{
    /* 해제 직전에는 어떤 접근도 없어야 함 */
    ASSERT_EXCLUSIVE_ACCESS(*f);
    kfree(f);
}

리포트 해석

==================================================================
BUG: KCSAN: data-race in task_struct_update / task_struct_read

write to 0xffff888103a5e000 of 4 bytes by task 1234 on cpu 0:
 task_struct_update+0x30/0x60
 worker_thread+0x1a0/0x340

read to 0xffff888103a5e000 of 4 bytes by task 5678 on cpu 2:
 task_struct_read+0x18/0x40
 another_worker+0x80/0x120

value changed: 0x00000001 → 0x00000002

Reported by Kernel Concurrency Sanitizer on:
CPU: 0 PID: 1234 Comm: worker Not tainted 6.8.0 #1
==================================================================

분석:
  - task 1234 (CPU 0)가 주소 0xffff888103a5e000에 4바이트 쓰기
  - task 5678 (CPU 2)가 같은 주소에서 4바이트 읽기
  - 동기화 없이 동시 접근 → 데이터 레이스
  - 해결: 적절한 락, READ_ONCE/WRITE_ONCE, 또는 atomic 연산 사용

False positive 억제 패턴

의도적 레이스(통계 카운터, 플래그 조기 읽기 등)는 명시적 표기로 False positive를 줄입니다.

패턴방법용도
1회 허용 레이스 data_race(expr)로 감싸기 통계·디버그 카운터
함수 전체 계측 제외 __no_kcsan 어트리뷰트 엔트리 경로, 인터럽트 프롤로그
파일/디렉토리 계측 제외 KCSAN_SANITIZE := n 부트스트랩, arch/ 특수 경로
엔트리 경로 noinstr (사실상 KCSAN도 제외) kernel/entry-common.c
RCU 읽기 측 rcu_dereference() 사용 lockless 읽기 경로
/* 통계 카운터는 약간의 부정확성을 감수하는 경우 */
static unsigned long stat_count;

void record_event(void)
{
    /* KCSAN 경고 없이 증가 허용 */
    data_race(stat_count++);
}

unsigned long get_stat(void)
{
    return data_race(stat_count);  /* 근사값 허용 */
}

RCU lockless 패턴과 KCSAN

RCU 읽기 측은 lockless로 쓰기 측과 동시에 수행되어도 괜찮다는 것이 보장되는 전형적 marked 접근입니다. rcu_dereference(head)는 내부적으로 READ_ONCE를 사용하며 KCSAN이 레이스로 간주하지 않습니다. 쓰기 측은 rcu_assign_pointer()(내부 smp_store_release)를 사용해야 하며, 둘 중 하나라도 plain 접근을 쓰면 KCSAN이 즉시 경고합니다.

/* 올바른 RCU 패턴 */
struct node *reader(void)
{
    struct node *p;
    rcu_read_lock();
    p = rcu_dereference(head);  /* marked 읽기 */
    /* ... p 사용 ... */
    rcu_read_unlock();
    return p;
}

void writer(struct node *new)
{
    spin_lock(&lock);
    rcu_assign_pointer(head, new);  /* marked 쓰기 (release) */
    spin_unlock(&lock);
    synchronize_rcu();
    /* 기존 노드 free */
}

워치포인트 슬롯 소진과 샘플 손실

per-CPU 워치포인트 슬롯은 기본 64개입니다. 짧은 시간에 많은 접근이 몰리면 슬롯이 모두 점유되어 새 샘플이 포기됩니다. /sys/kernel/debug/kcsan의 통계에서 skipped: ...로 손실률을 볼 수 있으며, CONFIG_KCSAN_SKIP_WATCH를 낮춰 샘플 빈도를 올리거나, 문제 재현이 안 되면 반대로 값을 올려 슬롯 소진을 줄이는 식으로 튜닝합니다.

WEAK_MEMORY 모델 플래그

CONFIG_KCSAN_WEAK_MEMORY=y는 CPU가 재배열이 심한(weak) 메모리 모델을 사용한다고 가정하고 더 많은 재배열 가능성 레이스를 경고합니다. 특히 ARM64·POWER·RISC-V처럼 기본적으로 acquire/release 배리어 없이는 순서가 보장되지 않는 아키텍처에서 의미가 큽니다. x86(TSO)에서는 일부 재배열이 하드웨어적으로 방지되지만, 코드가 다른 아키텍처로 이식될 때를 대비해 개발 단계에서 이 옵션을 켜면 좋습니다.

설정해석효과
WEAK_MEMORY=nTSO 가정 (x86 수준)재배열 가능성 무시, 쓰기-쓰기/읽기-쓰기 경합만 탐지
WEAK_MEMORY=yweak model 가정재배열로 인한 잠재 레이스도 경고 → ARM64/POWER 이식성 확보

Lockdep과의 조합 사용

KCSAN과 Lockdep은 서로 다른 층을 검증합니다. Lockdep은 락 순서(A→B vs B→A)와 불변식(같은 스레드가 같은 락을 재귀 획득하는지 등)을 본다면, KCSAN은 "락으로 보호된 변수에 락 없이 접근하는지"를 봅니다. 공통 디버그 커널에서는 둘을 함께 켜는 것이 이상적이지만 운영상 주의할 점이 있습니다.

조합 운영 팁:
  • Lockdep 경고를 먼저 해결 — Lockdep 오류가 있으면 KCSAN이 같은 변수에서 수십 건의 false positive를 낼 수 있습니다.
  • Lockdep splat 후 KCSAN 재실행 — Lockdep이 tainted 상태에서도 KCSAN은 계속 보고하지만 신뢰도는 낮아집니다.
  • 오버헤드 합산 ~2~3x — CI/퍼징 환경에서만 추천, 대화형 디버깅은 둘 중 하나씩.
  • RCU 보호 영역 — Lockdep이 rcu_read_lock held 여부를 체크해 주므로, KCSAN은 rcu_dereference 누락을 보완합니다.

litmus test와 교차 검증

LKMM은 herd7 시뮬레이터로 돌릴 수 있는 litmus 테스트를 제공합니다. 이론적으로 허용되는 상태를 구한 뒤 KCSAN 리포트와 교차 검증하면, 해당 패턴이 정말 "가능한 실제 레이스"인지 혹은 KCSAN 샘플링의 단순 관측인지 가릴 수 있습니다.

# herd7 예시: store-buffering (SB) 패턴
$ cat SB.litmus
C SB
{ }

P0(int *x, int *y) {
  WRITE_ONCE(*x, 1);
  r1 = READ_ONCE(*y);
}

P1(int *x, int *y) {
  WRITE_ONCE(*y, 1);
  r2 = READ_ONCE(*x);
}

exists (0:r1=0 /\ 1:r2=0)

$ herd7 -conf linux-kernel.cfg SB.litmus
Test SB Allowed
States 4
0:r1=0; 1:r2=0;   # LKMM이 가능하다고 인정하는 상태
...
Observation SB Sometimes 1 3

# KCSAN이 같은 변수에 대해 "write/read race"를 리포트했다면
# herd7 결과가 "Sometimes"인 것과 부합 (실제로 일어날 수 있는 레이스).
# 반대로 "Never"로 나왔다면 KCSAN의 거짓 경고 가능성을 의심.

atomic_t / atomic_long_t / KCSAN_ACCESS_* 판정 차이

KCSAN이 접근을 분류할 때는 "타입"이 아니라 계측 후 생성되는 KCSAN_ACCESS_* 플래그가 기준입니다. 같은 변수라도 어떤 API로 읽고 쓰는지에 따라 marked/unmarked가 달라집니다.

접근플래그marked 여부
atomic_read(&v)KCSAN_ACCESS_ATOMICmarked
atomic_set(&v, 1)ATOMIC | WRITEmarked
atomic_cmpxchg(&v, a, b)ATOMIC | WRITE | COMPOUNDmarked, release-acquire 순서
v.counter 직접 접근plainunmarked — strict 모드에서 경고
atomic_long_* 계열위와 동일하되 word-size에 의존marked
주의: atomic_t.counter 필드를 직접 읽거나 쓰는 드라이버 코드는 strict 모드에서 플래그가 없어 unmarked로 취급됩니다. 반드시 atomic_read/atomic_set API를 사용하세요.

흔한 실수와 디버깅(Debugging) 팁

커널 설정

KCSAN 구성 옵션: CONFIG_KCSAN=y로 활성화합니다. CONFIG_KCSAN_STRICT=yREAD_ONCE/WRITE_ONCE 없이 일반 C 접근으로 공유 변수에 접근하는 것도 데이터 레이스로 리포트합니다. KCSAN_SANITIZE := n으로 특정 파일/디렉토리를 제외할 수 있습니다.
CONFIG_KCSAN=y
CONFIG_KCSAN_STRICT=y                # LKMM 엄격 모드 (기본값 n, opt-in 권장)
CONFIG_KCSAN_WEAK_MEMORY=y           # weak memory model 가정
CONFIG_KCSAN_REPORT_ONCE_IN_MS=3000  # 같은 사이트 리포트 재발 간격
CONFIG_KCSAN_SKIP_WATCH=4000         # 접근 N개당 1회 샘플링
CONFIG_KCSAN_UDELAY_TASK=80          # 태스크 컨텍스트 딜레이 (μs)
CONFIG_KCSAN_UDELAY_INTERRUPT=20     # 인터럽트 컨텍스트 딜레이 (μs)
CONFIG_KCSAN_VERBOSE=y               # 값 변경 정보 포함

특정 접근을 데이터 레이스로 간주하지 않아도 될 때는 data_race(expr) 매크로, ASSERT_EXCLUSIVE_WRITER(), ASSERT_EXCLUSIVE_ACCESS() 등을 사용해 의도를 명시합니다.

참고자료

다음 학습: