커널 Livepatch

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

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

핵심 요약

  • 커널 라이브 패칭(Kernel Live Patching) — 재부팅 없이 실행 중인 커널 함수를 동적으로 교체하는 기술로, CONFIG_LIVEPATCH로 활성화합니다.
  • ftrace 기반 패칭 — 함수 진입점에 ftrace 훅을 설치하고 FTRACE_OPS_FL_IPMODIFY로 IP를 새 함수로 리다이렉트합니다.
  • 일관성 모델(Consistency Model) — 태스크별 일관성(Per-task Consistency)을 보장하여 패치 전환 중 함수 호출 혼합을 방지합니다.
  • klp_object / klp_func — 패치 대상 객체와 함수를 정의하는 핵심 구조체로, Livepatch 모듈의 골격을 구성합니다.
  • Shadow Variable(klp_shadow) — 원본 구조체를 수정하지 않고 패치에서 필요한 추가 데이터를 안전하게 연결하는 메커니즘입니다.
  • Atomic Replace — 누적 패치를 단일 패치로 대체하여 패치 스택 복잡도를 관리하는 기능입니다.
  • sysfs 인터페이스/sys/kernel/livepatch/를 통해 패치 상태 확인, 활성화/비활성화, 전환 진행률을 모니터링합니다.

단계별 이해

  1. Livepatch 필요성과 아키텍처 이해 — 재부팅이 불가능한 고가용성 환경에서 보안 패치를 적용해야 하는 상황과, ftrace 기반 함수 교체 원리를 이해합니다.

    기존 함수 호출이 ftrace 훅을 통해 새 함수로 리다이렉트되는 명령어 수준 동작을 추적합니다.

  2. 기본 Livepatch 모듈 작성klp_patch, klp_object, klp_func 구조체를 사용하여 간단한 함수 교체 모듈을 작성하고 로드합니다.

    klp_enable_patch()로 패치를 등록하고, insmod/rmmod로 적용/해제하는 전체 수명주기를 실습합니다.

  3. 전환 메커니즘과 일관성 보장 학습 — 태스크가 안전 지점(Patched/Unpatched 경계)에 도달할 때까지 대기하는 전환 프로세스를 분석합니다.

    /sys/kernel/livepatch/에서 전환 상태를 모니터링하고, 전환이 지연되는 원인(장기 실행 태스크 등)을 진단합니다.

  4. Shadow Variable과 Atomic Replace 활용 — 원본 구조체 변경 없이 추가 데이터를 연결하는 klp_shadow_alloc()과, 누적 패치를 단일 패치로 교체하는 Atomic Replace를 실습합니다.

    실제 CVE 패치 시나리오에서 kpatch-build를 이용한 자동 패치 생성 워크플로우를 따라해 봅니다.

  5. 안전성 검증과 운영 적용 — Reliable Stacktrace, 콜백 훅(Pre/Post), 보안 기능(모듈 서명, lockdown) 상호작용을 확인합니다.

    배포판별(Ubuntu Livepatch, RHEL kpatch, SUSE kGraft) 운영 환경에서의 적용과 트러블슈팅 방법을 비교합니다.

관련 표준: Kernel Livepatch Interface (kernel.org) — 커널 라이브 패칭 인터페이스 공식 문서입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 페이지(Page): 기본 디버깅(Debugging) 도구와 크래시 분석은 디버깅 & 트러블슈팅, 크래시 분석 페이지를 참고하세요.

커널 Livepatch

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

Livepatch 아키텍처

Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능을 기반으로 동작합니다. 패치 대상 함수의 진입점(Entry Point)에 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"(패치 함수) 중 하나에 속하며, 태스크가 패치 대상 함수의 스택 프레임(Stack Frame)을 벗어나는 안전한 지점(voluntary preemption, syscall 경계 등)에 도달했을 때 전환됩니다.

아키텍처 계층 상세 다이어그램

Livepatch 프레임워크는 사용자 공간(User Space) 인터페이스부터 하드웨어 수준의 명령어 패칭까지 여러 계층으로 구성됩니다. 아래 다이어그램은 각 계층의 역할과 구성 요소 간 상호작용을 보여줍니다.

Livepatch 아키텍처 계층도 사용자 공간 insmod / rmmod sysfs (enabled, force) kpatch / canonical-livepatch /proc/PID/patch_state Livepatch Core (kernel/livepatch/) core.c klp_enable_patch() klp_init_patch() transition.c klp_try_switch_task() klp_check_stack() patch.c klp_ftrace_handler() klp_patch_func() shadow.c klp_shadow_alloc() klp_shadow_get() state.c klp_get_state() 시스템 상태 관리 ftrace 프레임워크 FTRACE_OPS_FL_IPMODIFY register_ftrace_function() 스케줄러 통합 klp_update_patch_state() TIF_PATCH_PENDING 스택 언와인더 ORC unwinder Frame Pointer DWARF 모듈 로더 / 보안 MODULE_SIG Lockdown CFI / IBT 하드웨어 / ISA text_poke_bp() / INT3 IPI 동기화 NOP ↔ CALL 명령어 I-Cache 동기화 ENDBR64 사용자 공간 → Livepatch Core → ftrace/스케줄러 → 스택 언와인더/보안 → 하드웨어 ISA 각 계층은 독립적이며, 인터페이스를 통해 느슨하게 결합 (Loose Coupling)
그림: Livepatch 아키텍처 계층도 - 사용자 공간부터 하드웨어까지 전체 스택

내부 자료구조와 연결 리스트

Livepatch 프레임워크 내부에서 패치들은 전역 연결 리스트(klp_patches)로 관리됩니다. 각 패치 내의 오브젝트(Object)와 함수(Function)도 연결 리스트로 연결되어 있어, 런타임에 동적으로 패치를 추가/제거할 수 있습니다.

/*
 * Livepatch 내부 자료구조 관계
 * (kernel/livepatch/core.c, kernel/livepatch/patch.c)
 */

/* 전역 패치 목록: 현재 활성화된 모든 패치 */
static LIST_HEAD(klp_patches);

/*
 * klp_patches 리스트 구조:
 *
 * klp_patches ──→ klp_patch (patch_v1) ──→ klp_patch (patch_v2)
 *                   │                         │
 *                   ├─ objs[0] (vmlinux)      ├─ objs[0] (ext4)
 *                   │   ├─ funcs[0]           │   ├─ funcs[0]
 *                   │   ├─ funcs[1]           │   └─ funcs[1]
 *                   │   └─ { } (sentinel)     │
 *                   │                         └─ { } (sentinel)
 *                   ├─ objs[1] (ext4)
 *                   │   ├─ funcs[0]
 *                   │   └─ { } (sentinel)
 *                   │
 *                   └─ { } (sentinel)
 */

/* klp_ops: 같은 함수에 대한 여러 패치를 스택으로 관리 */
struct klp_ops {
    struct list_head node;       /* 전역 klp_ops 리스트 */
    struct list_head func_stack; /* 이 함수에 적용된 패치 스택 */
    struct ftrace_ops fops;      /* ftrace ops (하나만) */
};

/*
 * func_stack 예시 (같은 함수에 3개 패치 적용):
 *
 * ops->func_stack:
 *   [patch_v3.func] → [patch_v2.func] → [patch_v1.func]
 *     ↑ HEAD (최신)                        ↑ TAIL (최초)
 *
 * 전환 중이 아닐 때: HEAD (최신 패치) 사용
 * 전환 중:
 *   - old universe: 전환 전 최신 = 리스트의 두 번째
 *   - new universe: 전환 후 최신 = HEAD
 */

/* 현재 전환 중인 패치 (동시에 하나만 전환 가능) */
static struct klp_patch *klp_transition_patch;

/* 전환 목표 상태: KLP_PATCHED 또는 KLP_UNPATCHED */
static int klp_target_state = KLP_UNDEFINED;

/*
 * 패치 상태 열거형:
 * KLP_UNDEFINED  (0)  — 전환 중이 아님
 * KLP_UNPATCHED (-1)  — 패치 제거 방향 전환 중
 * KLP_PATCHED   (1)  — 패치 적용 방향 전환 중
 */

klp_enable_patch() 전체 호출 흐름

klp_enable_patch()는 Livepatch 모듈의 module_init()에서 호출되는 진입점입니다. 이 함수는 패치 구조체 검증, 심볼 해석, ftrace 훅 설치, 전환 시작까지의 전체 과정을 관장합니다.

