커널 개발 주의사항

리눅스 커널 개발 시 흔히 발생하는 실수와 주의사항, 그리고 올바른 해결 방법을 정리합니다. 특히 동기화 오류를 중점적으로 다룹니다.

중요: 이 문서에 나열된 실수들은 시스템 크래시, 보안 취약점, 데이터 손상으로 이어질 수 있습니다. 코드를 작성한 후 반드시 체크리스트를 확인하세요.
전제 조건: 커널 아키텍처빌드 시스템 문서를 먼저 읽으세요. 입문 문서는 개발 환경, 코드 위치, 변경 절차를 연결해 보는 것이 핵심이므로 기본 작업 흐름을 먼저 고정해야 합니다.
일상 비유: 이 주제는 현장 작업 시작 전 안전 교육과 비슷합니다. 도구 사용법과 작업 순서를 먼저 익혀야 실수를 줄일 수 있듯이, 커널 개발도 기본 루틴을 먼저 갖추는 것이 중요합니다.

핵심 요약

  • 동기화 오류 — 데드락, race condition, atomic context에서의 sleep 등 가장 찾기 어렵고 치명적인 버그입니다.
  • 메모리 관리 — 메모리 누수, use-after-free, NULL 포인터 역참조는 시스템 불안정의 주요 원인입니다.
  • 에러 처리 — 모든 함수의 반환값을 체크하고, 실패 시 적절히 정리(cleanup)해야 합니다.
  • 커널 API 오용 — 잘못된 컨텍스트에서 함수를 호출하면 크래시나 데이터 손상이 발생합니다.
  • 보안 — 사용자 입력을 절대 신뢰하지 말고, 모든 경계를 검증해야 합니다.

단계별 이해

  1. 코드 작성 전 — 어떤 컨텍스트에서 실행되는지(process/interrupt/softirq), 어떤 락이 필요한지 먼저 설계합니다.

    동시성 문제를 나중에 고치려면 10배 이상의 시간이 듭니다.

  2. 코드 작성 중 — 모든 메모리 할당/해제를 쌍으로 확인하고, 에러 경로에서 goto 레이블로 정리합니다.

    한 줄씩 작성할 때마다 "이게 동시에 실행되면?"을 자문하세요.

  3. 코드 작성 후scripts/checkpatch.pl로 코딩 스타일을 검증하고, lockdep, kmemleak, KASAN 등의 디버그 도구를 활성화합니다.

    정적 분석 도구가 발견한 경고는 대부분 실제 버그입니다.

  4. 테스트 — 스트레스 테스트, race condition 유발 테스트, 에러 주입(fault injection)을 수행합니다.

    정상 경로만 테스트하면 버그의 90%를 놓칩니다.

개요

리눅스 커널 개발은 높은 수준의 주의력과 정확성을 요구합니다. 사용자 공간 프로그래밍과 달리 커널 코드의 버그는 전체 시스템을 멈추거나 데이터를 손상시킬 수 있습니다. 이 문서는 수많은 커널 개발자들이 반복적으로 겪는 실수들을 정리하여, 같은 함정에 빠지지 않도록 돕는 것을 목표로 합니다.

학습 팁: 각 섹션의 "잘못된 코드"와 "올바른 코드"를 비교하며 읽으면 효과적입니다. 실제 프로젝트에서는 이 문서를 체크리스트로 활용하세요.

동기화 오류 ⚠️

동기화 오류는 가장 찾기 어렵고, 재현하기 어려우며, 가장 치명적인 버그입니다. race condition은 특정 타이밍에서만 발생하므로 테스트 환경에서는 나타나지 않다가 프로덕션에서 갑자기 발생할 수 있습니다. 커널에서 동기화 버그가 특히 위험한 이유는 사용자 공간과 달리 SIGSEGV로 프로세스만 죽는 것이 아니라 전체 시스템이 멈추거나 데이터가 영구적으로 손상될 수 있기 때문입니다.

동기화 오류 분류 맵 동기화 오류 데드락 (Deadlock) 락 순서 불일치 ABBA 패턴 자기 자신 재진입 Race Condition 보호 없는 공유 데이터 비원자적 read-modify-write TOCTOU 취약점 Atomic Context Sleep spinlock 내 GFP_KERNEL IRQ 핸들러 내 mutex RCU read-side에서 sleep 메모리 배리어 누락 RCU 오용 Preemption 실수 탐지 도구: lockdep · KCSAN · CONFIG_DEBUG_ATOMIC_SLEEP · KASAN · ftrace · RCU_PROVE

데드락 (Deadlock)

두 개 이상의 스레드가 서로 상대방이 보유한 락을 기다리며 영원히 멈추는 상태입니다. 커널이 완전히 멈추므로 시스템 재부팅이 필요합니다.

잘못된 예 — 락 순서 불일치로 인한 데드락
// Thread A
spin_lock(&lock_a);
spin_lock(&lock_b);  // A → B 순서
// critical section
spin_unlock(&lock_b);
spin_unlock(&lock_a);

// Thread B (동시에 실행)
spin_lock(&lock_b);
spin_lock(&lock_a);  // B → A 순서 (반대!)
// critical section
spin_unlock(&lock_a);
spin_unlock(&lock_b);

/* 결과: Thread A가 lock_a를 잡고 lock_b를 기다리는 동안,
   Thread B는 lock_b를 잡고 lock_a를 기다림 → 데드락 */
올바른 예 — 일관된 락 순서 유지
// Thread A
spin_lock(&lock_a);
spin_lock(&lock_b);  // 항상 A → B
// critical section
spin_unlock(&lock_b);
spin_unlock(&lock_a);

// Thread B
spin_lock(&lock_a);  // 동일하게 A → B
spin_lock(&lock_b);
// critical section
spin_unlock(&lock_b);
spin_unlock(&lock_a);
데드락 방지 규칙:
  • 모든 코드에서 동일한 순서로 락을 획득하라 (락 순서 계층 정의)
  • 하나의 락만 사용할 수 있다면 데드락 가능성 제거 (lock-free 알고리즘 고려)
  • CONFIG_PROVE_LOCKING (lockdep) 활성화하여 데드락 가능성을 컴파일/런타임에 탐지
  • 락을 보유한 채로 다른 서브시스템의 함수를 호출하지 말 것 (알 수 없는 락 의존성 생성)
ABBA 데드락 — 순환 의존성 잘못된 예 (데드락 발생) Thread A holds Lock A Thread B holds Lock B A→wants B B→wants A 영원히 대기 → 시스템 멈춤 올바른 예 (일관된 순서) Thread A Thread B ① Lock A ② Lock B ③ Unlock B ④ Unlock A ① Lock A ② Lock B ③ Unlock B ④ Unlock A 동일 순서 A→B 유지 → 안전 규칙: 전역 락 순서 계층 정의 후 모든 코드에서 준수

자기 자신 데드락 (Self-Deadlock)

같은 스레드가 이미 보유한 락을 다시 획득하려 하면 자기 자신과 데드락에 빠집니다. 일반 spinlockmutex는 재진입(re-entrant)을 지원하지 않습니다.

잘못된 예 — 동일 락 이중 획득
spin_lock(&my_lock);
helper_function();  // 내부에서 my_lock을 다시 잡음!
spin_unlock(&my_lock);

void helper_function(void)
{
    spin_lock(&my_lock);  // BUG! 이미 보유 중 → 영원히 대기
    // ...
    spin_unlock(&my_lock);
}
올바른 예 — 락 보유 여부를 함수 설계에 반영
// 방법 1: 내부 함수는 락 없이 호출 (caller가 잡음)
spin_lock(&my_lock);
__helper_function_locked();  // 락 보유 전제
spin_unlock(&my_lock);

// 방법 2: lockdep 어노테이션
void __helper_function_locked(void)
{
    lockdep_assert_held(&my_lock);  // 디버그: 락 보유 확인
    // ...
}

// 방법 3: 공개 API는 래퍼 제공
void helper_function(void)
{
    spin_lock(&my_lock);
    __helper_function_locked();
    spin_unlock(&my_lock);
}
네이밍 규칙: 커널에서는 락을 보유한 채로 호출해야 하는 내부 함수에 __ 접두사 또는 _locked 접미사를 붙이는 관례가 있습니다. 예: __list_add(), __slab_free().

Race Condition

여러 실행 흐름이 공유 데이터에 동시에 접근할 때 실행 순서에 따라 결과가 달라지는 버그입니다. 증상이 간헐적이고 재현이 어려워 디버깅이 매우 힘듭니다.

잘못된 예 — 보호되지 않은 공유 변수
static int counter = 0;  // 전역 변수

void increment(void)
{
    counter++;  // 원자적(atomic) 연산이 아님!
    /* 실제로는 3개 명령어:
       1. LOAD counter (레지스터로)
       2. ADD 1
       3. STORE counter (메모리로)
       → 이 사이에 인터럽트 발생 시 값 손실 */
}
올바른 예 — atomic 연산 사용
static atomic_t counter = ATOMIC_INIT(0);

void increment(void)
{
    atomic_inc(&counter);  // 하드웨어 수준에서 원자적 보장
}

// 또는 spinlock 사용
static int counter = 0;
static DEFINE_SPINLOCK(counter_lock);

void increment(void)
{
    spin_lock(&counter_lock);
    counter++;
    spin_unlock(&counter_lock);
}

Atomic Context에서 Sleep

인터럽트 핸들러, spinlock 보유 중, 일반적인 RCU read-side critical section(rcu_read_lock()) 등 atomic context에서는 sleep할 수 없습니다. (단, SRCU 같은 sleepable RCU 계열은 별도 규칙을 따릅니다.) 위반 시 시스템이 즉시 크래시하거나 무한 대기에 빠집니다.

잘못된 예 — spinlock 내에서 GFP_KERNEL 사용
spin_lock(&my_lock);
void *ptr = kmalloc(1024, GFP_KERNEL);  // BUG! sleep 가능
// ... 작업 ...
kfree(ptr);
spin_unlock(&my_lock);

/* GFP_KERNEL은 메모리가 부족하면 페이지를 swap out하기 위해
   블록 I/O를 수행하며 sleep할 수 있음 → 데드락 */
올바른 예 — GFP_ATOMIC 사용
spin_lock(&my_lock);
void *ptr = kmalloc(1024, GFP_ATOMIC);  // OK: sleep 안 함
if (!ptr) {
    spin_unlock(&my_lock);
    return -ENOMEM;
}
// ... 작업 ...
kfree(ptr);
spin_unlock(&my_lock);

// 또는 락 밖에서 미리 할당
void *ptr = kmalloc(1024, GFP_KERNEL);
if (!ptr)
    return -ENOMEM;
spin_lock(&my_lock);
// ... 작업 ...
spin_unlock(&my_lock);
kfree(ptr);
Atomic Context Sleep을 유발하는 흔한 함수들:
  • kmalloc(..., GFP_KERNEL) — GFP_ATOMIC 사용
  • mutex_lock() — spinlock 또는 완전히 다른 설계 사용
  • msleep(), schedule() — atomic context에서 호출 불가
  • copy_from_user() / copy_to_user() — 페이지 폴트로 sleep 가능
  • request_firmware() — 파일 I/O 수행, atomic에서 불가
  • printk() — 일부 상황에서 sleep 가능 (console 드라이버 의존)
탐지 방법: CONFIG_DEBUG_ATOMIC_SLEEP 커널 옵션 활성화
커널 실행 컨텍스트와 허용 작업 Process Context ✓ sleep 가능 ✓ mutex_lock() ✓ GFP_KERNEL ✓ copy_from_user() ✓ schedule() Softirq Context ✗ sleep 불가 ✓ spin_lock() ✓ GFP_ATOMIC ✗ copy_from_user() ✓ schedule_work() Hard IRQ Context ✗ sleep 불가 ✓ spin_lock_irqsave() ✓ GFP_ATOMIC ✗ mutex_lock() 최소한 짧게 실행 NMI ✗ 락 불가 ✗ sleep 불가 ✗ 할당 불가 읽기만 허용 (per-cpu 등) 제약 레벨 증가 → spinlock 보유 시 컨텍스트 변화 Process + spin_lock() → Atomic! Softirq + spin_lock() spin_lock_irqsave() 핵심: "지금 어떤 컨텍스트인가?"를 항상 먼저 파악 → 사용 가능한 API가 달라짐

Read-Write Lock 오용

Read-write lock은 읽기 작업이 쓰기보다 훨씬 많을 때만 유용합니다. 읽기 락에서 쓰기 락으로 업그레이드할 수 없으며, 시도하면 데드락이 발생합니다.

잘못된 예 — 읽기 락에서 쓰기 락으로 업그레이드 시도
read_lock(&my_rwlock);
if (need_modification) {
    read_unlock(&my_rwlock);
    write_lock(&my_rwlock);  // BUG! 사이에 다른 스레드가 쓸 수 있음
    // ... 수정 ...
    write_unlock(&my_rwlock);
}
/* 문제: unlock과 write_lock 사이에 다른 스레드가 데이터를 변경할 수 있어
   TOCTOU (Time-of-Check to Time-of-Use) 버그 발생 */
올바른 예 — 처음부터 쓰기 락 획득 또는 재검증
// 방법 1: 처음부터 쓰기 락
write_lock(&my_rwlock);
if (need_modification) {
    // ... 수정 ...
}
write_unlock(&my_rwlock);

// 방법 2: 읽기 후 다시 확인
retry:
read_lock(&my_rwlock);
if (need_modification) {
    read_unlock(&my_rwlock);
    write_lock(&my_rwlock);
    if (!need_modification) {  // 조건 재확인
        write_unlock(&my_rwlock);
        goto retry;
    }
    // ... 수정 ...
    write_unlock(&my_rwlock);
    return;
}
read_unlock(&my_rwlock);

메모리 배리어 누락

멀티코어 시스템에서는 CPU가 명령어 순서를 재배치할 수 있습니다. 락 없이 공유 데이터에 접근할 때는 명시적인 메모리 배리어가 필요합니다.

잘못된 예 — 배리어 없는 플래그 설정
// Producer
data->value = 42;
data->ready = 1;  // BUG! CPU가 순서를 바꿀 수 있음

// Consumer
if (data->ready)  // value가 아직 42가 아닐 수 있음
    process(data->value);
올바른 예 — 메모리 배리어 사용
// Producer
data->value = 42;
smp_wmb();  // Write Memory Barrier: 이전 쓰기가 먼저 완료됨을 보장
data->ready = 1;

// Consumer
if (data->ready) {
    smp_rmb();  // Read Memory Barrier: 이후 읽기가 나중에 수행됨을 보장
    process(data->value);
}

// 또는 smp_store_release / smp_load_acquire 사용 (권장)
smp_store_release(&data->ready, 1);  // 이전 모든 쓰기 완료 보장
if (smp_load_acquire(&data->ready))  // 이후 모든 읽기 보장
    process(data->value);

락 해제 누락

락을 획득한 후 모든 코드 경로에서 해제하지 않으면 다른 스레드가 영원히 대기합니다. 특히 에러 처리 경로에서 자주 발생합니다.

잘못된 예 — 에러 경로에서 unlock 누락
int process_data(struct device *dev)
{
    spin_lock(&dev->lock);

    if (!dev->ready) {
        return -EAGAIN;  // BUG! unlock 안 함
    }

    // ... 작업 ...

    spin_unlock(&dev->lock);
    return 0;
}
올바른 예 — goto를 사용한 에러 처리
int process_data(struct device *dev)
{
    int ret = 0;

    spin_lock(&dev->lock);

    if (!dev->ready) {
        ret = -EAGAIN;
        goto out_unlock;  // 정리 경로로 점프
    }

    // ... 작업 ...

out_unlock:
    spin_unlock(&dev->lock);
    return ret;
}

// 또는 guard/cleanup 계열 매크로 사용 (지원 커널에서 활용)
int process_data(struct device *dev)
{
    guard(spinlock)(&dev->lock);  // 스코프 벗어나면 자동 unlock

    if (!dev->ready)
        return -EAGAIN;  // OK: guard가 자동 정리

    // ... 작업 ...
    return 0;
}

RCU 오용

RCU(Read-Copy-Update)는 읽기가 대부분인 데이터 구조에 최적화된 동기화 메커니즘입니다. 그러나 사용 규칙이 까다로워 잘못 사용하면 use-after-free, 데이터 불일치, 무한 grace period 대기 등의 심각한 버그가 발생합니다.

RCU Grace Period — 올바른 해제 시점 시간 Reader 1 (rcu_read_lock) Reader 2 (rcu_read_lock) rcu_assign_pointer() Grace Period synchronize_rcu() 완료 kfree(old_ptr) ✓ kfree(old_ptr) ✗ Reader 2가 아직 사용 중! 핵심: 모든 기존 reader가 rcu_read_unlock()할 때까지 grace period 대기 후 해제
잘못된 예 — synchronize_rcu() 없이 즉시 해제
// 포인터 교체 후 즉시 해제 (RCU reader가 접근 중일 수 있음)
struct config *old = rcu_dereference(global_config);
rcu_assign_pointer(global_config, new_config);
kfree(old);  // BUG! reader가 아직 old를 사용 중일 수 있음

// 또는 rcu_read_lock() 없이 rcu_dereference() 사용
struct config *cfg = rcu_dereference(global_config);  // BUG! read-side 밖에서 호출
use_config(cfg);
올바른 예 — grace period 대기 후 해제
// 업데이트 측 (writer)
struct config *old = rcu_dereference_protected(global_config,
                        lockdep_is_held(&config_mutex));
rcu_assign_pointer(global_config, new_config);
synchronize_rcu();  // 모든 기존 reader 완료 대기
kfree(old);         // 이제 안전

// 또는 콜백으로 비동기 해제
call_rcu(&old->rcu_head, config_free_rcu);

// 읽기 측 (reader)
rcu_read_lock();
struct config *cfg = rcu_dereference(global_config);
use_config(cfg);
rcu_read_unlock();
잘못된 예 — RCU read-side에서 sleep
rcu_read_lock();
struct node *n = rcu_dereference(head);
mutex_lock(&n->lock);  // BUG! rcu_read_lock() 내에서 sleep 불가
// ...
mutex_unlock(&n->lock);
rcu_read_unlock();
올바른 예 — SRCU 사용 (sleep 허용)
static DEFINE_SRCU(my_srcu);

