커널 Livepatch

커널 Livepatch를 무중단 보안 패치와 운영 리스크 최소화 관점에서 심층 분석합니다. klp_patch/klp_object/klp_func 구조와 ftrace 기반 함수 교체 메커니즘, consistency model과 태스크 전환 보장, shadow variable 활용과 데이터 호환성 관리, atomic replace 전략, 패치 적용·롤백 절차, 배포 자동화와 검증 파이프라인, 충돌 가능 모듈/심볼 의존성 점검, 실서비스에서의 실패 대응과 모니터링 포인트까지 실전 운영 지침을 다룹니다.

전제 조건: 패치 제출커널 보안 문서를 먼저 읽으세요. Livepatch는 재부팅 없이 코드 교체를 수행하므로 함수 전환 시점 일관성과 롤백 전략을 먼저 설계해야 운영 리스크를 줄일 수 있습니다.
일상 비유: 이 주제는 가동 중 설비의 무정지 부품 교체와 비슷합니다. 설비를 멈추지 않고 부품을 바꾸려면 전환 순간 안전장치가 필요하듯이, 커널도 적용/해제 경계를 엄격히 관리해야 합니다.

핵심 요약

  • 재현 우선 — 증상보다 재현 조건 고정이 먼저입니다.
  • 도구 선택 — 로그, 트레이스, 코어덤프 용도를 분리합니다.
  • 증거 보존 — 원인 추적 전 관측 데이터를 먼저 확보합니다.
  • 오버헤드 관리 — 추적 범위를 최소화해 왜곡을 줄입니다.
  • 사후 검증 — 수정 후 동일 조건에서 재발 여부를 확인합니다.

단계별 이해

  1. 재현 시나리오 정의
    입력 조건과 타이밍을 고정합니다.
  2. 관측 포인트 배치
    핵심 함수/이벤트만 선별 추적합니다.
  3. 원인 축소
    가설을 하나씩 배제하며 범위를 줄입니다.
  4. 수정 검증
    회귀 테스트와 운영 지표를 함께 확인합니다.
관련 표준: Kernel Livepatch Interface (kernel.org) — 커널 라이브 패칭 인터페이스 공식 문서입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 페이지: 기본 디버깅 도구와 크래시 분석은 디버깅 & 트러블슈팅, 크래시 분석 심화 페이지를 참고하세요.

커널 Livepatch

Livepatch(커널 라이브 패칭)는 시스템 재부팅 없이 실행 중인 커널의 함수를 동적으로 교체하는 기술입니다. 보안 취약점 긴급 수정, 고가용성 서버 운영, 장기 실행 워크로드 보호 등에 활용됩니다. 리눅스 커널 4.0부터 CONFIG_LIVEPATCH로 공식 지원합니다.

Livepatch 아키텍처

Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능을 기반으로 동작합니다. 패치 대상 함수의 진입점에 ftrace 훅을 설치하고, 함수 호출 시 instruction pointer(IP)를 새 함수로 리다이렉트합니다.

원본 함수 old_func() ftrace trampoline klp_ftrace_handler() 패치 함수 new_func() NOP → call IP 변경 Consistency Model (전환 과정) Task A [old universe] Task B [in transition] Task C [new universe] All Tasks [patched] patched_func에 스택 프레임 있음 안전 지점 대기 (voluntary preempt) 전환 완료 new_func 사용 모든 태스크가 new universe klp_transition_patch() → 각 태스크가 안전 지점 도달 시 universe 전환
Consistency Model: Livepatch는 per-task consistency 모델을 사용합니다. 각 태스크는 "old universe"(원본 함수) 또는 "new universe"(패치 함수) 중 하나에 속하며, 태스크가 패치 대상 함수의 스택 프레임을 벗어나는 안전한 지점(voluntary preemption, syscall 경계 등)에 도달했을 때 전환됩니다.

ftrace 기반 명령어 수준 동작

Livepatch의 핵심은 ftrace의 동적 명령어 패칭입니다. gcc -pg 또는 -mfentry 옵션으로 컴파일된 커널 함수는 진입점에 call __fentry__ 명령어가 삽입됩니다. 부팅 시 이 명령어들은 NOP로 교체되어 오버헤드가 제거되고, ftrace 활성화 시 다시 call ftrace_caller로 복원됩니다.

; === x86_64에서의 함수 진입점 변환 과정 ===

; 1. 컴파일 직후 (gcc -mfentry)
original_func:
    call __fentry__          ; 5바이트: E8 xx xx xx xx
    push rbp
    mov  rbp, rsp
    ...

; 2. 부팅 시 ftrace 초기화 (ftrace_init)
original_func:
    nop DWORD ptr [rax+rax]  ; 5바이트 NOP: 0F 1F 44 00 00
    push rbp
    mov  rbp, rsp
    ...

; 3. Livepatch 활성화 시 (ftrace_modify_code → text_poke_bp)
original_func:
    call ftrace_caller       ; 5바이트: E8 xx xx xx xx
    push rbp                 ; ← ftrace_caller가 여기로 리턴
    mov  rbp, rsp
    ...

; ftrace_caller 내부에서 klp_ftrace_handler() 호출
; → handler가 regs->ip를 new_func 주소로 변경
; → ftrace_caller가 RET 대신 변경된 IP로 점프
; → new_func()이 실행됨 (원본 함수의 본문은 실행되지 않음)
/* klp_ftrace_handler (kernel/livepatch/patch.c) 핵심 로직 */
static void klp_ftrace_handler(unsigned long ip,
                               unsigned long parent_ip,
                               struct ftrace_ops *fops,
                               struct ftrace_regs *fregs)
{
    struct klp_ops *ops;
    struct klp_func *func;
    int patch_state;

    ops = container_of(fops, struct klp_ops, fops);

    /*
     * per-task consistency: 현재 태스크의 universe 확인
     * patch_state가 KLP_TRANSITION_PATCHED이면 new_func 사용
     * 아니면 old_func(nop) 사용 → 원본 함수 그대로 실행
     */
    patch_state = klp_get_patch_state(current);

    if (patch_state == KLP_UNDEFINED) {
        /* 전환 중이 아니면 최신 패치 함수 사용 */
        func = list_last_entry(&ops->func_stack,
                               struct klp_func, stack_node);
    } else {
        /* 전환 중: 태스크의 universe에 맞는 함수 선택 */
        func = list_first_or_last_entry(
            &ops->func_stack, struct klp_func, stack_node,
            patch_state == klp_target_state);
    }

    if (func->nop)
        return;  /* NOP 패치: 원본 함수 실행 */

    /* IP를 새 함수 주소로 변경 → 리턴 시 new_func으로 점프 */
    klp_arch_set_pc(fregs, (unsigned long)func->new_func);
}
1. 컴파일 직후 func: call __fentry__ ; E8 xx xx xx xx push rbp mov rbp, rsp boot 2. 부팅 (NOP 교체) func: nop DWORD [..] ; 0F 1F 44 00 00 push rbp mov rbp, rsp klp 3. Livepatch 활성 func: call ftrace_caller ; E8 xx xx xx xx push rbp mov rbp, rsp Livepatch 호출 흐름 (Phase 3 상세) caller() call original_func original_func call ftrace_caller klp_ftrace_handler regs->ip = new_func new_func() 패치된 로직 실행 원본 함수의 push rbp 이후 코드는 실행되지 않음 new_func()이 caller()에게 직접 리턴 text_poke_bp(): INT3 삽입 → IPI 동기화 → 최종 명령어 교체 (원자적)
text_poke_bp(): x86에서 코드 수정은 text_poke_bp()를 통해 수행됩니다. 이 함수는 먼저 대상 위치의 첫 바이트를 INT3(0xCC, breakpoint)로 교체하고, 모든 CPU에 IPI(Inter-Processor Interrupt)를 보내 instruction cache를 동기화합니다. 그 후 나머지 바이트를 수정하고, 마지막으로 첫 바이트를 최종 값으로 교체합니다. 이 과정은 원자적이어서 다른 CPU가 반쯤 수정된 명령어를 실행하는 것을 방지합니다. INT3을 실행한 CPU는 bp_patching_in_progress를 확인하고 올바른 명령어로 에뮬레이션합니다.

커널 CONFIG 옵션

# 필수 옵션
CONFIG_LIVEPATCH=y          # Livepatch 프레임워크 활성화
CONFIG_FTRACE=y              # ftrace 기반 (필수 의존성)
CONFIG_DYNAMIC_FTRACE=y      # 동적 ftrace (필수 의존성)
CONFIG_MODULES=y             # 모듈 지원 (패치를 모듈로 로드)
CONFIG_KALLSYMS_ALL=y        # 전체 심볼 해석 (권장)

# 스택 검증 관련 (안전한 전환에 필수)
CONFIG_HAVE_RELIABLE_STACKTRACE=y  # 신뢰성 있는 스택 트레이스
CONFIG_STACKTRACE=y                # 스택 트레이스 지원
CONFIG_FRAME_POINTER=y             # 또는 ORC unwinder (x86_64)
CONFIG_UNWINDER_ORC=y              # ORC unwinder (더 정확하고 성능 좋음)

# 아키텍처 지원 확인
# x86_64, s390, ppc64le에서 완전 지원
# arm64는 커널 6.x부터 지원

Livepatch API 핵심 구조체

#include <linux/livepatch.h>

/**
 * klp_func - 패치할 개별 함수 정의
 * @old_name:  교체할 원본 함수의 심볼 이름
 * @new_func:  대체할 새 함수의 포인터
 * @old_sympos: 동명 함수가 여러 개일 때 위치 (0=유일, 1=첫 번째, ...)
 * @nop:       true이면 NOP 패치 (atomic replace에서 활용)
 */
struct klp_func {
    const char *old_name;
    void *new_func;
    unsigned long old_sympos;
    bool nop;

    /* 내부 필드 (커널이 관리) */
    unsigned long old_func;        /* 런타임에 해석됨 */
    struct kobject kobj;
    struct list_head node;
    struct list_head stack_node;
    unsigned long old_size, new_size;
    bool patched;
    bool transition;
};

/**
 * klp_object - 패치 대상 오브젝트 (vmlinux 또는 모듈)
 * @name:   대상 모듈 이름 (NULL이면 vmlinux)
 * @funcs:  이 오브젝트에서 패치할 함수 배열 (sentinel 종료)
 */
struct klp_object {
    const char *name;
    struct klp_func *funcs;

    /* 내부 필드 */
    struct kobject kobj;
    struct list_head func_list;
    struct list_head node;
    struct module *mod;
    bool dynamic;
    bool patched;
};

/**
 * klp_patch - 라이브 패치 최상위 구조체
 * @mod:     패치 모듈 자신
 * @objs:    패치 대상 오브젝트 배열 (sentinel 종료)
 * @replace: true이면 이전 패치들을 모두 대체 (atomic replace)
 */
struct klp_patch {
    struct module *mod;
    struct klp_object *objs;
    bool replace;

    /* 내부 필드 */
    struct list_head list;
    struct kobject kobj;
    bool enabled;
    bool forced;
    struct work_struct free_work;
    struct completion finish;
};

기본 Livepatch 모듈 작성

/*
 * livepatch-sample.c - 커널 함수를 런타임에 교체하는 예제
 * cmdline_proc_show()를 패치하여 /proc/cmdline 출력을 변경
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/seq_file.h>

/* 새 함수: 원본과 동일한 시그니처 필수 */
static int livepatch_cmdline_proc_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%s [LIVEPATCHED]\\n", saved_command_line);
    return 0;
}

/* 패치할 함수 목록 (sentinel으로 빈 항목 필수) */
static struct klp_func funcs[] = {
    {
        .old_name = "cmdline_proc_show",
        .new_func = livepatch_cmdline_proc_show,
    },
    { }  /* sentinel */
};