klp_enable_patch() 전체 호출 흐름 module_init() klp_enable_patch(&patch) klp_init_patch() 구조체 검증 + sysfs 생성 klp_init_object() 각 obj 순회: 모듈 찾기 klp_init_func() kallsyms로 old_func 주소 해석 -ENODEV 심볼 없음 klp_try_enable_patch() ftrace 훅 설치 시작 klp_patch_object() 각 func에 ftrace ops 등록 pre_patch 콜백 실패 시 패치 중단 klp_start_transition() TIF_PATCH_PENDING 전체 설정 workqueue 예약 klp_try_complete_transition() 비동기 전환 각 태스크 schedule() 시 전환 klp_complete_transition() 전환 완료, post_patch 콜백 전체 흐름: init → 구조체 검증 → 심볼 해석 → ftrace 등록 → 콜백 → 전환 시작 → 비동기 완료 동기: klp_enable_patch()까지 | 비동기: 전환 완료는 모든 태스크의 schedule() 호출에 의존
그림: klp_enable_patch() 호출 흐름 - 동기 초기화부터 비동기 전환 완료까지
/*
 * klp_enable_patch() 핵심 흐름 (kernel/livepatch/core.c)
 * 리턴 값: 0=성공, 음수=실패
 */
int klp_enable_patch(struct klp_patch *patch)
{
    int ret;

    /* 1. MODULE_INFO(livepatch, "Y") 존재 확인 */
    if (!klp_is_patch_compatible(patch))
        return -EINVAL;

    /* 2. 동시에 하나의 전환만 허용 (mutex 보호) */
    mutex_lock(&klp_mutex);

    if (klp_transition_patch) {
        /* 이미 전환 중인 패치가 있으면 대기 */
        ret = -EBUSY;
        goto unlock;
    }

    /* 3. 패치 구조체 초기화: sysfs, kobject, 심볼 해석 */
    ret = klp_init_patch(patch);
    if (ret)
        goto unlock;

    /* 4. ftrace 훅 설치 + pre_patch 콜백 + 전환 시작 */
    ret = __klp_enable_patch(patch);
    if (ret)
        klp_free_patch_start(patch);

unlock:
    mutex_unlock(&klp_mutex);
    return ret;
}
EXPORT_SYMBOL_GPL(klp_enable_patch);

아키텍처별 지원 현황

Livepatch는 아키텍처별로 지원 수준이 다릅니다. reliable stacktrace 구현 여부가 핵심 기준입니다.

아키텍처지원 시작스택 언와인더fentry 지원비고
x86_644.0 (2015)ORC (기본) / Frame Pointer-mfentry가장 성숙한 지원, objtool 연동
s390x4.0 (2015)자체 구현-mhotpatchIBM z/Linux, 별도의 hotpatch 메커니즘
ppc64le4.7 (2016)Frame Pointer + 확장-mprofile-kernelTOC/GOT 처리 필요, 보조 트램폴린
arm646.3 (2023)DWARF (.eh_frame)-fpatchable-function-entryBTI (Branch Target Identification) 호환
x86_32미지원--reliable stacktrace 구현 없음
arm32미지원--reliable stacktrace 구현 없음
RISC-V진행 중진행 중진행 중커뮤니티 개발 중 (2025 기준)
아키텍처별 차이점:
  • x86_64: -mfentry로 함수 진입 전 5바이트 call __fentry__ 삽입. ORC unwinder가 정확한 reliable stacktrace 제공. text_poke_bp()로 원자적 코드 수정.
  • s390x: 독자적 -mhotpatch 옵션. 함수 앞에 6바이트 NOP 슬롯 예약. trampolines를 통해 리다이렉트하며, x86과 달리 ftrace를 거치지 않는 직접 패칭도 가능.
  • ppc64le: ELFv2 ABI의 TOC(Table of Contents) 포인터 관리가 복잡. 패치 함수가 다른 모듈에 있으면 TOC 복원 코드가 필요. -mprofile-kernel로 fentry 방식 지원.
  • arm64: -fpatchable-function-entry=4로 NOP 슬롯 생성. BTI(Branch Target Identification)가 활성화된 경우 간접 분기 대상에 BTI C 명령어 필요. DWARF 기반 reliable stacktrace 사용.

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를 동기화합니다. 그 후 나머지 바이트를 수정하고, 마지막으로 첫 바이트를 최종 값으로 교체합니다. 이 과정은 원자적(Atomic)이어서 다른 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 핵심 구조체(Struct)

#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뿐만 아니라 로드된 커널 모듈(Kernel Module)의 함수도 패치할 수 있습니다. 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를 사용합니다. 이는 원본 객체에 "그림자" 데이터를 연결하는 해시 테이블(Hash Table) 기반 메커니즘입니다.

#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를 설정한 패치를 적용하면 됩니다. 이렇게 하면 기존 모든 패치가 원자적으로 비활성화됩니다.

Atomic Replace 내부 동작 흐름

replace=true 패치가 활성화되면 커널은 기존 패치들의 함수를 자동으로 NOP 패치로 교체합니다. 이 과정은 전환 완료 후 klp_complete_transition()에서 처리됩니다.

Atomic Replace 동작: 누적 패치 → 단일 패치 전 (Before): 누적 패치 스택 Patch v1: func_A(old→v1), func_B(old→v1) Patch v2: func_A(v1→v2), func_C(old→v2) Patch v3: func_D(old→v3) (replace=false) replace =true 후 (After): 단일 누적 패치 Cumulative Patch (replace=true) func_A→v3, func_B→v3, func_C→v3, func_D→v3 이전 패치(v1, v2, v3): 자동 NOP화 + 비활성화 → rmmod로 이전 모듈 제거 가능 Atomic Replace 내부 처리 단계 1. NOP 패치 생성 이전 패치 함수 → NOP func 2. 전환 시작 새 패치 + NOP 동시 전환 3. 전환 완료 klp_complete_transition() 4. 이전 패치 정리 klp_unpatch_replaced() NOP 패치: func->nop=true인 klp_func — klp_ftrace_handler()에서 원본 함수 그대로 실행 이전 패치에만 있던 함수는 NOP으로 자동 복원 → 원본 동작 회복
그림: Atomic Replace 동작 - 누적 패치 스택을 단일 패치로 원자적 대체
/*
 * Atomic Replace 사용 패턴: 점진적 패치 업그레이드
 *
 * 시나리오: CVE-001 패치 후 CVE-002 추가 발견
 *
 * Step 1: CVE-001 패치 적용
 *   patch_v1 { replace=false, objs: { func_A→patched_A_v1 } }
 *   insmod patch_v1.ko
 *
 * Step 2: CVE-002 발견 → CVE-001+002 누적 패치 생성
 *   patch_v2 { replace=true, objs: { func_A→patched_A_v2, func_B→patched_B } }
 *   insmod patch_v2.ko
 *   → patch_v1은 자동 비활성화 (func_A는 v2로 업그레이드)
 *   → rmmod patch_v1.ko (안전하게 제거 가능)
 *
 * Step 3: 패치 완전 제거 (원본 복원)
 *   patch_empty { replace=true, objs: { } }  // 빈 패치
 *   insmod patch_empty.ko
 *   → 모든 이전 패치 비활성화 → 원본 함수 복원
 */

/* 패치 완전 제거용 빈 패치 모듈 */
static struct klp_object empty_objs[] = {
    { }  /* sentinel: 함수 없음 */
};

static struct klp_patch reset_patch = {
    .mod     = THIS_MODULE,
    .objs    = empty_objs,
    .replace = true,   /* 핵심: 모든 기존 패치 대체 */
};

static int __init reset_init(void)
{
    pr_info("Removing all livepatches (atomic replace with empty)\\n");
    return klp_enable_patch(&reset_patch);
}

module_init(reset_init);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

패치 스택과 func_stack 관리

같은 원본 함수에 여러 livepatch가 순차적으로 적용되면, 각 패치의 대체 함수가 func_stack이라는 LIFO 스택에 쌓입니다. klp_ftrace_handler()는 이 스택에서 적절한 함수를 선택하여 호출합니다.