int idx = srcu_read_lock(&my_srcu);
struct node *n = srcu_dereference(head, &my_srcu);
mutex_lock(&n->lock);  // OK: SRCU read-side에서는 sleep 가능
// ...
mutex_unlock(&n->lock);
srcu_read_unlock(&my_srcu, idx);
RCU 핵심 규칙:
  • 읽기: rcu_read_lock() 구간 안에서만 rcu_dereference() 사용
  • 쓰기: rcu_assign_pointer()로 새 데이터 발행 (store-release 배리어 포함)
  • 해제: synchronize_rcu() 또는 call_rcu() 후에만 이전 데이터 해제
  • sleep: 일반 RCU read-side에서 sleep 불가 → SRCU 사용
  • 모듈: module_exit()에서 rcu_barrier() 호출 필수 (콜백 완료 대기)
  • 탐지: CONFIG_PROVE_RCU=y, CONFIG_DEBUG_OBJECTS_RCU_HEAD=y 활성화
실제 CVE 사례:
  • CVE-2024-27053 (WILC1000 Wi-Fi) — rcu_dereference()rcu_read_lock() 밖에서 호출 → UAF
  • CVE-2024-27394 (TCP-AO) — call_rcu() 콜백과 reader 사이의 race condition → UAF
  • CVE-2025-40074 (IPv4 device) — 동시 hotplug 중 RCU 보호 없이 디바이스 포인터 사용
사용 사례 RCU 종류 이유
빠른 읽기, sleep 불필요 Standard RCU reader 오버헤드 거의 0
read-side에서 sleep 필요 SRCU 임의의 blocking/I/O 허용
커널 코드 실행 추적 RCU-Tasks 커널 entry/exit 지점 추적
긴 read-side SRCU 서브시스템별 grace period 범위

Preemption 관련 실수

CONFIG_PREEMPT 또는 CONFIG_PREEMPT_RT 커널에서는 거의 어디서든 선점이 발생할 수 있습니다. Per-CPU 변수 접근, 현재 CPU 정보 사용 등에서 선점을 비활성화하지 않으면 다른 CPU에서 실행이 재개되어 잘못된 데이터에 접근합니다.

잘못된 예 — preemption 미비로 per-CPU 변수 오용
// 선점 비활성화 없이 per-CPU 변수 사용
int cpu = smp_processor_id();  // BUG! 선점 후 다른 CPU에서 실행될 수 있음
per_cpu(my_counter, cpu)++;      // 잘못된 CPU의 카운터 수정

// 또는
struct data *d = this_cpu_ptr(&my_data);
/* 여기서 선점되면 d는 이전 CPU의 per-CPU 데이터를 가리킴 */
d->count++;  // BUG! 다른 CPU의 데이터를 수정
올바른 예 — preempt_disable()로 보호
// 방법 1: 명시적 preempt 비활성화
preempt_disable();
int cpu = smp_processor_id();
per_cpu(my_counter, cpu)++;
preempt_enable();

// 방법 2: get_cpu()/put_cpu() (preempt 관리 포함)
int cpu = get_cpu();  // preempt_disable() + smp_processor_id()
per_cpu(my_counter, cpu)++;
put_cpu();            // preempt_enable()

// 방법 3: this_cpu_inc() (원자적, preempt 불필요)
this_cpu_inc(my_counter);  // 가장 간결하고 안전
preempt_disable()이 필요한 경우:
  • smp_processor_id() 사용 전후
  • per-CPU 변수 접근 시 (this_cpu_* 매크로가 더 안전)
  • CPU별 캐시/버퍼에 접근할 때
  • get_cpu_var() / put_cpu_var()는 자동으로 선점 관리
주의: preempt_disable() 구간은 가능한 짧게 유지하세요. 긴 구간은 latency를 악화시킵니다.

rwlock 업그레이드 실수

Read-Write Lock은 읽기가 많고 쓰기가 드문 경우에 사용합니다. 그러나 잘못 사용하면 writer starvation(기아)이나 데드락이 발생합니다.

잘못된 예 — read lock 안에서 write lock 시도
read_lock(&my_rwlock);
// 읽기 작업 중 수정이 필요한 상황
if (needs_update) {
    write_lock(&my_rwlock);  // BUG! 데드락 — read lock 보유 중 write lock 불가
    do_update();
    write_unlock(&my_rwlock);
}
read_unlock(&my_rwlock);
올바른 예 — 락 업그레이드 대신 재획득
// 패턴 1: read 해제 후 write 획득
read_lock(&my_rwlock);
bool update = needs_update;
read_unlock(&my_rwlock);

if (update) {
    write_lock(&my_rwlock);
    // 다시 조건 확인 (그 사이 변경 가능)
    if (still_needs_update)
        do_update();
    write_unlock(&my_rwlock);
}

// 패턴 2: 읽기 대부분이면 RCU가 더 적합
rcu_read_lock();
entry = rcu_dereference(global_entry);
// 읽기 (락 없이, 대기 없이)
rcu_read_unlock();

wait_event 실수

wait_event() 계열 매크로는 조건이 충족될 때까지 프로세스를 sleep시킵니다. 조건 체크와 깨우기(wake_up) 사이의 race condition을 올바르게 처리해야 합니다.

잘못된 예 — 수동 sleep 루프
// BUG! 고전적인 lost wakeup 문제
while (!condition) {
    set_current_state(TASK_INTERRUPTIBLE);
    schedule();
    // 여기서 condition이 true가 되고 wake_up이 호출되면?
    // → set_current_state 전에 wake_up이 오면 깨우기가 손실됨
}

// BUG! wait_event에 부작용 있는 조건
wait_event(wq, counter_inc() > 10);
// 조건은 여러 번 평가될 수 있음! counter_inc()이 반복 호출됨
올바른 예 — wait_event 매크로 사용
// wait_event가 조건 체크와 sleep을 원자적으로 처리
wait_event(my_wq, data_ready);  // 조건: 부작용 없는 표현식

// 인터럽트 가능 (시그널로 취소)
ret = wait_event_interruptible(my_wq, data_ready);
if (ret)
    return -ERESTARTSYS;

// 타임아웃 (jiffies 단위)
timeout = wait_event_timeout(my_wq, data_ready,
    msecs_to_jiffies(5000));
if (!timeout)
    return -ETIMEDOUT;

// 깨우기 (다른 컨텍스트에서)
data_ready = true;
wake_up(&my_wq);          // 하나의 waiter 깨우기
wake_up_all(&my_wq);      // 모든 waiter 깨우기
wake_up_interruptible(&my_wq);  // interruptible만

Seqlock 오용

Seqlock은 writer가 드물고 reader가 매우 빈번한 경우에 적합합니다 (예: jiffies, xtime). Reader는 락 없이 읽되, 읽는 동안 writer가 수정했는지 시퀀스 번호로 확인합니다.

잘못된 예 — seqlock 오용
seqlock_t my_seqlock;

// BUG! reader에서 포인터 역참조 → 중간에 writer가 포인터를 바꾸면 크래시
unsigned seq;
do {
    seq = read_seqbegin(&my_seqlock);
    ptr = global_ptr;           // 포인터 복사
    val = ptr->data;             // BUG! ptr이 이미 free되었을 수 있음
} while (read_seqretry(&my_seqlock, seq));
올바른 예 — 값 타입만 seqlock으로 보호
// seqlock은 값 타입(구조체 복사)에만 사용
struct timestamp {
    u64 seconds;
    u32 nanoseconds;
};
static seqlock_t ts_lock;
static struct timestamp current_ts;

// Reader: 값 전체를 로컬에 복사
struct timestamp ts;
unsigned seq;
do {
    seq = read_seqbegin(&ts_lock);
    ts = current_ts;  // 구조체 전체 복사 (포인터 역참조 아님)
} while (read_seqretry(&ts_lock, seq));
// ts는 일관된 스냅샷

// Writer: 독점 접근
write_seqlock(&ts_lock);
current_ts.seconds = new_secs;
current_ts.nanoseconds = new_nsecs;
write_sequnlock(&ts_lock);

// 포인터가 관련되면 RCU를 사용할 것!

메모리 배리어 누락/오용

현대 CPU는 명령어를 재배치(reorder)하여 실행합니다. 컴파일러도 최적화를 위해 메모리 접근 순서를 바꿀 수 있습니다. 멀티코어 환경에서 이를 무시하면 다른 CPU에서 데이터가 일관되지 않게 보입니다.

잘못된 예 — 배리어 없는 플래그 통신
// CPU 0 (Producer)
buffer[idx] = data;   // ① 데이터 저장
ready = 1;             // ② 플래그 설정
// BUG! CPU가 ②를 ①보다 먼저 실행할 수 있음

// CPU 1 (Consumer)
while (!ready)          // ③ 플래그 확인
    cpu_relax();
val = buffer[idx];     // ④ 데이터 읽기
// BUG! ④가 ③보다 먼저 실행되어 오래된 데이터를 읽을 수 있음
올바른 예 — 배리어 사용
// CPU 0 (Producer)
buffer[idx] = data;
smp_wmb();  // Write Memory Barrier: ①이 ② 전에 완료 보장
WRITE_ONCE(ready, 1);  // 컴파일러 최적화 방지

// CPU 1 (Consumer)
while (!READ_ONCE(ready))  // 컴파일러가 루프 밖으로 끌어내지 못하게
    cpu_relax();
smp_rmb();  // Read Memory Barrier: ③이 ④ 전에 완료 보장
val = buffer[idx];

// 또는 acquire/release 의미론 (더 가벼움)
// Producer:
smp_store_release(&ready, 1);  // wmb + WRITE_ONCE
// Consumer:
while (!smp_load_acquire(&ready))
    cpu_relax();
배리어 종류:
배리어용도설명
smp_wmb()Write이전 쓰기가 이후 쓰기보다 먼저 관찰됨
smp_rmb()Read이전 읽기가 이후 읽기보다 먼저 완료됨
smp_mb()Full모든 이전 접근이 이후 접근보다 먼저 (가장 비쌈)
smp_store_release()Release이전 모든 접근 후에 쓰기 (wmb + WRITE_ONCE)
smp_load_acquire()Acquire읽기 후에만 이후 접근 (rmb + READ_ONCE)
READ_ONCE()Compiler컴파일러 최적화만 방지 (CPU 배리어 아님)
WRITE_ONCE()Compiler컴파일러 최적화만 방지

메모리 관리 실수

커널 메모리 생명주기와 버그 발생 지점 kmalloc() 사용 중 kfree() ptr = NULL (안전) 메모리 누수 kfree() 미호출 (참조 소실) Use-After-Free kfree() 후 ptr 접근 Double Free kfree() 두 번 호출 버퍼 오버플로우 할당 크기 초과 쓰기 NULL 역참조 kmalloc 실패 시 NULL 체크 누락 탐지 도구 KASAN (UAF, overflow) · kmemleak (누수) · SLUB_DEBUG (double-free, corruption) · KFENCE (샘플링 탐지)

메모리 누수 (Memory Leak)

할당한 메모리를 해제하지 않으면 시스템 메모리가 고갈되어 결국 OOM(Out Of Memory) 킬러가 발동합니다.

잘못된 예 — 에러 경로에서 메모리 누수
int init_device(struct device *dev)
{
    dev->buffer = kmalloc(4096, GFP_KERNEL);
    if (!dev->buffer)
        return -ENOMEM;

    dev->data = kmalloc(2048, GFP_KERNEL);
    if (!dev->data)
        return -ENOMEM;  // BUG! buffer 누수

    return 0;
}
올바른 예 — goto로 정리 경로 구성
int init_device(struct device *dev)
{
    dev->buffer = kmalloc(4096, GFP_KERNEL);
    if (!dev->buffer)
        return -ENOMEM;

    dev->data = kmalloc(2048, GFP_KERNEL);
    if (!dev->data)
        goto err_free_buffer;

    return 0;

err_free_buffer:
    kfree(dev->buffer);
    dev->buffer = NULL;
    return -ENOMEM;
}
메모리 누수 탐지:
  • CONFIG_DEBUG_KMEMLEAK — 커널 메모리 누수 자동 탐지
  • cat /sys/kernel/debug/kmemleak — 누수 보고서 확인
  • kmemleak=on 부트 파라미터로 활성화

Use-After-Free

해제된 메모리에 접근하는 버그로, 크래시나 보안 취약점의 주요 원인입니다. 메모리가 다른 용도로 재할당되면 데이터 손상도 발생합니다.

잘못된 예 — 해제 후 포인터 사용
struct data *ptr = kmalloc(sizeof(*ptr), GFP_KERNEL);
kfree(ptr);
ptr->value = 42;  // BUG! 해제된 메모리 접근
올바른 예 — 해제 후 NULL 설정
struct data *ptr = kmalloc(sizeof(*ptr), GFP_KERNEL);
kfree(ptr);
ptr = NULL;  // 다시 사용 방지

// 패턴: 해제 후 즉시 NULL 대입
kfree(ptr);
ptr = NULL;
Use-After-Free 탐지:
  • CONFIG_KASAN (Kernel Address Sanitizer) — 메모리 오류 즉시 탐지
  • CONFIG_SLUB_DEBUG — slab 할당자 디버깅 활성화
  • slub_debug=FZPU 부트 파라미터

Double Free

이미 해제된 메모리를 다시 해제하면 메모리 할당자가 손상되어 시스템 크래시나 보안 취약점이 발생합니다.

잘못된 예 — 동일 포인터를 두 번 해제
void cleanup_device(struct device *dev)
{
    kfree(dev->buffer);
    // ... 다른 작업 ...
    kfree(dev->buffer);  // BUG! 이미 해제됨
}
올바른 예 — 해제 후 NULL 설정
void cleanup_device(struct device *dev)
{
    kfree(dev->buffer);
    dev->buffer = NULL;  // kfree(NULL)은 안전
    // ... 다른 작업 ...
    kfree(dev->buffer);  // OK: NULL이므로 무시됨
}

버퍼 오버플로우/언더플로우

배열 경계를 넘어서 쓰면 인접한 메모리가 손상되어 예측 불가능한 동작이 발생합니다.

잘못된 예 — 경계 검사 없는 복사
char buf[64];
strcpy(buf, user_string);  // BUG! user_string이 64바이트 이상이면 오버플로우

// 또는
int len;
copy_from_user(&len, user_ptr, sizeof(len));
memcpy(dest, src, len);  // BUG! len이 음수면 언더플로우
올바른 예 — 안전한 문자열/메모리 함수 사용
char buf[64];
strscpy(buf, user_string, sizeof(buf));  // 크기 제한, NULL 종료 보장

// 또는
size_t len;
if (copy_from_user(&len, user_ptr, sizeof(len)))
    return -EFAULT;
if (len > MAX_SIZE)  // 상한 검사
    return -EINVAL;
memcpy(dest, src, len);
권장 함수:
  • strcpy()strscpy() 사용 (strlcpy()는 deprecated)
  • sprintf()snprintf() 또는 scnprintf() 사용
  • memcpy() — 반드시 크기 검증 후 사용

NULL 포인터 역참조

함수가 NULL을 반환할 수 있는데 검사하지 않고 사용하면 커널 패닉이 발생합니다.

잘못된 예 — NULL 체크 누락
struct device *dev = find_device(id);
dev->status = ACTIVE;  // BUG! dev가 NULL일 수 있음
올바른 예 — NULL 검사
struct device *dev = find_device(id);
if (!dev) {
    pr_err("Device %d not found\\n", id);
    return -ENODEV;
}
dev->status = ACTIVE;

DMA 메모리 관리 실수

DMA 버퍼는 물리적으로 연속되어야 하며, 디바이스가 접근 가능한 주소 범위여야 합니다. 일반 kmalloc 메모리를 DMA에 사용하거나 매핑을 해제하지 않으면 심각한 문제가 발생합니다.

잘못된 예 — 일반 메모리를 DMA에 사용
void *buf = kmalloc(4096, GFP_KERNEL);
dma_addr_t dma_addr = virt_to_phys(buf);  // BUG! DMA 불가능 영역일 수 있음
device_start_dma(dev, dma_addr);

// 또는
kfree(buf);  // BUG! DMA가 진행 중일 수 있음
올바른 예 — DMA API 사용
dma_addr_t dma_addr;
void *buf = dma_alloc_coherent(&dev->dev, 4096, &dma_addr, GFP_KERNEL);
if (!buf)
    return -ENOMEM;

device_start_dma(dev, dma_addr);
// ... DMA 완료 대기 ...

dma_free_coherent(&dev->dev, 4096, buf, dma_addr);

// 또는 streaming DMA
void *buf = kmalloc(4096, GFP_KERNEL);
dma_addr_t dma_addr = dma_map_single(&dev->dev, buf, 4096, DMA_TO_DEVICE);
if (dma_mapping_error(&dev->dev, dma_addr)) {
    kfree(buf);
    return -ENOMEM;
}
device_start_dma(dev, dma_addr);
// ... DMA 완료 대기 ...
dma_unmap_single(&dev->dev, dma_addr, 4096, DMA_TO_DEVICE);
kfree(buf);
DMA 주의사항:
  • DMA 진행 중에는 CPU가 버퍼에 접근하지 말 것 (캐시 일관성 문제)
  • dma_sync_single_for_cpu() / dma_sync_single_for_device()로 소유권 전환
  • DMA 완료 전에 unmap하면 데이터 손상
  • 64비트 DMA 마스크 설정 확인 (dma_set_mask())

참조 카운트(Reference Count) 실수

커널 객체는 참조 카운트로 수명을 관리합니다. 카운트를 잘못 조작하면 객체가 너무 일찍 해제되거나 영원히 해제되지 않습니다.

잘못된 예 — 참조 카운트 누락
struct device *dev = find_device(id);  // refcount 증가하지 않음
use_device(dev);  // 다른 스레드가 dev를 해제할 수 있음

// 또는
void create_device(void)
{
    struct device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    kref_init(&dev->kref);  // refcount = 1
    register_device(dev);   // BUG! register가 참조를 보유하는데 증가 안 함
    kref_put(&dev->kref, device_release);  // 즉시 해제됨!
}
올바른 예 — 참조 카운트 규칙 준수
struct device *dev = find_device(id);  // refcount++
if (!dev)
    return -ENODEV;
use_device(dev);
put_device(dev);  // refcount-- (짝을 맞춤)

// 또는
void create_device(void)
{
    struct device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    kref_init(&dev->kref);  // refcount = 1
    kref_get(&dev->kref);   // refcount = 2 (register를 위한 참조)
    register_device(dev);
    kref_put(&dev->kref, device_release);  // 생성자 참조 해제, 아직 살아있음
}
참조 카운트 규칙:
  • "얻은 곳에서 반환한다" (get과 put는 항상 쌍)
  • 함수가 객체를 반환하면 참조를 함께 반환 (호출자가 put 책임)
  • 포인터를 저장하면 참조 획득, 제거하면 해제
  • container_of()로 얻은 포인터도 적절한 refcount 함수 사용

스택 오버플로우

