1000sj
SJ CODE
1000sj
전체 방문자
오늘
어제
  • 분류 전체보기
    • Algorithms
      • Crypto
      • Formal Methods
    • Security
      • Fuzzing
      • Exploit
    • System Programming
      • Kernel
      • Compiler
      • Device Driver
      • Emulator
      • Assembly
      • Memory
      • Network
    • Architecture
      • ARM
      • RISC-V
    • Cloud Computing
      • Infrastructure
      • SDN
    • TroubleShooting
      • Debugging
      • Testing
    • Performance improvements
      • Parrelel Processing
      • HPC
    • ETC
      • 문화 생활
      • 커뮤니티

인기 글

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
1000sj

SJ CODE

Security/Fuzzing

Linux KCOV(Kernel Coverage)

2026. 1. 2. 20:34

KCOV란

커널 코드에 컴파일 시점에 계측(instrumentation)을 삽입해서, 테스트 케이스가 어떤 코드 경로를 실행했는지 알려주는 도구이다.

Syzkaller 같은 커버리지 기반 퍼저가 "흥미로운" 테스트 케이스(새로운 코드 경로를 발견한 케이스)를 판별하는 데 주로 사용된다.

 

1. 컴파일 시점 계측

KCOV는 컴파일러가 자동으로 커널 코드에 추적 함수를 삽입하는 방식이다. 개발자가 직접 코드를 수정할 필요가 없다.

 

GCC/Clang의 -fsanitize-coverage=trace-pc 플래그를 사용하면 컴파일러가 각 기본 블록(basic block) 시작점에 __sanitizer_cov_trace_pc() 함수 호출을 삽입한다.

이 함수가 호출되면 해당 위치의 주소(PC)를 공유 메모리에 기록한다.

 

1단계: 컴파일러 플래그

-fsanitize-coverage=trace-pc

 

이 플래그를 주면 GCC/Clang이 각 기본 블록(basic block) 시작 부분에 함수 호출을 삽입한다.

기본 블록이란

 

  • 진입점 하나, 출구점 하나, 중간에 분기 없는 코드 덩어리
  • 블록의 일부가 실행되면 전체가 실행된 것으로 간주 가능
  • 그래서 블록 시작점에만 계측하면 충분함

 

2단계: 삽입되는 함수

컴파일러가 삽입하는 건 호출뿐이고, 실제 함수 구현은 커널(kernel/kcov.c)에 있다.

void notrace __sanitizer_cov_trace_pc(void)
{
    struct task_struct *t;
    unsigned long *area;
    unsigned long ip = canonicalize_ip(_RET_IP_);  // 이 함수의 리턴 주소 = 호출 위치
    unsigned long pos;

    t = current;
    // 이 스레드에서 KCOV가 켜져 있는지 확인
    if (!check_kcov_mode(KCOV_MODE_TRACE_PC, t))
        return;  // 아니면 즉시 리턴

    area = t->kcov_area;
    // area[0]에는 지금까지 수집된 PC 개수가 저장됨
    pos = READ_ONCE(area[0]) + 1;
    if (likely(pos < t->kcov_size)) {
        WRITE_ONCE(area[0], pos);
        barrier();
        area[pos] = ip;  // 새 PC 기록
    }
}

 

 

실제 예시

원본 코드가 이렇다면

void some_function(int x) {
    if (x > 0) {
        do_something();
    } else {
        do_other();
    }
}

 

컴파일 후 실제로는 이렇게 된다.

void some_function(int x) {
    __sanitizer_cov_trace_pc();  // 블록 1 진입
    if (x > 0) {
        __sanitizer_cov_trace_pc();  // 블록 2 진입
        do_something();
    } else {
        __sanitizer_cov_trace_pc();  // 블록 3 진입
        do_other();
    }
}

 

핵심 포인트

항목 설명
_RET_IP_ 이 함수를 호출한 위치의 주소 (= 어느 기본 블록인지 식별)
t->kcov_area 유저 공간과 공유하는 메모리 버퍼
area[0] 수집된 PC 개수
area[1..n] 실제 PC 주소들

 

KCOV가 꺼져 있으면 check_kcov_mode()에서 바로 리턴하므로 오버헤드가 매우 적다.

 

2. 커널 설정 (Kconfig)

KCOV 관련 설정들