/*
 * func_stack 관리 상세
 *
 * 예시: sys_read()에 3개의 패치가 순차 적용
 *
 * 1. patch_v1 로드 → func_stack: [patched_read_v1]
 * 2. patch_v2 로드 → func_stack: [patched_read_v2] → [patched_read_v1]
 * 3. patch_v3 로드 → func_stack: [patched_read_v3] → [patched_read_v2] → [patched_read_v1]
 *
 * 전환 중이 아닐 때 (KLP_UNDEFINED):
 *   → list_last_entry() = patched_read_v3 (최신)
 *
 * patch_v3 전환 중:
 *   - old universe 태스크: patched_read_v2 (이전 최신)
 *   - new universe 태스크: patched_read_v3 (새 최신)
 *
 * patch_v2 비활성화 시:
 *   func_stack에서 patched_read_v2 제거
 *   → func_stack: [patched_read_v3] → [patched_read_v1]
 *   → patched_read_v3이 계속 활성 (영향 없음)
 */

/* klp_ftrace_handler 내부의 함수 선택 로직 */
if (patch_state == KLP_UNDEFINED) {
    /* 전환 중이 아님: 스택의 마지막(최신) 사용 */
    func = list_last_entry(&ops->func_stack,
                           struct klp_func, stack_node);
} else {
    /*
     * 전환 중: 태스크의 universe에 따라 선택
     *
     * patch_state == klp_target_state → new universe
     *   → list_first (새로 추가된 패치)
     * patch_state != klp_target_state → old universe
     *   → list_last (기존 최신 패치)
     *
     * 비활성화 전환(UNPATCHING)에서는 반대:
     *   new → NOP (원본 복원)
     *   old → 현재 패치
     */
    func = list_first_or_last_entry(
        &ops->func_stack, struct klp_func,
        stack_node, patch_state == klp_target_state);
}