커널 스택은 매우 작습니다 (x86_64에서 16KB, 일부 설정에서 8KB). 큰 배열을 스택에 할당하거나 깊은 재귀를 사용하면 스택이 고갈됩니다.

잘못된 예 — 스택에 큰 배열
void process_data(void)
{
    char buffer[8192];  // BUG! 스택의 절반 이상 사용
    // ...
}

// 또는
void recursive_function(int depth)
{
    if (depth > 0)
        recursive_function(depth - 1);  // BUG! 깊은 재귀
}
올바른 예 — 힙 할당 또는 반복
int process_data(void)
{
    char *buffer = kmalloc(8192, GFP_KERNEL);
    if (!buffer)
        return -ENOMEM;
    // ...
    kfree(buffer);
    return 0;
}

// 또는 반복으로 변환
void iterative_function(int depth)
{
    for (int i = 0; i < depth; i++) {
        // ...
    }
}
스택 사용 가이드:
  • 로컬 변수는 합계 512바이트 이하 권장
  • CONFIG_FRAME_WARN으로 큰 스택 프레임 경고 (기본 1024 또는 2048)
  • 재귀는 가능한 피하고, 불가피하면 깊이 제한
  • 큰 구조체는 포인터로 전달

Slab 할당자 오용

커널은 동일한 크기의 객체를 빈번하게 할당/해제할 때 slab 캐시(kmem_cache)를 사용합니다. 잘못된 캐시에서 해제하거나, 생성자(constructor)를 잘못 사용하면 메모리 손상이 발생합니다.

잘못된 예 — 잘못된 캐시에서 해제
static struct kmem_cache *my_cache;

void init(void)
{
    my_cache = kmem_cache_create("my_obj", sizeof(struct my_obj),
                                  0, 0, NULL);
}

void cleanup_obj(struct my_obj *obj)
{
    kfree(obj);  // BUG! kmem_cache_alloc()으로 할당했으면 kmem_cache_free() 사용
}

// 또는
struct my_obj *obj = kmalloc(sizeof(*obj), GFP_KERNEL);
kmem_cache_free(my_cache, obj);  // BUG! 다른 할당자에서 할당한 메모리
올바른 예 — 할당/해제 API 일관성 유지
// slab 캐시에서 할당 → slab 캐시에서 해제
struct my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// ...
kmem_cache_free(my_cache, obj);

// kmalloc으로 할당 → kfree로 해제
struct my_obj *obj = kmalloc(sizeof(*obj), GFP_KERNEL);
// ...
kfree(obj);

// 모듈 종료 시 캐시 파괴
kmem_cache_destroy(my_cache);  // 모든 객체 해제 후에만!
Slab 캐시 주의사항:
  • kmem_cache_destroy() 전에 모든 객체가 해제되어야 함 (아니면 커널 경고)
  • SLAB_TYPESAFE_BY_RCU 플래그는 RCU grace period 없이 재사용 가능하게 함 — 별도의 동기화 주의 필요
  • 생성자(ctor)는 재사용될 때 호출되지 않을 수 있으므로 상태 초기화에 의존하지 말 것

커널 스택 오버플로우

커널 스택은 8KB(x86_64 기본) 또는 16KB(일부 아키텍처)로 매우 제한적입니다. 깊은 호출 체인이나 큰 로컬 변수는 즉시 스택 오버플로우로 이어집니다.

잘못된 예 — 큰 로컬 버퍼
static void process_data(void)
{
    char path[PATH_MAX];  // BUG! 4096바이트 — 스택의 절반!
    struct big_context ctx;  // BUG! 구조체가 크면 위험
    int table[256];  // BUG! 1024바이트
}
올바른 예 — 힙 할당 또는 per-CPU
static void process_data(void)
{
    char *path = kmalloc(PATH_MAX, GFP_KERNEL);
    if (!path)
        return;

    // 사용 후 해제
    kfree(path);
}

// 또는 __getname()으로 커널 pathbuf 풀 사용
char *path = __getname();
// ...
__putname(path);

vmalloc과 kmalloc 혼동

kmalloc은 물리적으로 연속된 메모리를 할당하고, vmalloc은 가상적으로만 연속된 메모리를 할당합니다. DMA에는 반드시 물리적으로 연속된 메모리가 필요하지만, 큰 버퍼는 vmalloc이 더 성공 확률이 높습니다.

잘못된 예 — 할당자 혼동
// BUG! 큰 할당에 kmalloc → 물리 연속 메모리 부족으로 실패
void *buf = kmalloc(1024 * 1024, GFP_KERNEL);  // 1MB — 실패 가능성 높음

// BUG! vmalloc 메모리를 DMA에 사용
void *dma_buf = vmalloc(4096);
dma_handle = dma_map_single(dev, dma_buf, 4096, DMA_TO_DEVICE);
// vmalloc은 물리적으로 불연속 → 단일 DMA 매핑 불가

// BUG! vmalloc 메모리를 kfree로 해제
void *p = vmalloc(8192);
kfree(p);  // 크래시! vfree 사용해야 함
올바른 예 — 적절한 할당자 선택
// 작은 할당 (≤ 페이지 크기 수 배): kmalloc
void *small = kmalloc(256, GFP_KERNEL);
kfree(small);

// 큰 할당 (물리 연속 불필요): vmalloc
void *big = vmalloc(1024 * 1024);  // 1MB OK
vfree(big);

// 자동 선택: kvmalloc (작으면 kmalloc, 크면 vmalloc)
void *auto_buf = kvmalloc(size, GFP_KERNEL);
kvfree(auto_buf);  // 어떤 할당자든 올바르게 해제

// DMA 용: 반드시 DMA API 사용
void *dma_buf = dma_alloc_coherent(dev, size, &handle, GFP_KERNEL);
dma_free_coherent(dev, size, dma_buf, handle);
메모리 할당자 선택 가이드:
할당자크기물리 연속컨텍스트용도
kmalloc≤128KBYesGFP_KERNEL/ATOMIC일반 커널 객체
vmalloc큰 버퍼NoProcess only큰 배열, 모듈 메모리
kvmalloc가변가변Process only크기 불확실한 할당
dma_alloc_coherentDMAYesProcess onlyDMA 버퍼
alloc_pages2^n 페이지YesAny페이지 단위 할당
kmem_cache_alloc고정YesAny반복 할당 최적화

Slab 오염 감지

해제된 메모리에 쓰기(use-after-free)나 할당 범위를 넘는 쓰기(buffer overflow)는 slab 오염을 일으킵니다. SLUB_DEBUG를 활성화하면 이를 감지할 수 있습니다.

SLUB 디버그 활성화 및 사용
# 커널 부트 파라미터
slub_debug=FZPU                  # F=Red zone, Z=Poisoning, P=Debug, U=User tracking
slub_debug=FZPU,kmalloc-256      # 특정 캐시만

# F (Red zone): 할당 영역 앞뒤에 가드 패턴 배치
#   → 버퍼 오버플로우 시 패턴 손상 감지
# Z (Poisoning): 해제 시 0x6b 패턴으로 채움
#   → use-after-free 시 잘못된 데이터 노출
# P (Debug): 일관성 검사 활성화
# U (User tracking): 할당/해제 콜스택 기록

# slabinfo로 확인
cat /proc/slabinfo
slabtop  # 실시간 모니터링

에러 처리 실수

goto 클린업 캐스케이드 — 커널 에러 처리 패턴 할당 (초기화 순서) ① alloc_resource_A() ② alloc_resource_B() ③ alloc_resource_C() return 0 (성공) 해제 (역순 정리) 실패 goto err_a → return err 실패 goto err_b → free_A() 실패 goto err_c → free_B, free_A() err_c: free_resource_C() err_b: free_resource_B() err_a: free_resource_A() 핵심: 실패 지점부터 역순으로 이미 할당된 리소스만 해제 (fall-through cascade)

반환값 무시

커널 API는 int, NULL/ERR_PTR, bool, void 등 반환 계약이 다양합니다. 반환값을 확인하지 않으면 후속 코드가 잘못된 가정으로 실행됩니다.

잘못된 예 — copy_from_user 반환값 무시
copy_from_user(&kernel_buf, user_ptr, size);  // BUG! 실패 가능
process_data(&kernel_buf);  // 부분적으로만 복사됐을 수 있음
올바른 예 — 반환값 검사
if (copy_from_user(&kernel_buf, user_ptr, size)) {
    return -EFAULT;  // 사용자 메모리 접근 실패
}
process_data(&kernel_buf);

IS_ERR / PTR_ERR 오용

커널의 많은 함수는 실패 시 NULL 대신 ERR_PTR(-errno)를 반환합니다. NULL 체크만 하면 에러 포인터를 역참조하여 크래시가 발생합니다.

잘못된 예 — NULL만 체크
struct file *f = filp_open("/dev/null", O_RDONLY, 0);
if (!f) {  // BUG! filp_open은 ERR_PTR을 반환, NULL이 아님
    return -ENOENT;
}
// f가 ERR_PTR이면 크래시!
올바른 예 — IS_ERR 사용
struct file *f = filp_open("/dev/null", O_RDONLY, 0);
if (IS_ERR(f)) {
    return PTR_ERR(f);  // 에러 코드 추출
}
if (!inode) {  // NULL 체크는 별도로
    return -ENOENT;
}
// 이제 안전하게 사용 가능

// 또는 IS_ERR_OR_NULL() 사용
if (IS_ERR_OR_NULL(inode))
    return inode ? PTR_ERR(inode) : -ENOENT;
ERR_PTR 패밀리 함수:
  • IS_ERR(ptr) — 에러 포인터인지 확인
  • PTR_ERR(ptr) — 에러 코드 추출
  • ERR_PTR(err) — 에러 코드를 포인터로 변환
  • IS_ERR_OR_NULL(ptr) — 에러 또는 NULL 체크
  • ERR_CAST(ptr) — 에러 포인터 타입 변환

정리(Cleanup) 순서 오류

여러 리소스를 할당한 경우, 해제는 할당의 역순으로 해야 합니다. 순서가 잘못되면 의존성 문제로 크래시가 발생할 수 있습니다.

잘못된 예 — 잘못된 해제 순서
int init(void)
{
    register_chrdev(...);     // 1
    alloc_workqueue(...);     // 2
    request_irq(...);          // 3
    return 0;
}

void cleanup(void)
{
    destroy_workqueue(...);   // BUG! IRQ가 workqueue를 사용할 수 있음
    free_irq(...);
    unregister_chrdev(...);
}
올바른 예 — 역순 해제
void cleanup(void)
{
    free_irq(...);             // 3 (역순)
    destroy_workqueue(...);   // 2
    unregister_chrdev(...);   // 1
}

잘못된 에러 코드

적절한 에러 코드를 반환해야 사용자 공간이 올바르게 대응할 수 있습니다.

흔히 사용하는 에러 코드:
  • -EINVAL — 잘못된 인자
  • -ENOMEM — 메모리 부족
  • -ENODEV — 디바이스 없음
  • -EBUSY — 리소스 사용 중
  • -EAGAIN / -EWOULDBLOCK — 다시 시도하라
  • -EFAULT — 잘못된 사용자 공간 주소
  • -EPERM — 권한 없음
  • -EIO — I/O 에러
전체 목록: include/uapi/asm-generic/errno*.h

devm_add_action을 활용한 정리 패턴

devm_add_action_or_reset()을 사용하면 커스텀 정리 로직도 자동화할 수 있습니다. goto 레이블 없이도 안전한 cleanup이 가능합니다.

devm_add_action으로 goto-free 초기화
static void my_clk_disable(void *data)
{
    clk_disable_unprepare(data);
}

static int my_probe(struct platform_device *pdev)
{
    struct clk *clk;
    int ret;

    clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(clk))
        return PTR_ERR(clk);

    ret = clk_prepare_enable(clk);
    if (ret)
        return ret;

    // 자동 정리 등록: probe 실패 시 또는 remove 시 자동 호출됨
    ret = devm_add_action_or_reset(&pdev->dev, my_clk_disable, clk);
    if (ret)
        return ret;  // _or_reset: 등록 실패 시 즉시 action 호출

    // 이후 실패해도 clk은 자동으로 disable됨 — goto 불필요!
    ret = some_other_init();
    if (ret)
        return ret;  // 그냥 반환하면 됨

    return 0;
}

에러 전파 실수

하위 함수에서 반환된 에러 코드를 상위 함수에서 올바르게 전파하지 않으면, 사용자 공간에서 원인을 파악할 수 없고, 디버깅이 어려워집니다.

잘못된 예 — 에러 코드 무시/변환 실수
static int init_subsystem(void)
{
    int ret;

    ret = alloc_resources();
    if (ret)
        return -1;  // BUG! 구체적 에러(-ENOMEM 등)를 -1로 변환

    ret = register_device();
    if (ret) {
        pr_err("failed\\n");  // BUG! 에러 코드를 로그에 포함하지 않음
        return -EINVAL;  // BUG! 원래 에러를 다른 코드로 교체
    }
}
올바른 예 — 정확한 에러 전파
static int init_subsystem(void)
{
    int ret;

    ret = alloc_resources();
    if (ret) {
        pr_err("alloc_resources failed: %d\\n", ret);  // 에러 코드 포함
        return ret;  // 원래 에러 그대로 전파
    }

    ret = register_device();
    if (ret) {
        pr_err("register_device failed: %d\\n", ret);
        goto err_register;
    }
    return 0;

err_register:
    free_resources();
    return ret;  // 원래 에러 코드
}

커널 API 오용

커널 API는 호출 가능한 컨텍스트, 반환값 계약, 메모리 의미론이 사용자 공간 라이브러리와 근본적으로 다릅니다. 사용자 공간에서는 대부분의 함수를 어디서든 호출할 수 있지만, 커널에서는 실행 컨텍스트(process/softirq/hardirq/NMI)에 따라 호출 가능한 함수가 엄격히 제한됩니다.

커널 API 호출 — 컨텍스트별 허용 여부 결정 현재 실행 컨텍스트는? Process Context sleep OK, GFP_KERNEL OK mutex OK, copy_to/from_user OK schedule() OK Softirq / Tasklet sleep 불가, mutex 불가 GFP_ATOMIC만 허용 spinlock OK, per-CPU OK Hard IRQ / NMI sleep 절대 불가 GFP_ATOMIC만, 최소 작업만 spin_lock_irqsave 필수 Sleep이 필요한 작업인가? Yes Process context 필수 kmalloc(GFP_KERNEL), mutex_lock(), msleep() copy_from_user(), wait_event(), schedule() No 모든 컨텍스트에서 사용 가능 kmalloc(GFP_ATOMIC), spin_lock(), atomic_* readl/writel(), this_cpu_*(), local_irq_* Atomic 컨텍스트에서 긴 작업이 필요하면? ① schedule_work() → workqueue로 지연 | ② tasklet_schedule() → softirq로 지연 ③ threaded IRQ (IRQF_ONESHOT) → process context에서 처리 | ④ GFP_NOWAIT + 재시도 로직

잘못된 컨텍스트에서 함수 호출

커널 함수는 호출 가능한 컨텍스트가 제한되어 있습니다.

함수 호출 가능 컨텍스트 불가능 컨텍스트
kmalloc(..., GFP_KERNEL) Process context Interrupt, atomic
kmalloc(..., GFP_ATOMIC) Any
mutex_lock() Process context Interrupt, atomic
spin_lock() Any
copy_to_user() Process context Interrupt
schedule() Process context Interrupt, atomic

타이머 콜백 오용

타이머 콜백은 softirq 컨텍스트에서 실행되므로 sleep할 수 없고, 실행 시간이 짧아야 합니다. 타이머 삭제 시 경쟁 조건(race condition)에 주의해야 합니다.

잘못된 예 — 타이머 콜백에서 sleep
void timer_callback(struct timer_list *t)
{
    struct my_device *dev = from_timer(dev, t, timer);

    mutex_lock(&dev->lock);  // BUG! softirq에서 sleep 불가
    // ... 작업 ...
    mutex_unlock(&dev->lock);
}

// 또는
void cleanup(void)
{
    del_timer(&my_timer);  // BUG! 콜백이 실행 중일 수 있음
    kfree(dev);  // 콜백이 dev를 사용 중이면 use-after-free
}
올바른 예 — spinlock과 안전한 삭제
void timer_callback(struct timer_list *t)
{
    struct my_device *dev = from_timer(dev, t, timer);

    spin_lock(&dev->lock);  // OK: spinlock 사용
    // ... 짧은 작업 ...
    spin_unlock(&dev->lock);

    // 긴 작업은 workqueue로 스케줄링
    schedule_work(&dev->work);
}

void cleanup(void)
{
    del_timer_sync(&my_timer);  // 콜백 완료를 기다림
    kfree(dev);  // 이제 안전
}

Workqueue 오용

Workqueue work는 이미 큐에 있을 때 다시 큐잉하면 안 되며, 취소 시 경쟁 조건에 주의해야 합니다.

잘못된 예 — work 중복 큐잉
void trigger_work(void)
{
    schedule_work(&my_work);  // 이미 큐에 있으면?
    schedule_work(&my_work);  // BUG! 두 번째는 무시되지만 의도와 다를 수 있음
}

void cleanup(void)
{
    cancel_delayed_work(&my_dwork);  // BUG! 이미 실행 중이면 완료를 기다리지 않음
    kfree(dev);  // work가 실행 중이면 use-after-free
}
올바른 예 — 상태 체크와 동기화된 취소
void trigger_work(void)
{
    // work가 이미 큐에 있는지 확인
    if (!work_pending(&my_work))
        schedule_work(&my_work);
}

void cleanup(void)
{
    cancel_work_sync(&my_work);  // work 완료를 기다림
    kfree(dev);  // 이제 안전
}

IRQ 플래그 관리 실수

spin_lock_irqsave()는 IRQ 상태를 저장하고 복원하므로, 중첩된 락에서도 올바르게 동작합니다. spin_lock_irq()는 IRQ를 무조건 켜므로 중첩 불가능합니다.

잘못된 예 — 중첩된 락에서 spin_lock_irq 사용
void outer_function(void)
{
    spin_lock_irq(&lock_a);  // IRQ 비활성화
    inner_function();
    spin_unlock_irq(&lock_a);  // IRQ 활성화
}

void inner_function(void)
{
    spin_lock_irq(&lock_b);
    // ... 작업 ...
    spin_unlock_irq(&lock_b);  // BUG! IRQ를 너무 일찍 켬 (lock_a가 아직 보유 중)
}
올바른 예 — spin_lock_irqsave 사용
void outer_function(void)
{
    unsigned long flags;
    spin_lock_irqsave(&lock_a, flags);  // IRQ 상태 저장
    inner_function();
    spin_unlock_irqrestore(&lock_a, flags);  // 원래 상태 복원
}