/* 패치 대상 오브젝트: NULL name = vmlinux */
static struct klp_object objs[] = {
    {
        .name  = NULL,    /* vmlinux (커널 코어) */
        .funcs = funcs,
    },
    { }  /* sentinel */
};

static struct klp_patch patch = {
    .mod  = THIS_MODULE,
    .objs = objs,
};

/* 모듈 로드 시 패치 활성화 */
static int __init livepatch_init(void)
{
    return klp_enable_patch(&patch);
}

/* 모듈 언로드 시 자동 복원 (커널이 처리) */
static void __exit livepatch_exit(void)
{
    /* klp_unpatch는 모듈 제거 시 커널이 자동 호출 */
}

module_init(livepatch_init);
module_exit(livepatch_exit);

MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");  /* 필수: livepatch 모듈 표시 */
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Livepatch sample: cmdline_proc_show");
MODULE_INFO(livepatch, "Y"): 이 선언이 없으면 커널이 해당 모듈을 livepatch 모듈로 인식하지 못하며, klp_enable_patch() 호출이 -EINVAL로 실패합니다. 반드시 포함해야 합니다.

Livepatch 모듈 Makefile

# Makefile for livepatch module
obj-m += livepatch-sample.o

# 커널 빌드 디렉터리
KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

모듈 함수 패치

vmlinux뿐만 아니라 로드된 커널 모듈의 함수도 패치할 수 있습니다. klp_object.name에 대상 모듈 이름을 지정합니다.

/* ext4 모듈의 함수를 패치하는 예제 */
static int patched_ext4_file_write_iter(
    struct kiocb *iocb, struct iov_iter *from)
{
    /* 원본 함수 호출 전 추가 검증 로직 */
    struct inode *inode = file_inode(iocb->ki_filp);

    if (iov_iter_count(from) > (1ULL << 30)) {
        pr_warn_ratelimited("ext4: unusually large write %zu on ino %lu\\n",
                            iov_iter_count(from), inode->i_ino);
    }

    /* klp_shadow 또는 직접 심볼 참조로 원본 호출 가능 */
    return ext4_file_write_iter(iocb, from);
}

static struct klp_func ext4_funcs[] = {
    {
        .old_name = "ext4_file_write_iter",
        .new_func = patched_ext4_file_write_iter,
    },
    { }
};

static struct klp_object objs[] = {
    {
        .name  = "ext4",     /* 대상 모듈 이름 */
        .funcs = ext4_funcs,
    },
    { }
};
모듈 로딩 순서: 패치 대상 모듈(ext4)이 아직 로드되지 않은 상태에서도 livepatch 모듈을 로드할 수 있습니다. 대상 모듈이 나중에 로드되면 커널이 자동으로 패치를 적용합니다. 대상 모듈이 언로드되면 패치도 자동으로 비활성화됩니다.

Shadow Variable (klp_shadow)

패치 함수에서 원본 구조체에 없는 추가 데이터를 저장해야 할 때 klp_shadow_* API를 사용합니다. 이는 원본 객체에 "그림자" 데이터를 연결하는 해시 테이블 기반 메커니즘입니다.

#include <linux/livepatch.h>

#define SV_LEAK_TRACKER  1   /* shadow variable ID (패치별 고유) */

struct leak_info {
    u64 alloc_time;
    unsigned long alloc_site;
    size_t size;
};

/* 생성자: shadow variable 할당 시 호출 */
static int leak_info_ctor(void *obj, void *shadow_data, void *ctor_data)
{
    struct leak_info *info = shadow_data;
    info->alloc_time = ktime_get_ns();
    info->alloc_site = (unsigned long)ctor_data;
    info->size = 0;
    return 0;
}

/* 패치 함수에서 shadow variable 사용 */
static void *patched_kmalloc(size_t size, gfp_t flags)
{
    void *ptr = kmalloc(size, flags);
    if (ptr) {
        struct leak_info *info;

        /* shadow variable 생성 및 연결 */
        info = klp_shadow_get_or_alloc(
            ptr,                        /* 연결할 원본 객체 */
            SV_LEAK_TRACKER,             /* shadow variable ID */
            sizeof(*info),               /* shadow data 크기 */
            GFP_KERNEL,
            leak_info_ctor,              /* 생성자 */
            (void *)_RET_IP_              /* 생성자에 전달할 데이터 */
        );
        if (info)
            info->size = size;
    }
    return ptr;
}

static void patched_kfree(const void *ptr)
{
    if (ptr) {
        struct leak_info *info;

        /* shadow variable 조회 */
        info = klp_shadow_get(ptr, SV_LEAK_TRACKER);
        if (info) {
            pr_debug("free: obj=%p size=%zu alive=%llu ns\\n",
                     ptr, info->size,
                     ktime_get_ns() - info->alloc_time);

            /* shadow variable 해제 */
            klp_shadow_free(ptr, SV_LEAK_TRACKER, NULL);
        }
    }
    kfree(ptr);
}
API설명
klp_shadow_get(obj, id)기존 shadow variable 조회 (없으면 NULL)
klp_shadow_alloc(obj, id, size, gfp, ctor, ctor_data)새 shadow variable 생성 (이미 존재하면 NULL)
klp_shadow_get_or_alloc(obj, id, size, gfp, ctor, ctor_data)조회 후 없으면 생성
klp_shadow_free(obj, id, dtor)특정 shadow variable 해제
klp_shadow_free_all(id, dtor)특정 ID의 모든 shadow variable 일괄 해제

Atomic Replace (누적 패치 관리)

여러 livepatch가 순차적으로 적용되면 패치 스택이 누적됩니다. replace 플래그를 사용하면 기존 모든 패치를 단일 패치로 원자적으로 대체할 수 있어 패치 관리가 간소화됩니다.

/* Atomic replace 패치: 기존 모든 livepatch를 대체 */
static struct klp_patch cumulative_patch = {
    .mod     = THIS_MODULE,
    .objs    = objs,
    .replace = true,   /* 핵심: 이전 패치 모두 대체 */
};

/*
 * replace=true의 동작:
 *
 * 1. 이 패치가 활성화되면 기존 모든 livepatch가 비활성화됨
 * 2. 이 패치에 포함되지 않은 함수는 원본으로 복원됨
 * 3. 전환은 per-task consistency model을 따름
 *
 * 사용 시나리오:
 * - 보안 패치 v1 적용 후 v2로 업그레이드
 * - 여러 개별 패치를 하나의 누적 패치로 통합
 * - 패치 완전 제거 (빈 패치에 replace=true)
 */
패치 완전 제거: 모든 livepatch를 제거하고 원본 상태로 돌아가려면, 빈 함수 목록에 replace=true를 설정한 패치를 적용하면 됩니다. 이렇게 하면 기존 모든 패치가 원자적으로 비활성화됩니다.

동명 함수 처리 (old_sympos)

커널에는 static 함수 중 이름이 같은 것들이 다른 파일에 존재할 수 있습니다. old_sympos로 어떤 함수를 패치할지 지정합니다.

/*
 * 예: 커널 내에 cleanup()이라는 static 함수가 3개 존재
 *
 * kallsyms에서 확인:
 * $ grep ' cleanup$' /proc/kallsyms
 * ffffffff81234560 t cleanup    [drivers/foo/bar.c]  ← sympos=1
 * ffffffff81234780 t cleanup    [drivers/baz/qux.c]  ← sympos=2
 * ffffffff81234900 t cleanup    [net/core/sock.c]    ← sympos=3
 */
static struct klp_func funcs[] = {
    {
        .old_name   = "cleanup",
        .new_func   = patched_cleanup,
        .old_sympos = 2,   /* 두 번째 cleanup (drivers/baz/qux.c) */
    },
    { }
};
/*
 * old_sympos 값:
 *   0 = 유일한 심볼 (동명이 있으면 에러)
 *   1 = kallsyms 순서로 첫 번째
 *   N = kallsyms 순서로 N번째
 */

sysfs 인터페이스와 운영

# Livepatch 모듈 로드 (패치 자동 활성화)
insmod livepatch-sample.ko

# sysfs에서 패치 상태 확인
ls /sys/kernel/livepatch/
# livepatch_sample/

# 패치 활성화 상태
cat /sys/kernel/livepatch/livepatch_sample/enabled
# 1

# 전환(transition) 진행 중인지 확인
cat /sys/kernel/livepatch/livepatch_sample/transition
# 0 (완료) / 1 (진행 중)

# 강제 전환 (전환이 멈춘 경우 — 주의: 일관성 보장 안 됨)
echo 1 > /sys/kernel/livepatch/livepatch_sample/force

# 패치 비활성화 (원본 함수로 복원 전환 시작)
echo 0 > /sys/kernel/livepatch/livepatch_sample/enabled

# 비활성화 후 모듈 제거
rmmod livepatch-sample

# 개별 함수 패치 상태 확인
cat /sys/kernel/livepatch/livepatch_sample/vmlinux/cmdline_proc_show/transition
# 전환 상태 모니터링: 아직 old universe에 있는 태스크 확인
for pid in /proc/[0-9]*; do
    patch_state=$(cat "$pid/patch_state" 2>/dev/null)
    if [ "$patch_state" = "-1" ]; then
        comm=$(cat "$pid/comm" 2>/dev/null)
        echo "Blocking: PID=$(basename $pid) COMM=$comm"
    fi
done

# /proc/<pid>/patch_state 값:
#  -1 = old universe (전환 대기 중)
#   0 = undefined (전환 진행 안 됨)
#   1 = new universe (전환 완료)

전환 메커니즘 상세

Livepatch 전환은 각 태스크가 안전한 지점에 도달할 때까지 대기하는 과정입니다. 안전한 지점이란 패치 대상 함수가 해당 태스크의 스택에 존재하지 않는 상태를 의미합니다.

설명 요약:
  • 전환 흐름 (kernel/livepatch/transition.c):
  • klp_enable_patch()
  • → klp_init_patch() : 패치 구조체 초기화, sysfs 등록
  • → klp_try_enable_patch() : ftrace 훅 설치
  • → klp_start_transition() : 전환 시작
  • → 모든 태스크를 TIF_PATCH_PENDING 플래그 설정
  • → klp_target_state = KLP_PATCHED
  • 각 태스크 (스케줄러에서):
  • schedule()
  • → __schedule()
  • → klp_update_patch_state(current)
  • → klp_try_switch_task(current)
  • → klp_check_stack() : reliable stacktrace로 스택 검사
  • → 패치 대상 함수가 스택에 없으면:
  • → 태스크를 new universe로 전환
  • → TIF_PATCH_PENDING 해제
  • → 스택에 있으면:
  • → 다음 스케줄링까지 대기
  • 모든 태스크 전환 완료:
  • klp_complete_transition()
  • → klp_unpatch_replaced_patches() (replace=true일 때)
  • → klp_clear_object_relocations()
  • → 전환 완료
전환 지연 원인: 다음과 같은 태스크는 전환을 지연시킬 수 있습니다:
  • TASK_IDLE 상태 CPU: idle 루프의 스택이 간단하여 보통 빠르게 전환
  • 장기 sleep (TASK_UNINTERRUPTIBLE): 패치 대상 함수 내부에서 I/O 대기 중일 때
  • RT 태스크: 선점이 억제된 상태에서 패치 대상 함수 실행 중
  • kthread: cond_resched()를 드물게 호출하는 커널 스레드
이런 경우 /proc/<pid>/patch_state로 블로킹 태스크를 식별하고, 시그널 전송이나 force 전환을 고려합니다.

Livepatch 상태 전이 다이어그램

Livepatch 시스템은 여러 상태를 거쳐 전환됩니다. 아래 다이어그램은 패치 로드부터 완전 전환까지 전체 흐름을 보여줍니다.