동명 함수 처리 (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
  • 각 태스크 (스케줄러(Scheduler)에서):
  • 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()
  • → 전환 완료
전환 지연(Latency) 원인: 다음과 같은 태스크는 전환을 지연시킬 수 있습니다:
  • TASK_IDLE 상태 CPU: idle 루프의 스택이 간단하여 보통 빠르게 전환
  • 장기 sleep (TASK_UNINTERRUPTIBLE): 패치 대상 함수 내부에서 I/O 대기 중일 때
  • RT 태스크: 선점(Preemption)이 억제된 상태에서 패치 대상 함수 실행 중
  • kthread: cond_resched()를 드물게 호출하는 커널 스레드(Kernel Thread)
이런 경우 /proc/<pid>/patch_state로 블로킹 태스크를 식별하고, 시그널(Signal) 전송이나 force 전환을 고려합니다.

klp_check_stack() 동작 원리

전환의 안전성을 보장하는 핵심 함수는 klp_check_stack()입니다. 이 함수는 태스크의 스택 트레이스를 수집하고, 각 리턴 주소가 패치 대상 함수의 주소 범위에 속하는지 검사합니다. 한 개라도 일치하면 전환을 거부합니다.

/*
 * klp_check_stack() 의사코드 (간소화)
 *
 * for each task:
 *   entries[] = stack_trace_save_tsk_reliable(task)
 *   if (reliable != true)
 *     return -EINVAL  // 스택을 신뢰할 수 없음 → 전환 불가
 *
 *   for each entry in entries[]:
 *     for each patched function:
 *       if (entry >= func->old_func &&
 *           entry <  func->old_func + func->old_size)
 *         return -EADDRINUSE  // 스택에 패치 대상 함수 존재
 *
 *   return 0  // 스택 클린 → 전환 가능
 */

/* 주소 범위 검사의 중요성:
 *
 * 단순히 old_func 시작 주소만 비교하면 안 됩니다.
 * 함수 내부의 어떤 지점에서든 실행 중일 수 있으므로
 * [old_func, old_func + old_size) 전체 범위를 검사합니다.
 *
 * old_size는 klp_init_func()에서 kallsyms를 통해
 * 자동으로 계산됩니다.
 */

/* stack_trace_save_tsk_reliable() 반환값:
 *
 * >= 0: 스택 엔트리 수 (신뢰할 수 있음)
 * -EINVAL: 신뢰할 수 없는 스택
 *   원인:
 *   - 인터럽트/예외 프레임이 스택에 존재
 *   - ORC 엔트리 없는 코드 영역 (JIT 등)
 *   - kretprobe trampoline 존재
 *   - 어셈블리 코드에서 프레임 정보 누락
 *
 * reliable하지 않은 스택 → 보수적 판단 → 전환 거부
 * → 다음 schedule()에서 재시도
 */

강제 전환 (force)과 위험성

전환이 장시간 완료되지 않을 때 /sys/kernel/livepatch/<patch>/force1을 기록하면 스택 검사를 건너뛰고 강제로 모든 태스크를 전환합니다. 이는 최후의 수단으로만 사용해야 합니다.

# 강제 전환 수행 (주의: 일관성 보장 안 됨)
echo 1 > /sys/kernel/livepatch/livepatch_sample/force

# 강제 전환의 위험성:
#
# 1. 패치 대상 함수가 실행 중인 태스크를 강제 전환
#    → 같은 호출에서 old_func 전반부 + new_func 후반부 섞임
#    → 데이터 불일치, 메모리 손상 가능
#
# 2. 커널 tainted 상태에 TAINT_LIVEPATCH_FORCE (K) 추가
#    → 이후 커널 버그 리포트에 표시됨
#
# 3. 되돌릴 수 없음
#    → force 후 역전환도 일관성 보장 안 됨
#
# 사용 가능한 시나리오:
# - 패치 대상 함수 내부에서 무한 대기하는 kthread
# - 시스템 재부팅 직전 긴급 패치 적용
# - 개발/테스트 환경에서 디버깅 목적

# 전환 지연 원인 파악 (force 전에 먼저 확인)
for pid in /proc/[0-9]*; do
    state=$(cat "$pid/patch_state" 2>/dev/null)
    if [ "$state" = "-1" ]; then
        comm=$(cat "$pid/comm")
        echo "=== Blocking: PID=$(basename $pid) ($comm) ==="
        cat "$pid/stack"
        echo "---"
    fi
done

# 시그널로 안전하게 깨우기 (force보다 안전)
# SIGSTOP+SIGCONT: 태스크를 깨워서 schedule() 유발
kill -SIGSTOP <blocking_pid>
kill -SIGCONT <blocking_pid>
force 사용 시 반드시 알아야 할 사항:
  • 강제 전환은 되돌릴 수 없습니다. 한 번 force를 사용하면 해당 패치의 역전환(unpatch)도 일관성이 보장되지 않습니다.
  • 커널 tainted 비트에 TAINT_LIVEPATCH_FORCE가 영구적으로 설정됩니다.
  • force 전에 반드시 블로킹 태스크의 스택을 확인하여, 해당 태스크가 패치 대상 함수를 실제로 실행 중인지 확인해야 합니다. reliable stacktrace 실패(인터럽트 프레임 등)로 인한 거짓 양성일 수 있습니다.
  • 시그널(SIGSTOP/SIGCONT)로 태스크를 깨우는 것이 force보다 안전한 대안입니다.

전환 완료 감시 워크큐

Livepatch는 전환 진행 상황을 주기적으로 감시하는 delayed_work를 사용합니다. 이 워크큐는 klp_try_complete_transition()을 반복 호출하여 아직 전환되지 않은 태스크를 확인하고, 모든 태스크의 전환이 완료되면 klp_complete_transition()을 호출합니다.

/*
 * klp_try_complete_transition() — 주기적으로 호출
 * (kernel/livepatch/transition.c)
 *
 * 모든 태스크를 순회하며 전환 가능 여부 검사
 */
void klp_try_complete_transition(void)
{
    struct task_struct *g, *task;
    unsigned int cpu;
    bool complete = true;

    /* PREEMPT_DISABLE 없이 모든 태스크 순회 */
    read_lock(&tasklist_lock);
    for_each_process_thread(g, task) {
        if (!klp_try_switch_task(task))
            complete = false;
    }
    read_unlock(&tasklist_lock);

    /* 모든 CPU의 idle 태스크도 검사 */
    cpus_read_lock();
    for_each_online_cpu(cpu) {
        task = idle_task(cpu);
        if (!klp_try_switch_task(task))
            complete = false;
    }
    cpus_read_unlock();

    if (!complete) {
        /* 미완료 태스크 존재: 1초 후 재시도 */
        schedule_delayed_work(&klp_transition_work,
                             round_jiffies_relative(HZ));
        return;
    }

    /* 모든 태스크 전환 완료 */
    klp_complete_transition();
}

/*
 * 전환 타이밍 정리:
 *
 * 1. klp_start_transition() 호출 → 모든 태스크에 PENDING 설정
 * 2. 즉시 klp_try_complete_transition() 첫 실행
 * 3. 이미 안전한 태스크(idle, 새 태스크 등) 즉시 전환
 * 4. 전환 미완료 시 1초 간격으로 재시도
 * 5. 각 태스크가 schedule()할 때도 자체 전환 시도
 *    → klp_update_patch_state(current)
 * 6. 전형적 서버: 수 초 내 전환 완료
 *    I/O 집약 워크로드: 수 분까지 소요 가능
 */

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인 프로세스(Process) 식별 → /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 적용 시 반드시 확인할 사항:
  • 함수 시그니처 일치: 패치 함수와 원본 함수의 인자, 반환 타입, 호출 규약(Calling Convention)이 정확히 일치해야 합니다. 불일치 시 스택 손상이나 커널 패닉(Kernel Panic)이 발생합니다.
  • 데이터 구조 호환성: 구조체 레이아웃이 변경된 패치는 적용할 수 없습니다. Livepatch는 함수만 교체하며 데이터 구조는 변경하지 않습니다.
  • 인라인 함수(Inline Function): 컴파일러가 인라인한 함수는 패치할 수 없습니다. noinline 속성이 필요합니다.
  • 컴파일러 최적화(Compiler Optimization): 패치 모듈과 원본 커널은 동일한 컴파일러 버전과 최적화 수준으로 빌드해야 합니다.
  • 시맨틱 변경 범위: 호출자가 기대하는 함수의 의미론(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를 적용하기 전 반드시 확인해야 할 항목들입니다.

#!/bin/bash
# livepatch-safety-check.sh — 프로덕션 적용 전 안전성 검증 스크립트

echo "=== Livepatch 프로덕션 안전성 검증 ==="
PASS=0
FAIL=0

# 1. CONFIG_LIVEPATCH 확인
if [ -d /sys/kernel/livepatch ]; then
    echo "[PASS] CONFIG_LIVEPATCH=y"; PASS=$((PASS+1))
else
    echo "[FAIL] CONFIG_LIVEPATCH not enabled"; FAIL=$((FAIL+1))
fi

# 2. reliable stacktrace 지원 확인
if grep -q "HAVE_RELIABLE_STACKTRACE=y" /boot/config-$(uname -r) 2>/dev/null; then
    echo "[PASS] HAVE_RELIABLE_STACKTRACE=y"; PASS=$((PASS+1))
else
    echo "[WARN] HAVE_RELIABLE_STACKTRACE not confirmed"
fi

# 3. ORC unwinder 확인 (x86_64)
if grep -q "UNWINDER_ORC=y" /boot/config-$(uname -r) 2>/dev/null; then
    echo "[PASS] ORC unwinder enabled"; PASS=$((PASS+1))
else
    echo "[WARN] ORC unwinder not enabled (frame pointer 사용)"
fi

# 4. 현재 진행 중인 전환 확인
transitioning=0
for patch in /sys/kernel/livepatch/*/transition; do
    if [ "$(cat "$patch" 2>/dev/null)" = "1" ]; then
        transitioning=$((transitioning+1))
    fi
done
if [ $transitioning -eq 0 ]; then
    echo "[PASS] No transition in progress"; PASS=$((PASS+1))
else
    echo "[FAIL] $transitioning transition(s) in progress"; FAIL=$((FAIL+1))
fi

# 5. 모듈 서명 정책 확인
if grep -q "MODULE_SIG_FORCE=y" /boot/config-$(uname -r) 2>/dev/null; then
    echo "[INFO] MODULE_SIG_FORCE=y — 모듈 서명 필수"
fi

# 6. Secure Boot 상태 확인
if [ -d /sys/firmware/efi ]; then
    sb_state=$(cat /sys/firmware/efi/efivars/SecureBoot-* 2>/dev/null | xxd -p | tail -c3)
    if [ "$sb_state" = "01" ]; then
        echo "[INFO] Secure Boot ENABLED — MOK 서명 필요"
    fi
fi

# 7. 심볼 존재 확인 (패치 대상 함수)
check_symbol() {
    if grep -q " $1$" /proc/kallsyms; then
        echo "[PASS] Symbol found: $1"; PASS=$((PASS+1))
    else
        echo "[FAIL] Symbol NOT found: $1"; FAIL=$((FAIL+1))
    fi
}
# 사용: check_symbol "cmdline_proc_show"

# 8. 디스크 공간 확인 (vmcore 덤프 가능 여부)
avail_gb=$(df -BG /var/crash 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G')
if [ "${avail_gb:-0}" -ge 10 ]; then
    echo "[PASS] Crash dump space: ${avail_gb}GB"; PASS=$((PASS+1))
else
    echo "[WARN] Low crash dump space: ${avail_gb:-?}GB"
fi

echo "=== Results: PASS=$PASS FAIL=$FAIL ==="
if [ $FAIL -gt 0 ]; then
    echo "WARNING: $FAIL check(s) failed. Fix before applying livepatch."
    exit 1
fi

objtool과 스택 신뢰성 검증

objtool은 커널 빌드 시 ELF 오브젝트 파일을 분석하여 스택 신뢰성 문제를 미리 감지하는 도구입니다. Livepatch의 reliable stacktrace가 정확히 동작하려면 모든 커널 코드가 objtool의 검증을 통과해야 합니다.

# objtool 경고 예시와 livepatch 영향

# 1. 프레임 포인터 누락 경고
# vmlinux.o: warning: objtool: foo() - call without frame pointer save/setup
# → 이 함수의 스택은 reliable하지 않음
# → livepatch 전환 시 이 함수가 스택에 있으면 항상 전환 거부

# 2. 어셈블리 코드 누락 ORC 어노테이션
# arch/x86/entry/entry_64.S: warning: objtool: missing ORC annotation
# → 해결: UNWIND_HINT_* 매크로 추가

# 3. objtool 실행 (빌드 중 자동)
# make 시 CONFIG_OBJTOOL=y면 자동으로 각 .o 파일 검증
# 수동 실행:
./tools/objtool/objtool check -f -a -r vmlinux.o

# objtool 주요 옵션:
#   -f : 프레임 포인터 검증
#   -a : ORC 데이터 생성
#   -r : retpoline 검증
#   -l : unreachable 명령어 경고
; === ORC 어노테이션 예시 (arch/x86/entry/entry_64.S) ===

; UNWIND_HINT_REGS: 레지스터 저장 상태 표시
entry_SYSCALL_64:
    UNWIND_HINT_ENTRY
    swapgs
    movq   %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
    movq   PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp
    UNWIND_HINT_REGS extra=0  ; ← ORC에게 레지스터 상태 알림

; UNWIND_HINT_EMPTY: 스택이 비어있음을 표시
; objtool이 이 정보로 정확한 ORC 테이블 생성
; → reliable stacktrace 정확도 향상
; → livepatch 전환 성공률 향상

Livepatch 역사: kGraft vs kpatch

커널 라이브 패칭 기술은 2008년 Ksplice에서 시작되어, 2014년 Red Hat의 kpatch와 SUSE의 kGraft가 독립적으로 개발되었습니다. 두 프로젝트의 핵심 개발자들이 협력하여 2015년 커널 4.0에 통합 프레임워크인 Livepatch가 탄생했습니다.

역사적 타임라인

커널 라이브 패칭 기술 발전 타임라인 2008 Ksplice (MIT) stop_machine 방식 2011: Oracle 인수 2014 kpatch (Red Hat) stop_machine + ftrace 2014.2 LKML 제출 kGraft (SUSE) per-task lazy 전환 2014.5 LKML 제출 2015 Livepatch (v4.0) kGraft + kpatch 통합 per-task + stack check 2017- 성숙기 shadow var, atomic replace 콜백, arm64 지원 두 프로젝트 통합 주요 기능 추가 타임라인 v4.0: 기본 프레임워크 v4.12: shadow var v4.15: consistency v5.1: atomic replace v5.1: 콜백 API v6.3: arm64 지원 v4.15 consistency model: Seth Jennings/Josh Poimboeuf(Red Hat)의 per-task 모델이 최종 채택 kGraft의 lazy 전환 + kpatch의 stack checking = 안전하면서도 무중단 전환
그림: 커널 라이브 패칭 기술 발전 타임라인 - Ksplice(2008)부터 현재까지

세 접근법의 기술적 차이

세 프로젝트의 핵심 차이는 일관성 모델(Consistency Model)에 있습니다. kpatch는 모든 CPU를 정지시켜 원자적으로 교체하는 보수적 방식을, kGraft는 태스크별로 점진적으로 전환하는 진보적 방식을 채택했습니다. Livepatch는 두 접근법의 장점을 결합하여, 태스크별 점진 전환(kGraft)에 스택 검사(kpatch의 안전성 보장)를 추가했습니다.

/*
 * === kpatch (Red Hat) 방식 ===
 * stop_machine()으로 모든 CPU를 정지한 후 교체
 *
 * 장점: 원자적 교체로 의미론적 일관성 완벽 보장
 * 단점: 전체 시스템 정지 (수~수십 ms)
 *       PREEMPT_RT 환경에서 허용 불가한 지연
 *
 * stop_machine() {
 *   // 모든 CPU가 여기서 대기
 *   for each patched function:
 *     NOP → call ftrace_caller 교체
 *   // 모든 CPU가 동시에 새 함수 사용 시작
 * }
 */

/*
 * === kGraft (SUSE) 방식 ===
 * 태스크별로 점진적 전환 (lazy switching)
 *
 * 장점: 시스템 정지 없음
 * 단점: 전환 중 두 함수 동시 실행 가능
 *       스택 검사 미수행 → 의미론적 일관성 이슈
 *
 * // 각 태스크가 syscall 경계에서 universe 전환
 * // 하지만 스택에 old_func이 있어도 전환 허용
 * // → 같은 호출에서 old/new가 섞일 수 있음
 */

/*
 * === Livepatch (통합) 방식 ===
 * kGraft + kpatch 결합
 *
 * 장점: 무중단 + 스택 검증으로 안전한 전환
 * 유일한 단점: 전환 완료까지 시간 소요 가능
 *
 * klp_try_switch_task(task) {
 *   // 1. 스택 검사 (kpatch에서 차용)
 *   if (klp_check_stack(task) has old_func)
 *     return false;  // 다음 기회에 재시도
 *
 *   // 2. 안전 확인 후 전환 (kGraft에서 차용)
 *   task->patch_state = new_universe;
 * }
 */
항목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") 누락모듈 소스에 해당 매크로(Macro) 추가
전환이 완료되지 않음태스크가 패치 대상 함수 내 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 필수, 인라인 어셈블리(Assembly)에서 깨질 수 있음
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로 임시 매핑(Mapping) 생성하여 수정 후 복원
IBTx86 CET(Control-flow Enforcement Technology)의 간접 분기 추적커널 6.2+에서 livepatch가 IBT 호환, ENDBR64 명령어 자동 처리
FineIBTkCFI + IBT 조합으로 간접 호출 시 해시(Hash) 검증패치 함수의 프로토타입이 원본과 동일해야 해시가 일치
# === 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~2h) ☐ CVE 공지 모니터링 (RSS/메일) ☐ 영향도 평가 (CVSS, 사용 여부) → 패치 원본 코드 확보 2단계: 개발 (2~4h) ☐ kpatch-build로 자동 생성 ☐ klp_patch 구조체 작성 → 모듈 빌드 및 심볼 검증 3단계: 테스트 (4~8h) ☐ QEMU 가상머신 적용 테스트 ☐ 전환 시간 측정 (전체 태스크) → 기능/성능 회귀 테스트 4단계: 스테이징 (24h) ☐ 스테이징 서버 1~3대 적용 ☐ 전환 확인 (transition=0) → 24시간 안정성 모니터링 5단계: 카나리 (1~3일) ☐ 프로덕션 1~5% 서버 배포 ☐ 메트릭 비교 (CPU/메모리/에러율) → A/B 테스트로 영향도 측정 6단계: 전체 배포 (1주) ☐ 점진적 롤아웃 (10%→50%→100%) ☐ 각 단계마다 24시간 관찰 → 배포 완료 및 문서화 지속 모니터링 (전 과정) ☐ CPU/메모리 사용률 ☐ 애플리케이션 에러율 → dmesg 커널 로그 감시 긴급 롤백 (문제 발견 시) ☐ echo 0 > enabled (패치 비활성화) ☐ 전환 완료 확인 (transition=0) → rmmod로 모듈 제거 전체 타임라인 1~2h 2~4h 4~8h 24h 1~3일 1주 긴급 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")은 반드시 포함해야 합니다.

CVE 패치 실전: Dirty Pipe (CVE-2022-0847) 유형

Dirty Pipe는 파이프(Pipe) 버퍼의 PIPE_BUF_FLAG_CAN_MERGE 플래그가 잘못 설정되어 임의 파일 쓰기가 가능한 권한 상승 취약점이었습니다. 이런 유형의 취약점은 livepatch로 효과적으로 대응할 수 있습니다.

/*
 * Dirty Pipe 유형 취약점의 livepatch 수정 예시
 *
 * 원본 (취약한 코드):
 *   copy_page_to_iter_pipe()에서 pipe 버퍼 플래그를
 *   초기화하지 않아 이전 플래그(CAN_MERGE)가 남음
 *
 * 수정: 새 페이지를 파이프에 추가할 때 플래그 초기화
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/livepatch.h>
#include <linux/pipe_fs_i.h>
#include <linux/uio.h>

/*
 * 패치된 함수: copy_page_to_iter_pipe()
 * 변경점: buf->flags = 0 으로 초기화 (1줄 추가)
 *
 * 함수 시그니처는 원본과 정확히 동일해야 함
 * 컴파일러/최적화 레벨도 동일해야 함
 */
static size_t patched_copy_page_to_iter_pipe(
    struct page *page, size_t offset, size_t bytes,
    struct iov_iter *i)
{
    struct pipe_inode_info *pipe = i->pipe;
    struct pipe_buffer *buf;
    unsigned int p_tail = pipe->tail;
    unsigned int p_mask = pipe->ring_size - 1;
    unsigned int i_head = i->head;

    if (unlikely(bytes > i->count))
        bytes = i->count;

    if (unlikely(!bytes))
        return 0;

    if (!sanity(i))
        return 0;

    buf = &pipe->bufs[i_head & p_mask];
    buf->ops = &page_cache_pipe_buf_ops;
    get_page(page);
    buf->page = page;
    buf->offset = offset;
    buf->len = bytes;

    /* === 패치 핵심: 플래그 초기화 (Dirty Pipe 수정) === */
    buf->flags = 0;  /* CAN_MERGE 플래그 제거 */

    pipe->head = i_head + 1;
    i->iov_offset = offset + bytes;
    i->head = i_head;

    return bytes;
}

static struct klp_func funcs[] = {
    {
        .old_name = "copy_page_to_iter_pipe",
        .new_func = patched_copy_page_to_iter_pipe,
    },
    { }
};

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

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

static int __init dirty_pipe_fix_init(void)
{
    pr_info("Applying CVE-2022-0847 (Dirty Pipe) livepatch\\n");
    return klp_enable_patch(&patch);
}

static void __exit dirty_pipe_fix_exit(void) { }

module_init(dirty_pipe_fix_init);
module_exit(dirty_pipe_fix_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");
MODULE_DESCRIPTION("Livepatch: CVE-2022-0847 Dirty Pipe fix");

CVE 패치 실전: 버퍼 오버플로우 유형

버퍼 오버플로우(Buffer Overflow) 취약점은 입력 크기 검증이 누락되어 발생합니다. livepatch로 검증 로직을 추가하는 패턴입니다.

/*
 * 버퍼 오버플로우 취약점 livepatch 수정 예시
 *
 * 원본 (취약):
 *   static ssize_t ioctl_handler(... unsigned long arg) {
 *     char buf[256];
 *     if (copy_from_user(buf, (void __user *)arg, size))
 *       → size 미검증 → 스택 버퍼 오버플로우
 *
 * 수정: size 범위 검사 추가
 */
#include <linux/module.h>
#include <linux/livepatch.h>
#include <linux/uaccess.h>

#define BUF_SIZE 256

static long patched_ioctl_handler(
    struct file *filp, unsigned int cmd,
    unsigned long arg)
{
    char buf[BUF_SIZE];
    size_t size;

    switch (cmd) {
    case CUSTOM_IOCTL_WRITE:
        /* 패치 핵심: size 검증 추가 */
        if (get_user(size, (size_t __user *)arg))
            return -EFAULT;

        if (size > BUF_SIZE) {
            pr_warn_ratelimited(
                "ioctl: size %zu exceeds buffer %d\\n",
                size, BUF_SIZE);
            return -EINVAL;  /* 오버플로우 방지 */
        }

        if (copy_from_user(buf, (void __user *)(arg + sizeof(size)),
                          size))
            return -EFAULT;

        /* 나머지 처리... */
        break;
    }

    return 0;
}

CVE 패치 실전: 레이스 컨디션(Race Condition) 유형

레이스 컨디션 취약점은 동시 접근 보호가 미흡하여 발생합니다. livepatch로 적절한 잠금(Lock)을 추가하거나 검사 순서를 변경할 수 있습니다.

/*
 * TOCTOU(Time-of-Check-Time-of-Use) 레이스 수정 예시
 *
 * 원본 (취약):
 *   if (check_permission(obj))     ← 검사 시점
 *     ...
 *     use(obj);                    ← 사용 시점 (obj가 변경됨)
 *
 * 수정: rcu_read_lock()으로 참조 보호 + 재검증
 */
static int patched_vulnerable_lookup(
    struct ns_common *ns, unsigned int id)
{
    struct target_obj *obj;
    int ret = -ENOENT;

    /* 패치: RCU 보호 구간 확장 + 재검증 */
    rcu_read_lock();

    obj = idr_find(&ns->idr, id);
    if (!obj)
        goto out_unlock;

    /* 패치 핵심: 참조 카운터 확인 (해제 중이 아닌지) */
    if (!refcount_inc_not_zero(&obj->refcount))
        goto out_unlock;

    /* 안전하게 obj 사용 (refcount 보호) */
    ret = process_obj(obj);
    refcount_dec(&obj->refcount);

out_unlock:
    rcu_read_unlock();
    return ret;
}

/*
 * CVE 유형별 livepatch 적용 가능성:
 *
 * ✓ 적용 가능:
 *   - NULL 포인터 역참조 → NULL 체크 추가
 *   - 경계 검사 누락 → 범위 검증 추가
 *   - UAF (Use-After-Free) → 참조 카운터/RCU 보호
 *   - TOCTOU 레이스 → 잠금 추가/검사 순서 변경
 *   - 정수 오버플로우 → 오버플로우 검사 추가
 *   - 정보 누출 → 버퍼 초기화 추가
 *
 * ✗ 적용 불가:
 *   - 구조체 레이아웃 변경 필요
 *   - 전역 변수 초기화 값 변경
 *   - 부팅 시 한 번만 실행되는 코드
 *   - 인라인 함수 내부 수정
 *   - ABI 변경이 필요한 수정
 */

운영 모니터링 자동화

대규모 서버 환경에서 livepatch 상태를 자동으로 모니터링하려면 Prometheus 등의 모니터링 시스템과 연동합니다.

#!/bin/bash
# livepatch-exporter.sh — Prometheus node_exporter textfile 형식
# cron으로 1분마다 실행: * * * * * /opt/scripts/livepatch-exporter.sh

OUTPUT=/var/lib/node_exporter/livepatch.prom
TMPFILE=$(mktemp)

echo "# HELP livepatch_enabled Livepatch enabled status" > $TMPFILE
echo "# TYPE livepatch_enabled gauge" >> $TMPFILE

echo "# HELP livepatch_transition Livepatch transition status" >> $TMPFILE
echo "# TYPE livepatch_transition gauge" >> $TMPFILE

echo "# HELP livepatch_blocking_tasks Tasks blocking transition" >> $TMPFILE
echo "# TYPE livepatch_blocking_tasks gauge" >> $TMPFILE

for patch_dir in /sys/kernel/livepatch/*/; do
    [ -d "$patch_dir" ] || continue
    name=$(basename "$patch_dir")
    enabled=$(cat "${patch_dir}enabled" 2>/dev/null || echo "0")
    transition=$(cat "${patch_dir}transition" 2>/dev/null || echo "0")

    echo "livepatch_enabled{patch=\"$name\"} $enabled" >> $TMPFILE
    echo "livepatch_transition{patch=\"$name\"} $transition" >> $TMPFILE
done

# 블로킹 태스크 수 카운트
blocking=0
for pid in /proc/[0-9]*; do
    state=$(cat "$pid/patch_state" 2>/dev/null)
    [ "$state" = "-1" ] && blocking=$((blocking+1))
done
echo "livepatch_blocking_tasks $blocking" >> $TMPFILE

mv $TMPFILE $OUTPUT
# Prometheus alerting rules for livepatch
groups:
- name: livepatch
  rules:
  - alert: LivepatchTransitionStuck
    expr: livepatch_transition == 1
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "Livepatch transition stuck for > 10 minutes"
      description: "Check /proc/PID/patch_state for blocking tasks"

  - alert: LivepatchBlockingTasks
    expr: livepatch_blocking_tasks > 10
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "{{ $value }} tasks blocking livepatch transition"

  - alert: LivepatchDisabled
    expr: livepatch_enabled == 0
    for: 1h
    labels:
      severity: info
    annotations:
      summary: "Livepatch disabled — vulnerability may be unpatched"

systemd 서비스 통합

부팅 시 자동으로 livepatch 모듈을 로드하고 상태를 모니터링하는 systemd 서비스를 구성할 수 있습니다.

# /etc/systemd/system/livepatch@.service
# 사용: systemctl enable livepatch@cve-2024-xxxx

[Unit]
Description=Kernel Livepatch: %i
After=systemd-modules-load.service
ConditionPathExists=/opt/livepatches/%i.ko

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/insmod /opt/livepatches/%i.ko
ExecStartPost=/bin/bash -c '\
  timeout=60; \
  while [ $timeout -gt 0 ]; do \
    trans=$(cat /sys/kernel/livepatch/%i/transition 2>/dev/null); \
    [ "$trans" = "0" ] && break; \
    sleep 1; \
    timeout=$((timeout-1)); \
  done; \
  [ $timeout -eq 0 ] && echo "WARNING: transition not complete" >&2'
ExecStop=/bin/bash -c '\
  echo 0 > /sys/kernel/livepatch/%i/enabled; \
  timeout=30; \
  while [ $timeout -gt 0 ]; do \
    trans=$(cat /sys/kernel/livepatch/%i/transition 2>/dev/null); \
    [ "$trans" = "0" ] && break; \
    sleep 1; \
    timeout=$((timeout-1)); \
  done; \
  /sbin/rmmod %i'

[Install]
WantedBy=multi-user.target
# === Ansible playbook 형태의 대규모 배포 예시 ===

# 1. 패치 모듈 배포
# ansible -i inventory all -m copy \
#   -a "src=cve-2024-xxxx.ko dest=/opt/livepatches/"

# 2. 단계적 적용 (10% → 50% → 100%)
# ansible -i inventory canary -m shell \
#   -a "insmod /opt/livepatches/cve-2024-xxxx.ko"

# 3. 전환 완료 확인
# ansible -i inventory canary -m shell \
#   -a "cat /sys/kernel/livepatch/cve_2024_xxxx/transition"

# 4. 메트릭 확인 후 전체 배포
# ansible -i inventory all -m shell \
#   -a "insmod /opt/livepatches/cve-2024-xxxx.ko"

# 5. 긴급 롤백
# ansible -i inventory all -m shell \
#   -a "echo 0 > /sys/kernel/livepatch/cve_2024_xxxx/enabled && \
#       sleep 5 && rmmod cve_2024_xxxx"

일관성 모델

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 경계: 시스템 콜(System Call) 진입/리턴 시 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를 양보(Yield)할 때
cond_resched()__cond_resched() → schedule()선점 가능 지점
syscall returnexit_to_user_mode_loop()사용자 공간(User Space) 복귀 시
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전체 레지스터(Register) 저장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 플래그: 인터럽트(Interrupt) 컨텍스트에서는 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

Shadow Variable 내부 구현

Shadow variable은 커널 내부에서 해시 테이블로 관리됩니다. 해시 키는 원본 객체 포인터와 shadow ID의 조합으로 생성됩니다. 이 구조는 O(1) 평균 시간 복잡도로 조회할 수 있어 성능 영향이 최소화됩니다.

/*
 * Shadow Variable 내부 구현 (kernel/livepatch/shadow.c)
 *
 * 전역 해시 테이블: klp_shadow_hash
 * 해시 키: hash(obj_ptr ^ shadow_id)
 * 동기화: spinlock (klp_shadow_lock)
 */

/* 내부 shadow 구조체 */
struct klp_shadow {
    struct hlist_node node;   /* 해시 버킷 연결 */
    struct rcu_head rcu_head;  /* RCU 해제용 */
    void *obj;                 /* 연결된 원본 객체 */
    unsigned long id;          /* shadow variable ID */
    char data[];               /* 가변 길이 사용자 데이터 */
};

/* 해시 테이블 (256 버킷, 전역) */
static DEFINE_HASHTABLE(klp_shadow_hash, 8);
static DEFINE_SPINLOCK(klp_shadow_lock);

/*
 * 해시 함수: 객체 주소와 ID를 XOR하여 해시
 * → 같은 객체의 서로 다른 shadow variable도 분산
 */
static inline unsigned long klp_shadow_hash_key(
    void *obj, unsigned long id)
{
    return (unsigned long)obj ^ id;
}

/*
 * klp_shadow_get() 내부 구현:
 *
 * 1. 해시 키 계산
 * 2. 해당 버킷을 RCU로 순회
 * 3. obj와 id가 모두 일치하는 엔트리 반환
 * 4. 없으면 NULL 반환
 *
 * 조회는 RCU read-side critical section에서 수행
 * → spinlock 없이 병렬 조회 가능
 * → 할당/해제만 spinlock 필요
 */

/* 하나의 객체에 여러 shadow variable 연결 가능 */
/*
 * 예시: struct sock *sk에 2개의 shadow 연결
 *
 * klp_shadow_alloc(sk, SV_RATE_LIMIT, ...)  → shadow #1
 * klp_shadow_alloc(sk, SV_STATS, ...)       → shadow #2
 *
 * klp_shadow_get(sk, SV_RATE_LIMIT)  → shadow #1의 data
 * klp_shadow_get(sk, SV_STATS)       → shadow #2의 data
 *
 * klp_shadow_free(sk, SV_RATE_LIMIT) → shadow #1만 해제
 * klp_shadow_free_all(SV_STATS, dtor) → 모든 객체의 #2 일괄 해제
 */

Shadow Variable 고급 사용 패턴

실전에서 shadow variable이 필요한 대표적인 시나리오와 설계 패턴입니다.

/*
 * 패턴 1: 상태 추적 — 이전 값을 기억하여 변화 감지
 */
#define SV_PREV_STATE  0x3001

struct prev_state {
    u64 prev_value;
    u64 change_count;
    ktime_t last_change;
};

static int patched_update_config(struct config_obj *cfg,
                                u64 new_value)
{
    struct prev_state *ps;

    ps = klp_shadow_get_or_alloc(cfg, SV_PREV_STATE,
        sizeof(*ps), GFP_KERNEL, NULL, NULL);

    if (ps) {
        if (ps->prev_value != new_value) {
            pr_info("config changed: %llu -> %llu (count=%llu)\\n",
                    ps->prev_value, new_value, ps->change_count);
            ps->prev_value = new_value;
            ps->change_count++;
            ps->last_change = ktime_get();
        }
    }

    /* 원본 로직 실행 */
    return original_update_config(cfg, new_value);
}

/*
 * 패턴 2: 속도 제한 — 과도한 호출 탐지 및 제한
 */
#define SV_RATE_LIMIT  0x3002

struct rate_limiter {
    u64 last_ts;
    u32 count;
    u32 suppressed;
};

static int patched_frequent_handler(struct device *dev,
                                    int event)
{
    struct rate_limiter *rl;
    u64 now = ktime_get_ns();

    rl = klp_shadow_get_or_alloc(dev, SV_RATE_LIMIT,
        sizeof(*rl), GFP_ATOMIC, NULL, NULL);

    if (rl) {
        /* 1초 윈도우 내 100회 이상이면 제한 */
        if (now - rl->last_ts < NSEC_PER_SEC) {
            rl->count++;
            if (rl->count > 100) {
                rl->suppressed++;
                return -EBUSY;
            }
        } else {
            if (rl->suppressed)
                pr_info("dev %s: suppressed %u events\\n",
                        dev_name(dev), rl->suppressed);
            rl->last_ts = now;
            rl->count = 1;
            rl->suppressed = 0;
        }
    }

    return original_handler(dev, event);
}

/*
 * 패턴 3: 파괴자(destructor) — 해제 시 정리 로직
 */
static void my_shadow_dtor(void *obj, void *shadow_data)
{
    struct my_data *data = shadow_data;
    /* 내부 자원 정리 */
    if (data->buffer)
        kfree(data->buffer);
    pr_debug("shadow dtor: obj=%p cleaned up\\n", obj);
}

/* 정리 시 destructor 전달 */
klp_shadow_free(obj, MY_ID, my_shadow_dtor);
klp_shadow_free_all(MY_ID, my_shadow_dtor);
Shadow Variable 주의사항:
  • 메모리 누수: 원본 객체가 해제될 때 shadow를 해제하지 않으면 영구 메모리 누수. 패치 대상 함수의 해제 경로도 패치하여 klp_shadow_free() 호출을 추가해야 합니다.
  • 경쟁 조건: klp_shadow_alloc()은 이미 존재하면 NULL 반환. 병렬 환경에서는 klp_shadow_get_or_alloc()을 사용해야 원자적 조회+생성이 보장됩니다.
  • ID 충돌: 서로 다른 livepatch 모듈이 같은 ID를 사용하면 데이터가 오염됩니다. 모듈별로 고유한 ID 범위를 할당하세요.
  • 성능: 해시 테이블 접근은 O(1)이지만, spinlock 경합이 발생할 수 있습니다. hot path에서의 과도한 alloc/free는 피하세요.

라이브패치 수명주기

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로 빌드되어야 함

QEMU 기반 테스트 환경 구축

Livepatch 모듈을 프로덕션에 적용하기 전 QEMU 가상 머신(Virtual Machine)에서 충분히 테스트해야 합니다. 다음은 효율적인 테스트 환경 구축 방법입니다.

# === QEMU 기반 livepatch 테스트 환경 ===

# 1. 테스트용 커널 빌드 (필수 CONFIG)
cat >> .config <<'EOF'
CONFIG_LIVEPATCH=y
CONFIG_FTRACE=y
CONFIG_DYNAMIC_FTRACE=y
CONFIG_DYNAMIC_FTRACE_WITH_REGS=y
CONFIG_HAVE_DYNAMIC_FTRACE_WITH_REGS=y
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_HAVE_RELIABLE_STACKTRACE=y
CONFIG_UNWINDER_ORC=y
CONFIG_STACKTRACE=y
CONFIG_TEST_LIVEPATCH=m
EOF
make olddefconfig
make -j$(nproc) bzImage modules

# 2. rootfs 생성 (최소 BusyBox 기반)
mkdir -p rootfs/{proc,sys,dev,lib/modules}
# ... (busybox static 바이너리 복사)

# 3. QEMU 실행 (9p 공유로 모듈 전달)
qemu-system-x86_64 \
    -kernel arch/x86/boot/bzImage \
    -initrd rootfs.cpio.gz \
    -append "console=ttyS0 nokaslr" \
    -nographic \
    -m 1G \
    -virtfs local,path=./modules,mount_tag=modules,security_model=none

# 게스트 내부에서:
mount -t 9p -o trans=virtio modules /mnt
insmod /mnt/my-livepatch.ko
cat /sys/kernel/livepatch/*/enabled
cat /sys/kernel/livepatch/*/transition

# 4. 자동화 테스트 스크립트
#!/bin/bash
# test-livepatch-qemu.sh
set -e

# 모듈 로드
insmod /mnt/my-livepatch.ko
echo "Module loaded"

# 전환 완료 대기 (최대 30초)
timeout=30
while [ $timeout -gt 0 ]; do
    trans=$(cat /sys/kernel/livepatch/*/transition 2>/dev/null)
    [ "$trans" = "0" ] && break
    sleep 1
    timeout=$((timeout-1))
done

if [ $timeout -eq 0 ]; then
    echo "FAIL: transition not complete in 30s"
    exit 1
fi

echo "PASS: transition complete"

# 기능 검증
# ... (패치된 동작 확인)

# 롤백 테스트
echo 0 > /sys/kernel/livepatch/*/enabled
timeout=30
while [ $timeout -gt 0 ]; do
    trans=$(cat /sys/kernel/livepatch/*/transition 2>/dev/null)
    [ "$trans" = "0" ] && break
    sleep 1
    timeout=$((timeout-1))
done

rmmod my-livepatch
echo "PASS: rollback complete"

# dmesg 에러 확인
if dmesg | grep -qi "error\|panic\|bug\|oops"; then
    echo "FAIL: errors found in dmesg"
    dmesg | grep -i "error\|panic\|bug\|oops"
    exit 1
fi

echo "ALL TESTS PASSED"

자주 발생하는 오류와 해결법

오류 코드발생 시점원인해결 방법
-EINVALklp_enable_patch()MODULE_INFO(livepatch, "Y") 누락모듈 소스에 매크로 추가
-ENODEVklp_init_func()심볼을 /proc/kallsyms에서 찾을 수 없음심볼 이름 확인, old_sympos 설정
-EBUSYklp_enable_patch()이미 진행 중인 전환이 있음전환 완료 후 재시도
-EEXISTregister_ftrace_function()대상 함수에 이미 IPMODIFY ops 존재kretprobe 등 충돌 도구 확인
-ENOMEM다양메모리 할당 실패시스템 메모리 상태 확인
-EAGAINklp_check_stack()스택 검사에서 패치 대상 함수 발견정상 동작 (다음 schedule에서 재시도)
tainted K모듈 로드 후정상 — livepatch 적용 시 자동 설정조치 불필요 (정보 표시 목적)
커널 패닉패치 함수 실행 시함수 시그니처 불일치, ABI 차이컴파일러/최적화 레벨 일치 확인
# === 오류 진단 도구 모음 ===

# 1. 심볼 존재 확인
grep ' vulnerable_handler$' /proc/kallsyms
# 없으면 -ENODEV 발생

# 2. 동명 함수 확인 (old_sympos 결정)
grep ' cleanup$' /proc/kallsyms | nl
# 여러 개면 old_sympos로 위치 지정

# 3. 함수 크기 확인
grep 'cmdline_proc_show' /proc/kallsyms
nm -S vmlinux | grep 'cmdline_proc_show'
# 크기가 0이면 인라인되었을 가능성

# 4. CONFIG 확인
for cfg in LIVEPATCH DYNAMIC_FTRACE HAVE_RELIABLE_STACKTRACE UNWINDER_ORC; do
    val=$(grep "CONFIG_${cfg}" /boot/config-$(uname -r) || echo "NOT SET")
    echo "  $cfg: $val"
done

# 5. 모듈 정보 확인
modinfo my-livepatch.ko | grep -E "(livepatch|license|vermagic)"
# livepatch: Y 필수, license: GPL 필수
# vermagic: 현재 커널과 일치해야 함

# 6. ftrace 충돌 확인
cat /sys/kernel/debug/tracing/enabled_functions | grep 'vulnerable_handler'
# 이미 IPMODIFY가 등록된 함수면 -EEXIST

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로 강제 전환 (주의: 일관성 보장 안 됨)

bpftrace를 활용한 전환 추적

bpftrace를 사용하면 livepatch 전환 과정을 실시간으로 추적할 수 있습니다. 각 태스크의 전환 시점, 소요 시간, 블로킹 원인을 정밀하게 분석할 수 있습니다.

#!/usr/bin/env bpftrace
// livepatch-transition-trace.bt
// 용도: livepatch 전환 과정 실시간 추적

// 1. klp_try_switch_task 호출 추적
kprobe:klp_try_switch_task
{
    printf("switch_task: pid=%d comm=%s\n",
           pid, comm);
}

// 2. klp_check_stack 결과 추적
kretprobe:klp_check_stack
{
    if (retval != 0) {
        printf("stack_check BLOCKED: pid=%d comm=%s ret=%d\n",
               pid, comm, retval);
        printf("  -EINVAL=unreliable stack, -EADDRINUSE=func on stack\n");
    }
}

// 3. 전환 완료 추적
kprobe:klp_complete_transition
{
    printf("=== TRANSITION COMPLETE ===\n");
    printf("  time: %llu ns\n", nsecs);
}

// 4. klp_update_patch_state 호출 빈도 (schedule 경로)
kprobe:klp_update_patch_state
{
    @update_count[comm] = count();
}

// 5. ftrace handler 실행 시간
kprobe:klp_ftrace_handler
{
    @start[tid] = nsecs;
}

kretprobe:klp_ftrace_handler
/@start[tid]/
{
    @handler_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
# === 실행 예시 ===

# 1. 전환 추적 시작
sudo bpftrace livepatch-transition-trace.bt &

# 2. livepatch 로드
sudo insmod cve-fix.ko

# 3. 출력 예시:
# switch_task: pid=1 comm=systemd
# switch_task: pid=2 comm=kthreadd
# stack_check BLOCKED: pid=1234 comm=kworker/0:1 ret=-22
#   -EINVAL=unreliable stack, -EADDRINUSE=func on stack
# switch_task: pid=1234 comm=kworker/0:1
# === TRANSITION COMPLETE ===
#   time: 2345678901 ns

# 4. ftrace 이벤트로도 추적 가능
echo 1 > /sys/kernel/debug/tracing/events/livepatch/enable
cat /sys/kernel/debug/tracing/trace_pipe

# 출력:
#  klp_try_switch_task: pid=1234 comm=kworker result=blocked
#  klp_try_switch_task: pid=1234 comm=kworker result=ok
#  klp_complete_transition: patch=cve_fix duration_ms=4523

Livepatch 성능 영향 분석

Livepatch의 성능 영향은 두 단계로 나뉩니다: 전환 중(transitioning) 오버헤드와 전환 완료 후(patched) 오버헤드입니다.

단계오버헤드 원인영향 수준지속 시간
전환 전 (NOP)5바이트 NOP 명령어0 (무시 가능)영구
전환 중klp_ftrace_handler의 per-task 분기낮음 (~20ns/call)수 초 ~ 수 분
전환 완료ftrace trampoline 경유매우 낮음 (~10ns/call)패치 활성 기간
전환 완료 (NOP 패치)ftrace handler → 즉시 리턴매우 낮음패치 활성 기간
# ftrace handler 오버헤드 측정 (perf 활용)

# 1. 패치 적용 전 기준선 측정
perf stat -e cycles,instructions -r 5 -- \
    dd if=/dev/zero of=/dev/null bs=4k count=100000

# 2. livepatch 적용
sudo insmod my-patch.ko

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

# 4. 패치 후 성능 측정
perf stat -e cycles,instructions -r 5 -- \
    dd if=/dev/zero of=/dev/null bs=4k count=100000

# 5. 함수 호출 경로 오버헤드 확인
perf record -g -a sleep 5
perf report --sort=overhead,symbol | head -30

# 일반적 결과:
# - ftrace trampoline 오버헤드: 함수 호출당 ~10-20ns
# - 전체 시스템 성능 영향: <0.1% (대부분의 워크로드)
# - hot path 함수 패치 시: 최대 ~1% (측정 필요)
성능 영향 최소화 전략:
  • hot path 함수 피하기: 초당 수백만 번 호출되는 함수(예: memcpy, spin_lock)는 패치 시 성능 영향이 누적됩니다. 가능하면 호출 빈도가 낮은 상위 함수를 패치합니다.
  • 전환 시간 단축: cond_resched()를 드물게 호출하는 커널 스레드가 있으면 전환이 지연됩니다. 사전에 워크로드 패턴을 파악합니다.
  • NOP 패치 정리: replace=true로 이전 패치를 정리하면 불필요한 NOP 패치의 ftrace handler 호출이 제거됩니다.
  • 벤치마크: 프로덕션 적용 전 대표 워크로드로 반드시 성능 비교 테스트를 수행합니다.

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

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

커널 라이브 패칭 솔루션 발전 타임라인 시간 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 발전 방향

커널 Livepatch 프레임워크는 지속적으로 발전하고 있습니다. 커뮤니티에서 논의 중인 주요 개선 방향입니다.

개선 방향현재 상태기대 효과
RISC-V 지원개발 중 (2025)RISC-V 서버에서 무중단 패치 가능
데이터 패치연구 단계구조체 레이아웃 변경까지 지원
전환 병렬화논의 중대규모 태스크 환경에서 전환 시간 단축
eBPF 연동부분 구현BPF 프로그램으로 패치 로직 정의
CLang CFI 완전 호환부분 지원CFI 활성 커널에서의 안정적 패치
패치 의존성 관리미지원패치 간 순서/의존성 자동 해석
실시간(RT) 커널실험적PREEMPT_RT 환경에서의 결정적 전환 시간
커뮤니티 참여: Livepatch 관련 패치와 논의는 live-patching@vger.kernel.org 메일링 리스트에서 진행됩니다. 커널 소스의 Documentation/livepatch/ 디렉터리에 공식 문서가 있으며, kernel/livepatch/에 프레임워크 구현, lib/livepatch/에 셀프테스트 모듈이 포함되어 있습니다.

참고 자료

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