void inner_function(void)
{
    unsigned long flags;
    spin_lock_irqsave(&lock_b, flags);
    // ... 작업 ...
    spin_unlock_irqrestore(&lock_b, flags);  // OK: 올바른 상태 복원
}

사용자 메모리 접근 시 copy_to/from_user 누락

커널은 사용자 공간 포인터를 직접 역참조하면 안 됩니다. 보안 문제와 페이지 폴트 처리 문제가 발생합니다.

잘못된 예 — 직접 역참조
// user_ptr은 사용자 공간 주소
int value = *user_ptr;  // BUG! 직접 접근 불가
올바른 예 — copy_from_user 사용
int value;
if (copy_from_user(&value, user_ptr, sizeof(value)))
    return -EFAULT;

Completion 메커니즘 오용

struct completion은 한 스레드가 작업을 완료할 때까지 다른 스레드가 대기하는 동기화 메커니즘입니다. 타임아웃 처리, 재초기화, 완료 시그널 누락 등에서 실수가 발생합니다.

잘못된 예 — 완료 시그널 누락과 타임아웃 무시
DECLARE_COMPLETION(my_completion);

// 스레드 A: 무한 대기
wait_for_completion(&my_completion);  // BUG! complete()가 호출 안 되면 영원히 대기

// 스레드 B: 에러 경로에서 complete() 누락
int worker(void *data)
{
    if (error_condition)
        return -EIO;  // BUG! complete() 호출 안 함

    do_work();
    complete(&my_completion);
    return 0;
}
올바른 예 — 타임아웃과 모든 경로에서 완료
// 스레드 A: 타임아웃 대기
unsigned long timeout = wait_for_completion_timeout(&my_completion,
                                                    msecs_to_jiffies(5000));
if (!timeout) {
    pr_err("Operation timed out\\n");
    return -ETIMEDOUT;
}

// 인터럽트 가능한 대기
if (wait_for_completion_interruptible(&my_completion)) {
    return -ERESTARTSYS;  // 시그널 수신 시
}

// 스레드 B: 모든 경로에서 complete()
int worker(void *data)
{
    int ret = 0;
    if (error_condition) {
        ret = -EIO;
        goto done;
    }
    do_work();
done:
    complete(&my_completion);  // 에러/성공 모두 완료 시그널
    return ret;
}

// 재사용 시 반드시 재초기화
reinit_completion(&my_completion);
잘못된 예 — complete_all() 후 재초기화 누락
complete_all(&comp);         // done = UINT_MAX (모든 대기자 깨움)
start_new_operation();
wait_for_completion(&comp);  // BUG! 즉시 반환됨 (done > 0이므로)
올바른 예 — reinit_completion() 호출
complete_all(&comp);
reinit_completion(&comp);    // done = 0으로 초기화
start_new_operation();
wait_for_completion(&comp);  // OK: 새 complete() 대기
Completion 구분:
  • complete(): done++ (대기자 1개만 깨움)
  • complete_all(): done = UINT_MAX (모든 대기자 깨움, 이후 대기도 즉시 반환)
  • reinit_completion(): done = 0 (재사용 전 필수 호출)

커널에서 부동소수점 사용

커널 코드에서는 부동소수점 연산(float, double)을 직접 사용할 수 없습니다. 커널은 컨텍스트 스위칭 시 FPU/SSE 레지스터를 저장하지 않으므로, 사용자 프로세스의 FPU 상태를 손상시킵니다.

잘못된 예 — 커널에서 float 사용
float calculate_ratio(int a, int b)
{
    return (float)a / (float)b;  // BUG! 커널에서 FPU 사용 금지
}

double temp = sensor_value * 0.001;  // BUG!
올바른 예 — 정수 연산 또는 고정소수점
// 방법 1: 정수 나눗셈 (밀리 단위 사용)
int calculate_ratio_milli(int a, int b)
{
    return (a * 1000) / b;  // 밀리 단위 비율
}

// 방법 2: 고정소수점 (10^6 스케일)
int temp_micro = sensor_value;  // 이미 μ단위로 저장

// 방법 3: 반드시 FPU가 필요하면 kernel_fpu_begin/end
kernel_fpu_begin();
// FPU 연산 (최소한으로, 짧게)
kernel_fpu_end();
// ⚠ kernel_fpu_begin()은 preempt_disable() 포함 → 짧게 사용

printk 포맷 지정자 오류

커널의 printk는 사용자 공간의 printf와 다른 확장 포맷 지정자를 사용합니다. 잘못된 포맷 사용은 잘못된 출력부터 보안 취약점까지 이어집니다.

잘못된 예 — 위험한 포맷 지정자
// 커널 포인터를 그대로 출력 (KASLR 우회 → 보안 취약점)
printk("ptr = %px\\n", ptr);  // BUG! 실제 주소 노출
printk("ptr = 0x%lx\\n", (unsigned long)ptr);  // BUG! 캐스트하여 노출

// size_t에 잘못된 포맷
printk("size = %d\\n", size);  // BUG! size_t는 %zu 사용

// dma_addr_t에 잘못된 포맷
printk("dma = %x\\n", dma_addr);  // BUG! 32/64비트 크기 다름
올바른 예 — 커널 확장 포맷 지정자
// 포인터: %p (해시된 값), %pK (kptr_restrict 따름)
printk("ptr = %p\\n", ptr);    // 해시된 주소 출력 (안전)
printk("ptr = %pK\\n", ptr);   // kptr_restrict에 따라 제어

// 특수 포인터 포맷
printk("func = %ps\\n", func);   // 심볼 이름 출력 (my_function)
printk("func = %pS\\n", func);   // 심볼+오프셋 (my_function+0x10/0x20)
printk("res = %pr\\n", &res);    // struct resource 출력
printk("mac = %pM\\n", mac);     // MAC 주소 (01:02:03:04:05:06)
printk("ip4 = %pI4\\n", &addr);  // IPv4 주소
printk("ip6 = %pI6c\\n", &addr); // IPv6 주소 (축약형)

// 올바른 크기 포맷
printk("size = %zu\\n", size);      // size_t
printk("dma = %pad\\n", &dma_addr); // dma_addr_t
printk("phys = %pa\\n", &phys);     // phys_addr_t
dev_* 함수 사용 권장: 드라이버에서는 printk() 대신 dev_err(), dev_info(), dev_dbg() 등을 사용하면 디바이스 이름이 자동으로 포함됩니다.
pr_err(), pr_info()pr_fmt() 매크로로 모듈 이름을 자동 추가할 수 있습니다.

Completion 타임아웃과 재사용

struct completion은 한 컨텍스트에서 다른 컨텍스트로 완료 신호를 보내는 동기화 메커니즘입니다. 타임아웃 없이 대기하면 영원히 블록될 수 있고, 재초기화 없이 재사용하면 의도치 않게 즉시 통과합니다.

잘못된 예 — 무한 대기와 재사용 실수
DECLARE_COMPLETION(my_done);

// 핸들러: 하드웨어 완료 시 호출
static irqreturn_t irq_handler(int irq, void *data)
{
    complete(&my_done);
    return IRQ_HANDLED;
}

static int do_operation(void)
{
    start_hw_operation();
    wait_for_completion(&my_done);  // BUG! 하드웨어가 응답하지 않으면 영원히 멈춤
    return 0;
}

static int do_again(void)
{
    start_hw_operation();
    wait_for_completion(&my_done);  // BUG! 이전 complete()으로 즉시 통과
    return 0;
}
올바른 예 — 타임아웃과 재초기화
static int do_operation(void)
{
    unsigned long timeout;

    reinit_completion(&my_done);  // 재사용 전 반드시 재초기화
    start_hw_operation();

    timeout = wait_for_completion_timeout(&my_done,
                msecs_to_jiffies(5000));  // 5초 타임아웃
    if (!timeout) {
        dev_err(dev, "operation timed out\\n");
        return -ETIMEDOUT;
    }

    return 0;
}

// 인터럽트 가능한 대기 (시그널로 취소 가능)
static int do_interruptible(void)
{
    long ret;

    reinit_completion(&my_done);
    start_hw_operation();

    ret = wait_for_completion_interruptible_timeout(&my_done,
            msecs_to_jiffies(5000));
    if (ret == 0)
        return -ETIMEDOUT;
    if (ret < 0)
        return ret;  // -ERESTARTSYS (시그널에 의해 중단)

    return 0;
}

커널 스레드(kthread) 실수

커널 스레드는 kthread_create() 또는 kthread_run()으로 생성합니다. 정리하지 않으면 좀비 스레드가 남고, 시그널 처리를 잘못하면 CPU를 100% 사용합니다.

잘못된 예 — kthread 정리 누락
static struct task_struct *my_thread;

static int thread_fn(void *data)
{
    while (1) {  // BUG! kthread_should_stop() 체크 없음
        do_work();
        msleep(1000);
    }
    return 0;
}

static int __init my_init(void)
{
    my_thread = kthread_run(thread_fn, NULL, "my_worker");
    return 0;
}

static void __exit my_exit(void)
{
    // BUG! kthread_stop()을 호출하지 않아 스레드가 계속 실행됨
    // 모듈 언로드 후 thread_fn 코드가 사라져서 크래시
}
올바른 예 — 안전한 kthread 패턴
static struct task_struct *my_thread;

static int thread_fn(void *data)
{
    while (!kthread_should_stop()) {  // 중지 요청 확인
        if (kthread_should_park()) {
            kthread_parkme();  // 일시 정지 지원
            continue;
        }

        do_work();

        // 인터럽트 가능한 sleep (kthread_stop 시 깨어남)
        set_current_state(TASK_INTERRUPTIBLE);
        if (!kthread_should_stop())
            schedule_timeout(msecs_to_jiffies(1000));
        set_current_state(TASK_RUNNING);
    }
    return 0;
}

static int __init my_init(void)
{
    my_thread = kthread_run(thread_fn, NULL, "my_worker");
    if (IS_ERR(my_thread))
        return PTR_ERR(my_thread);
    return 0;
}

static void __exit my_exit(void)
{
    if (my_thread)
        kthread_stop(my_thread);  // 스레드 중지 요청 + 완료 대기
}

리스트 API 오용

커널의 list_head 이중 연결 리스트는 가장 많이 사용되는 자료구조입니다. 순회 중 삭제, RCU 리스트 오용, 초기화 누락이 빈번한 실수입니다.

잘못된 예 — 순회 중 삭제
struct my_entry *entry;

// BUG! list_for_each_entry로 순회 중 삭제하면 next 포인터 손상
list_for_each_entry(entry, &my_list, list) {
    if (entry->expired) {
        list_del(&entry->list);  // next를 먼저 저장하지 않음!
        kfree(entry);
    }
}

// BUG! RCU 보호 없이 RCU 리스트 수정
list_add(&entry->list, &my_rcu_list);  // list_add_rcu 사용해야 함

// BUG! 초기화 안 된 list_head 사용
struct my_struct s;
list_del(&s.list);  // 초기화 안 된 포인터 역참조!
올바른 예 — 안전한 리스트 조작
struct my_entry *entry, *tmp;

// 삭제 안전한 순회: _safe 버전 사용
list_for_each_entry_safe(entry, tmp, &my_list, list) {
    if (entry->expired) {
        list_del(&entry->list);
        kfree(entry);  // tmp이 다음 노드를 이미 저장
    }
}

// RCU 리스트: _rcu 버전 사용
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_rcu_list, list) {
    process(entry);
}
rcu_read_unlock();

// 추가/삭제는 적절한 락 아래에서
spin_lock(&list_lock);
list_add_rcu(&entry->list, &my_rcu_list);
spin_unlock(&list_lock);

// 항상 초기화
INIT_LIST_HEAD(&s.list);
// 또는 선언 시: LIST_HEAD(my_list);
리스트 API 요약:
작업일반RCU안전 순회
추가list_add()list_add_rcu()
삭제list_del()list_del_rcu()
순회list_for_each_entry()list_for_each_entry_rcu()list_for_each_entry_safe()
비어있는지list_empty()list_empty_rcu()

printk / 로깅 실수

printk는 커널의 주요 디버깅 도구이지만, 오용하면 성능 저하, 정보 유출, 로그 폭발을 일으킵니다.

잘못된 예 — 로깅 실수 모음
// BUG! hot path에서 매번 출력 → 로그 폭발, 성능 저하
static irqreturn_t irq_handler(int irq, void *data)
{
    printk(KERN_INFO "IRQ %d received\\n", irq);  // 초당 수천 번!
    return IRQ_HANDLED;
}

// BUG! %px로 커널 주소 노출 (KASLR 우회)
printk(KERN_DEBUG "buf at %px\\n", buf);

// BUG! 구 스타일 printk 사용
printk(KERN_ERR "mydrv: error in %s\\n", __func__);
// dev 구조체가 있는데 사용하지 않음
올바른 예 — 올바른 로깅
// rate-limited 출력 (5초에 1번만)
static irqreturn_t irq_handler(int irq, void *data)
{
    printk_ratelimited(KERN_DEBUG "IRQ %d received\\n", irq);
    return IRQ_HANDLED;
}

// 한 번만 출력
pr_info_once("driver loaded\\n");

// 디바이스가 있으면 dev_* 사용 (디바이스 이름 자동 출력)
dev_err(&pdev->dev, "failed to init: %d\\n", ret);
dev_warn(&pdev->dev, "low battery\\n");
dev_dbg(&pdev->dev, "register 0x%x = 0x%x\\n", reg, val);

// 포인터 출력: %p (해시), %ps (심볼), %pS (심볼+오프셋)
pr_info("callback: %ps\\n", callback_fn);

// 동적 디버그 (런타임에 활성화/비활성화)
// echo 'module mydriver +p' > /sys/kernel/debug/dynamic_debug/control
pr_debug("detail: %d\\n", val);  // 기본 비활성화
printk 포맷 특수 지정자:
지정자용도예시 출력
%p해시된 포인터 (안전)00000000deadbeef
%ps심볼 이름my_function
%pS심볼 + 오프셋my_function+0x12/0x34
%pI4IPv4 주소192.168.1.1
%pI6IPv6 주소fe80::1
%pMMAC 주소00:11:22:33:44:55
%pUbUUID01020304-0506-...
%prstruct resource[mem 0x10000-0x1ffff]
%pOFDevice Tree 노드/soc/serial@10000

부동소수점 대안 패턴

리눅스 커널은 성능과 이식성 이유로 부동소수점 연산을 사용할 수 없습니다. FPU 레지스터는 사용자 프로세스가 소유하며, 커널이 건드리면 프로세스의 FPU 상태가 손상됩니다.

잘못된 예 — 부동소수점 사용
double calculate_ratio(int a, int b)
{
    return (double)a / (double)b;  // BUG! 커널에서 float/double 사용 금지
}
올바른 예 — 정수 연산으로 대체
// 고정 소수점: 1000배로 스케일링
int calculate_ratio_milli(int a, int b)
{
    if (b == 0)
        return 0;
    return (a * 1000) / b;  // 결과: 밀리 단위
}

// 또는 커널 제공 매크로
#include <linux/math64.h>
u64 result = div64_u64(a * 1000ULL, b);

// 퍼센트 계산
unsigned int pct = (count * 100) / total;

// 반드시 FPU가 필요하면 (암호화 등 극히 제한적):
kernel_fpu_begin();
// ... FPU 사용 ...
kernel_fpu_end();
// 주의: 이 구간에서 preempt 비활성화됨

Procfs/Sysfs 인터페이스 실수

Procfs(/proc)와 Sysfs(/sys)는 커널과 사용자 공간 사이의 주요 정보 교환 인터페이스입니다. Debugfs(/sys/kernel/debug)는 개발/디버깅 전용입니다. 이 인터페이스를 잘못 구현하면 버퍼 오버플로우, 정보 유출, race condition이 발생합니다.

커널 ↔ 사용자공간 인터페이스와 흔한 실수 사용자 공간: cat, echo, read(), write(), ioctl() /proc (Procfs) 용도: 프로세스/시스템 정보 API: proc_create(), seq_file 실수: 고정 버퍼, 락 누락 실수: seq_file 미사용 /sys (Sysfs) 용도: 디바이스 속성 API: DEVICE_ATTR, sysfs_emit 실수: 동기화 누락 실수: 복수 값 한 파일 /sys/kernel/debug (Debugfs) 용도: 개발/디버깅 전용 API: debugfs_create_* 실수: 프로덕션에 사용 실수: 에러 체크 불필요 인터페이스 선택 가이드 Procfs: 프로세스/시스템 정보 → seq_file 필수, 단일 값 출력 Sysfs: 디바이스 속성(ABI 안정) → 파일당 하나의 값, sysfs_emit() 사용 권장 Debugfs: 디버깅 전용(ABI 불안정) → 에러 코드 무시 OK, 프로덕션 의존 금지

seq_file 오용

Procfs에서 큰 데이터를 출력할 때는 seq_file API를 사용해야 합니다. 단순 sprintf()로 고정 버퍼에 쓰면 버퍼 오버플로우가 발생할 수 있습니다.

잘못된 예 — 고정 버퍼 사용
static ssize_t my_read(struct file *file, char __user *buf,
                      size_t count, loff_t *ppos)
{
    char kbuf[256];
    int len = 0;

    // BUG! 많은 엔트리가 있으면 버퍼 오버플로우
    for (int i = 0; i < num_entries; i++) {
        len += sprintf(kbuf + len, "%d: %s\\n", i, entries[i].name);
    }

    return simple_read_from_buffer(buf, count, ppos, kbuf, len);
}
올바른 예 — seq_file 사용
static int my_seq_show(struct seq_file *m, void *v)
{
    int i = *(int *)v;
    seq_printf(m, "%d: %s\\n", i, entries[i].name);  // 자동 버퍼 관리
    return 0;
}

static void *my_seq_start(struct seq_file *m, loff_t *pos)
{
    if (*pos >= num_entries)
        return NULL;
    return pos;
}

static void *my_seq_next(struct seq_file *m, void *v, loff_t *pos)
{
    (*pos)++;
    if (*pos >= num_entries)
        return NULL;
    return pos;
}

static void my_seq_stop(struct seq_file *m, void *v)
{
}

static const struct seq_operations my_seq_ops = {
    .start = my_seq_start,
    .next  = my_seq_next,
    .stop  = my_seq_stop,
    .show  = my_seq_show,
};

static int my_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &my_seq_ops);
}

Sysfs show/store에서 동기화 누락

Sysfs 속성은 여러 프로세스가 동시에 읽고 쓸 수 있습니다. 적절한 동기화 없이 공유 데이터를 접근하면 race condition이 발생합니다.