Livepatch 상태 전이 다이어그램 UNLOADED 패치 모듈 미로드 /sys/kernel/livepatch 없음 insmod klp_enable_patch() DISABLED 패치 등록됨 enabled=0, transition=0 echo 1 > enabled klp_start_transition() TRANSITIONING 태스크별 전환 중 enabled=1, transition=1 모든 태스크 전환 klp_complete_transition() PATCHED 패치 완전 적용 enabled=1, transition=0 태스크별 전환 상세 (TRANSITIONING 중) Old Universe 원본 함수 실행 TIF_PATCH_PENDING=1 patch_state=0 schedule() klp_update_patch_state() New Universe 패치 함수 실행 TIF_PATCH_PENDING=0 patch_state=1 스택 검사 (klp_check_stack) 스택에 패치 함수 없음 ✓ 전환 허용 TIF_PATCH_PENDING 해제 → New Universe 진입 스택에 패치 함수 있음 ✗ 전환 지연 TIF_PATCH_PENDING 유지 → 다음 schedule() 대기 REVERTING 패치 제거 중 enabled=0, transition=1 echo 0 > enabled 전환 완료 DISABLED 패치 제거됨 enabled=0, transition=0 rmmod 핵심: 모든 태스크가 안전 지점(패치 함수가 스택에 없는 상태)에 도달해야 전환 완료
그림: Livepatch 상태 전이 다이어그램 - 패치 로드부터 완전 전환까지
💡

상태 확인 명령어:

  • cat /sys/kernel/livepatch/<patch>/enabled — 0: DISABLED, 1: PATCHED/TRANSITIONING
  • cat /sys/kernel/livepatch/<patch>/transition — 0: 전환 완료, 1: 전환 중
  • cat /proc/<pid>/patch_state — 0: Old Universe, 1: New Universe, -1: 전환 대기
  • grep livepatch /proc/cmdline — 부팅 시 자동 로드 확인

전환 지연 해결: /proc/<pid>/patch_state가 -1인 프로세스 식별 → /proc/<pid>/stack으로 블로킹 지점 확인 → 시그널 전송 또는 echo 1 > /sys/kernel/livepatch/<patch>/force 사용 (주의: 강제 전환은 일관성 깨질 수 있음)

Livepatch 콜백 (Pre/Post hooks)

패치 적용 전후에 실행되는 콜백을 등록하여 초기화/정리 작업을 수행할 수 있습니다. 커널 5.1+에서 지원합니다.

/* 콜백: 패치 활성화/비활성화 전후에 실행 */
static int pre_patch_callback(struct klp_object *obj)
{
    pr_info("pre-patch: %s\\n", obj->name ? obj->name : "vmlinux");

    /* 패치 적용 전 사전 조건 검증 */
    /* 실패 시 음수 반환하면 패치 적용이 중단됨 */
    return 0;
}

static void post_patch_callback(struct klp_object *obj)
{
    pr_info("post-patch: %s\\n", obj->name ? obj->name : "vmlinux");
    /* 패치 적용 완료 후 추가 초기화 */
}

static void pre_unpatch_callback(struct klp_object *obj)
{
    pr_info("pre-unpatch: %s\\n", obj->name ? obj->name : "vmlinux");
    /* 패치 제거 전 정리 작업 */
}

static void post_unpatch_callback(struct klp_object *obj)
{
    pr_info("post-unpatch: %s\\n", obj->name ? obj->name : "vmlinux");

    /* 모든 shadow variable 정리 */
    klp_shadow_free_all(SV_LEAK_TRACKER, NULL);
}

static struct klp_object objs[] = {
    {
        .name  = NULL,
        .funcs = funcs,
        /* 콜백 등록 */
        .callbacks = {
            .pre_patch   = pre_patch_callback,
            .post_patch  = post_patch_callback,
            .pre_unpatch = pre_unpatch_callback,
            .post_unpatch = post_unpatch_callback,
        },
    },
    { }
};
콜백호출 시점실행 컨텍스트
pre_patch함수 교체 직전전환 시작 전 (실패 시 패치 중단 가능)
post_patch모든 전환 완료 직후모든 태스크가 new universe
pre_unpatch함수 복원 직전역전환 시작 전
post_unpatch모든 역전환 완료 직후모든 태스크가 old universe

안전성 고려사항

Livepatch 적용 시 반드시 확인할 사항:
  • 함수 시그니처 일치: 패치 함수와 원본 함수의 인자, 반환 타입, 호출 규약이 정확히 일치해야 합니다. 불일치 시 스택 손상이나 커널 패닉이 발생합니다.
  • 데이터 구조 호환성: 구조체 레이아웃이 변경된 패치는 적용할 수 없습니다. Livepatch는 함수만 교체하며 데이터 구조는 변경하지 않습니다.
  • 인라인 함수: 컴파일러가 인라인한 함수는 패치할 수 없습니다. noinline 속성이 필요합니다.
  • 컴파일러 최적화: 패치 모듈과 원본 커널은 동일한 컴파일러 버전과 최적화 수준으로 빌드해야 합니다.
  • 시맨틱 변경 범위: 호출자가 기대하는 함수의 의미론(side effect, 에러 코드 등)을 유지해야 합니다.
# Livepatch 적용 전후 검증 스크립트

# 1. 커널 Livepatch 지원 확인
if [ ! -d /sys/kernel/livepatch ]; then
    echo "ERROR: CONFIG_LIVEPATCH not enabled"
    exit 1
fi

# 2. 현재 적용된 패치 목록
echo "=== Active Livepatches ==="
for patch in /sys/kernel/livepatch/*; do
    [ -d "$patch" ] || continue
    name=$(basename "$patch")
    enabled=$(cat "$patch/enabled")
    transition=$(cat "$patch/transition")
    echo "  $name: enabled=$enabled transition=$transition"

    # 패치된 함수 목록
    for obj in "$patch"/*/; do
        [ -d "$obj" ] || continue
        obj_name=$(basename "$obj")
        for func in "$obj"/*/; do
            [ -d "$func" ] || continue
            func_name=$(basename "$func")
            echo "    [$obj_name] $func_name"
        done
    done
done

# 3. tainted 상태 확인 (Livepatch 적용 시 K 플래그 설정)
tainted=$(cat /proc/sys/kernel/tainted)
if [ $((tainted & 32768)) -ne 0 ]; then
    echo "Kernel tainted: TAINT_LIVEPATCH (K)"
fi

# 4. 전환 블로킹 태스크 확인
echo "=== Blocking Tasks ==="
blocking=0
for pid in /proc/[0-9]*; do
    state=$(cat "$pid/patch_state" 2>/dev/null)
    if [ "$state" = "-1" ]; then
        comm=$(cat "$pid/comm" 2>/dev/null)
        wchan=$(cat "$pid/wchan" 2>/dev/null)
        echo "  PID=$(basename $pid) COMM=$comm WCHAN=$wchan"
        blocking=$((blocking + 1))
    fi
done
echo "Total blocking: $blocking"

안전성 검증 의사결정 트리

Livepatch를 프로덕션에 적용하기 전 반드시 안전성 검증을 수행해야 합니다. 아래 의사결정 트리는 체계적인 검증 절차를 안내합니다.

Livepatch 안전성 검증 의사결정 트리 Livepatch 적용? 함수 시그니처 일치? NO ❌ 적용 불가 인자/반환타입 불일치 → 커널 재부팅 필요 YES 구조체 변경 필요? YES ❌ 적용 불가 데이터 변경 불가 → shadow 변수 검토 NO 인라인 함수? YES ⚠ 주의 필요 noinline 속성 필요 kpatch-build 사용 권장 NO CONFIG_LIVEPATCH=y? NO ❌ 커널 재빌드 CONFIG 활성화 필요 YES 테스트 환경 검증? NO ⚠ 위험 테스트 먼저 수행 QEMU/스테이징 환경 YES ✓ 적용 안전 프로덕션 배포 가능 모니터링 유지 추가 검증 항목 ☐ 컴파일러 버전 일치 ☐ 최적화 레벨 동일 ☐ 심볼 존재 확인 ☐ 롤백 계획 준비 ☐ 모니터링 설정 ☐ 백업 vmcore 확보 ☐ 시맨틱 보존 ☐ 전환 시간 허용 ☐ 레이스 조건 검토 ☐ 보안 정책 확인 ☐ 문서화 완료 ☐ 팀 승인 안전성 우선순위 1. 함수 시그니처 일치 (필수) → 2. 구조체 불변성 (필수) → 3. 테스트 환경 검증 (필수) 4. 컴파일러 일관성 (권장) → 5. 롤백 준비 (권장) → 6. 모니터링 (필수)
그림: Livepatch 안전성 검증 의사결정 트리 - 적용 전 필수 체크리스트
⚠️

절대 Livepatch를 사용하면 안 되는 경우:

  • 함수 시그니처 변경이 필요한 경우 (인자 개수/타입 변경)
  • 전역 구조체 레이아웃 변경이 필요한 경우
  • 초기화 코드 변경 (부팅 시 한 번만 실행되는 코드)
  • 인라인 함수 수정 (noinline 추가가 불가능한 경우)
  • 데이터 섹션 변경 (.data, .bss, .rodata)
  • 테스트 없이 프로덕션 직행

이런 경우 커널 재부팅이 유일한 안전한 방법입니다.

Livepatch 역사: kGraft vs kpatch

항목kpatch (Red Hat)kGraft (SUSE)Livepatch (통합)
도입2014, RHEL2014, SLES4.0 (2015), mainline
일관성 모델stop_machine (전체 정지)per-task lazyper-task + stack checking
전환 방식모든 CPU 동시 교체태스크별 점진 교체태스크별 + 스택 검증
성능 영향전환 시 짧은 정지전환 중 오버헤드최소 오버헤드
안전성높음 (동시 교체)의미론적 일관성 이슈높음 (스택 검증)
상태유저스페이스 도구 유지mainline에 통합공식 커널 프레임워크

배포판별 Livepatch 운영

# === Ubuntu / Canonical Livepatch ===
# Canonical Livepatch 서비스 활성화
sudo snap install canonical-livepatch
sudo canonical-livepatch enable <TOKEN>

# 상태 확인
canonical-livepatch status --verbose

# === RHEL / Red Hat kpatch ===
# kpatch 유틸리티 설치
sudo yum install kpatch kpatch-patch

# 패치 모듈 목록
kpatch list

# 패치 적용
sudo kpatch load kpatch-module.ko

# 자동 적용 등록 (부팅 시)
sudo kpatch install kpatch-module.ko

# === SUSE / kLP (kernel Live Patching) ===
# SUSE Live Patching 패키지
sudo zypper install kernel-livepatch-tools

# 패치 상태 확인
klp status

# === 공통: sysfs로 직접 확인 ===
# 모든 배포판에서 동일하게 사용 가능
ls /sys/kernel/livepatch/
dmesg | grep livepatch

Livepatch 트러블슈팅

증상원인해결 방법
klp_enable_patch(): -ENODEV대상 함수 심볼을 찾을 수 없음/proc/kallsyms에서 심볼 존재 여부 확인, old_sympos 검토
klp_enable_patch(): -EINVALMODULE_INFO(livepatch, "Y") 누락모듈 소스에 해당 매크로 추가
전환이 완료되지 않음태스크가 패치 대상 함수 내 sleep/proc/<pid>/patch_state로 블로킹 태스크 식별, 시그널 전송
전환이 영원히 완료되지 않음kthread가 cond_resched() 미호출force 전환 또는 해당 kthread 종료
패치 후 커널 패닉함수 시그니처 불일치 또는 ABI 차이동일 컴파일러/옵션으로 빌드 확인, 시그니처 검증
TAINT_LIVEPATCH (K)정상 동작 (livepatch 적용 시 자동 설정)제거 필요 없음, 정보 표시 목적
모듈 언로드 실패패치 비활성화 전 rmmod 시도먼저 echo 0 > .../enabled로 비활성화 후 제거
# Livepatch 디버깅: 상세 로그 활성화
echo 'module livepatch +p' > /sys/kernel/debug/dynamic_debug/control
echo 'file kernel/livepatch/* +p' > /sys/kernel/debug/dynamic_debug/control

# ftrace로 livepatch 전환 과정 추적
echo 1 > /sys/kernel/debug/tracing/events/livepatch/enable
cat /sys/kernel/debug/tracing/trace_pipe &

# livepatch 모듈 로드하여 이벤트 확인
insmod livepatch-sample.ko

# 출력 예:
# livepatch: enabling patch 'livepatch_sample'
# livepatch: 'livepatch_sample': starting patching transition
# livepatch: 'livepatch_sample': patching complete

Reliable Stacktrace 메커니즘

Livepatch 전환의 핵심은 reliable stacktrace입니다. 태스크의 스택을 정확히 분석하여 패치 대상 함수가 현재 실행 중인지 판단해야 합니다. 신뢰할 수 없는 스택 트레이스로 잘못 판단하면 — 패치 대상 함수가 스택에 있는데도 전환하면 — 데이터 손상이나 커널 패닉이 발생할 수 있습니다.

/*
 * klp_check_stack() — 태스크 스택에 패치 대상 함수가 있는지 검사
 * (kernel/livepatch/transition.c)
 */