CONFIG_KCOV_ENABLE_COMPARISONS=y # 비교문/분기문 주변에 추가 계측 삽입
CONFIG_KCOV_IRQ_AREA_SIZE=0x40000 # 소프트 인터럽트에서 커버리지 수집용 per-CPU 버퍼 크기
CONFIG_DEBUG_INFO_DWARF4=y       # DWARF 디버그 정보를 커널에 포함
CONFIG_KCOV=y                    # KCOV 활성화
CONFIG_KCOV_INSTRUMENT_ALL=y     # 전체 커널 계측
CONFIG_DEBUG_FS=y                # debugfs 활성화
CONFIG_RANDOMIZE_BASE=n          # KASLR 비활성화 (디버깅 편의), 켜면 부팅마다 커널 주소가 랜덤하게 바뀜

 

3. 커버리지 수집 방법

커버리지 수집은 로컬과 리모트 두 가지 방식이 있다.

 

Local Coverage

현재 스레드에서 발생하는 커버리지를 수집한다.

KCOV는 스레드 단위로 동작한다. __sanitizer_cov_trace_pc()는 현재 스레드에서 KCOV가 켜져 있는지 확인하고, 아니면 바로 리턴한다. 

유저 공간에서 시스템 콜을 하면 스레드 ID(PID/TID)가 유지된 채로 커널로 진입하므로, KCOV가 커널 코드 실행을 추적할 수 있다.

 

1단계: 헤더와 상수 정의

#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>

#define KCOV_INIT_TRACE   _IOR('c', 1, unsigned long)
#define KCOV_ENABLE       _IO('c', 100)
#define KCOV_DISABLE      _IO('c', 101)
#define COVER_SIZE        (64<<10)    // 64K 엔트리

#define KCOV_TRACE_PC     0

 

  • ioctl 명령어들은 커널의 include/uapi/linux/kcov.h와 동일
  • COVER_SIZE는 수집할 PC 개수 (필요에 따라 증가 가능)

 

2단계: KCOV 파일 열기

int fd = open("/sys/kernel/debug/kcov", O_RDWR);
if (fd < 0)
    perror("open"), exit(1);

 

  • debugfs의 KCOV 파일을 열어서 커널과 통신 채널 생성
  • 파일이 없으면: mount -t debugfs none /sys/kernel/debug

 

3단계: 버퍼 초기화

if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
    perror("ioctl$KCOV_INIT_TRACE"), exit(1);

 

  • 커널 메모리에 커버리지 버퍼 할당
  • KCOV를 trace 모드로 설정

 

4단계: 공유 메모리 매핑

unsigned long *cover = (unsigned long*)mmap(
    NULL,
    COVER_SIZE * sizeof(unsigned long),
    PROT_READ | PROT_WRITE,
    MAP_SHARED,
    fd,
    0
);
if ((void*)cover == MAP_FAILED)
    perror("mmap"), exit(1);

 

 

  • 커널이 할당한 버퍼를 유저 공간에 매핑
  • 이제 cover 배열로 커버리지 데이터를 직접 읽을 수 있음

 

 

5단계: KCOV 활성화

if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC))
    perror("ioctl$KCOV_ENABLE"), exit(1);

__atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);

 

 

  • 이 스레드에서 KCOV 수집 시작
  • cover[0]을 0으로 리셋 (이전 ioctl에서 발생한 커버리지 무시)

 

6단계: 시스템 콜 실행

// 여기서 테스트하고 싶은 시스템 콜 실행
read(fd, buf, size);
write(fd, buf, size);
// 등등...

 

 

이 시스템 콜들이 커널에서 실행될 때 KCOV가 PC를 수집한다.

 

7단계: 커버리지 읽기

// 1. 수집된 PC 개수 확인
unsigned long n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);

// 2. 유저 공간에 작업 버퍼 할당
unsigned long *work_cover = (unsigned long *)malloc(COVER_SIZE * sizeof(unsigned long));

// 3. 공유 버퍼 → 작업 버퍼로 복사
memcpy(work_cover, *covermem, n);

// 4. 공유 버퍼 리셋 (다음 수집을 위해)
__atomic_store_n(&(*covermem)[0], 0, __ATOMIC_RELAXED);

// 5. 복사본에서 PC 출력
// work_cover[0]: 개수
// work_cover[1..n]: 실제 PC들
for (int i = 0; i < n; i++)
    printf("0x%lx\n", cover[i + 1]);
    