잘못된 예 — 락 없는 접근
static ssize_t value_store(struct device *dev,
                          struct device_attribute *attr,
                          const char *buf, size_t count)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int val;

    if (kstrtoint(buf, 10, &val))
        return -EINVAL;

    mydev->value = val;  // BUG! 동기화 없음
    return count;
}

static ssize_t value_show(struct device *dev,
                         struct device_attribute *attr,
                         char *buf)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    return sprintf(buf, "%d\\n", mydev->value);  // BUG! 동기화 없음
}
올바른 예 — 적절한 동기화
static ssize_t value_store(struct device *dev,
                          struct device_attribute *attr,
                          const char *buf, size_t count)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int val;

    if (kstrtoint(buf, 10, &val))
        return -EINVAL;

    mutex_lock(&mydev->lock);
    mydev->value = val;
    mutex_unlock(&mydev->lock);

    return count;
}

static ssize_t value_show(struct device *dev,
                         struct device_attribute *attr,
                         char *buf)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int val;

    mutex_lock(&mydev->lock);
    val = mydev->value;
    mutex_unlock(&mydev->lock);

    return sprintf(buf, "%d\\n", val);
}
Sysfs 속성 가이드:
  • 한 파일에 하나의 값만 (UNIX philosophy)
  • Binary 데이터는 BIN_ATTR 사용
  • Show 함수는 sprintf(), scnprintf(), sysfs_emit() 사용
  • Store 함수는 kstrtoint(), kstrtoul() 등으로 파싱

Debugfs 오용

Debugfs는 커널 개발자를 위한 디버깅 전용 인터페이스입니다. ABI 안정성이 보장되지 않으므로 사용자 공간 프로그램이 의존해서는 안 됩니다. 또한 debugfs 함수의 반환값을 체크할 필요가 없습니다 (Greg KH의 커밋으로 확정).

잘못된 예 — debugfs 반환값 체크 및 프로덕션 의존
static struct dentry *dbg_dir;

static int my_probe(struct device *dev)
{
    // BUG! debugfs 실패 시 전체 probe 실패 → debugfs가 없는 환경에서 동작 불가
    dbg_dir = debugfs_create_dir("mydriver", NULL);
    if (IS_ERR(dbg_dir))
        return PTR_ERR(dbg_dir);  // 불필요한 에러 처리

    // BUG! 프로덕션 코드가 debugfs 파일에 의존
    // 사용자 공간 유틸리티가 /sys/kernel/debug/mydriver/status를 파싱
}
올바른 예 — debugfs는 선택적, 에러 무시
static struct dentry *dbg_dir;

static int my_probe(struct device *dev)
{
    // debugfs 실패는 무시 — 기능 동작에 영향 없음
    dbg_dir = debugfs_create_dir("mydriver", NULL);
    debugfs_create_u32("register_dump", 0444, dbg_dir, &mydev->reg_val);
    debugfs_create_file("stats", 0444, dbg_dir, mydev, &stats_fops);
    // 반환값 체크 불필요 (커널 정책)

    // 프로덕션 정보는 sysfs나 procfs로 노출
    return 0;
}

static void my_remove(struct device *dev)
{
    debugfs_remove_recursive(dbg_dir);  // 디렉토리 전체 제거
}

sysfs_emit() vs sprintf()

sysfs의 show 함수에서 sprintf() 대신 sysfs_emit()을 사용해야 합니다. sysfs_emit()PAGE_SIZE 경계를 자동으로 확인하여 버퍼 오버플로우를 방지합니다. 커널 5.10부터 추가되었으며, 새 코드에서는 필수입니다.

잘못된 예 — sprintf 사용
static ssize_t status_show(struct device *dev,
                          struct device_attribute *attr, char *buf)
{
    // BUG! buf은 PAGE_SIZE 크기이지만 sprintf는 경계를 체크하지 않음
    return sprintf(buf, "status: %d\\n", mydev->status);

    // BUG! 여러 줄 출력 시 오버플로우 위험
    int len = 0;
    for (int i = 0; i < count; i++)
        len += sprintf(buf + len, "%d\\n", values[i]);
    return len;
}
올바른 예 — sysfs_emit 사용
static ssize_t status_show(struct device *dev,
                          struct device_attribute *attr, char *buf)
{
    return sysfs_emit(buf, "%d\\n", mydev->status);
    // PAGE_SIZE 경계 자동 체크, 안전
}

// 여러 값 출력 시
static ssize_t multi_show(struct device *dev,
                        struct device_attribute *attr, char *buf)
{
    int len = 0;
    for (int i = 0; i < count && len < PAGE_SIZE; i++)
        len += sysfs_emit_at(buf, len, "%d\\n", values[i]);
    return len;
}

보안 취약점

사용자 입력 검증 누락

절대 사용자 입력을 신뢰하지 마세요. 모든 경계를 검증해야 합니다.

잘못된 예 — 배열 경계 검사 없음
// ioctl 핸들러
int index;
copy_from_user(&index, user_ptr, sizeof(index));
return device_array[index];  // BUG! 범위 밖 접근 가능
올바른 예 — 경계 검사
int index;
if (copy_from_user(&index, user_ptr, sizeof(index)))
    return -EFAULT;

if (index < 0 || index >= ARRAY_SIZE(device_array))
    return -EINVAL;

return device_array[index];

정수 오버플로우

크기 계산 시 오버플로우가 발생하면 작은 버퍼가 할당되어 버퍼 오버플로우로 이어집니다.

잘못된 예 — 오버플로우 체크 없음
size_t size = count * item_size;  // 오버플로우 가능!
void *buf = kmalloc(size, GFP_KERNEL);
올바른 예 — 안전한 할당 함수 사용
void *buf = kmalloc_array(count, item_size, GFP_KERNEL);
// 또는 kcalloc (0으로 초기화)
void *buf = kcalloc(count, item_size, GFP_KERNEL);

TOCTOU (Time-of-Check to Time-of-Use)

검사 시점과 사용 시점 사이에 데이터가 변경되어 보안 취약점이 발생하는 고전적인 race condition입니다. 커널에서는 특히 사용자 공간 메모리를 여러 번 읽는 "double fetch" 패턴이 위험합니다.

TOCTOU Double Fetch 공격 타임라인 시간 커널 ① copy_from_user() ② 검증: OK ④ copy_from_user() ⑤ 악용! 공격자 ③ 값 변조! 메모리 index = 3 (유효한 값) index = 99999 (범위 초과!) 방지: 사용자 메모리는 딱 한 번만 copy_from_user()로 읽고, 커널 로컬 복사본으로만 작업
잘못된 예 — Double Fetch (사용자 메모리 이중 읽기)
// ioctl 핸들러
struct user_request req;
copy_from_user(&req, user_ptr, sizeof(req));

if (req.index >= MAX_ENTRIES)  // 검사
    return -EINVAL;

// ... 다른 작업 ...

// copy_from_user를 다시 호출하면 사용자가 값을 변경했을 수 있음 (double fetch)
copy_from_user(&req, user_ptr, sizeof(req));
return entries[req.index];  // BUG! 두 번째 fetch에서 범위 밖 값이 올 수 있음
올바른 예 — 복사본 사용
struct user_request req;
copy_from_user(&req, user_ptr, sizeof(req));

// 로컬 변수에 저장 (사용자가 변경 불가)
int index = req.index;

if (index >= MAX_ENTRIES)
    return -EINVAL;

// ... 다른 작업 ...

return entries[index];  // OK: index는 검증된 값

정보 유출 (Information Leak)

초기화되지 않은 커널 메모리를 사용자 공간에 복사하면 민감한 정보(포인터, 다른 프로세스 데이터)가 유출됩니다.

잘못된 예 — 패딩 바이트 유출
struct stats {
    int count;         // 4바이트
    // [4바이트 패딩]
    long timestamp;   // 8바이트
};

struct stats s;
s.count = 100;
s.timestamp = ktime_get_seconds();
copy_to_user(user_ptr, &s, sizeof(s));  // BUG! 패딩 바이트 초기화 안 됨
올바른 예 — 구조체 초기화
struct stats s = {};  // 0으로 초기화 (패딩 포함)
s.count = 100;
s.timestamp = ktime_get_seconds();
copy_to_user(user_ptr, &s, sizeof(s));  // OK

// 또는 memset 사용
struct stats s;
memset(&s, 0, sizeof(s));
s.count = 100;
s.timestamp = ktime_get_seconds();
copy_to_user(user_ptr, &s, sizeof(s));
정보 유출 방지:
  • 사용자 공간으로 복사하는 모든 구조체를 명시적으로 0으로 초기화
  • __attribute__((packed))로 패딩 제거 (단, 정렬 성능 저하 주의)
  • 각 필드를 개별적으로 복사 (패딩 제외)
  • CONFIG_GCC_PLUGIN_STRUCTLEAK 활성화 (컴파일러가 자동 초기화)

커널 포인터 유출 (KASLR 우회)

KASLR(Kernel Address Space Layout Randomization)은 커널 주소를 무작위화하여 공격을 어렵게 합니다. 그러나 커널 포인터를 사용자 공간에 노출하면 KASLR이 무력화되어 권한 상승 공격의 발판이 됩니다.

잘못된 예 — 커널 주소 노출
// procfs에서 커널 포인터 출력
seq_printf(m, "handler: 0x%lx\\n", (unsigned long)dev->handler);
// BUG! 실제 커널 함수 주소가 사용자에게 노출

// dmesg에서 주소 노출
printk(KERN_INFO "buffer at %px\\n", buf);
// BUG! %px는 실제 주소 출력 (디버깅용 한정 사용)

// ioctl 응답에 커널 포인터 포함
response.internal_ptr = (unsigned long)dev->priv;
copy_to_user(user_resp, &response, sizeof(response));
올바른 예 — 포인터 보호
// %p 사용: 해시된 값 출력 (주소 노출 안 됨)
seq_printf(m, "handler: %ps\\n", dev->handler);  // 심볼 이름만

// dmesg: 해시 포인터 또는 심볼
printk(KERN_INFO "buffer at %p\\n", buf);  // 해시된 값

// ioctl: ID나 핸들로 대체
response.device_id = dev->unique_id;  // 포인터 대신 ID
copy_to_user(user_resp, &response, sizeof(response));

// /proc/sys/kernel/kptr_restrict 설정:
// 0 = 모두 볼 수 있음 (비권장)
// 1 = CAP_SYSLOG 필요
// 2 = 항상 0으로 표시 (가장 안전)

ioctl 설계 실수

ioctl은 커널-사용자 공간 인터페이스의 주요 진입점입니다. 번호 체계, 호환성, 인자 검증에서 실수가 빈번합니다.

잘못된 예 — ioctl 번호 및 32비트 호환성
// 임의의 ioctl 번호 (다른 드라이버와 충돌 가능)
#define MY_IOCTL_SET  0x1234  // BUG! _IOW 매크로 미사용

// 포인터 크기 의존 구조체 (32비트 compat 깨짐)
struct my_ioctl_data {
    void *buffer;     // 4/8바이트 (아키텍처 의존)
    unsigned long size;  // 4/8바이트
};
올바른 예 — 올바른 ioctl 설계
// _IO, _IOR, _IOW, _IOWR 매크로 사용
#define MY_MAGIC 'M'  // Documentation/userspace-api/ioctl/ioctl-number.rst 참조
#define MY_IOCTL_GET  _IOR(MY_MAGIC, 0, struct my_ioctl_data)
#define MY_IOCTL_SET  _IOW(MY_MAGIC, 1, struct my_ioctl_data)

// 고정 크기 타입으로 32/64비트 호환
struct my_ioctl_data {
    __u64 buffer;     // 포인터 대신 __u64
    __u32 size;       // 고정 크기
    __u32 reserved;   // 미래 확장용 (0으로 초기화 검증)
};

// ioctl 핸들러
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    struct my_ioctl_data data;
    if (copy_from_user(&data, (void __user *)arg, sizeof(data)))
        return -EFAULT;
    if (data.reserved != 0)  // 미래 호환성 검증
        return -EINVAL;
    // ...
}

스택 버퍼 오버플로우

커널 스택은 프로세스당 8KB~16KB로 매우 작습니다 (사용자 공간의 수 MB와 비교). 큰 로컬 변수나 깊은 재귀는 스택 오버플로우로 이어져 인접 메모리를 덮어쓰고, 보안 공격의 발판이 될 수 있습니다.

잘못된 예 — 큰 스택 변수와 깊은 재귀
static int my_function(void)
{
    char buf[4096];  // BUG! 4KB는 스택의 절반 이상 소비
    struct big_struct data;  // BUG! 구조체가 수 KB이면 위험

    memset(buf, 0, sizeof(buf));
    // ...
}

// BUG! 재귀적 파일시스템 탐색 — 깊이 제한 없음
static int walk_tree(struct dentry *d)
{
    struct dentry *child;
    list_for_each_entry(child, &d->d_subdirs, d_child)
        walk_tree(child);  // 재귀! 깊은 디렉토리에서 스택 오버플로우
    return 0;
}
올바른 예 — 동적 할당과 반복문
static int my_function(void)
{
    char *buf;
    buf = kmalloc(4096, GFP_KERNEL);  // 힙에 할당
    if (!buf)
        return -ENOMEM;

    memset(buf, 0, 4096);
    // ...
    kfree(buf);
    return 0;
}

// 재귀 대신 명시적 스택(워크리스트) 사용
static int walk_tree_iterative(struct dentry *root)
{
    struct list_head work_list;
    INIT_LIST_HEAD(&work_list);
    list_add(&root->d_lru, &work_list);

    while (!list_empty(&work_list)) {
        struct dentry *d = list_first_entry(&work_list,
                            struct dentry, d_lru);
        list_del(&d->d_lru);
        process_dentry(d);
        // 자식을 워크리스트에 추가 (재귀 아님)
    }
    return 0;
}
스택 사용 가이드라인:
  • 로컬 변수 총합을 512바이트 이하로 유지
  • make W=1로 빌드하면 큰 스택 프레임 경고
  • scripts/checkstack.pl로 함수별 스택 사용량 분석: objdump -d vmlinux | scripts/checkstack.pl
  • CONFIG_FRAME_WARN=1024: 1KB 초과 스택 프레임에 컴파일 경고
  • CONFIG_VMAP_STACK=y: 가드 페이지로 스택 오버플로우 감지 (크래시하지만 덮어쓰기 방지)
  • VLA(Variable Length Array)는 커널에서 금지 (-Wvla)

Capability 검사 실수

커널 코드에서 권한 검사를 누락하면 비특권 사용자가 특권 작업을 수행할 수 있습니다. root 검사보다 세밀한 capability 체계를 사용해야 합니다.

잘못된 예 — 권한 검사 누락 또는 부정확
// BUG! ioctl에 권한 검사 없음 → 누구나 하드웨어 제어 가능
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
    case MY_IOCTL_RESET_HW:
        reset_hardware();  // 위험한 작업인데 권한 검사 없음!
        break;
    }
    return 0;
}

// BUG! uid == 0 검사 (구식, 네임스페이스 무시)
if (current_uid().val != 0)
    return -EPERM;
올바른 예 — 적절한 capability 검사
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
    case MY_IOCTL_RESET_HW:
        if (!capable(CAP_SYS_ADMIN))  // 관리 권한 필요
            return -EPERM;
        reset_hardware();
        break;

    case MY_IOCTL_READ_RAW:
        if (!capable(CAP_SYS_RAWIO))  // Raw I/O 권한
            return -EPERM;
        break;

    case MY_IOCTL_NET_CONFIG:
        if (!capable(CAP_NET_ADMIN))  // 네트워크 관리
            return -EPERM;
        break;
    }
    return 0;
}

// 네임스페이스 인식 검사 (컨테이너 환경)
if (!ns_capable(current_user_ns(), CAP_SYS_ADMIN))
    return -EPERM;
주요 Capability:
Capability용도
CAP_SYS_ADMIN시스템 관리 전반 (mount, swapon, 장치 제어)
CAP_SYS_RAWIORaw I/O 접근 (ioperm, iopl, MMIO)
CAP_NET_ADMIN네트워크 설정 (인터페이스 구성, 라우팅)
CAP_SYS_MODULE커널 모듈 로드/언로드
CAP_SYS_PTRACE프로세스 추적/디버깅
CAP_DAC_OVERRIDE파일 접근 권한 무시
CAP_SYS_BOOT재부팅

보안 강화 기능 미활용

현대 커널은 다양한 보안 강화 기능을 제공합니다. 이를 활용하지 않으면 불필요한 공격 표면이 노출됩니다.

권장 보안 강화 커널 옵션
# 스택 보호
CONFIG_STACKPROTECTOR=y              # 스택 카나리 (버퍼 오버플로우 감지)
CONFIG_STACKPROTECTOR_STRONG=y       # 더 많은 함수에 카나리 적용
CONFIG_VMAP_STACK=y                  # 가드 페이지 스택 (오버플로우 시 즉시 탐지)

# 메모리 보호
CONFIG_STRICT_KERNEL_RWX=y           # 커널 코드 영역 실행 전용
CONFIG_STRICT_MODULE_RWX=y           # 모듈 코드 영역 실행 전용
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y    # 할당 시 0 초기화
CONFIG_INIT_ON_FREE_DEFAULT_ON=y     # 해제 시 0으로 덮어쓰기
CONFIG_HARDENED_USERCOPY=y           # copy_to/from_user 경계 검사 강화

# 주소 공간 보호
CONFIG_RANDOMIZE_BASE=y              # KASLR
CONFIG_RANDOMIZE_MEMORY=y            # 메모리 레이아웃 무작위화

# 정보 유출 방지
CONFIG_SECURITY_DMESG_RESTRICT=y     # 비특권 사용자 dmesg 차단
CONFIG_GCC_PLUGIN_STRUCTLEAK=y       # 구조체 패딩 자동 초기화

# CFI (Control Flow Integrity)
CONFIG_CFI_CLANG=y                   # 간접 호출 무결성 검사

성능 관련 실수

기능은 동작하지만 성능에 심각한 영향을 주는 실수들입니다. 특히 멀티코어 시스템에서 동기화 오버헤드는 성능 저하의 주요 원인입니다.

과도한 락 사용

필요 이상으로 큰 범위를 보호하거나, 읽기 전용 데이터를 락으로 보호하면 병렬성이 크게 저하됩니다.

잘못된 예 — 불필요하게 큰 임계 영역
spin_lock(&dev->lock);

// 락이 필요한 부분
dev->counter++;

// 락이 불필요한 긴 계산
int result = expensive_calculation(dev->counter);  // 수백 사이클 소요

// 다시 락이 필요
dev->result = result;