static int klp_check_stack(struct task_struct *task,
                           const char **oldname)
{
    static unsigned long entries[MAX_STACK_ENTRIES];
    struct klp_object *obj;
    struct klp_func *func;
    int ret, nr_entries;

    /* reliable stacktrace 수집 — 핵심 호출 */
    ret = stack_trace_save_tsk_reliable(task, entries,
                                        MAX_STACK_ENTRIES);
    if (ret < 0) {
        /*
         * -EINVAL: 신뢰할 수 없는 스택
         *   - 인터럽트/예외 프레임이 포함된 경우
         *   - 어셈블리 코드에서 프레임 포인터 누락
         *   - generated code (eBPF JIT 등)
         * → 이 태스크는 아직 전환 불가, 다음 기회에 재시도
         */
        return -EINVAL;
    }
    nr_entries = ret;

    /* 스택의 각 리턴 주소를 패치 대상 함수와 비교 */
    klp_for_each_object(klp_transition_patch, obj) {
        klp_for_each_func(obj, func) {
            for (int i = 0; i < nr_entries; i++) {
                if (entries[i] >= func->old_func &&
                    entries[i] < func->old_func + func->old_size) {
                    *oldname = func->old_name;
                    return -EADDRINUSE;
                    /* 패치 대상 함수가 스택에 있음 → 전환 불가 */
                }
            }
        }
    }

    return 0;  /* 스택 클린: 안전하게 전환 가능 */
}
Stack Unwinder아키텍처신뢰성설명
ORC unwinderx86_64높음컴파일 시 objtool이 생성한 ORC 메타데이터(.orc_unwind) 사용, frame pointer 불필요
Frame pointer범용중간CONFIG_FRAME_POINTER=y 필수, 인라인 어셈블리에서 깨질 수 있음
DWARF unwinderarm64 등높음.eh_frame 기반, 커널 6.3+에서 arm64 livepatch 지원
/*
 * ORC unwinder vs Frame Pointer unwinder 비교
 *
 * Frame Pointer 방식:
 *   - 모든 함수에서 push rbp; mov rbp, rsp 프롤로그 필요
 *   - rbp 레지스터를 프레임 포인터로 예약 → 레지스터 하나 손실
 *   - 어셈블리 코드나 -fomit-frame-pointer로 깨질 수 있음
 *   - 결과: 일부 스택에서 RELIABLE 판단 불가 → 전환 지연
 *
 * ORC 방식 (x86_64 권장):
 *   - objtool이 컴파일 시 .orc_unwind, .orc_unwind_ip 섹션 생성
 *   - 각 명령어 위치별 SP/FP 오프셋과 레지스터 정보 기록
 *   - 런타임에 IP 기반으로 unwind 규칙 조회 → 정확한 스택 해석
 *   - frame pointer 불필요 → 레지스터 하나 추가 사용 가능
 *   - 인터럽트 프레임, 어셈블리 코드도 정확히 처리
 */

/* ORC entry 구조 (arch/x86/include/asm/orc_types.h) */
struct orc_entry {
    s16 sp_offset;       /* CFA에서 SP까지의 오프셋 */
    s16 bp_offset;       /* CFA에서 BP 저장 위치 오프셋 */
    unsigned sp_reg:4;   /* CFA 계산에 사용할 레지스터 */
    unsigned bp_reg:4;   /* BP 복원에 사용할 레지스터 */
    unsigned type:3;     /* 프레임 타입 (call, regs, signal 등) */
    unsigned signal:1;   /* 시그널/인터럽트 프레임 여부 */
};

/*
 * stack_trace_save_tsk_reliable()가 신뢰할 수 없는 스택을 감지하는 조건:
 *
 * 1. 인터럽트 또는 예외 프레임이 스택에 존재
 *    → 인터럽트 시점의 코드 위치가 불확실할 수 있음
 * 2. ORC 엔트리가 없는 코드 영역 (JIT, 생성된 코드)
 *    → unwind 불가능
 * 3. kretprobe trampoline이 스택에 존재
 *    → 실제 리턴 주소가 가려져 있음
 * 4. 비표준 프레임 (inline assembly 등)
 *    → objtool이 경고하는 코드 패턴
 */
CONFIG_UNWINDER_ORC 권장: x86_64에서 livepatch를 사용한다면 ORC unwinder(CONFIG_UNWINDER_ORC=y)를 사용해야 합니다. Frame pointer 기반 unwinder는 모든 함수에서 push rbp; mov rbp, rsp 프롤로그가 있어야 하지만, 컴파일러 최적화나 어셈블리 코드에서 이를 생략하는 경우가 있어 신뢰할 수 없는 스택을 생성합니다. ORC는 objtool이 컴파일 시 생성한 별도의 메타데이터 테이블을 사용하므로 이런 문제가 없습니다. objtool은 빌드 시 unreliable한 코드 패턴을 경고하여 문제를 조기에 발견할 수 있습니다.

Livepatch와 보안 기능 상호작용

최신 커널의 보안 강화 기능들은 livepatch와 복잡한 상호작용을 가집니다. 프로덕션 환경에서 livepatch를 운영할 때 이러한 보안 기능과의 호환성을 반드시 고려해야 합니다.

보안 기능Livepatch 영향대응
KASLR커널 주소 랜덤화로 심볼 주소가 부팅마다 변경Livepatch는 kallsyms를 통해 런타임에 심볼을 해석하므로 영향 없음
모듈 서명CONFIG_MODULE_SIG_FORCE=y이면 서명 없는 모듈 로드 불가livepatch 모듈도 유효한 서명 필요, scripts/sign-file 도구 사용
Lockdownkernel_lockdown(LOCKDOWN_INTEGRITY)에서 unsigned 모듈 차단Secure Boot 환경에서 MOK(Machine Owner Key)으로 모듈 서명 필수
CFIClang CFI가 간접 호출 대상을 타입 기반으로 검증패치 함수의 타입이 원본과 정확히 일치해야 CFI 검증 통과
W^XCONFIG_STRICT_KERNEL_RWX로 코드 영역 쓰기 금지ftrace는 text_poke로 임시 매핑 생성하여 수정 후 복원
IBTx86 CET(Control-flow Enforcement Technology)의 간접 분기 추적커널 6.2+에서 livepatch가 IBT 호환, ENDBR64 명령어 자동 처리
FineIBTkCFI + IBT 조합으로 간접 호출 시 해시 검증패치 함수의 프로토타입이 원본과 동일해야 해시가 일치
# === Secure Boot 환경에서 livepatch 모듈 서명 ===

# 1. 커널 빌드 시 생성된 키로 서명 (개발 환경)
/usr/src/linux/scripts/sign-file sha256 \
    /usr/src/linux/certs/signing_key.pem \
    /usr/src/linux/certs/signing_key.x509 \
    livepatch-sample.ko

# 2. 서명 확인
modinfo livepatch-sample.ko | grep sig
# sig_id:          PKCS#7
# signer:          Build time autogenerated kernel key
# sig_hashalgo:    sha256

# 3. Secure Boot + MOK 환경 (배포판)
# MOK 키 생성
openssl req -new -x509 -newkey rsa:2048 \
    -keyout MOK.priv -outform DER -out MOK.der \
    -nodes -days 36500 -subj "/CN=Livepatch Signing Key/"

# MOK 등록 (재부팅 시 UEFI에서 승인 필요)
sudo mokutil --import MOK.der

# MOK 키로 모듈 서명
/usr/src/linux/scripts/sign-file sha256 \
    MOK.priv MOK.der livepatch-sample.ko

# 4. 서명 검증
sudo keyctl list %:.builtin_trusted_keys
sudo keyctl list %:.secondary_trusted_keys
Lockdown과 livepatch: kernel_lockdown(LOCKDOWN_INTEGRITY) 모드에서는 서명되지 않은 모듈 로드가 차단되지만, 유효한 키로 서명된 livepatch 모듈은 정상적으로 로드됩니다. LOCKDOWN_CONFIDENTIALITY 모드에서는 추가로 /dev/mem, kprobes 등이 차단되지만 livepatch 자체는 영향받지 않습니다. Secure Boot가 활성화된 환경에서는 반드시 MOK 또는 커널 빌드 키로 서명해야 합니다.

kpatch-build를 이용한 자동 패치 생성

kpatch-build는 소스 코드 diff에서 livepatch 모듈을 자동 생성하는 도구입니다. klp_func/klp_object/klp_patch 구조체를 수동으로 작성하는 대신, 일반적인 커널 패치(unified diff)만 제공하면 livepatch 모듈이 자동으로 만들어집니다.

# kpatch-build 설치 (소스에서)
git clone https://github.com/dynup/kpatch.git
cd kpatch
make
sudo make install

# 의존성 (Fedora/RHEL)
sudo dnf install gcc kernel-devel elfutils elfutils-devel
sudo dnf debuginfo-install kernel

# 의존성 (Ubuntu/Debian)
sudo apt install build-essential linux-source \
    linux-headers-$(uname -r) elfutils libelf-dev dpkg-dev
# === kpatch-build 사용 워크플로우 ===

# 1. 패치 파일 준비 (일반 커널 패치 형식)
cat > fix-null-deref.patch <<'EOF'
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -1234,6 +1234,9 @@ static int ext4_write_begin(...)
     handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed);