free(work_cover);
work_cover = NULL;

 

  • cover[0]: 수집된 PC 개수
  • cover[1..n]: 실제 PC 주소들

 

8단계: 정리

if (ioctl(fd, KCOV_DISABLE, 0))
    perror("ioctl$KCOV_DISABLE"), exit(1);

munmap(cover, COVER_SIZE * sizeof(unsigned long));
close(fd);

 

또는 프로그램 종료 시 KCOV가 알아서 정리한다.

 

 

 

Remote Coverage (백그라운드 스레드, 워커, softirq 등)

백그라운드 스레드, 워커 스레드, softirq 등 다른 컨텍스트의 커버리지를 수집한다.

로컬 커버리지는 현재 스레드만 추적합니다. 하지만 커널에는 워커 스레드, softirq 핸들러, USB 드라이버 스레드등 별도로 실행되는 코드가 많다.

 

Handle 시스템

리모트 KCOV는 핸들로 어떤 스레드의 커버리지를 수집할지 식별한다.

 

서브시스템 종류

subsystem 값 용도
KCOV_SUBSYSTEM_COMMON 0x00 일반 워커 스레드
KCOV_SUBSYSTEM_USB 0x01 USB 드라이버

 

 

ID 결정 방법

 

  • COMMON: 유저가 임의로 지정, task_struct로 전파됨
  • USB: USB 버스 번호

 

핸들 생성 함수

static inline __u64 kcov_remote_handle(__u64 subsys, __u64 inst)
{
    if (subsys & ~KCOV_SUBSYSTEM_MASK || inst & ~KCOV_INSTANCE_MASK)
        return 0;
    return subsys | inst;
}

 

 

1단계: 추가 상수와 구조체 정의

struct kcov_remote_arg {
    __u32           trace_mode;
    __u32           area_size;
    __u32           num_handles;
    __aligned_u64   common_handle;
    __aligned_u64   handles[0];    // 가변 길이 배열
};

#define KCOV_REMOTE_ENABLE  _IOW('c', 102, struct kcov_remote_arg)

#define KCOV_SUBSYSTEM_COMMON  (0x00ull << 56)
#define KCOV_SUBSYSTEM_USB     (0x01ull << 56)

#define KCOV_COMMON_ID    0x42
#define KCOV_USB_BUS_NUM  1
  • KCOV_REMOTE_ENABLE ioctl 사용
  • 현재 지원: KCOV_SUBSYSTEM_COMMON, KCOV_SUBSYSTEM_USB
    • 리모트 KCOV가 작동하려면 커널 개발자가 직접 해당 코드에 kcov_remote_start/stop()을 삽입해야 함 
    • 현재 mainline 커널에는 다음 두 곳에만 삽입되어 있음
    • COMMON: vhost 워커 등 일부 워커 스레드
    • USB: USB 드라이버

 

2단계: 초기 설정 (로컬과 동일)

int fd = open("/sys/kernel/debug/kcov", O_RDWR);
ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE);
unsigned long *cover = mmap(...);

 

3단계: 리모트 인자 구성

struct kcov_remote_arg *arg;
// 구조체 + 핸들 1개 공간 할당
arg = calloc(1, sizeof(*arg) + sizeof(uint64_t));

arg->trace_mode = KCOV_TRACE_PC;
arg->area_size = COVER_SIZE;
arg->num_handles = 1;  // uncommon 핸들 개수 (USB)

// COMMON 핸들: 워커 스레드용
arg->common_handle = kcov_remote_handle(KCOV_SUBSYSTEM_COMMON, KCOV_COMMON_ID);

// USB 핸들: 버스 1번
arg->handles[0] = kcov_remote_handle(KCOV_SUBSYSTEM_USB, KCOV_USB_BUS_NUM);
  • 핸들(handle) 시스템으로 어떤 스레드의 커버리지를 수집할지 지정

 

4단계: 리모트 KCOV 활성화

if (ioctl(fd, KCOV_REMOTE_ENABLE, arg))
    perror("ioctl$KCOV_REMOTE_ENABLE"), exit(1);
free(arg);  // 활성화 후 해제 가능

 

 

5단계: 커버리지 읽기

로컬과 동일하게 cover 배열에서 읽는다.

unsigned long n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
for (int i = 0; i < n; i++)
    printf("0x%lx\n", cover[i + 1]);

 

 