spin_unlock(&dev->lock);
/* 문제: expensive_calculation 동안 다른 CPU가 모두 대기 */
올바른 예 — 최소 임계 영역
spin_lock(&dev->lock);
int local_counter = dev->counter++;
spin_unlock(&dev->lock);

// 락 없이 계산
int result = expensive_calculation(local_counter);

spin_lock(&dev->lock);
dev->result = result;
spin_unlock(&dev->lock);

False Sharing

서로 다른 CPU가 동일한 캐시 라인의 다른 변수를 수정하면, 캐시 라인이 계속 무효화되어 성능이 급격히 저하됩니다. x86_64에서 캐시 라인은 일반적으로 64바이트이며, 이 안에 있는 어떤 바이트든 수정되면 전체 캐시 라인이 무효화됩니다.

False Sharing — 캐시 라인 바운싱 하나의 캐시 라인 (64 bytes) cpu0_count (8B) cpu1_count (8B) cpu2_count (8B) cpu3_count (8B) CPU 0 CPU 1 캐시 무효화 핑퐁! 해결: 캐시 라인 정렬 또는 per-CPU 변수 cpu0_count + 패딩 cpu1_count + 패딩 ← 캐시 라인 1 → ← 캐시 라인 2 → 각 변수가 별도 캐시 라인 → 바운싱 제거
잘못된 예 — 인접한 per-CPU 카운터
struct stats {
    long cpu0_count;  // 8바이트
    long cpu1_count;  // 8바이트 (동일 캐시 라인!)
    long cpu2_count;
    long cpu3_count;
};
/* 각 CPU가 자기 카운터만 수정해도 캐시 라인 경쟁 발생 */
올바른 예 — 캐시 라인 정렬
struct stats {
    long cpu0_count;
    char pad0[L1_CACHE_BYTES - sizeof(long)];  // 패딩
    long cpu1_count;
    char pad1[L1_CACHE_BYTES - sizeof(long)];
    // ...
} __aligned(L1_CACHE_BYTES);

// 또는 per-CPU 변수 사용 (권장)
DEFINE_PER_CPU(long, cpu_count);

// 사용
this_cpu_inc(cpu_count);  // 현재 CPU의 변수만 수정

캐시 라인 바운싱

읽기 전용 데이터와 쓰기 데이터를 같은 구조체에 섞으면 불필요한 캐시 무효화가 발생합니다.

잘못된 예 — 읽기/쓰기 필드 혼재
struct device {
    const char *name;       // 읽기 전용 (hot)
    atomic_t refcount;      // 쓰기 빈번 (hot)
    int id;                  // 읽기 전용 (hot)
    struct list_head list;  // 쓰기 빈번
};
/* refcount 변경 시마다 name, id를 캐싱한 CPU들의 캐시가 무효화됨 */
올바른 예 — 접근 패턴별 분리
struct device {
    /* 읽기 전용 필드 (캐시 라인 1) */
    const char *name;
    int id;
    const struct device_ops *ops;

    /* 쓰기 빈번 필드 (캐시 라인 2) */
    atomic_t refcount ____cacheline_aligned;
    struct list_head list;
};

느린 경로에서 spinlock 사용

블록 I/O나 긴 계산 중에 spinlock을 보유하면 다른 CPU가 busy-wait하며 CPU 사이클을 낭비합니다.

잘못된 예 — I/O 중 spinlock 보유
spin_lock(&dev->lock);
struct bio *bio = bio_alloc(...);
submit_bio(bio);  // BUG! 블록 I/O 요청 (느림)
spin_unlock(&dev->lock);
올바른 예 — mutex 사용 또는 락 분리
mutex_lock(&dev->io_mutex);  // 긴 작업엔 mutex
struct bio *bio = bio_alloc(...);
submit_bio(bio);
mutex_unlock(&dev->io_mutex);
락 선택 가이드:
  • Spinlock — 임계 영역이 매우 짧을 때 (수십 사이클), interrupt context
  • Mutex — 임계 영역이 길 때, sleep 가능한 context
  • RCU — 읽기가 대부분이고 쓰기가 드물 때
  • Atomic — 단일 변수만 보호할 때

NUMA 인식 부족

다중 소켓 시스템에서 NUMA(Non-Uniform Memory Access) 토폴로지를 무시하면 원격 노드 메모리 접근으로 인해 2~4배의 지연 시간 증가가 발생합니다.

잘못된 예 — NUMA 무시 할당
// BUG! CPU가 Node 0에 있는데 메모리가 Node 1에 할당될 수 있음
void *buf = kmalloc(4096, GFP_KERNEL);

// BUG! 디바이스와 다른 NUMA 노드에서 DMA 버퍼 할당
void *dma_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// dev가 Node 1에 있으면 Node 1에 할당해야 최적
올바른 예 — NUMA 인식 할당
// 현재 CPU의 NUMA 노드에 할당
void *buf = kmalloc_node(4096, GFP_KERNEL, numa_node_id());

// 디바이스의 NUMA 노드에 할당
int node = dev_to_node(dev);
void *buf = kmalloc_node(4096, GFP_KERNEL, node);

// per-CPU 변수도 NUMA 인식
void *p = alloc_percpu(struct my_stats);
// 각 CPU의 로컬 노드에 자동 할당

// kthread를 특정 노드에 바인딩
struct task_struct *t = kthread_create_on_node(
    thread_fn, data, node, "worker/%d", node);
kthread_bind_mask(t, cpumask_of_node(node));

Hot Path에서 메모리 할당

패킷 처리, I/O 경로 등 매우 빈번하게 실행되는 코드에서 kmalloc()을 호출하면 slab 할당자의 오버헤드가 누적되어 성능이 크게 저하됩니다.

잘못된 예 — 매 요청마다 할당/해제
static int handle_request(struct request *req)
{
    // BUG! 초당 수십만 번 호출되는 경로에서 매번 할당
    struct work_context *ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;

    process(ctx, req);
    kfree(ctx);
    return 0;
}
올바른 예 — 전용 slab 캐시 또는 풀
// 방법 1: 전용 slab 캐시 (동일 크기 반복 할당에 최적)
static struct kmem_cache *ctx_cache;

static int __init my_init(void)
{
    ctx_cache = kmem_cache_create("my_ctx",
        sizeof(struct work_context), 0,
        SLAB_HWCACHE_ALIGN, NULL);
    return ctx_cache ? 0 : -ENOMEM;
}

static int handle_request(struct request *req)
{
    struct work_context *ctx = kmem_cache_zalloc(ctx_cache, GFP_KERNEL);
    process(ctx, req);
    kmem_cache_free(ctx_cache, ctx);
    return 0;
}

// 방법 2: mempool (할당 실패가 허용되지 않는 경우)
static mempool_t *ctx_pool;
ctx_pool = mempool_create_slab_pool(64, ctx_cache);  // 최소 64개 예약

struct work_context *ctx = mempool_alloc(ctx_pool, GFP_NOIO);
// mempool은 예약 메모리에서 할당하므로 실패하지 않음
mempool_free(ctx, ctx_pool);

비효율적 알고리즘 선택

Hot path에서 O(n²) 이상의 알고리즘을 사용하면 데이터가 늘어날 때 성능이 급격히 저하됩니다. 커널은 다양한 효율적 자료구조를 제공합니다.

커널 제공 자료구조:
자료구조헤더용도검색
list_headlinux/list.h이중 연결 리스트O(n)
hlist_headlinux/list.h해시 버킷용 단일 연결O(1) 평균
rbtreelinux/rbtree.h정렬된 데이터 (VMA, I/O 스케줄러)O(log n)
radix_tree / xarraylinux/xarray.h정수 키 기반 (페이지 캐시)O(log n)
idrlinux/idr.h정수 ID 할당/조회O(log n)
maple_treelinux/maple_tree.h범위 기반 (VMA, 커널 6.1+)O(log n)

디바이스 드라이버 실수

Probe/Remove 순서 불일치

probe()에서 할당/등록한 리소스는 remove()에서 역순으로 해제/해지해야 합니다. 순서가 맞지 않으면 사용 중인 리소스가 해제되어 크래시가 발생합니다.

잘못된 예 — 불일치하는 초기화/정리 순서
static int my_probe(struct platform_device *pdev)
{
    request_mem_region(...);           // 1
    ioremap(...);                       // 2
    alloc_chrdev_region(...);          // 3
    cdev_add(...);                      // 4 (사용자 접근 가능해짐!)
    request_irq(...);                   // 5
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    iounmap(...);                       // BUG! IRQ/cdev가 아직 활성
    free_irq(...);
    cdev_del(...);
    unregister_chrdev_region(...);
    release_mem_region(...);
    return 0;
}
올바른 예 — 역순 정리
static int my_probe(struct platform_device *pdev)
{
    request_mem_region(...);
    ioremap(...);
    alloc_chrdev_region(...);
    request_irq(...);           // IRQ 먼저 등록
    cdev_add(...);               // 모든 준비 완료 후 장치 공개
    return 0;

err_irq:
    free_irq(...);
err_chrdev:
    unregister_chrdev_region(...);
err_ioremap:
    iounmap(...);
err_mem:
    release_mem_region(...);
    return ret;
}

static int my_remove(struct platform_device *pdev)
{
    cdev_del(...);                // 5 (역순) - 사용자 접근 차단
    free_irq(...);                // 4
    unregister_chrdev_region(...);  // 3
    iounmap(...);                 // 2
    release_mem_region(...);      // 1
    return 0;
}
Probe/Remove 원칙:
  • 장치를 공개(register, cdev_add)하기 전에 모든 리소스를 준비
  • Remove에서는 먼저 장치를 숨기고(unregister) 나머지 정리
  • Probe 실패 시 goto 레이블로 부분 정리
  • Managed API (devm_*) 사용 고려 — 자동 정리

Managed Device Resource (devm) 오용

devm_* 함수는 드라이버 detach 시 자동으로 해제되지만, 수동 해제와 혼용하거나 해제 순서를 잘못 이해하면 문제가 발생합니다.

잘못된 예 — devm과 수동 해제 혼용
static int my_probe(struct device *dev)
{
    void *buf = devm_kzalloc(dev, 1024, GFP_KERNEL);
    // ...
}

static void my_remove(struct device *dev)
{
    kfree(buf);  // BUG! devm이 할당한 메모리는 자동 해제됨 → double free
}

// 또는
void *buf = devm_kzalloc(dev, 1024, GFP_KERNEL);
request_irq(..., buf, ...);  // BUG! IRQ는 수동 해제인데 buf는 먼저 해제될 수 있음
올바른 예 — 일관된 devm 사용
static int my_probe(struct device *dev)
{
    void *buf = devm_kzalloc(dev, 1024, GFP_KERNEL);
    devm_request_irq(dev, ..., buf, ...);  // devm 버전 사용
    return 0;
}

static void my_remove(struct device *dev)
{
    // 아무것도 안 함 - 자동 정리됨
}

인터럽트 핸들러 실수

인터럽트 핸들러는 hard IRQ 컨텍스트에서 실행되므로 매우 빠르게 완료해야 하고, sleep할 수 없으며, 사용자 공간 메모리에 접근할 수 없습니다.

잘못된 예 — IRQ 핸들러에서 긴 작업
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    // BUG! 긴 처리를 IRQ에서 수행
    for (int i = 0; i < 1000; i++) {
        process_data(&dev->buffer[i]);  // 수천 사이클
    }

    return IRQ_HANDLED;
}
올바른 예 — Top-half / Bottom-half 분리
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    // Top-half: 최소한의 작업만
    u32 status = readl(dev->regs + STATUS_REG);
    if (!(status & IRQ_PENDING))
        return IRQ_NONE;  // 우리 인터럽트 아님

    // IRQ 비트 클리어
    writel(status, dev->regs + STATUS_REG);

    // Bottom-half로 지연
    schedule_work(&dev->work);

    return IRQ_HANDLED;
}

static void my_work_handler(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, work);

    // Bottom-half: 긴 처리 수행
    for (int i = 0; i < 1000; i++) {
        process_data(&dev->buffer[i]);
    }
}

공유 인터럽트 처리 실수

IRQF_SHARED 플래그로 등록된 인터럽트는 여러 디바이스가 공유합니다. 핸들러는 자신의 인터럽트인지 확인하고, 아니면 IRQ_NONE을 반환해야 합니다.

잘못된 예 — 무조건 IRQ_HANDLED 반환
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    process_interrupt(dev);
    return IRQ_HANDLED;  // BUG! 다른 장치의 IRQ도 처리한 것으로 인식
}
올바른 예 — 하드웨어 상태 확인
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = readl(dev->regs + STATUS_REG);

    if (!(status & MY_IRQ_BITS))
        return IRQ_NONE;  // 우리 인터럽트 아님

    process_interrupt(dev);
    return IRQ_HANDLED;
}

엔디안/바이트 순서 실수

리눅스 커널은 다양한 아키텍처에서 동작하므로, 하드웨어 레지스터나 네트워크 데이터에 접근할 때 반드시 바이트 순서 변환 함수를 사용해야 합니다. 직접 캐스트하면 다른 아키텍처에서 동작하지 않습니다.

잘못된 예 — 엔디안 변환 누락
// 하드웨어 레지스터 (리틀 엔디안)
u32 val = *(u32 *)hw_reg;  // BUG! 빅 엔디안 CPU에서 바이트 순서 잘못됨

// 네트워크 패킷 헤더 (빅 엔디안)
u16 port = hdr->dest_port;  // BUG! 리틀 엔디안 CPU에서 바이트 순서 뒤바뀜

// 디스크 구조체 (리틀 엔디안)
u32 inode_num = disk_inode->i_num;  // BUG! 엔디안 변환 필요
올바른 예 — 명시적 바이트 순서 변환
// 리틀 엔디안 하드웨어 → CPU 바이트 순서
u32 val = le32_to_cpu(*(__le32 *)hw_reg);
// 또는 MMIO: readl()이 자동 변환
u32 val = readl(hw_reg);

// 네트워크 (빅 엔디안) → CPU 바이트 순서
u16 port = ntohs(hdr->dest_port);   // 또는 be16_to_cpu()
u32 addr = ntohl(hdr->src_addr);     // 또는 be32_to_cpu()

// CPU → 네트워크 바이트 순서
hdr->dest_port = htons(port);

// 디스크 (리틀 엔디안) → CPU
u32 inode_num = le32_to_cpu(disk_inode->i_num);

// sparse 타입 어노테이션 (컴파일 시 체크)
__le32 hw_value;   // 리틀 엔디안으로 선언
__be16 net_port;   // 빅 엔디안으로 선언
// make C=1로 sparse 검사 시 엔디안 불일치 경고

MMIO 접근 실수

Memory-Mapped I/O(MMIO) 레지스터는 일반 메모리처럼 보이지만, 직접 포인터 역참조로 접근하면 컴파일러 최적화로 인해 접근이 생략되거나 순서가 바뀔 수 있습니다.

잘못된 예 — 직접 포인터 역참조
void __iomem *base = ioremap(phys_addr, size);

// BUG! 컴파일러가 접근을 최적화하거나 재배치할 수 있음
u32 status = *((volatile u32 *)base + STATUS_REG);

// BUG! __iomem 포인터를 일반 포인터로 캐스트
memcpy(buf, (void *)base, 1024);
올바른 예 — MMIO 접근 함수 사용
void __iomem *base = ioremap(phys_addr, size);
if (!base)
    return -ENOMEM;

// 올바른 MMIO 접근
u32 status = readl(base + STATUS_REG);  // 순서 보장, 엔디안 변환 포함
writel(value, base + CTRL_REG);           // 쓰기 완료 보장

// relaxed 버전: 배리어 없음 (성능 필요 시)
u32 data = readl_relaxed(base + DATA_REG);

// 블록 복사
memcpy_fromio(buf, base + FIFO_REG, 1024);
memcpy_toio(base + FIFO_REG, buf, 1024);

// 해제
iounmap(base);
MMIO 접근 규칙:
  • readl()/writel(): 순서 보장 + 메모리 배리어 포함
  • readl_relaxed()/writel_relaxed(): 배리어 없음, 성능 최적화 시 사용
  • ioread32()/iowrite32(): PIO와 MMIO 모두 지원하는 범용 함수
  • Posted Write 플러시: PCI 등에서 writel() 후 실제 디바이스에 도착했는지 확인하려면 readl()로 플러시: writel(val, reg); readl(reg);
  • sparse (make C=1)로 __iomem 어노테이션 위반 검사

전원 관리 (Suspend/Resume) 실수

시스템 suspend/resume 또는 runtime PM에서 하드웨어 상태 관리를 잘못하면 resume 후 디바이스가 동작하지 않거나 데이터가 손실됩니다.

잘못된 예 — suspend 시 DMA/IRQ 미정리
static int my_suspend(struct device *dev)
{
    // BUG! DMA 전송이 진행 중일 수 있음
    // BUG! IRQ가 여전히 활성 상태
    // BUG! 레지스터 값을 저장하지 않음
    return 0;
}

static int my_resume(struct device *dev)
{
    // BUG! 하드웨어 재초기화 없이 사용 시도
    return 0;
}
올바른 예 — 완전한 suspend/resume
static int my_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    // 1. 새 작업 수신 차단
    netif_stop_queue(mydev->ndev);  // 네트워크 드라이버 예시

    // 2. 진행 중인 작업 완료 대기
    cancel_work_sync(&mydev->work);

    // 3. DMA 중지
    dma_stop(mydev);

    // 4. IRQ 비활성화
    disable_irq(mydev->irq);

    // 5. 하드웨어 상태 저장
    mydev->saved_ctrl = readl(mydev->regs + CTRL_REG);
    mydev->saved_config = readl(mydev->regs + CONFIG_REG);

    // 6. 클럭/전원 끄기
    clk_disable_unprepare(mydev->clk);

    return 0;
}

static int my_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    // 역순으로 복원
    clk_prepare_enable(mydev->clk);
    writel(mydev->saved_ctrl, mydev->regs + CTRL_REG);
    writel(mydev->saved_config, mydev->regs + CONFIG_REG);
    enable_irq(mydev->irq);
    netif_wake_queue(mydev->ndev);

    return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(my_pm_ops, my_suspend, my_resume);
Runtime PM 주의사항:
  • pm_runtime_get_sync()pm_runtime_put()는 항상 쌍으로 호출
  • 에러 경로에서 pm_runtime_put() 누락은 디바이스가 영원히 깨어있는 결과
  • pm_runtime_put_autosuspend()로 지연된 suspend 사용 권장
  • probe에서 pm_runtime_enable(), remove에서 pm_runtime_disable()