+    if (IS_ERR(handle)) {
+        ret = PTR_ERR(handle);
+        goto out;
+    }
     if (ext4_should_dioread_nolock(inode)) {
EOF

# 2. livepatch 모듈 자동 생성
# kpatch-build는 커널을 패치 전/후로 두 번 빌드하고,
# 변경된 함수를 비교하여 livepatch 모듈을 생성합니다.
kpatch-build -t vmlinux fix-null-deref.patch

# 소스 RPM/DEB 사용 시:
kpatch-build --sourcerpm kernel-5.14.0-362.el9.src.rpm fix-null-deref.patch
kpatch-build --sourcedir /usr/src/linux fix-null-deref.patch

# 3. 생성된 모듈 확인
ls livepatch-fix-null-deref.ko
modinfo livepatch-fix-null-deref.ko

# 4. 패치 적용
sudo kpatch load livepatch-fix-null-deref.ko

# 5. 상태 확인
kpatch list
# Loaded patch modules:
#   livepatch_fix_null_deref [enabled]

# 6. 부팅 시 자동 적용 등록
sudo kpatch install livepatch-fix-null-deref.ko
# → /var/lib/kpatch/$(uname -r)/ 에 복사, systemd 서비스 등록

# 7. 패치 제거
sudo kpatch unload livepatch-fix-null-deref
sudo kpatch uninstall livepatch-fix-null-deref
kpatch-build 내부 동작 흐름 커널 소스 + diff 패치 원본 빌드 original.o 패치 빌드 patched.o create-diff-object 변경 함수 추출 livepatch 모듈 .ko 생성 create-diff-object: ELF 섹션별 비교 → 변경된 함수만 추출 → klp_func/klp_object/klp_patch 자동 생성 의존하는 심볼(전역 변수, 호출 함수)은 klp relocation으로 자동 처리
kpatch-build의 장점: 수동으로 livepatch 모듈을 작성할 때는 klp_func 구조체 정의, 심볼 이름 매칭, old_sympos 결정 등이 필요하지만, kpatch-build는 이 과정을 모두 자동화합니다. 특히 create-diff-object 도구가 변경된 함수만 정확히 추출하고, 해당 함수가 참조하는 외부 심볼에 대한 relocation도 자동으로 처리합니다. 커널 보안 팀에서도 CVE 긴급 패치 시 kpatch-build를 활용합니다.
kpatch-build 제약사항:
  • 데이터 구조 변경 불가: 구조체 레이아웃이 변경되는 패치는 생성할 수 없습니다 (livepatch 자체의 제한)
  • 전체 커널 빌드 필요: 패치 전/후로 커널을 두 번 빌드하므로 시간과 디스크 공간이 필요합니다
  • 인라인 함수: 컴파일러가 인라인한 함수의 변경은 호출자를 모두 패치해야 하므로 패치 범위가 커질 수 있습니다
  • 디버그 심볼: 커널 debuginfo 패키지가 반드시 필요합니다 (vmlinux with DEBUG_INFO)

Livepatch 셀프테스트

커널 소스 트리에는 livepatch 프레임워크의 동작을 검증하는 셀프테스트가 포함되어 있습니다. 커스텀 패치 개발 후 이 셀프테스트를 실행하여 프레임워크 호환성을 확인하고, 테스트 모듈의 구조를 참고하여 자체 패치를 작성할 수 있습니다.

# 셀프테스트 파일 구조
# tools/testing/selftests/livepatch/
#   test-livepatch.sh      — 기본 패치 적용/제거/전환
#   test-callbacks.sh      — pre/post 콜백 동작 검증
#   test-shadow-vars.sh    — shadow variable CRUD 테스트
#   test-state.sh          — 패치 상태 전환 테스트
#   test-ftrace.sh         — ftrace 연동 (동시 사용 시나리오)
#   test-sysfs.sh          — sysfs 인터페이스 검증
#   README                 — 테스트 가이드
#
# 테스트 커널 모듈 (lib/livepatch/):
#   test_klp_livepatch.c         — 기본 패치 모듈
#   test_klp_atomic_replace.c    — atomic replace 테스트
#   test_klp_callbacks_busy.c    — 바쁜 상태에서의 콜백
#   test_klp_callbacks_demo.c    — 콜백 데모
#   test_klp_shadow_vars.c       — shadow variable 테스트
#   test_klp_state.c             — 상태 전환 테스트

# 셀프테스트 실행 (root 권한 필요)
cd tools/testing/selftests/livepatch
sudo make run_tests

# 개별 테스트 실행
sudo ./test-livepatch.sh
sudo ./test-callbacks.sh

# 테스트 결과 예시:
# TAP version 13
# 1..6
# ok 1 basic function patching
# ok 2 multiple livepatches
# ok 3 atomic replace
# ok 4 livepatch transition
# ok 5 force transition
# ok 6 livepatch + module load/unload

# 특정 CONFIG 확인 (테스트 전 필수)
for cfg in LIVEPATCH DYNAMIC_FTRACE TEST_LIVEPATCH; do
    grep "CONFIG_${cfg}" /boot/config-$(uname -r)
done
# CONFIG_LIVEPATCH=y
# CONFIG_DYNAMIC_FTRACE=y
# CONFIG_TEST_LIVEPATCH=m
/*
 * test_klp_livepatch.c — 셀프테스트용 기본 패치 모듈 구조
 * (lib/livepatch/test_klp_livepatch.c 참조)
 *
 * /proc/cmdline 출력을 변경하여 패치 적용 여부를 확인하는 테스트
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/seq_file.h>

static int test_klp_cmdline_proc_show(
    struct seq_file *m, void *v)
{
    seq_printf(m, "%s [test_klp_livepatch]\\n",
               saved_command_line);
    return 0;
}

static struct klp_func funcs[] = {
    {
        .old_name = "cmdline_proc_show",
        .new_func = test_klp_cmdline_proc_show,
    },
    { }
};

static struct klp_object objs[] = {
    { .funcs = funcs },
    { }
};

static struct klp_patch patch = {
    .mod  = THIS_MODULE,
    .objs = objs,
};

static int test_klp_livepatch_init(void)
{
    return klp_enable_patch(&patch);
}

static void test_klp_livepatch_exit(void) { }

module_init(test_klp_livepatch_init);
module_exit(test_klp_livepatch_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

/*
 * 셀프테스트 스크립트(test-livepatch.sh)의 검증 흐름:
 *
 * 1. insmod test_klp_livepatch.ko
 * 2. cat /proc/cmdline → "[test_klp_livepatch]" 포함 확인
 * 3. cat /sys/kernel/livepatch/test_klp_livepatch/enabled → "1"
 * 4. echo 0 > /sys/kernel/livepatch/test_klp_livepatch/enabled
 * 5. cat /proc/cmdline → "[test_klp_livepatch]" 미포함 확인
 * 6. rmmod test_klp_livepatch
 * 7. dmesg에서 에러 메시지 없음 확인
 */

CVE 패치 적용 실전 워크플로우

Livepatch의 가장 일반적인 활용은 보안 취약점(CVE) 긴급 수정입니다. 서버 재부팅 없이 취약점을 즉시 패치할 수 있어, 고가용성 환경에서 핵심 운영 도구입니다. 다음은 CVE 발견부터 livepatch 적용까지의 실전 워크플로우입니다.

Livepatch 배포 파이프라인

프로덕션 환경에 Livepatch를 안전하게 배포하려면 체계적인 파이프라인이 필요합니다. 아래 다이어그램은 CVE 발견부터 전체 플릿 배포까지 전 과정을 보여줍니다.

Livepatch 배포 파이프라인 1단계: 탐지 & 분석 (1~2시간) ☐ CVE 공지 모니터링 (RSS/메일) ☐ 영향도 평가 (CVSS 점수, 사용 여부) → 패치 원본 코드 확보 2단계: 패치 개발 (2~4시간) ☐ kpatch-build로 자동 생성 ☐ klp_patch 구조체 작성 → 모듈 빌드 및 심볼 검증 3단계: 테스트 (4~8시간) ☐ QEMU 가상머신 적용 테스트 ☐ 전환 시간 측정 (전체 태스크) → 기능/성능 회귀 테스트 4단계: 스테이징 (24시간) ☐ 스테이징 서버 1~3대 적용 ☐ 전환 완료 확인 (transition=0) → 24시간 안정성 모니터링 5단계: 카나리 (1~3일) ☐ 프로덕션 1~5% 서버 배포 ☐ 메트릭 비교 (CPU/메모리/에러율) → A/B 테스트로 영향도 측정 6단계: 전체 배포 (1주) ☐ 점진적 롤아웃 (10% → 50% → 100%) ☐ 각 단계마다 24시간 관찰 → 배포 완료 및 문서화 긴급 롤백 (문제 발견 시) ☐ echo 0 > enabled (패치 비활성화) ☐ 전환 완료 확인 (transition=0) → rmmod로 모듈 제거 지속 모니터링 (전 과정) ☐ CPU/메모리 사용률 ☐ 애플리케이션 에러율 → dmesg 커널 로그 감시 전체 타임라인 탐지(1~2h) → 개발(2~4h) → 테스트(4~8h) → 스테이징(24h) → 카나리(1~3d) → 전체 배포(1w) 긴급 CVE: 카나리 단축 가능 (위험도에 따라 조정) | 일반 패치: 정상 프로세스 준수
그림: Livepatch 배포 파이프라인 - CVE 발견부터 전체 플릿 배포까지 6단계
💡

배포 자동화 도구:

  • Ansible/Salt: 대규모 서버 플릿에 모듈 배포 자동화
  • systemd unit: 부팅 시 자동 livepatch 로드 (livepatch@.service)
  • CI/CD 통합: GitLab/Jenkins에서 빌드 → 테스트 → 배포 파이프라인
  • 모니터링: Prometheus로 /sys/kernel/livepatch 메트릭 수집

롤백 준비사항: 원본 커널로 재부팅 준비, 패치 전 vmcore 백업, 롤백 스크립트 사전 테스트

CVE 패치 코드 예시

/*
 * 실전 예시: use-after-free 취약점 livepatch 수정
 *
 * 원본 (취약):
 *   rcu_read_lock();
 *   obj = rcu_dereference(global_ptr);
 *   rcu_read_unlock();
 *   use(obj);  ← rcu_read_unlock() 이후 obj 접근 → UAF!
 *
 * 수정: RCU 보호 구간을 use() 이후로 확장
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/livepatch.h>
#include <linux/rcupdate.h>

/* 패치된 함수: RCU 보호 범위 수정 */
static int patched_vulnerable_handler(struct request *req)
{
    struct shared_obj *obj;
    int ret;

    rcu_read_lock();
    obj = rcu_dereference(global_ptr);
    if (!obj) {
        rcu_read_unlock();
        return -ENOENT;
    }
    /* 수정: use()를 RCU 보호 구간 안에서 수행 */
    ret = use(obj, req);
    rcu_read_unlock();    /* ← 이동: use() 이후로 */
    return ret;
}

static struct klp_func funcs[] = {
    {
        .old_name = "vulnerable_handler",
        .new_func = patched_vulnerable_handler,
    },
    { }
};

static struct klp_object objs[] = {
    {
        .funcs = funcs,  /* name=NULL → vmlinux */
    },
    { }
};

static struct klp_patch patch = {
    .mod     = THIS_MODULE,
    .objs    = objs,
    .replace = true,   /* 기존 패치 대체 (누적 패치 관리) */
};

static int __init cve_fix_init(void)
{
    int ret = klp_enable_patch(&patch);
    if (ret)
        pr_err("failed to enable CVE patch: %d\\n", ret);
    else
        pr_info("CVE-2024-XXXX livepatch applied\\n");
    return ret;
}

static void __exit cve_fix_exit(void) { }

module_init(cve_fix_init);
module_exit(cve_fix_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");
MODULE_DESCRIPTION("Livepatch for CVE-2024-XXXX: fix UAF in handler");
# === CVE livepatch 운영 워크플로우 ===

# 1. CVE 분석 및 패치 작성
# - 취약점 원인 파악 (커널 소스 분석)
# - 최소한의 변경으로 수정하는 패치 함수 작성
# - 함수 시그니처 동일 여부 확인

# 2. 빌드 및 서명
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
/usr/src/linux/scripts/sign-file sha256 \
    MOK.priv MOK.der cve-2024-xxxx.ko

# 3. 테스트 환경에서 검증
# 3-1. 패치 로드
sudo insmod cve-2024-xxxx.ko
dmesg | tail -5
# cve_2024_xxxx: CVE-2024-XXXX livepatch applied

# 3-2. 전환 완료 대기
while [ "$(cat /sys/kernel/livepatch/cve_2024_xxxx/transition)" = "1" ]; do
    sleep 1
done
echo "Transition complete"

# 3-3. 취약점 재현 불가 확인
# (PoC 실행 → 더 이상 크래시 발생하지 않음)

# 4. 프로덕션 배포
# 4-1. 패치 모듈 배포
scp cve-2024-xxxx.ko admin@prod-server:/opt/livepatches/

# 4-2. 프로덕션 적용
sudo insmod /opt/livepatches/cve-2024-xxxx.ko

# 4-3. 모니터링
# 전환 상태, 블로킹 태스크, dmesg 에러 확인
cat /sys/kernel/livepatch/cve_2024_xxxx/enabled
cat /sys/kernel/livepatch/cve_2024_xxxx/transition

# 5. 장기 계획
# 다음 정기 유지보수 윈도우에서:
# - 수정된 커널 패키지로 업데이트
# - 재부팅하여 livepatch 의존성 제거
# - livepatch는 긴급 임시 조치, 재부팅이 최종 해결
CVE livepatch 베스트 프랙티스:
  • replace=true 사용: 누적 패치 방식으로 패치 스택 관리를 단순화합니다. 여러 CVE를 하나의 누적 패치로 통합할 수 있습니다.
  • 최소 변경 원칙: 취약점 수정에 필요한 최소한의 로직만 변경합니다. 리팩터링이나 개선은 포함하지 않습니다.
  • 전환 모니터링: 적용 후 /sys/kernel/livepatch/*/transition으로 전환 완료를 확인합니다. 장시간 완료되지 않으면 블로킹 태스크를 식별합니다.
  • 재부팅 계획: Livepatch는 긴급 임시 조치입니다. 정기 유지보수 윈도우에서 수정된 커널로 재부팅하여 완전히 반영합니다.
  • 필수 매크로: MODULE_INFO(livepatch, "Y")MODULE_LICENSE("GPL")은 반드시 포함해야 합니다.

일관성 모델 심화

Livepatch의 per-task consistency model은 각 태스크를 독립적으로 "old universe"에서 "new universe"로 전환하는 메커니즘입니다. 이 모델의 핵심은 태스크가 패치 대상 함수를 실행 중이 아닌 안전한 지점에서만 전환을 허용한다는 것입니다. stop_machine 방식(전체 CPU 정지)과 달리, 시스템 전체를 멈추지 않으면서도 의미론적 일관성을 보장합니다.

Per-Task Consistency Model 상세 흐름 Old Universe Task A: old_func() 실행 중 TIF_PATCH_PENDING = 1 Task B: sleep(old_func 스택) TIF_PATCH_PENDING = 1 Task C: syscall 경계 도달 스택 클린 - 전환 가능 전환 과정 klp_try_switch_task() 1. klp_check_stack() 2. 스택에 old_func 없으면 전환 schedule() / return-to-user klp_update_patch_state(current) New Universe Task C: new_func() 사용 TIF_PATCH_PENDING = 0 (전환 완료) Task D: 처음부터 new_func() 패치 후 생성된 태스크 스택 검사(klp_check_stack) 판정 과정 안전 (전환 허용) stack[0]: schedule+0x2c stack[1]: do_syslog+0x1a stack[2]: sys_syslog+0x15 stack[3]: do_syscall_64+0x5c 차단 (전환 불가) stack[0]: schedule+0x2c stack[1]: io_schedule+0x12 stack[2]: vulnerable_handler+0x3f (!!) stack[3]: do_syscall_64+0x5c 모든 태스크가 New Universe로 전환되면 klp_complete_transition() 호출 전환 완료 후: transition=0, 이전 패치 정리 (replace=true 시) 전환 지연 시: /proc/PID/patch_state=-1 확인 → /proc/PID/stack으로 블로킹 위치 파악 → 시그널/force

일관성 모델의 핵심 동작을 커널 코드 수준에서 살펴보겠습니다.

/*
 * klp_try_switch_task() - 태스크를 새 universe로 전환 시도
 * (kernel/livepatch/transition.c)
 *
 * 호출 경로:
 *   schedule() → __schedule() → klp_update_patch_state()
 *                              → klp_try_switch_task()
 *
 * 또는:
 *   syscall return → klp_update_patch_state()
 */
static bool klp_try_switch_task(struct task_struct *task)
{
    const char *old_name;
    int ret;

    /* 이미 전환 완료된 태스크는 스킵 */
    if (!test_tsk_thread_flag(task, TIF_PATCH_PENDING))
        return true;

    /*
     * 스택 검사: 패치 대상 함수가 스택에 있는지 확인
     * reliable stacktrace가 필수 — 불확실하면 전환 거부
     */
    ret = klp_check_stack(task, &old_name);
    if (ret) {
        /*
         * -EINVAL: 신뢰할 수 없는 스택 (인터럽트 프레임 등)
         * -EADDRINUSE: 패치 대상 함수가 스택에 존재
         * 두 경우 모두 다음 schedule()까지 대기
         */
        return false;
    }

    /* 안전 확인 완료 → universe 전환 */
    clear_tsk_thread_flag(task, TIF_PATCH_PENDING);

    /*
     * task->patch_state를 klp_target_state로 설정
     * 이후 klp_ftrace_handler()가 이 값을 확인하여
     * 해당 태스크에 맞는 함수(old 또는 new)를 선택
     */
    task->patch_state = klp_target_state;

    return true;
}

/*
 * klp_complete_transition() - 모든 태스크 전환 완료 시 호출
 *
 * 동작:
 * 1. ftrace 핸들러에서 per-task 분기 제거 (최적화)
 * 2. replace=true이면 이전 패치들의 ftrace 훅 해제
 * 3. sysfs transition 값을 0으로 설정
 * 4. NOP 패치 정리
 */
static void klp_complete_transition(void)
{
    struct klp_object *obj;
    struct klp_patch *patch = klp_transition_patch;

    klp_for_each_object(patch, obj) {
        if (!obj->patched)
            continue;
        /* 전환 상태 플래그 정리 */
        klp_post_patch_callback(obj);
    }

    if (patch->replace) {
        /* atomic replace: 이전 모든 패치 비활성화 및 정리 */
        klp_unpatch_replaced_patches(patch);
        klp_discard_nops(patch);
    }

    klp_transition_patch = NULL;
    pr_notice("'%s': patching complete\\n",
              patch->mod->name);
}
전환 시점 정리: 태스크가 new universe로 전환될 수 있는 안전한 지점은 다음과 같습니다:
  • voluntary preemption: schedule(), cond_resched() 호출 지점
  • syscall 경계: 시스템 콜 진입/리턴 시 klp_update_patch_state() 호출
  • idle 진입: CPU idle 루프에서 자동 전환
  • signal 처리: 시그널 전달 시 klp_update_patch_state() 트리거
PREEMPT_NONE 커널에서는 cond_resched()를 거의 호출하지 않는 커널 스레드가 전환을 장시간 지연시킬 수 있습니다. 이런 경우 /proc/PID/patch_state-1로 남아 있으며, 시그널 전송이나 force 전환을 검토해야 합니다.
전환 트리거호출 경로조건
schedule()__schedule() → klp_update_patch_state()태스크가 CPU를 양보할 때
cond_resched()__cond_resched() → schedule()선점 가능 지점
syscall returnexit_to_user_mode_loop()사용자 공간 복귀 시
signal 전달get_signal() → klp_update_patch_state()pending signal 처리 시
idle 진입do_idle() → klp_update_patch_state()CPU idle 시
새 태스크 생성copy_process()fork/clone 시 부모 상태 상속

ftrace 기반 패칭 메커니즘 심화

Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능에 전적으로 의존합니다. 이 플래그가 설정된 ftrace ops는 대상 함수의 instruction pointer(IP)를 수정할 수 있는 특별한 권한을 갖습니다. 일반 ftrace 트레이서와 달리 IP를 변경하여 함수 호출 자체를 리다이렉트할 수 있습니다.

ftrace 기반 함수 리다이렉트 메커니즘 gcc -mfentry 컴파일 결과 func: call __fentry__ (E8 xx xx xx xx) push rbp mov rbp, rsp ... (함수 본문) 부팅 시: ftrace_init()이 NOP으로 교체 → 0F 1F 44 00 00 (5-byte NOP) → 오버헤드 제로 (트레이싱 비활성 시) Livepatch 활성화 후 func: call ftrace_caller (E8 xx xx xx xx) push rbp (실행되지 않음!) mov rbp, rsp ... (원본 본문 - 건너뜀) text_poke_bp()로 원자적 명령어 교체: 1. INT3(CC) 삽입 → 2. IPI 동기화 3. 나머지 바이트 수정 → 4. 첫 바이트 최종 ftrace_caller 내부 실행 흐름 (IPMODIFY) ftrace_caller regs 저장 klp_ftrace_handler() regs->ip = new_func ftrace_caller RET 변경된 IP로 점프 new_func() 패치된 로직 실행 FTRACE_OPS_FL_IPMODIFY: 하나의 함수에 하나의 IPMODIFY ops만 등록 가능 → 같은 함수에 두 개의 livepatch가 동시 적용 불가 (func_stack으로 관리) -mfentry: 함수 프롤로그 전 훅 (x86_64 기본) -pg (mcount): 프롤로그 후 훅 (레거시, arm 등) fentry는 프롤로그 실행 전에 리다이렉트하므로 new_func()이 독립적인 스택 프레임을 구성
/*
 * klp_patch_func() - 함수에 livepatch ftrace ops 등록
 * (kernel/livepatch/patch.c)
 */
static int klp_patch_func(struct klp_func *func)
{
    struct klp_ops *ops;
    int ret;

    /* func_stack에서 같은 원본 함수에 대한 기존 ops 검색 */
    ops = klp_find_ops(func->old_func);
    if (!ops) {
        /* 새 ops 생성 */
        ops = kzalloc(sizeof(*ops), GFP_KERNEL);

        /* IPMODIFY 플래그 설정: IP 수정 권한 요청 */
        ops->fops.func = klp_ftrace_handler;
        ops->fops.flags = FTRACE_OPS_FL_DYNAMIC |
                          FTRACE_OPS_FL_IPMODIFY |
                          FTRACE_OPS_FL_SAVE_REGS;

        /* 대상 함수 주소 등록 */
        ret = ftrace_set_filter_ip(&ops->fops,
                                    func->old_func, 0, 0);

        /* ftrace ops 활성화: NOP → call ftrace_caller 교체 */
        ret = register_ftrace_function(&ops->fops);
    }

    /* func_stack에 패치 함수 추가 (LIFO: 최신 패치 우선) */
    list_add_rcu(&func->stack_node, &ops->func_stack);

    return 0;
}

/*
 * func_stack 관리:
 *
 * 같은 원본 함수에 여러 livepatch가 적용될 수 있음
 * func_stack은 LIFO(Last-In-First-Out) 스택:
 *
 *   func_stack: [patch_v3] → [patch_v2] → [patch_v1]
 *                 ↑ 최신 패치 (list_first)
 *
 * 전환 중이 아닐 때: 항상 list_last (최신) 사용
 * 전환 중일 때: 태스크의 universe에 따라 선택
 *   - old universe → list_last (이전 최신)
 *   - new universe → list_first (새 최신)
 *
 * replace=true 패치가 적용되면:
 * → 이전 모든 패치의 func_stack 정리
 * → 단일 패치만 남음
 */
ftrace ops 플래그의미Livepatch 사용
FTRACE_OPS_FL_IPMODIFYregs->ip 수정 권한함수 리다이렉트 핵심
FTRACE_OPS_FL_SAVE_REGS전체 레지스터 저장IP 수정을 위해 필수
FTRACE_OPS_FL_DYNAMIC동적 할당된 ops모듈 로드 시 생성
FTRACE_OPS_FL_PERMANENT해제 불가Livepatch 미사용
IPMODIFY 충돌: 하나의 함수에는 하나의 FTRACE_OPS_FL_IPMODIFY ops만 등록할 수 있습니다. 이미 다른 도구(예: kretprobe)가 IPMODIFY를 사용 중이면 livepatch 등록이 -EEXIST로 실패합니다. kprobes는 IPMODIFY를 사용하지 않으므로 공존 가능하지만, kretprobe는 리턴 주소를 수정하므로 reliable stacktrace에 영향을 줄 수 있어 주의가 필요합니다.

섀도우 변수(Shadow Variable) 심화

Livepatch는 함수만 교체할 수 있고 데이터 구조는 변경할 수 없습니다. 그런데 패치된 함수에서 원본 구조체에 없는 추가 필드가 필요한 경우가 빈번합니다. klp_shadow_* API는 이 문제를 해결합니다. 기존 커널 객체에 "그림자" 데이터를 해시 테이블로 연결하여, 구조체 레이아웃을 변경하지 않고도 추가 상태를 관리할 수 있습니다.

Shadow Variable 해시 테이블 구조 원본 커널 객체 struct sock *sk struct inode *inode struct task_struct *p klp_shadow 해시 테이블 key = hash(obj_ptr, id) bucket[0]: → shadow_A bucket[1]: → shadow_B bucket[2]: (empty) bucket[N]: → shadow_C Shadow Data 구조체 struct klp_shadow { obj, id, hash_node data[]; /* 사용자 정의 */ struct my_extra_data { u64 timestamp; int count; Shadow Variable 사용 패턴 1. 할당 (alloc) klp_shadow_alloc(obj, id, size, gfp, ctor, data) 2. 조회 (get) klp_shadow_get(obj, id) → NULL이면 미생성 상태 3. 해제 (free) klp_shadow_free(obj, id, dtor) klp_shadow_free_all(id, dtor) 핵심 규칙: shadow variable의 수명은 원본 객체의 수명과 동기화해야 함 원본 객체가 해제될 때 반드시 shadow도 해제 → 메모리 누수 방지 패치 해제 시(post_unpatch): klp_shadow_free_all()로 일괄 정리 필수

구조체 확장이 필요한 실전 패턴을 살펴보겠습니다. 원본 struct net_device에 새로운 통계 필드를 추가하는 예제입니다.

#include <linux/livepatch.h>
#include <linux/netdevice.h>

/* shadow variable ID: 패치 내에서 고유해야 함 */
#define SV_NET_STATS   0x1001

/* 원본 net_device에 추가할 확장 데이터 */
struct net_extra_stats {
    atomic64_t rx_oversized;     /* 초과 크기 패킷 수 */
    atomic64_t tx_throttled;     /* 쓰로틀링된 전송 수 */
    u64        last_anomaly_ts;  /* 마지막 이상 탐지 시각 */
    u32        anomaly_count;    /* 누적 이상 횟수 */
};

/* 생성자: shadow variable 초기화 */
static int net_stats_ctor(void *obj, void *shadow_data,
                          void *ctor_data)
{
    struct net_extra_stats *stats = shadow_data;
    memset(stats, 0, sizeof(*stats));
    atomic64_set(&stats->rx_oversized, 0);
    atomic64_set(&stats->tx_throttled, 0);
    return 0;
}

/* 패치 함수: rx 경로에서 shadow variable 활용 */
static int patched_netif_receive_skb(struct sk_buff *skb)
{
    struct net_device *dev = skb->dev;
    struct net_extra_stats *stats;

    /* shadow variable 조회 (없으면 생성) */
    stats = klp_shadow_get_or_alloc(
        dev, SV_NET_STATS,
        sizeof(struct net_extra_stats),
        GFP_ATOMIC,
        net_stats_ctor, NULL);

    if (stats && skb->len > dev->mtu + 14) {
        atomic64_inc(&stats->rx_oversized);
        stats->last_anomaly_ts = ktime_get_ns();
        stats->anomaly_count++;

        if (stats->anomaly_count > 1000)
            pr_warn_ratelimited(
                "dev %s: excessive oversized pkts\\n",
                dev->name);
    }

    /* 원본 함수 호출 (패치 버전에서 직접 로직 구현) */
    return __netif_receive_skb(skb);
}

/* 패치 해제 시 정리 콜백 */
static void post_unpatch_cleanup(struct klp_object *obj)
{
    /* 모든 net_device에 연결된 shadow variable 일괄 해제 */
    klp_shadow_free_all(SV_NET_STATS, NULL);
    pr_info("cleaned up all net_extra_stats shadows\\n");
}
Shadow Variable 설계 가이드:
  • ID 관리: 각 패치 모듈 내에서 #define SV_xxx로 고유 ID를 정의합니다. 다른 패치 모듈과 ID가 충돌하면 데이터가 오염됩니다.
  • 생성자 패턴: klp_shadow_get_or_alloc()을 사용하면 첫 호출에서 생성, 이후 호출에서 조회하는 패턴을 원자적으로 처리합니다.
  • GFP 플래그: 인터럽트 컨텍스트에서는 GFP_ATOMIC, 프로세스 컨텍스트에서는 GFP_KERNEL을 사용합니다.
  • 수명 관리: 원본 객체가 해제될 때 반드시 klp_shadow_free()를 호출해야 합니다. 패치 해제 시에는 post_unpatch 콜백에서 klp_shadow_free_all()로 일괄 정리합니다.
API용도GFP 컨텍스트반환값
klp_shadow_alloc()새 shadow 생성 (이미 존재 시 NULL)GFP_KERNEL / GFP_ATOMICshadow data 포인터 또는 NULL
klp_shadow_get_or_alloc()조회 후 없으면 생성 (원자적)GFP_KERNEL / GFP_ATOMICshadow data 포인터 또는 NULL
klp_shadow_get()기존 shadow 조회 (할당 없음)해당 없음shadow data 포인터 또는 NULL
klp_shadow_free()특정 객체의 shadow 해제해당 없음void
klp_shadow_free_all()특정 ID의 모든 shadow 일괄 해제해당 없음void

라이브패치 수명주기

Livepatch 모듈은 로드부터 언로드까지 명확한 상태 머신을 따릅니다. 각 상태에서의 API 호출, sysfs 값, 커널 내부 동작을 정확히 이해해야 운영 중 문제를 신속하게 진단할 수 있습니다.

Livepatch 수명주기 상태 머신 INIT insmod klp_init_patch() klp_enable_patch() ENABLING ftrace 훅 설치 pre_patch 콜백 start TRANSITIONING enabled=1, transition=1 per-task 전환 진행 PATCHED enabled=1, transition=0 post_patch 콜백 echo 0 > enabled UNPATCHING enabled=0, transition=1 역방향 per-task 전환 DISABLED enabled=0, transition=0 post_unpatch 콜백 UNLOADED rmmod 모듈 메모리 해제 각 단계별 커널 내부 동작 INIT (klp_init_patch) sysfs 디렉터리 생성, kobject 초기화 심볼 해석 (kallsyms), old_func 주소 확인 ENABLING (klp_try_enable_patch) ftrace_set_filter_ip() + register_ftrace_function() pre_patch 콜백 실행, NOP → call 변환 TRANSITIONING (klp_start_transition) 모든 태스크에 TIF_PATCH_PENDING 설정 klp_target_state = KLP_PATCHED PATCHED (klp_complete_transition) 전환 완료, post_patch 콜백 실행 replace=true 시 이전 패치 정리 UNPATCHING (역방향 전환) klp_target_state = KLP_UNPATCHED 태스크별 old universe 복귀 DISABLED → UNLOADED ftrace 훅 해제, sysfs 정리 rmmod로 모듈 메모리 최종 해제 INIT → ENABLING → TRANSITIONING → PATCHED ↔ UNPATCHING → DISABLED → UNLOADED
/*
 * Livepatch 수명주기 핵심 API 호출 순서
 *
 * === 패치 활성화 ===
 * module_init()
 *   → klp_enable_patch(&patch)
 *     → klp_init_patch()
 *       → klp_init_object()        : 각 obj의 심볼 해석
 *         → klp_init_func()        : 각 func의 old_func 주소 설정
 *       → klp_init_object_loaded() : 이미 로드된 모듈 처리
 *     → klp_try_enable_patch()
 *       → klp_pre_patch_callback() : pre_patch 호출
 *       → klp_patch_object()       : ftrace 훅 설치
 *       → klp_start_transition()   : TIF_PATCH_PENDING 설정
 *     → klp_try_complete_transition()
 *       → 주기적으로 호출 (workqueue)
 *       → 모든 태스크 전환 시 klp_complete_transition()
 *
 * === 패치 비활성화 ===
 * echo 0 > /sys/kernel/livepatch/xxx/enabled
 *   → klp_disable_patch()
 *     → klp_pre_unpatch_callback()
 *     → klp_start_transition()    : 역방향 (KLP_UNPATCHED)
 *     → 모든 태스크 old universe 복귀
 *     → klp_complete_transition()
 *       → klp_unpatch_object()    : ftrace 훅 해제
 *       → klp_post_unpatch_callback()
 *
 * === 모듈 언로드 ===
 * rmmod
 *   → module_exit()
 *   → 커널이 자동으로 klp_unregister_patch() 처리
 *   → sysfs 정리, kobject 해제
 */
수명주기 모니터링: 각 상태 전이는 dmesg와 sysfs로 추적할 수 있습니다:
  • livepatch: enabling patch 'xxx' → ENABLING 진입
  • livepatch: 'xxx': starting patching transition → TRANSITIONING 진입
  • livepatch: 'xxx': patching complete → PATCHED 도달
  • livepatch: 'xxx': starting unpatching transition → UNPATCHING 시작
  • livepatch: 'xxx': unpatching complete → DISABLED 복귀

라이브패치 모듈 작성 실습

실제 운영 환경에서 사용할 수 있는 라이브패치 모듈을 단계별로 작성해보겠습니다. klp_patch/klp_object/klp_func 구조체의 관계를 이해하고, 콜백과 shadow variable을 조합한 완성도 높은 모듈을 만드는 것이 목표입니다.

klp_patch / klp_object / klp_func 구조체 관계 klp_patch .mod = THIS_MODULE .objs = klp_object[] (sentinel) .replace = true/false 내부: enabled, list, kobj, forced klp_object [vmlinux] .name = NULL (vmlinux) .funcs = klp_func[] (sentinel) .callbacks = { pre/post patch/unpatch } klp_object [ext4] .name = "ext4" .funcs = klp_func[] (sentinel) 모듈 미로드 시 지연 패치 klp_func [0] .old_name = "sys_read" .new_func = patched_sys_read .old_sympos = 0 klp_func [1] .old_name = "do_filp_open" .new_func = patched_do_filp_open .old_sympos = 0 klp_func [0] .old_name = "ext4_write_begin" .new_func = patched_ext4_wb .old_sympos = 0 각 배열은 빈 항목 { }으로 종료 (sentinel 패턴)

다중 함수 패치와 콜백, shadow variable을 결합한 완전한 라이브패치 모듈 예제입니다.

/*
 * livepatch-complete-example.c
 * 여러 함수를 패치하고 shadow variable + 콜백을 조합한 실전 모듈
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/slab.h>
#include <linux/fs.h>

#define SV_WRITE_AUDIT  0x2001

/* shadow variable: 파일 쓰기 감사 데이터 */
struct write_audit {
    u64    total_bytes;
    u32    write_count;
    ktime_t first_write;
};

static int audit_ctor(void *obj, void *shadow_data,
                      void *ctor_data)
{
    struct write_audit *audit = shadow_data;
    memset(audit, 0, sizeof(*audit));
    audit->first_write = ktime_get();
    return 0;
}

/* === 패치 함수 1: vfs_write 감사 ===  */
static ssize_t patched_vfs_write(
    struct file *file, const char __user *buf,
    size_t count, loff_t *pos)
{
    struct write_audit *audit;
    ssize_t ret;

    /* 원본 로직 실행 */
    ret = vfs_write(file, buf, count, pos);

    if (ret > 0) {
        audit = klp_shadow_get_or_alloc(
            file->f_inode, SV_WRITE_AUDIT,
            sizeof(*audit), GFP_KERNEL,
            audit_ctor, NULL);
        if (audit) {
            audit->total_bytes += ret;
            audit->write_count++;
        }
    }

    return ret;
}

/* === 패치 함수 2: 대용량 쓰기 경고 === */
static ssize_t patched_generic_file_write_iter(
    struct kiocb *iocb, struct iov_iter *from)
{
    size_t len = iov_iter_count(from);

    if (len > (1ULL << 30)) {
        pr_warn_ratelimited(
            "large write: %zu bytes on ino %lu\\n",
            len, file_inode(iocb->ki_filp)->i_ino);
    }

    return generic_file_write_iter(iocb, from);
}

/* === 콜백 정의 === */
static int pre_patch_cb(struct klp_object *obj)
{
    pr_info("pre-patch: initializing audit tracking\\n");
    return 0;
}

static void post_unpatch_cb(struct klp_object *obj)
{
    pr_info("post-unpatch: cleaning shadow vars\\n");
    klp_shadow_free_all(SV_WRITE_AUDIT, NULL);
}

/* === 구조체 조립 === */
static struct klp_func vmlinux_funcs[] = {
    {
        .old_name = "vfs_write",
        .new_func = patched_vfs_write,
    },
    {
        .old_name = "generic_file_write_iter",
        .new_func = patched_generic_file_write_iter,
    },
    { }  /* sentinel */
};

static struct klp_object objs[] = {
    {
        .name  = NULL,   /* vmlinux */
        .funcs = vmlinux_funcs,
        .callbacks = {
            .pre_patch    = pre_patch_cb,
            .post_unpatch = post_unpatch_cb,
        },
    },
    { }  /* sentinel */
};

static struct klp_patch patch = {
    .mod     = THIS_MODULE,
    .objs    = objs,
    .replace = true,   /* 이전 패치 대체 */
};

static int __init audit_patch_init(void)
{
    return klp_enable_patch(&patch);
}

static void __exit audit_patch_exit(void) { }

module_init(audit_patch_init);
module_exit(audit_patch_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");
MODULE_DESCRIPTION("Write audit livepatch with shadow vars");
# Makefile for the complete livepatch example
obj-m += livepatch-complete-example.o

KDIR ?= /lib/modules/$(shell uname -r)/build

# 빌드 (커널 CONFIG_LIVEPATCH=y 필수)
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

# 빌드 후 테스트 절차
test: all
	sudo insmod livepatch-complete-example.ko
	@echo "=== 패치 상태 ==="
	cat /sys/kernel/livepatch/livepatch_complete_example/enabled
	cat /sys/kernel/livepatch/livepatch_complete_example/transition
	@echo "=== 전환 완료 대기 ==="
	@while [ "$$(cat /sys/kernel/livepatch/livepatch_complete_example/transition)" = "1" ]; do sleep 1; done
	@echo "전환 완료"

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
모듈 작성 필수 체크리스트:
  • MODULE_INFO(livepatch, "Y") 필수 — 없으면 klp_enable_patch()-EINVAL
  • MODULE_LICENSE("GPL") 필수 — GPL이 아니면 livepatch 심볼 접근 불가
  • 패치 함수 시그니처가 원본과 정확히 일치해야 함 (인자 타입, 순서, 반환 타입)
  • 모든 배열은 빈 항목 { }으로 종료해야 함 (sentinel 패턴)
  • 커널이 CONFIG_LIVEPATCH=y, CONFIG_FTRACE=y, CONFIG_DYNAMIC_FTRACE=y로 빌드되어야 함

Livepatch 디버깅과 전환 모니터링

Livepatch 전환 과정에서 문제가 발생하면 ftrace, /proc 인터페이스, 동적 디버그, bpftrace 등을 활용하여 진단할 수 있습니다. 특히 전환이 완료되지 않는 경우 블로킹 태스크를 신속하게 식별하는 것이 핵심입니다.

Livepatch 디버깅 도구 체계 Livepatch 전환 과정 TRANSITIONING → PATCHED sysfs 모니터링 /sys/kernel/livepatch/*/enabled /sys/kernel/livepatch/*/transition /sys/kernel/livepatch/*/force /proc 인터페이스 /proc/PID/patch_state /proc/PID/stack (블로킹 확인) /proc/PID/wchan ftrace 이벤트 events/livepatch/enable function_graph (klp_* 함수) dynamic_debug (livepatch +p) bpftrace / perf kprobe:klp_try_switch_task kprobe:klp_complete_transition tracepoint:livepatch:* dmesg 로그 livepatch: enabling patch ... livepatch: patching complete livepatch: error / warning 진단 순서: sysfs(상태 확인) → /proc(블로킹 태스크) → ftrace/bpftrace(실행 흐름) → dmesg(에러 원인)
# === 1. ftrace로 livepatch 이벤트 추적 ===

# livepatch 트레이스포인트 활성화
echo 1 > /sys/kernel/debug/tracing/events/livepatch/enable
cat /sys/kernel/debug/tracing/trace_pipe &

# 출력 예시:
#  insmod-1234  klp_try_enable_patch: patch=livepatch_sample
#  insmod-1234  klp_start_transition: target_state=PATCHED
#  kworker-56   klp_try_switch_task: pid=789 success=1
#  kworker-56   klp_complete_transition: patch=livepatch_sample

# === 2. dynamic_debug로 상세 로그 ===

# livepatch 하위시스템 전체 디버그
echo 'file kernel/livepatch/* +p' > \
    /sys/kernel/debug/dynamic_debug/control

# 특정 함수만 디버그
echo 'func klp_try_switch_task +p' > \
    /sys/kernel/debug/dynamic_debug/control

# === 3. 전환 블로킹 태스크 상세 분석 ===

# 블로킹 태스크 식별 스크립트
echo "=== Blocking Tasks ==="
for pid_dir in /proc/[0-9]*; do
    pid=$(basename "$pid_dir")
    state=$(cat "$pid_dir/patch_state" 2>/dev/null)
    if [ "$state" = "-1" ]; then
        comm=$(cat "$pid_dir/comm" 2>/dev/null)
        wchan=$(cat "$pid_dir/wchan" 2>/dev/null)
        echo "PID=$pid COMM=$comm WCHAN=$wchan"
        echo "  Stack:"
        cat "$pid_dir/stack" 2>/dev/null | head -5
        echo "---"
    fi
done

# === 4. function_graph로 klp 함수 호출 추적 ===

echo klp_ftrace_handler > \
    /sys/kernel/debug/tracing/set_ftrace_filter
echo function_graph > \
    /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 호출 흐름과 소요 시간 확인
cat /sys/kernel/debug/tracing/trace
# === 5. bpftrace로 실시간 전환 모니터링 ===

# 전환 시도 추적: 성공/실패 분리
bpftrace -e '
kprobe:klp_try_switch_task {
    @task[tid] = comm;
}
kretprobe:klp_try_switch_task /retval == 1/ {
    printf("switched: pid=%d comm=%s\n", tid, comm);
    delete(@task[tid]);
}
kretprobe:klp_try_switch_task /retval == 0/ {
    printf("blocked:  pid=%d comm=%s\n", tid, comm);
    delete(@task[tid]);
}
'

# 전환 완료 시간 측정
bpftrace -e '
kprobe:klp_start_transition {
    @start = nsecs;
    printf("transition started\n");
}
kprobe:klp_complete_transition {
    $elapsed = (nsecs - @start) / 1000000;
    printf("transition complete: %d ms\n", $elapsed);
    delete(@start);
}
'

# klp_ftrace_handler 호출 빈도 모니터링
bpftrace -e '
kprobe:klp_ftrace_handler {
    @calls = count();
    @by_cpu[cpu] = count();
}
interval:s:5 {
    printf("handler calls in 5s: ");
    print(@calls);
    clear(@calls);
}
'
전환 지연 해결 체크리스트:
  • 1단계: cat /sys/kernel/livepatch/*/transition으로 전환 진행 중인지 확인
  • 2단계: 위 스크립트로 patch_state=-1인 태스크 목록 확보
  • 3단계: 각 블로킹 태스크의 /proc/PID/stack에서 패치 대상 함수 존재 여부 확인
  • 4단계: 사용자 프로세스면 시그널(SIGUSR1 등) 전송, 커널 스레드면 해당 워크로드 완료 대기
  • 5단계: 장시간 해결 불가 시 echo 1 > .../force로 강제 전환 (주의: 일관성 보장 안 됨)

커널 라이브 패칭 솔루션 비교

리눅스 커널 라이브 패칭 분야에는 여러 솔루션이 존재합니다. 각각의 역사, 기술 접근 방식, 장단점을 비교하여 환경에 맞는 도구를 선택할 수 있습니다.

커널 라이브 패칭 솔루션 발전 타임라인 시간 2008 Ksplice MIT 연구, Oracle 인수 stop_machine 방식 상용 (Oracle Linux) 2014 kpatch Red Hat stop_machine + ftrace 오픈소스 도구 kGraft SUSE per-task lazy 전환 SLES 내장 2015 (v4.0) Livepatch mainline 통합 per-task + stack check kGraft + kpatch 장점 통합 공식 커널 프레임워크 현재 상용 서비스 Canonical Livepatch RHEL kpatch (사용자 도구) SUSE kLP 모두 커널 Livepatch 기반 핵심 차이점 요약 stop_machine: 전 CPU 정지 빠른 전환, 짧은 지연 시간 per-task lazy: 점진적 전환 무중단, 스택 미검사 위험 per-task + stack: 최적 균형 무중단 + 안전한 전환 Livepatch = kGraft의 per-task 방식 + kpatch의 stack checking → 안전성과 가용성 동시 확보
항목KsplicekpatchkGraftLivepatch (mainline)
개발사MIT → OracleRed HatSUSE커널 커뮤니티
도입 시기2008201420142015 (v4.0)
일관성 모델stop_machinestop_machine + ftraceper-task lazyper-task + stack check
전환 방식전 CPU 정지 후 교체전 CPU 정지 후 교체태스크별 점진 전환태스크별 + 스택 검증
시스템 영향짧은 정지 시간 발생짧은 정지 시간 발생정지 없음정지 없음
안전성높음 (동시 교체)높음 (동시 교체)중간 (스택 미검사)높음 (스택 검증)
전환 지연없음 (강제)없음 (강제)가능 (장기 sleep)가능 (장기 sleep)
라이선스상용 (Oracle)오픈소스 (GPLv2)오픈소스 (GPLv2)오픈소스 (GPLv2)
현재 상태Oracle Linux 전용사용자 도구 유지mainline 통합공식 프레임워크
자동 빌드ksplice-createkpatch-build직접 작성직접 / kpatch-build
shadow 변수미지원미지원 (직접 구현)미지원klp_shadow_* API
atomic replace미지원미지원미지원replace=true
콜백제한적제한적미지원pre/post patch/unpatch
아키텍처x86_64x86_64x86_64, s390x86_64, s390, ppc64le, arm64
솔루션 선택 가이드:
  • mainline 커널 사용자: 커널 내장 Livepatch 프레임워크가 유일한 선택입니다. CONFIG_LIVEPATCH=y로 활성화합니다.
  • RHEL/CentOS: kpatch 사용자 도구 + 커널 Livepatch 프레임워크 조합. kpatch-build로 패치 자동 생성이 용이합니다.
  • Ubuntu: Canonical Livepatch Service로 관리형 서비스 이용 가능. 수동 패치도 가능합니다.
  • SUSE: kLP 도구 + 커널 Livepatch 프레임워크. SLE Live Patching 구독으로 자동 패치 제공.
  • Oracle Linux: Ksplice가 여전히 기본 도구이며, Livepatch와 독립적입니다.
사용 시나리오권장 솔루션이유
긴급 CVE 수정 (프로덕션)배포판 livepatch 서비스검증된 패치, 자동 배포, SLA 지원
커스텀 패치 개발kpatch-build + Livepatchdiff에서 자동 모듈 생성, 빠른 개발
디버깅용 임시 패치수동 klp_patch 작성세밀한 제어, shadow variable 활용
대규모 플릿 운영Ansible + kpatch/livepatch자동화 배포, 롤백 파이프라인
고가용성 (99.999%)Canonical/RHEL 서비스무중단 보장, 전문 지원

커널 Livepatch와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.