커널 개발 주의사항
리눅스 커널 개발 시 흔히 발생하는 실수와 주의사항, 그리고 올바른 해결 방법을 정리합니다. 특히 동기화 오류를 중점적으로 다룹니다.
핵심 요약
- 동기화 오류 — 데드락, race condition, atomic context에서의 sleep 등 가장 찾기 어렵고 치명적인 버그입니다.
- 메모리 관리 — 메모리 누수, use-after-free, NULL 포인터 역참조는 시스템 불안정의 주요 원인입니다.
- 에러 처리 — 모든 함수의 반환값을 체크하고, 실패 시 적절히 정리(cleanup)해야 합니다.
- 커널 API 오용 — 잘못된 컨텍스트에서 함수를 호출하면 크래시나 데이터 손상이 발생합니다.
- 보안 — 사용자 입력을 절대 신뢰하지 말고, 모든 경계를 검증해야 합니다.
단계별 이해
- 코드 작성 전 — 어떤 컨텍스트에서 실행되는지(process/interrupt/softirq), 어떤 락이 필요한지 먼저 설계합니다.
동시성 문제를 나중에 고치려면 10배 이상의 시간이 듭니다.
- 코드 작성 중 — 모든 메모리 할당/해제를 쌍으로 확인하고, 에러 경로에서 goto 레이블로 정리합니다.
한 줄씩 작성할 때마다 "이게 동시에 실행되면?"을 자문하세요.
- 코드 작성 후 —
scripts/checkpatch.pl로 코딩 스타일을 검증하고, lockdep, kmemleak, KASAN 등의 디버그 도구를 활성화합니다.정적 분석 도구가 발견한 경고는 대부분 실제 버그입니다.
- 테스트 — 스트레스 테스트, race condition 유발 테스트, 에러 주입(fault injection)을 수행합니다.
정상 경로만 테스트하면 버그의 90%를 놓칩니다.
개요
리눅스 커널 개발은 높은 수준의 주의력과 정확성을 요구합니다. 사용자 공간 프로그래밍과 달리 커널 코드의 버그는 전체 시스템을 멈추거나 데이터를 손상시킬 수 있습니다. 이 문서는 수많은 커널 개발자들이 반복적으로 겪는 실수들을 정리하여, 같은 함정에 빠지지 않도록 돕는 것을 목표로 합니다.
동기화 오류 ⚠️
동기화 오류는 가장 찾기 어렵고, 재현하기 어려우며, 가장 치명적인 버그입니다. race condition은 특정 타이밍에서만 발생하므로 테스트 환경에서는 나타나지 않다가 프로덕션에서 갑자기 발생할 수 있습니다. 커널에서 동기화 버그가 특히 위험한 이유는 사용자 공간과 달리 SIGSEGV로 프로세스만 죽는 것이 아니라 전체 시스템이 멈추거나 데이터가 영구적으로 손상될 수 있기 때문입니다.
데드락 (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) 활성화하여 데드락 가능성을 컴파일/런타임에 탐지- 락을 보유한 채로 다른 서브시스템의 함수를 호출하지 말 것 (알 수 없는 락 의존성 생성)
자기 자신 데드락 (Self-Deadlock)
같은 스레드가 이미 보유한 락을 다시 획득하려 하면 자기 자신과 데드락에 빠집니다.
일반 spinlock과 mutex는 재진입(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 (메모리로)
→ 이 사이에 인터럽트 발생 시 값 손실 */
}
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 계열은 별도 규칙을 따릅니다.)
위반 시 시스템이 즉시 크래시하거나 무한 대기에 빠집니다.
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할 수 있음 → 데드락 */
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);
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 커널 옵션 활성화
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);
락 해제 누락
락을 획득한 후 모든 코드 경로에서 해제하지 않으면 다른 스레드가 영원히 대기합니다. 특히 에러 처리 경로에서 자주 발생합니다.
int process_data(struct device *dev)
{
spin_lock(&dev->lock);
if (!dev->ready) {
return -EAGAIN; // BUG! unlock 안 함
}
// ... 작업 ...
spin_unlock(&dev->lock);
return 0;
}
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 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);
// 업데이트 측 (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_lock();
struct node *n = rcu_dereference(head);
mutex_lock(&n->lock); // BUG! rcu_read_lock() 내에서 sleep 불가
// ...
mutex_unlock(&n->lock);
rcu_read_unlock();
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_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-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에서 실행이 재개되어 잘못된 데이터에 접근합니다.
// 선점 비활성화 없이 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의 데이터를 수정
// 방법 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); // 가장 간결하고 안전
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(&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을 올바르게 처리해야 합니다.
// 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가 조건 체크와 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_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은 값 타입(구조체 복사)에만 사용
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 | 컴파일러 최적화만 방지 |
메모리 관리 실수
메모리 누수 (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;
}
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! 해제된 메모리 접근
struct data *ptr = kmalloc(sizeof(*ptr), GFP_KERNEL);
kfree(ptr);
ptr = NULL; // 다시 사용 방지
// 패턴: 해제 후 즉시 NULL 대입
kfree(ptr);
ptr = NULL;
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! 이미 해제됨
}
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을 반환할 수 있는데 검사하지 않고 사용하면 커널 패닉이 발생합니다.
struct device *dev = find_device(id);
dev->status = ACTIVE; // BUG! dev가 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에 사용하거나 매핑을 해제하지 않으면 심각한 문제가 발생합니다.
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_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 진행 중에는 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! 다른 할당자에서 할당한 메모리
// 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); // 모든 객체 해제 후에만!
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바이트
}
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 | ≤128KB | Yes | GFP_KERNEL/ATOMIC | 일반 커널 객체 |
vmalloc | 큰 버퍼 | No | Process only | 큰 배열, 모듈 메모리 |
kvmalloc | 가변 | 가변 | Process only | 크기 불확실한 할당 |
dma_alloc_coherent | DMA | Yes | Process only | DMA 버퍼 |
alloc_pages | 2^n 페이지 | Yes | Any | 페이지 단위 할당 |
kmem_cache_alloc | 고정 | Yes | Any | 반복 할당 최적화 |
Slab 오염 감지
해제된 메모리에 쓰기(use-after-free)나 할당 범위를 넘는 쓰기(buffer overflow)는
slab 오염을 일으킵니다. SLUB_DEBUG를 활성화하면 이를 감지할 수 있습니다.
# 커널 부트 파라미터
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 # 실시간 모니터링
에러 처리 실수
반환값 무시
커널 API는 int, NULL/ERR_PTR, bool, void 등 반환 계약이 다양합니다. 반환값을 확인하지 않으면 후속 코드가 잘못된 가정으로 실행됩니다.
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 체크만 하면 에러 포인터를 역참조하여 크래시가 발생합니다.
struct file *f = filp_open("/dev/null", O_RDONLY, 0);
if (!f) { // BUG! filp_open은 ERR_PTR을 반환, NULL이 아님
return -ENOENT;
}
// f가 ERR_PTR이면 크래시!
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;
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이 가능합니다.
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)에 따라 호출 가능한 함수가 엄격히 제한됩니다.
잘못된 컨텍스트에서 함수 호출
커널 함수는 호출 가능한 컨텍스트가 제한되어 있습니다.
| 함수 | 호출 가능 컨텍스트 | 불가능 컨텍스트 |
|---|---|---|
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)에 주의해야 합니다.
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
}
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는 이미 큐에 있을 때 다시 큐잉하면 안 되며, 취소 시 경쟁 조건에 주의해야 합니다.
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를 무조건 켜므로 중첩 불가능합니다.
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가 아직 보유 중)
}
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! 직접 접근 불가
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(&comp); // done = UINT_MAX (모든 대기자 깨움)
start_new_operation();
wait_for_completion(&comp); // BUG! 즉시 반환됨 (done > 0이므로)
complete_all(&comp);
reinit_completion(&comp); // done = 0으로 초기화
start_new_operation();
wait_for_completion(&comp); // OK: 새 complete() 대기
complete(): done++ (대기자 1개만 깨움)complete_all(): done = UINT_MAX (모든 대기자 깨움, 이후 대기도 즉시 반환)reinit_completion(): done = 0 (재사용 전 필수 호출)
커널에서 부동소수점 사용
커널 코드에서는 부동소수점 연산(float, double)을 직접 사용할 수 없습니다. 커널은 컨텍스트 스위칭 시 FPU/SSE 레지스터를 저장하지 않으므로, 사용자 프로세스의 FPU 상태를 손상시킵니다.
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
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% 사용합니다.
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 코드가 사라져서 크래시
}
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);
| 작업 | 일반 | 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); // 기본 비활성화
| 지정자 | 용도 | 예시 출력 |
|---|---|---|
%p | 해시된 포인터 (안전) | 00000000deadbeef |
%ps | 심볼 이름 | my_function |
%pS | 심볼 + 오프셋 | my_function+0x12/0x34 |
%pI4 | IPv4 주소 | 192.168.1.1 |
%pI6 | IPv6 주소 | fe80::1 |
%pM | MAC 주소 | 00:11:22:33:44:55 |
%pUb | UUID | 01020304-0506-... |
%pr | struct resource | [mem 0x10000-0x1ffff] |
%pOF | Device 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이 발생합니다.
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);
}
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);
}
- 한 파일에 하나의 값만 (UNIX philosophy)
- Binary 데이터는
BIN_ATTR사용 - Show 함수는
sprintf(),scnprintf(),sysfs_emit()사용 - Store 함수는
kstrtoint(),kstrtoul()등으로 파싱
Debugfs 오용
Debugfs는 커널 개발자를 위한 디버깅 전용 인터페이스입니다. ABI 안정성이 보장되지 않으므로 사용자 공간 프로그램이 의존해서는 안 됩니다. 또한 debugfs 함수의 반환값을 체크할 필요가 없습니다 (Greg KH의 커밋으로 확정).
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를 파싱
}
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부터 추가되었으며, 새 코드에서는 필수입니다.
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;
}
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" 패턴이 위험합니다.
// 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 번호 (다른 드라이버와 충돌 가능)
#define MY_IOCTL_SET 0x1234 // BUG! _IOW 매크로 미사용
// 포인터 크기 의존 구조체 (32비트 compat 깨짐)
struct my_ioctl_data {
void *buffer; // 4/8바이트 (아키텍처 의존)
unsigned long size; // 4/8바이트
};
// _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.plCONFIG_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;
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 | 용도 |
|---|---|
CAP_SYS_ADMIN | 시스템 관리 전반 (mount, swapon, 장치 제어) |
CAP_SYS_RAWIO | Raw 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바이트이며, 이 안에 있는 어떤 바이트든 수정되면 전체 캐시 라인이 무효화됩니다.
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 사이클을 낭비합니다.
spin_lock(&dev->lock);
struct bio *bio = bio_alloc(...);
submit_bio(bio); // BUG! 블록 I/O 요청 (느림)
spin_unlock(&dev->lock);
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배의 지연 시간 증가가 발생합니다.
// 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에 할당해야 최적
// 현재 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;
}
// 방법 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_head | linux/list.h | 이중 연결 리스트 | O(n) |
hlist_head | linux/list.h | 해시 버킷용 단일 연결 | O(1) 평균 |
rbtree | linux/rbtree.h | 정렬된 데이터 (VMA, I/O 스케줄러) | O(log n) |
radix_tree / xarray | linux/xarray.h | 정수 키 기반 (페이지 캐시) | O(log n) |
idr | linux/idr.h | 정수 ID 할당/조회 | O(log n) |
maple_tree | linux/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;
}
- 장치를 공개(register, cdev_add)하기 전에 모든 리소스를 준비
- Remove에서는 먼저 장치를 숨기고(unregister) 나머지 정리
- Probe 실패 시 goto 레이블로 부분 정리
- Managed API (devm_*) 사용 고려 — 자동 정리
Managed Device Resource (devm) 오용
devm_* 함수는 드라이버 detach 시 자동으로 해제되지만,
수동 해제와 혼용하거나 해제 순서를 잘못 이해하면 문제가 발생합니다.
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는 먼저 해제될 수 있음
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할 수 없으며, 사용자 공간 메모리에 접근할 수 없습니다.
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;
}
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을 반환해야 합니다.
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);
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);
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 후 디바이스가 동작하지 않거나 데이터가 손실됩니다.
static int my_suspend(struct device *dev)
{
// BUG! DMA 전송이 진행 중일 수 있음
// BUG! IRQ가 여전히 활성 상태
// BUG! 레지스터 값을 저장하지 않음
return 0;
}
static int my_resume(struct device *dev)
{
// BUG! 하드웨어 재초기화 없이 사용 시도
return 0;
}
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);
pm_runtime_get_sync()와pm_runtime_put()는 항상 쌍으로 호출- 에러 경로에서
pm_runtime_put()누락은 디바이스가 영원히 깨어있는 결과 pm_runtime_put_autosuspend()로 지연된 suspend 사용 권장- probe에서
pm_runtime_enable(), remove에서pm_runtime_disable()
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 리소스 누수
// 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 — 양방향 (성능 약간 저하)
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는 하드웨어 설명의 표준입니다. 바인딩을 잘못 구현하면 드라이버가 하드웨어를 찾지 못하거나 잘못된 리소스를 사용합니다.
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" 형식이어야 함
}
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_*로 마이그레이션 권장
모듈 관리 실수
모듈 참조 카운트 실수
모듈이 사용 중인데 언로드되면 실행 중인 코드가 사라져서 크래시가 발생합니다. 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 호환성 부담이 생깁니다.
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 안 함
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 레벨 사용
// 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);
}
// ...
}
| 레벨 | 매크로 | 용도 |
|---|---|---|
| 0 | early_initcall | 메모리, CPU 초기화 (매우 이른 단계) |
| 1 | core_initcall | 코어 서브시스템 (irq, timer) |
| 2 | postcore_initcall | 코어 이후 초기화 |
| 3 | arch_initcall | 아키텍처 종속 초기화 |
| 4 | subsys_initcall | 서브시스템 (PCI, USB, 네트워크) |
| 5 | fs_initcall | 파일시스템 |
| 6 | device_initcall | 일반 디바이스 드라이버 (module_init 기본값) |
| 7 | late_initcall | 가장 마지막 단계 |
모듈 exit에서 RCU 콜백 대기 누락
RCU 콜백(call_rcu()로 등록된 함수)은 grace period 이후에 실행됩니다.
모듈 언로드 시 모든 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()가 실행되면 → 크래시
}
static void __exit my_exit(void)
{
delete_all_entries();
rcu_barrier(); // 모든 RCU 콜백 완료 대기
// 이제 안전하게 모듈 언로드 가능
}
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되는 주요 원인입니다.
| 경고 | 문제 | 수정 |
|---|---|---|
WARNING: line over 80 columns | 긴 줄 | 줄 나누기 (100자 soft limit, 가독성 우선) |
ERROR: trailing whitespace | 줄 끝 공백 | sed -i 's/[[:space:]]*$//' file.c |
WARNING: prefer pr_err over printk | 구식 printk | pr_err(), dev_err() 사용 |
ERROR: space prohibited before ; | 세미콜론 앞 공백 | foo() ; → foo(); |
WARNING: sizeof *ptr preferred | sizeof 사용 | sizeof(struct foo) → sizeof(*ptr) |
WARNING: braces {} not necessary | 단일문 중괄호 | if/for 단일문에서 {} 제거 |
ERROR: do not use BUG() | BUG() 사용 | WARN_ON() 또는 에러 반환으로 대체 |
WARNING: Use of volatile | volatile 사용 | READ_ONCE()/WRITE_ONCE() 또는 적절한 배리어 |
# 패치 파일 검사
./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
코드 리뷰 체크리스트
패치를 제출하기 전에 다음 항목을 확인하세요:
동기화
- [ ] 모든 공유 데이터가 적절한 락으로 보호되는가?
- [ ] 락 순서가 일관되는가? (데드락 방지)
- [ ] Atomic context에서 sleep 가능한 함수를 호출하지 않는가?
- [ ] 모든 코드 경로에서 락이 해제되는가?
- [ ] RCU를 사용한다면 올바른 동기화 primitive를 사용하는가?
메모리
- [ ] 모든 메모리 할당에 대응하는 해제가 있는가?
- [ ] 에러 경로에서도 메모리가 해제되는가?
- [ ] 해제 후 포인터를 NULL로 설정하는가?
- [ ] NULL 포인터를 역참조하기 전에 검사하는가?
- [ ] 적절한 GFP(Get Free Pages) 플래그를 사용하는가? (GFP_KERNEL vs GFP_ATOMIC)
에러 처리
- [ ] 모든 함수 반환값을 검사하는가?
- [ ] 적절한 에러 코드를 반환하는가?
- [ ] goto 레이블로 정리 경로를 구성했는가?
- [ ] 에러 로그가 충분한가? (pr_err, dev_err)
보안
- [ ] 사용자 입력의 경계를 검증하는가?
- [ ] copy_to_user / copy_from_user를 사용하는가?
- [ ] 정수 오버플로우 가능성을 체크했는가?
- [ ] TOCTOU 취약점이 없는가? (검사 후 사용 전 값 변경)
- [ ] 구조체를 사용자에게 복사할 때 패딩을 초기화했는가?
- [ ] 권한 검사를 수행하는가? (capable())
디바이스 드라이버
- [ ] Probe/remove 순서가 정확한 역순인가?
- [ ] 장치 등록 전에 모든 리소스가 준비되었는가?
- [ ] DMA 버퍼를 적절한 API로 할당/해제하는가?
- [ ] 인터럽트 핸들러가 충분히 빠른가? (긴 작업은 bottom-half로)
- [ ] 공유 인터럽트에서 IRQ_NONE을 올바르게 반환하는가?
- [ ] devm_* 함수를 일관되게 사용하는가?
모듈
- [ ] 비동기 작업 중 모듈 언로드를 막았는가?
- [ ] Exit 시 모든 비동기 작업을 취소/완료 대기하는가?
- [ ] 필요한 함수만 EXPORT_SYMBOL로 내보내는가?
- [ ] 참조 카운트가 균형있게 증가/감소하는가?
커널 인터페이스
- [ ] Procfs에서 큰 데이터를 출력할 때 seq_file을 사용하는가?
- [ ] Sysfs show/store 함수에서 적절히 동기화하는가?
- [ ] Sysfs 파일 하나에 값 하나만 담는가?
- [ ] 스택 사용량이 512바이트 이하인가?
- [ ] DMA 버퍼를 올바른 API로 관리하는가?
API 사용
- [ ] 올바른 컨텍스트에서 함수를 호출하는가?
- [ ] Timer/workqueue를 안전하게 취소하는가? (del_timer_sync, cancel_work_sync)
- [ ] IRQ 플래그 관리가 올바른가? (irqsave/irqrestore)
- [ ] IS_ERR/PTR_ERR을 올바르게 사용하는가?
- [ ] 사용자 메모리 접근 시 copy_to/from_user를 사용하는가?
- [ ] RCU read-side에서 sleep하지 않는가? (또는 SRCU 사용)
- [ ] rcu_dereference()를 rcu_read_lock() 구간 안에서 호출하는가?
- [ ] Completion 대기에 타임아웃이 있는가?
- [ ] 커널에서 부동소수점을 사용하지 않는가?
- [ ] printk에서 %p (해시) 또는 %ps (심볼)를 사용하는가? (%px 사용 금지)
- [ ] MMIO 접근에 readl()/writel()을 사용하는가? (직접 역참조 금지)
- [ ] 엔디안 변환 함수(le32_to_cpu 등)를 사용하는가?
전원 관리
- [ ] Suspend 시 DMA를 중지하는가?
- [ ] Suspend 시 IRQ를 비활성화하는가?
- [ ] 하드웨어 레지스터 상태를 저장/복원하는가?
- [ ] pm_runtime_get/put이 균형을 이루는가?
- [ ] 모듈 파라미터에 범위 검증이 있는가?
성능
- [ ] 임계 영역이 최소화되어 있는가?
- [ ] False sharing을 피하기 위해 필드를 적절히 정렬했는가?
- [ ] Hot path에서 불필요한 락을 보유하지 않는가?
- [ ] Per-CPU 변수를 사용할 수 있는가?
- [ ] 읽기 전용 필드와 쓰기 필드를 분리했는가?
스타일 및 테스트
- [ ] checkpatch.pl이 통과하는가?
- [ ] sparse 경고가 없는가? (
make C=1) - [ ] lockdep, KASAN, kmemleak으로 테스트했는가?
- [ ] 스트레스 테스트와 에러 주입을 수행했는가?
- [ ] 주석이 충분한가? (복잡한 로직, 비직관적인 코드)
- [ ] 커밋 메시지가 형식에 맞는가?
디버깅 도구 활성화
개발 환경에서는 다음 커널 옵션을 활성화하여 버그를 조기에 발견하세요:
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
디버깅 도구 사용법
# 커널 빌드 시 활성화
make menuconfig
# Kernel hacking → Memory Debugging → KASAN
# 부팅 후 자동으로 오류 탐지
# dmesg에서 확인:
dmesg | grep -i kasan
# 부트 파라미터로 활성화
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
# ...
# 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
# 특정 함수 추적
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
# 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
# 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 # 모든 경고
참고 자료
내부 문서
- 동기화 기법 — 락, atomic, RCU 등
- 메모리 배리어 — 메모리 순서 보장
- 메모리 관리 — kmalloc, 페이지 할당 등
- DMA — DMA 메모리 관리
- 디바이스 드라이버 — 드라이버 작성 가이드
- 인터럽트 — IRQ 핸들러 작성
- 커널 모듈 — 모듈 작성과 로딩
- 디버깅 — 커널 디버깅 기법
- 패치 제출 — 커널 패치 작성 가이드
- 커널 보안 — 보안 취약점 방지
커널 공식 문서
- Linux Kernel Coding Style
- Linux Kernel Patch Submission Checklist
- Memory Allocation Guide
- Locking in the Kernel
- Workqueue Documentation
- RCU Documentation
- printk Format Specifiers
- Device I/O (MMIO) Documentation
- Device Power Management
- ioctl Number Registry
디버깅 도구
- KASAN (Kernel Address Sanitizer)
- Kernel Memory Leak Detector
- Runtime Locking Correctness Validator (lockdep)
- Sparse - Semantic Parser for C
장애 대응 플레이북
실수가 실제 장애로 이어졌을 때는 빠른 복구와 원인 축소가 최우선입니다. 아래 절차는 커널 버그 대응 시 재현성과 분석 속도를 높이는 기본 운영 루틴입니다.
- 증거 보존: 커널 로그, 콜트레이스, 커밋 해시,
.config즉시 저장 - 영향 축소: 문제 커밋 롤백 또는 기능 플래그 비활성화로 서비스 안정화
- 재현 최소화: 가장 작은 재현 시나리오(입력/부하/타이밍) 작성
- 원인 분리: 동기화/메모리/API 오용 중 어느 축인지 먼저 분류
- 회귀 방지: 수정 후 재현 테스트를 자동화 스크립트로 남김
| 장애 유형 | 즉시 조치 | 추가 분석 도구 | 핵심 커널 옵션 |
|---|---|---|---|
| 커널 패닉 | 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 유틸리티로 사후 분석이 가능하며, 이를 통해 패닉 시점의 레지스터, 스택, 메모리 상태를 재구성할 수 있습니다.
# 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)번의 빌드/테스트로 원인 커밋을 특정합니다.
# 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 활성화
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 오류 등을 인위적으로 발생시킬 수 있습니다.
# 커널 옵션 활성화
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
관련 문서
커널 개발 시 주의사항과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.