Probe/Remove — 리소스 생명주기 (거울상 패턴) probe() — 할당 순서 ① request_mem_region() ② ioremap() ③ request_irq() ④ alloc_workqueue() ⑤ cdev_add() ← 사용자 접근 허용 ⟵ 거울상 (역순) ⟶ remove() — 해제 순서 (역순) ⑤ cdev_del() ← 접근 차단 먼저! ④ destroy_workqueue() ③ free_irq() ② iounmap() ① release_mem_region() 핵심: 사용자 접근 인터페이스를 가장 나중에 열고, 가장 먼저 닫는다 (안전 구간 최대화)

DMA 매핑 실수

DMA 매핑은 디바이스가 메모리에 직접 접근할 수 있도록 물리 주소를 디바이스 주소로 변환합니다. 매핑/언매핑 불일치, 방향 플래그 오류, 캐시 일관성 문제가 빈번합니다.

잘못된 예 — DMA 매핑 실수
// BUG! virt_to_phys로 DMA 주소 생성 (IOMMU 무시)
dma_addr_t dma = virt_to_phys(buf);  // IOMMU 없이는 동작하지만 비이식적

// BUG! DMA 방향 불일치
dma_handle = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
// 디바이스가 데이터를 쓰는데 TO_DEVICE로 매핑 → 캐시 불일치

// BUG! 매핑 에러 미검사
dma_handle = dma_map_single(dev, buf, size, DMA_FROM_DEVICE);
// dma_mapping_error() 체크 없이 사용 → IOMMU 실패 시 크래시

// BUG! 매핑 해제 없이 버퍼 해제
kfree(buf);  // dma_unmap_single 호출 안 함 → IOMMU 리소스 누수
올바른 예 — 올바른 DMA 매핑
// Streaming DMA: 일시적 매핑
dma_handle = dma_map_single(dev, buf, size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
    dev_err(dev, "DMA mapping failed\\n");
    return -ENOMEM;
}

// DMA 완료 후 반드시 언매핑
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);

// Coherent DMA: 영구 매핑 (일관성 보장)
void *coherent_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!coherent_buf)
    return -ENOMEM;
// 사용 후
dma_free_coherent(dev, size, coherent_buf, dma_handle);

// DMA 방향 가이드:
// DMA_TO_DEVICE   — CPU → Device (전송)
// DMA_FROM_DEVICE — Device → CPU (수신)
// DMA_BIDIRECTIONAL — 양방향 (성능 약간 저하)
DMA 디버깅: CONFIG_DMA_API_DEBUG=y를 활성화하면 매핑/언매핑 불일치, 방향 오류, 중복 해제 등을 런타임에 자동 탐지합니다. dmesg | grep DMA-API로 경고를 확인하세요.

펌웨어 로딩 실수

디바이스 초기화 시 펌웨어를 로드해야 하는 경우, 파일 경로, 비동기 로딩, 에러 처리에서 실수가 빈번합니다.

잘못된 예 — 펌웨어 로딩 실수
static int my_probe(struct device *dev)
{
    const struct firmware *fw;

    // BUG! 절대 경로 사용 (이식성 없음)
    ret = request_firmware(&fw, "/lib/firmware/mydevice.bin", dev);

    // BUG! 동기 로딩 — 부팅 시 루트 파일시스템이 없으면 hang
    // 특히 initramfs에 펌웨어가 없을 때

    // BUG! 펌웨어 헤더/버전 검증 없이 사용
    memcpy_toio(dev->fw_mem, fw->data, fw->size);
    release_firmware(fw);
}
올바른 예 — 안전한 펌웨어 로딩
static void fw_loaded(const struct firmware *fw, void *context)
{
    struct my_device *mydev = context;

    if (!fw) {
        dev_err(mydev->dev, "firmware not found\\n");
        return;
    }

    // 펌웨어 헤더 검증
    if (fw->size < sizeof(struct fw_header)) {
        dev_err(mydev->dev, "firmware too small\\n");
        goto out;
    }

    struct fw_header *hdr = (struct fw_header *)fw->data;
    if (le32_to_cpu(hdr->magic) != FW_MAGIC) {
        dev_err(mydev->dev, "invalid firmware magic\\n");
        goto out;
    }

    memcpy_toio(mydev->fw_mem, fw->data, fw->size);
out:
    release_firmware(fw);
}

static int my_probe(struct device *dev)
{
    // 비동기 로딩 (파일시스템 준비 안 돼도 대기)
    ret = request_firmware_nowait(THIS_MODULE, true,
        "mydevice.bin", dev, GFP_KERNEL, mydev, fw_loaded);

    // 또는 동기 로딩 (probe 시점에 반드시 필요하면)
    ret = request_firmware(&fw, "mydevice.bin", dev);
    // 파일명만 지정 (커널이 /lib/firmware/ 검색)
}

클럭/레귤레이터 관리 실수

SoC 기반 디바이스에서 클럭과 전원 레귤레이터를 올바르게 관리하지 않으면 하드웨어가 동작하지 않거나, 전력 소비가 불필요하게 높아집니다.

잘못된 예 — 클럭/레귤레이터 실수
static int my_probe(struct platform_device *pdev)
{
    struct clk *clk = devm_clk_get(&pdev->dev, NULL);

    // BUG! prepare 없이 enable
    clk_enable(clk);  // clk_prepare_enable()이어야 함

    // BUG! 레귤레이터 활성화 후 안정화 시간 미대기
    regulator_enable(reg);
    readl(dev->regs);  // 전원이 아직 안정되지 않았을 수 있음

    // BUG! 에러 경로에서 클럭 비활성화 누락
    ret = some_init();
    if (ret)
        return ret;  // 클럭이 켜진 채로 반환!
}
올바른 예 — 올바른 클럭/레귤레이터 관리
static int my_probe(struct platform_device *pdev)
{
    struct clk *clk;
    struct regulator *reg;

    clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(clk))
        return PTR_ERR(clk);

    reg = devm_regulator_get(&pdev->dev, "vdd");
    if (IS_ERR(reg))
        return PTR_ERR(reg);

    // 1. 레귤레이터 활성화
    ret = regulator_enable(reg);
    if (ret)
        return ret;

    // 2. 안정화 대기 (데이터시트 참조)
    usleep_range(1000, 2000);  // 1-2ms

    // 3. 클럭 활성화 (prepare + enable)
    ret = clk_prepare_enable(clk);
    if (ret)
        goto err_reg;

    ret = some_init();
    if (ret)
        goto err_clk;

    return 0;

err_clk:
    clk_disable_unprepare(clk);
err_reg:
    regulator_disable(reg);
    return ret;
}

Device Tree 바인딩 실수

임베디드 시스템에서 Device Tree는 하드웨어 설명의 표준입니다. 바인딩을 잘못 구현하면 드라이버가 하드웨어를 찾지 못하거나 잘못된 리소스를 사용합니다.

잘못된 예 — DT 파싱 실수
static int my_probe(struct platform_device *pdev)
{
    u32 val;

    // BUG! 프로퍼티 이름 오타 → 기본값 사용되거나 실패
    of_property_read_u32(pdev->dev.of_node, "clock-frequncy", &val);
    // "frequency" 오타!

    // BUG! 선택적 프로퍼티인데 에러 반환
    ret = of_property_read_u32(pdev->dev.of_node, "optional-val", &val);
    if (ret)
        return ret;  // 선택적인데 필수로 처리

    // BUG! compatible 문자열 형식 잘못됨
    // { .compatible = "my-device" } — "vendor,device" 형식이어야 함
}
올바른 예 — 올바른 DT 파싱
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    u32 freq;

    // 필수 프로퍼티: 에러 시 probe 실패
    ret = device_property_read_u32(dev, "clock-frequency", &freq);
    if (ret) {
        dev_err(dev, "missing clock-frequency property\\n");
        return ret;
    }

    // 선택적 프로퍼티: 기본값 사용
    u32 optional_val = 100;  // 기본값
    device_property_read_u32(dev, "optional-val", &optional_val);

    // MMIO 리소스 (platform_get_resource 대신 devm 사용)
    mydev->regs = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(mydev->regs))
        return PTR_ERR(mydev->regs);

    return 0;
}

// compatible: "vendor,device" 형식 필수
static const struct of_device_id my_dt_ids[] = {
    { .compatible = "myvendor,mydevice-v2" },
    { .compatible = "myvendor,mydevice" },  // 이전 버전 호환
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_dt_ids);

// device_property API 사용 (DT + ACPI 양쪽 지원)
// of_property_read_* → device_property_read_*로 마이그레이션 권장

모듈 관리 실수

커널 모듈 생명주기 — 위험 구간 시간 insmod module_init() 모듈 동작 중 (refcount > 0) rmmod module_exit() 위험 ① init 부분 실패 시 이미 등록한 리소스 누수 위험 ② rmmod 후 비동기 콜백(timer/work/RCU) 이 실행되면 → 해제된 코드 실행 → 크래시 안전한 모듈 패턴 module_init() 안전 규칙 1. 리소스 할당 (역순 goto 레이블 준비) 2. 등록은 마지막에 (외부 접근 허용 시점 최소화) 3. 실패 시 이미 할당/등록한 것만 역순 해제 4. 에러 코드를 정확히 반환 (-ENOMEM, -ENODEV) TIP: devm_* 사용 시 자동 정리 module_exit() 안전 규칙 1. 외부 접근 차단 (unregister, cdev_del 등) 2. 비동기 작업 완료 대기 cancel_work_sync(), del_timer_sync() 3. RCU 콜백 대기: rcu_barrier() 4. 리소스 해제 (init의 역순)

모듈 참조 카운트 실수

모듈이 사용 중인데 언로드되면 실행 중인 코드가 사라져서 크래시가 발생합니다. file_operations, device_driver 등을 통해 모듈이 사용 중이면 자동으로 refcount가 증가하지만, 특수한 경우 수동 관리가 필요합니다.

잘못된 예 — 콜백 실행 중 언로드 가능
static void delayed_callback(struct work_struct *work)
{
    // 이 함수가 실행되는 동안 모듈이 언로드될 수 있음!
    do_something();
}

static int my_ioctl(struct file *file, ...)
{
    schedule_work(&my_work);  // work가 실행되기 전에 모듈 언로드 가능
    return 0;
}
올바른 예 — 모듈 참조 보호
static void delayed_callback(struct work_struct *work)
{
    do_something();
    module_put(THIS_MODULE);  // 작업 완료 후 참조 해제
}

static int my_ioctl(struct file *file, ...)
{
    if (!try_module_get(THIS_MODULE))  // 언로드 중이면 실패
        return -ENODEV;
    schedule_work(&my_work);
    return 0;
}

static void my_module_exit(void)
{
    cancel_work_sync(&my_work);  // work 완료 대기
    // ...
}
주의: 대부분의 경우 커널이 자동으로 관리하므로 명시적 try_module_get()은 드물게 필요합니다. 비동기 콜백, 타이머, workqueue 같은 지연 실행에서만 고려하세요.

심볼 Export 실수

다른 모듈이 사용할 함수는 EXPORT_SYMBOL()로 내보내야 하지만, 내부 함수를 불필요하게 export하면 ABI 호환성 부담이 생깁니다.

잘못된 예 — 내부 함수 export
static void internal_helper(void)  // static인데...
{
    // ...
}
EXPORT_SYMBOL(internal_helper);  // BUG! static 함수는 export 불가

// 또는
void my_public_api(void)  // GPL 아닌 일반 export
{
    // ...
}
EXPORT_SYMBOL_GPL(my_public_api);  // 불일치: GPL only여야 함
올바른 예 — 적절한 export
// 내부 함수 - export 안 함
static void internal_helper(void)
{
    // ...
}

// 공개 API - 다른 모듈이 사용 가능
void my_public_api(void)
{
    // ...
}
EXPORT_SYMBOL_GPL(my_public_api);  // GPL 모듈만 사용 가능

// 또는 독점 드라이버도 사용 가능하게
EXPORT_SYMBOL(my_public_api);

모듈 파라미터 검증 실수

모듈 파라미터는 사용자가 임의의 값을 설정할 수 있으므로, 검증 없이 사용하면 배열 범위 초과, 정수 오버플로우 등의 버그가 발생합니다.

잘못된 예 — 모듈 파라미터 무검증
static int ring_size = 256;
module_param(ring_size, int, 0644);

static int my_init(void)
{
    // BUG! ring_size가 0, 음수, 또는 거대한 값일 수 있음
    buf = kmalloc_array(ring_size, sizeof(*buf), GFP_KERNEL);
}
올바른 예 — 범위 검증
static int ring_size = 256;
module_param(ring_size, int, 0644);
MODULE_PARM_DESC(ring_size, "Ring buffer size (16-4096, default 256)");

static int my_init(void)
{
    // 범위 검증 + 2의 거듭제곱 강제
    if (ring_size < 16 || ring_size > 4096) {
        pr_err("ring_size %d out of range [16, 4096]\\n", ring_size);
        return -EINVAL;
    }
    ring_size = roundup_pow_of_two(ring_size);  // 2의 거듭제곱으로 정렬
    buf = kmalloc_array(ring_size, sizeof(*buf), GFP_KERNEL);
}

// 또는 set 콜백으로 런타임 검증
static int set_ring_size(const char *val, const struct kernel_param *kp)
{
    int n;
    if (kstrtoint(val, 10, &n) || n < 16 || n > 4096)
        return -EINVAL;
    return param_set_int(val, kp);
}
static const struct kernel_param_ops ring_size_ops = {
    .set = set_ring_size,
    .get = param_get_int,
};
module_param_cb(ring_size, &ring_size_ops, &ring_size, 0644);

초기화 순서 의존성

모듈이 다른 서브시스템에 의존할 때, 해당 서브시스템이 아직 초기화되지 않았으면 실패합니다. module_init의 실행 순서는 링크 순서에 의존하므로 명시적으로 제어해야 합니다.

잘못된 예 — 의존성 무시
static int __init my_init(void)
{
    // BUG! PCI 서브시스템이 초기화되기 전에 호출될 수 있음
    struct pci_dev *dev = pci_get_device(MY_VID, MY_PID, NULL);
    if (!dev)
        return -ENODEV;

    // BUG! 빌트인 모듈에서 initcall 레벨을 고려하지 않음
    return 0;
}
module_init(my_init);
올바른 예 — 적절한 initcall 레벨과 deferred probe
// 빌트인 모듈: 적절한 initcall 레벨 사용
// early_initcall → pure_initcall → core_initcall → postcore_initcall
//   → arch_initcall → subsys_initcall → fs_initcall → device_initcall
//     → late_initcall

static int __init my_late_init(void)
{
    // PCI, USB 등 서브시스템이 이미 초기화된 후 실행
    return 0;
}
late_initcall(my_late_init);  // device_initcall 이후

// 드라이버 probe에서 의존성 처리: -EPROBE_DEFER
static int my_probe(struct platform_device *pdev)
{
    struct clk *clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(clk)) {
        if (PTR_ERR(clk) == -EPROBE_DEFER)
            return -EPROBE_DEFER;  // 나중에 다시 시도
        return PTR_ERR(clk);
    }
    // ...
}
빌트인 initcall 레벨:
레벨매크로용도
0early_initcall메모리, CPU 초기화 (매우 이른 단계)
1core_initcall코어 서브시스템 (irq, timer)
2postcore_initcall코어 이후 초기화
3arch_initcall아키텍처 종속 초기화
4subsys_initcall서브시스템 (PCI, USB, 네트워크)
5fs_initcall파일시스템
6device_initcall일반 디바이스 드라이버 (module_init 기본값)
7late_initcall가장 마지막 단계

모듈 exit에서 RCU 콜백 대기 누락

RCU 콜백(call_rcu()로 등록된 함수)은 grace period 이후에 실행됩니다. 모듈 언로드 시 모든 RCU 콜백이 완료되지 않으면, 이미 언로드된 모듈의 코드를 실행하게 됩니다.

잘못된 예 — RCU 콜백 미대기
static void free_entry_rcu(struct rcu_head *head)
{
    struct my_entry *entry = container_of(head, struct my_entry, rcu);
    kfree(entry);
}

static void delete_entry(struct my_entry *entry)
{
    list_del_rcu(&entry->list);
    call_rcu(&entry->rcu, free_entry_rcu);  // 콜백 등록
}

static void __exit my_exit(void)
{
    delete_all_entries();
    // BUG! call_rcu 콜백이 아직 실행되지 않았을 수 있음
    // 모듈 언로드 후 free_entry_rcu()가 실행되면 → 크래시
}
올바른 예 — rcu_barrier() 사용
static void __exit my_exit(void)
{
    delete_all_entries();
    rcu_barrier();  // 모든 RCU 콜백 완료 대기
    // 이제 안전하게 모듈 언로드 가능
}
module_exit 안전 체크리스트:
  • del_timer_sync() — 타이머 콜백 완료 대기
  • cancel_work_sync() / cancel_delayed_work_sync() — workqueue 작업 완료 대기
  • flush_workqueue() — 특정 workqueue의 모든 작업 완료 대기
  • rcu_barrier() — RCU 콜백 완료 대기
  • synchronize_rcu() — 현재 RCU grace period 대기
  • unregister_* — 외부 접근 인터페이스 해제
  • flush_scheduled_work() — system workqueue 작업 완료 대기

MODULE_LICENSE 오류

MODULE_LICENSE를 올바르게 설정하지 않으면 GPL-only 심볼 접근이 차단되고, 커널 로그에 "tainted kernel" 경고가 발생합니다.

잘못된 예 — 라이선스 누락 또는 불일치
// LICENSE 선언 누락 → 모듈 로드 시 kernel tainted
// GPL-only 심볼 사용 불가

// 또는
MODULE_LICENSE("Proprietary");  // GPL-only 함수를 사용하면서
// → 모듈 로드 시 심볼 해석 실패
올바른 예 — 올바른 라이선스 선언
MODULE_LICENSE("GPL");            // GPL v2
MODULE_LICENSE("GPL v2");         // GPL v2 only
MODULE_LICENSE("Dual BSD/GPL");   // 듀얼 라이선스
MODULE_LICENSE("Dual MIT/GPL");   // 듀얼 라이선스

MODULE_AUTHOR("Your Name <email@example.com>");
MODULE_DESCRIPTION("My kernel module description");
MODULE_VERSION("1.0");

코딩 스타일 및 패치 제출

checkpatch 경고 무시

scripts/checkpatch.pl의 경고는 대부분 실제 문제를 가리킵니다.

패치 검증
./scripts/checkpatch.pl --strict my.patch
./scripts/checkpatch.pl --file drivers/mydriver.c

잘못된 커밋 메시지

커널 패치는 엄격한 커밋 메시지 형식을 따릅니다.

올바른 커밋 메시지 형식
subsystem: 간결한 제목 (50자 이내)