COMMON의 경우 핸들이 전파되는 방식은 다음과 같다.

user가 vhost 디바이스를 열면

int fd = open("/dev/vhost-net", O_RDWR);

 

이때 현재 태스크의 kcov_handle이 vhost 워커에 복사된다.

current->kcov_handle → worker->kcov_handle

 

 

커널 코드에서 kcov_remote_start(handle)이 호출되면

// drivers/vhost/vhost.c 
// vhost_worker(): VM I/O 처리하는 워커 스레드의 메인 루프
static int vhost_worker(void *data)
{
    ...
    kcov_remote_start_common(worker->kcov_handle);
    // 작업 수행
    kcov_remote_stop();
    ...
}

// 스레드에서 커버리지 수집 start 함수
void kcov_remote_start(u64 handle)
{
    // 1. 이 핸들이 등록되어 있나?
    kcov = kcov_remote_find(handle);
    if (!kcov)
        return;  // 등록 안 됨 → 무시

    // 2. 등록되어 있으면 현재 스레드에서 KCOV 활성화
    t = current;
    t->kcov = kcov;
    t->kcov_mode = KCOV_MODE_TRACE_PC;
    t->kcov_area = kcov->area;  // 유저와 공유하는 버퍼
    ...
}

 

이후 __sanitizer_cov_trace_pc()가 호출되면

void __sanitizer_cov_trace_pc(void)
{
    t = current;
    
    // kcov_remote_start()가 설정해둔 값 확인
    if (!check_kcov_mode(KCOV_MODE_TRACE_PC, t))
        return;
    
    // PC 기록!
    area = t->kcov_area;
    area[++area[0]] = _RET_IP_;
}

 

  1. 전달된 핸들이 등록되어 있는지 확인
  2. 등록되어 있으면 해당 스레드에서 KCOV 활성화
  3. kcov_remote_stop() 호출 시 비활성화

 

kcov_remote_start/stop은 그냥 스위치이다. 실제 일은 vhost_worker가 한다.

 

 

커스텀 서브시스템 추가

IPv6 스택의 커버리지를 수집하기 위해 직접 커널을 수정해보자.

문제는 IPv6용 서브시스템이 mainline 커널에 없어서 직접 만들어야한다.

 

수정 1: 서브시스템 ID 정의

// include/uapi/linux/kcov.h

#define KCOV_SUBSYSTEM_COMMON  (0x00ull << 56)
#define KCOV_SUBSYSTEM_USB     (0x01ull << 56)
#define KCOV_SUBSYSTEM_IPV6    (0x02ull << 56)  // ← 새로 추가!

 

수정 2: 헬퍼 함수 추가

// include/linux/kcov.h

// 일반 컨텍스트용
static inline void kcov_remote_start_ipv6(u64 id)
{
    kcov_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_IPV6, id));
}

// softirq 전용 (중복 시작 방지)
static inline void kcov_remote_start_ipv6_softirq(u64 id)
{
    if (in_serving_softirq()) {  // softirq 안에서만 실행
        kcov_remote_start(kcov_remote_handle(KCOV_SUBSYSTEM_IPV6, id));
    }
}

 

왜 softirq 체크가 필요한가?

 

  • https://ics.uci.edu/~jbursey/kcov-for-dummies.html
  • ㄴㅇㄹㄴㅇㄹ

 

 

'Security > Fuzzing' 카테고리의 다른 글

LLVM Sanitizer 작성하는 법  (0) 2026.02.04
syzbot, qemu, gdb를 사용하여 linux kernel의 버그 수정  (0) 2025.12.12
syzkaller를 이용한 커널 퍼징 #4 실제 커널 퍼징  (0) 2025.12.11
syzkaller를 이용한 커널 퍼징 #3 syz-manager 설정 및 syz-executor 실행  (0) 2025.12.10
syzkaller를 이용한 커널 퍼징 #2 Syzkaller의 동작 구조  (0) 2025.12.10
    'Security/Fuzzing' 카테고리의 다른 글
    • LLVM Sanitizer 작성하는 법
    • syzbot, qemu, gdb를 사용하여 linux kernel의 버그 수정
    • syzkaller를 이용한 커널 퍼징 #4 실제 커널 퍼징
    • syzkaller를 이용한 커널 퍼징 #3 syz-manager 설정 및 syz-executor 실행
    1000sj
    1000sj

    티스토리툴바