왜 이 변경이 필요한지 자세히 설명합니다.
무엇을 바꿨는지가 아니라 '왜' 바꿨는지를 설명하세요.

Signed-off-by: Your Name <your@email.com>
커밋 메시지 팁:
  • 제목은 명령형으로 ("Fix bug" not "Fixed bug")
  • 제목과 본문 사이에 빈 줄
  • 본문은 72자 이내로 줄바꿈
  • Fixes, Reported-by, Reviewed-by 등의 태그 사용
자세한 내용: 패치 제출

매크로 작성 실수

커널은 매크로를 광범위하게 사용합니다. 잘못 작성된 매크로는 찾기 어려운 버그를 만들고, 디버거에서 추적이 불가능합니다.

잘못된 예 — 위험한 매크로
// BUG! 인자를 괄호로 감싸지 않음
#define DOUBLE(x) x * 2
// DOUBLE(a + b) → a + b * 2 (의도와 다름!)

// BUG! 인자가 여러 번 평가됨
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// MAX(i++, j) → i가 두 번 증가할 수 있음

// BUG! 여러 문장 매크로에 do-while 없음
#define INIT_DEV(d) d->status = 0; d->count = 0;
// if (condition) INIT_DEV(dev); → 두 번째 문장이 항상 실행됨

// BUG! 타입 안전하지 않은 매크로
#define container_of_bad(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))
// ptr 타입 불일치 시 컴파일 경고 없음
올바른 예 — 안전한 매크로
// 인자를 괄호로 감싸기
#define DOUBLE(x) ((x) * 2)

// 인자 다중 평가 방지: GCC 확장 사용
#define MAX(a, b) ({          \
    typeof(a) _a = (a);    \
    typeof(b) _b = (b);    \
    _a > _b ? _a : _b;      \
})
// 또는 커널 제공: max(), min(), clamp()

// 여러 문장: do { } while (0)
#define INIT_DEV(d) do {  \
    (d)->status = 0;       \
    (d)->count = 0;        \
} while (0)

// 가능하면 static inline 함수 사용 (타입 체크, 디버깅 가능)
static inline int safe_max(int a, int b)
{
    return a > b ? a : b;
}

흔한 checkpatch 위반

scripts/checkpatch.pl이 자주 잡아내는 스타일 위반들입니다. 이들은 단순한 스타일 문제가 아니라, 코드 리뷰에서 reject되는 주요 원인입니다.

자주 나오는 checkpatch 경고와 수정법:
경고문제수정
WARNING: line over 80 columns긴 줄줄 나누기 (100자 soft limit, 가독성 우선)
ERROR: trailing whitespace줄 끝 공백sed -i 's/[[:space:]]*$//' file.c
WARNING: prefer pr_err over printk구식 printkpr_err(), dev_err() 사용
ERROR: space prohibited before ;세미콜론 앞 공백foo() ;foo();
WARNING: sizeof *ptr preferredsizeof 사용sizeof(struct foo)sizeof(*ptr)
WARNING: braces {} not necessary단일문 중괄호if/for 단일문에서 {} 제거
ERROR: do not use BUG()BUG() 사용WARN_ON() 또는 에러 반환으로 대체
WARNING: Use of volatilevolatile 사용READ_ONCE()/WRITE_ONCE() 또는 적절한 배리어
checkpatch 실행 방법
# 패치 파일 검사
./scripts/checkpatch.pl my-patch.patch

# 파일 직접 검사 (strict 모드)
./scripts/checkpatch.pl --strict --file drivers/mydriver/mydriver.c

# git 커밋 검사
./scripts/checkpatch.pl -g HEAD
./scripts/checkpatch.pl -g HEAD~5..HEAD  # 최근 5개 커밋

# 경고만 표시 (에러 제외)
./scripts/checkpatch.pl --types WARNING my.patch

# 자동 수정 가능한 항목 적용
./scripts/checkpatch.pl --fix-inplace --file mydriver.c

코드 리뷰 체크리스트

패치를 제출하기 전에 다음 항목을 확인하세요:

동기화

메모리

에러 처리

보안

디바이스 드라이버

모듈

커널 인터페이스

API 사용

전원 관리

성능

스타일 및 테스트

디버깅 도구 활성화

개발 환경에서는 다음 커널 옵션을 활성화하여 버그를 조기에 발견하세요:

.config 권장 설정
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_DWARF4=y      # GDB 디버깅용

# 동기화
CONFIG_PROVE_LOCKING=y          # lockdep (데드락 탐지)
CONFIG_DEBUG_ATOMIC_SLEEP=y     # atomic context sleep 탐지
CONFIG_DEBUG_SPINLOCK=y
CONFIG_DEBUG_MUTEXES=y
CONFIG_DEBUG_WW_MUTEX_SLOWPATH=y
CONFIG_LOCK_STAT=y              # 락 경합 통계

# 메모리
CONFIG_KASAN=y                  # 메모리 오류 즉시 탐지 (큰 오버헤드)
CONFIG_DEBUG_KMEMLEAK=y         # 메모리 누수 탐지
CONFIG_SLUB_DEBUG=y
CONFIG_DEBUG_PAGEALLOC=y
CONFIG_PAGE_POISONING=y         # 해제된 페이지에 독 패턴

# 스택
CONFIG_DEBUG_STACK_USAGE=y      # 스택 사용량 추적
CONFIG_STACKTRACE=y

# Lockup 탐지
CONFIG_LOCKUP_DETECTOR=y
CONFIG_SOFTLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_DETECT_HUNG_TASK=y

# 기타
CONFIG_DEBUG_LIST=y             # 리스트 손상 탐지
CONFIG_DEBUG_NOTIFIERS=y
CONFIG_DEBUG_CREDENTIALS=y

디버깅 도구 사용법

KASAN 사용 (메모리 오류)
# 커널 빌드 시 활성화
make menuconfig
# Kernel hacking → Memory Debugging → KASAN

# 부팅 후 자동으로 오류 탐지
# dmesg에서 확인:
dmesg | grep -i kasan
kmemleak 사용 (메모리 누수)
# 부트 파라미터로 활성화
kmemleak=on

# 또는 런타임에 활성화
echo scan > /sys/kernel/debug/kmemleak

# 누수 보고서 확인
cat /sys/kernel/debug/kmemleak

# 샘플 출력:
# unreferenced object 0xffff888012345678 (size 1024):
#   comm "my_module", pid 1234, jiffies 4294967295
#   backtrace:
#     kmalloc
#     my_init_function
#     ...
lockdep 사용 (데드락 탐지)
# CONFIG_PROVE_LOCKING=y로 빌드하면 자동 활성화
# 데드락 가능성 발견 시 경고 출력:

# dmesg 예시:
# ======================================================
# WARNING: possible circular locking dependency detected
# ======================================================
# task/1234 is trying to acquire lock:
#   (&lock_b){+.+.}, at: my_function+0x12/0x34
# but task is already holding lock:
#   (&lock_a){+.+.}, at: my_function+0x56/0x78

# 락 통계 확인
cat /proc/lock_stat
ftrace 사용 (함수 추적)
# 특정 함수 추적
cd /sys/kernel/debug/tracing
echo my_function > set_ftrace_filter
echo function > current_tracer
echo 1 > tracing_on

# 결과 확인
cat trace

# 추적 중지
echo 0 > tracing_on
KCSAN 사용 (동시성 버그)
# CONFIG_KCSAN=y로 빌드
# Kernel Concurrency Sanitizer: data race를 동적으로 탐지

# 부팅 후 자동 감지, dmesg 예시:
# ==================================================================
# BUG: KCSAN: data-race in my_read / my_write
# 
# write to 0xffff888012345678 of 4 bytes by task 1234:
#  my_write+0x12/0x34
# 
# read to 0xffff888012345678 of 4 bytes by task 5678:
#  my_read+0x56/0x78
# ==================================================================

# 특정 변수에 대해 의도된 race는 data_race() 매크로로 표시
# int val = data_race(shared_counter);  // KCSAN이 무시

# KCSAN 통계
cat /sys/kernel/debug/kcsan
static 분석 도구
# sparse: __iomem, __user, __rcu 어노테이션 검사
make C=1           # 변경된 파일만
make C=2           # 모든 파일
make C=1 CF='-Wbitwise -D__CHECK_ENDIAN__'  # 엔디안 검사 포함

# coccinelle: 의미론적 패치 (패턴 기반 버그 탐지)
make coccicheck MODE=report
# 커널 트리의 scripts/coccinelle/ 에 다양한 규칙 파일 제공

# smatch: 정적 분석기 (범위 분석, 정수 오버플로우 등)
make CHECK=smatch C=1

# gcc 경고 활성화
make W=1           # 추가 경고
make W=2           # 더 많은 경고
make W=12          # 모든 경고
버그 유형별 디버깅 도구 선택 버그 유형 빌드 시간 도구 런타임 도구 메모리 오류 sparse, W=1 KASAN, kmemleak, SLUB debug 동기화 오류 sparse (__rcu 체크) lockdep, KCSAN, RCU_PROVE 정보 유출 GCC_PLUGIN_STRUCTLEAK INIT_ON_ALLOC, kptr_restrict 성능 문제 (해당 없음) perf, ftrace, bpftrace, LOCK_STAT API 오용 / 스타일 checkpatch, coccinelle, smatch DEBUG_ATOMIC_SLEEP, DEBUG_LIST 런타임 오버헤드 비교 KASAN: 2-3x 느림 | lockdep: 10-30% | KCSAN: 소폭 | SLUB_DEBUG: 10-20% | kmemleak: 약간 | ftrace: 5-10%
주의: 이러한 디버그 옵션은 성능을 크게 저하시키므로 프로덕션에서는 비활성화하세요. KASAN은 특히 메모리 사용량과 속도에 큰 영향(2-3배 느림)을 줍니다.

참고 자료

내부 문서

커널 공식 문서

디버깅 도구

장애 대응 플레이북

실수가 실제 장애로 이어졌을 때는 빠른 복구와 원인 축소가 최우선입니다. 아래 절차는 커널 버그 대응 시 재현성과 분석 속도를 높이는 기본 운영 루틴입니다.

커널 장애 대응 워크플로 (5단계) ① 탐지 oops/panic/hang ② 증거 보존 로그/config/커밋 ③ 영향 축소 롤백/차단 ④ 원인 분석 bisect/crash/ftrace ⑤ 회귀 방지 패치+자동 테스트 증거 보존 세부 항목 • dmesg -T > incident.log • cp .config incident-config • git rev-parse HEAD > commit.txt • /proc/vmstat, /proc/slabinfo 스냅샷 • kdump vmcore (패닉 시) • perf record (성능 문제 시) 원인 분석 도구 매트릭스 패닉/Oops: addr2line, crash utility, scripts/decode_stacktrace.sh 메모리 버그: KASAN, kmemleak, slub_debug=FZPU, page_owner 동기화 버그: lockdep, KCSAN, CONFIG_DEBUG_ATOMIC_SLEEP, RCU_PROVE 성능 문제: perf top/record/report, trace-cmd, bpftrace 회귀 탐지: git bisect, syzkaller, LTP, kselftest 행(Hang): SysRq-t, /proc/*/stack, hung_task_timeout_secs 핵심: "재현 → 분리 → 수정 → 자동 검증" 사이클을 최대한 빠르게 돌린다
  1. 증거 보존: 커널 로그, 콜트레이스, 커밋 해시, .config 즉시 저장
  2. 영향 축소: 문제 커밋 롤백 또는 기능 플래그 비활성화로 서비스 안정화
  3. 재현 최소화: 가장 작은 재현 시나리오(입력/부하/타이밍) 작성
  4. 원인 분리: 동기화/메모리/API 오용 중 어느 축인지 먼저 분류
  5. 회귀 방지: 수정 후 재현 테스트를 자동화 스크립트로 남김
장애 유형 즉시 조치 추가 분석 도구 핵심 커널 옵션
커널 패닉 oops 로그와 직전 커밋 수집 kdump, crash, addr2line CONFIG_DEBUG_INFO=y
메모리 누수 문제 경로 임시 차단, 반복 재현 kmemleak, KASAN CONFIG_DEBUG_KMEMLEAK=y
간헐 레이스 락 경합 경로 로깅 강화 lockdep, ftrace, KCSAN CONFIG_KCSAN=y
성능 급락 최근 변경점 범위 축소 perf, trace-cmd CONFIG_FTRACE=y
Soft/Hard lockup SysRq-t로 모든 태스크 덤프 hung_task, watchdog CONFIG_LOCKUP_DETECTOR=y
Use-after-free KASAN 리포트 수집, 재현 환경 격리 KASAN, SLUB debug CONFIG_KASAN=y
스택 오버플로우 콜체인 깊이 분석, 재귀 호출 확인 STACK_USAGE, objdump CONFIG_DEBUG_STACK_USAGE=y

Crash Dump 분석

커널 패닉이 발생하면 kdump가 vmcore를 저장합니다. crash 유틸리티로 사후 분석이 가능하며, 이를 통해 패닉 시점의 레지스터, 스택, 메모리 상태를 재구성할 수 있습니다.

kdump 설정 및 crash 분석
# 1. kdump 커널 설정
# 부트 파라미터에 crashkernel 메모리 예약
crashkernel=256M

# 2. kdump 서비스 활성화
systemctl enable kdump
systemctl start kdump

# 3. 패닉 발생 후 vmcore 분석
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/*/vmcore

# crash> 기본 명령어
crash> bt          # 현재 태스크 백트레이스
crash> bt -a       # 모든 CPU 백트레이스
crash> log         # 커널 로그 (dmesg)
crash> ps          # 프로세스 목록
crash> vm          # 가상 메모리 정보
crash> files       # 열린 파일 목록
crash> rd -S       # 스택 덤프
crash> struct task_struct ffff888012345678  # 구조체 내용
crash> dis -l my_function  # 디스어셈블리 (소스 라인 포함)

# 4. 주소를 소스 라인으로 변환
scripts/decode_stacktrace.sh vmlinux < oops.txt

# 5. addr2line으로 개별 주소 변환
addr2line -e vmlinux -f ffffffffc0123456

git bisect로 회귀 찾기

수백 개의 커밋 중 버그를 도입한 정확한 커밋을 찾는 가장 효율적인 방법입니다. 이진 탐색으로 log₂(N)번의 빌드/테스트로 원인 커밋을 특정합니다.

git bisect 자동화
# 1. 수동 bisect
git bisect start
git bisect bad HEAD          # 현재(버그 있음)
git bisect good v6.1         # 이전 버전(정상)

# 커널 빌드 & 테스트 후
git bisect good  # 또는 git bisect bad
# 반복... 최종적으로 원인 커밋 출력

git bisect reset  # 완료 후 원래 브랜치로

# 2. 자동 bisect (스크립트 기반)
git bisect start HEAD v6.1
git bisect run ./test-script.sh

# test-script.sh 예시:
#!/bin/bash
make -j$(nproc) || exit 125  # 빌드 실패 시 skip
# QEMU나 실제 머신에서 테스트
./run-test.sh
exit $?  # 0=good, 1=bad, 125=skip

SysRq 긴급 디버깅

시스템이 응답하지 않을 때 Magic SysRq 키로 커널에 직접 명령을 보낼 수 있습니다. 네트워크나 셸이 동작하지 않아도 키보드나 시리얼 콘솔로 사용 가능합니다.

SysRq 핵심 명령
# SysRq 활성화
echo 1 > /proc/sys/kernel/sysrq

# 키보드: Alt+SysRq+ 또는 직접 트리거
echo t > /proc/sysrq-trigger   # 모든 태스크 스택 트레이스 (hang 분석)
echo w > /proc/sysrq-trigger   # D-state(uninterruptible) 태스크 표시
echo l > /proc/sysrq-trigger   # 모든 CPU 백트레이스
echo m > /proc/sysrq-trigger   # 메모리 정보 출력
echo p > /proc/sysrq-trigger   # 현재 CPU 레지스터 출력

# 안전한 재부팅 순서 (REISUB)
echo r > /proc/sysrq-trigger   # Raw 키보드 모드 해제
echo e > /proc/sysrq-trigger   # 모든 프로세스에 SIGTERM
echo i > /proc/sysrq-trigger   # 모든 프로세스에 SIGKILL
echo s > /proc/sysrq-trigger   # 파일시스템 Sync
echo u > /proc/sysrq-trigger   # 파일시스템 read-only 리마운트
echo b > /proc/sysrq-trigger   # 재부팅

Fault Injection으로 에러 경로 테스트

정상 경로만 테스트하면 에러 처리 코드의 버그를 놓칩니다. 커널의 fault injection 프레임워크를 사용하면 kmalloc 실패, I/O 오류 등을 인위적으로 발생시킬 수 있습니다.

Fault Injection 사용법
# 커널 옵션 활성화
CONFIG_FAULT_INJECTION=y
CONFIG_FAILSLAB=y            # slab 할당 실패 주입
CONFIG_FAIL_PAGE_ALLOC=y     # 페이지 할당 실패
CONFIG_FAIL_IO_TIMEOUT=y     # I/O 타임아웃 주입
CONFIG_FAULT_INJECTION_DEBUG_FS=y

# kmalloc 실패 주입 (10% 확률)
echo 10 > /sys/kernel/debug/failslab/probability
echo 1  > /sys/kernel/debug/failslab/times  # 1회만

# 특정 프로세스에만 적용
echo 1 > /proc/self/make-it-fail

# 또는 코드에서 직접
# should_fail(&failslab.attr, size) 호출

# C 코드에서 에러 주입 포인트 추가:
# #include <linux/fault-inject.h>
# static DECLARE_FAULT_ATTR(my_fault);
# if (should_fail(&my_fault, 1)) return -ENOMEM;
자동 퍼징 테스트:
  • syzkaller — 커널 시스콜 퍼저, 자동으로 크래시 유발 입력을 생성하고 최소 재현 프로그램(C repro)까지 생성
  • LTP (Linux Test Project) — 커널 기능별 회귀 테스트 스위트, ./runltp -f syscalls
  • kselftest — 커널 트리 내장 테스트, make -C tools/testing/selftests run_tests
  • xfstests — 파일시스템 전용 스트레스/회귀 테스트
  • blktests — 블록 레이어 테스트 프레임워크
# 장애 분석 최소 수집 세트
dmesg -T > incident-dmesg.log
cp .config incident-config.txt
git rev-parse HEAD > incident-commit.txt
uname -a > incident-kernel.txt
cat /proc/vmstat > incident-vmstat.txt
cat /proc/slabinfo > incident-slabinfo.txt
cp /sys/kernel/debug/tracing/trace incident-ftrace.txt 2>/dev/null

커널 개발 시 주의사항과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.