커널 Livepatch
커널 Livepatch를 무중단 보안 패치(Patch)와 운영 리스크 최소화 관점에서 심층 분석합니다. klp_patch/klp_object/klp_func 구조와 ftrace 기반 함수 교체 메커니즘, consistency model과 태스크(Task) 전환 보장, shadow variable 활용과 데이터 호환성 관리, atomic replace 전략, 패치 적용·롤백(Rollback) 절차, 배포 자동화와 검증 파이프라인(Pipeline), 충돌 가능 모듈/심볼 의존성 점검, 실서비스에서의 실패 대응과 모니터링 포인트까지 실전 운영 지침을 다룹니다.
핵심 요약
- 커널 라이브 패칭(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/를 통해 패치 상태 확인, 활성화/비활성화, 전환 진행률을 모니터링합니다.
단계별 이해
- Livepatch 필요성과 아키텍처 이해 — 재부팅이 불가능한 고가용성 환경에서 보안 패치를 적용해야 하는 상황과, ftrace 기반 함수 교체 원리를 이해합니다.
기존 함수 호출이 ftrace 훅을 통해 새 함수로 리다이렉트되는 명령어 수준 동작을 추적합니다.
- 기본 Livepatch 모듈 작성 —
klp_patch,klp_object,klp_func구조체를 사용하여 간단한 함수 교체 모듈을 작성하고 로드합니다.klp_enable_patch()로 패치를 등록하고,insmod/rmmod로 적용/해제하는 전체 수명주기를 실습합니다. - 전환 메커니즘과 일관성 보장 학습 — 태스크가 안전 지점(Patched/Unpatched 경계)에 도달할 때까지 대기하는 전환 프로세스를 분석합니다.
/sys/kernel/livepatch/에서 전환 상태를 모니터링하고, 전환이 지연되는 원인(장기 실행 태스크 등)을 진단합니다. - Shadow Variable과 Atomic Replace 활용 — 원본 구조체 변경 없이 추가 데이터를 연결하는
klp_shadow_alloc()과, 누적 패치를 단일 패치로 교체하는 Atomic Replace를 실습합니다.실제 CVE 패치 시나리오에서 kpatch-build를 이용한 자동 패치 생성 워크플로우를 따라해 봅니다.
- 안전성 검증과 운영 적용 — Reliable Stacktrace, 콜백 훅(Pre/Post), 보안 기능(모듈 서명, lockdown) 상호작용을 확인합니다.
배포판별(Ubuntu Livepatch, RHEL kpatch, SUSE kGraft) 운영 환경에서의 적용과 트러블슈팅 방법을 비교합니다.
커널 Livepatch
Livepatch(커널 라이브 패칭)는 시스템 재부팅 없이 실행 중인 커널의 함수를 동적으로 교체하는 기술입니다. 보안 취약점(Vulnerability) 긴급 수정, 고가용성 서버 운영, 장기 실행 워크로드 보호 등에 활용됩니다. 리눅스 커널 4.0부터 CONFIG_LIVEPATCH로 공식 지원합니다.
Livepatch 아키텍처
Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능을 기반으로 동작합니다. 패치 대상 함수의 진입점(Entry Point)에 ftrace 훅을 설치하고, 함수 호출 시 instruction pointer(IP)를 새 함수로 리다이렉트합니다.
아키텍처 계층 상세 다이어그램
Livepatch 프레임워크는 사용자 공간(User Space) 인터페이스부터 하드웨어 수준의 명령어 패칭까지 여러 계층으로 구성됩니다. 아래 다이어그램은 각 계층의 역할과 구성 요소 간 상호작용을 보여줍니다.
내부 자료구조와 연결 리스트
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() 핵심 흐름 (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_64 | 4.0 (2015) | ORC (기본) / Frame Pointer | -mfentry | 가장 성숙한 지원, objtool 연동 |
| s390x | 4.0 (2015) | 자체 구현 | -mhotpatch | IBM z/Linux, 별도의 hotpatch 메커니즘 |
| ppc64le | 4.7 (2016) | Frame Pointer + 확장 | -mprofile-kernel | TOC/GOT 처리 필요, 보조 트램폴린 |
| arm64 | 6.3 (2023) | DWARF (.eh_frame) | -fpatchable-function-entry | BTI (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);
}
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");
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)
*/
replace=true를 설정한 패치를 적용하면 됩니다. 이렇게 하면 기존 모든 패치가 원자적으로 비활성화됩니다.
Atomic Replace 내부 동작 흐름
replace=true 패치가 활성화되면 커널은 기존 패치들의 함수를 자동으로 NOP 패치로 교체합니다. 이 과정은 전환 완료 후 klp_complete_transition()에서 처리됩니다.
/*
* 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()
- → 전환 완료
- 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>/force에 1을 기록하면 스택 검사를 건너뛰고 강제로 모든 태스크를 전환합니다. 이는 최후의 수단으로만 사용해야 합니다.
# 강제 전환 수행 (주의: 일관성 보장 안 됨)
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를 사용하면 해당 패치의 역전환(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 시스템은 여러 상태를 거쳐 전환됩니다. 아래 다이어그램은 패치 로드부터 완전 전환까지 전체 흐름을 보여줍니다.
상태 확인 명령어:
cat /sys/kernel/livepatch/<patch>/enabled— 0: DISABLED, 1: PATCHED/TRANSITIONINGcat /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 |
안전성 고려사항
- 함수 시그니처 일치: 패치 함수와 원본 함수의 인자, 반환 타입, 호출 규약(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를 사용하면 안 되는 경우:
- 함수 시그니처 변경이 필요한 경우 (인자 개수/타입 변경)
- 전역 구조체 레이아웃 변경이 필요한 경우
- 초기화 코드 변경 (부팅 시 한 번만 실행되는 코드)
- 인라인 함수 수정 (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가 탄생했습니다.
역사적 타임라인
세 접근법의 기술적 차이
세 프로젝트의 핵심 차이는 일관성 모델(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, RHEL | 2014, SLES | 4.0 (2015), mainline |
| 일관성 모델 | stop_machine (전체 정지) | per-task lazy | per-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(): -EINVAL | MODULE_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 unwinder | x86_64 | 높음 | 컴파일 시 objtool이 생성한 ORC 메타데이터(.orc_unwind) 사용, frame pointer 불필요 |
| Frame pointer | 범용 | 중간 | CONFIG_FRAME_POINTER=y 필수, 인라인 어셈블리(Assembly)에서 깨질 수 있음 |
| DWARF unwinder | arm64 등 | 높음 | .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=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 도구 사용 |
| Lockdown | kernel_lockdown(LOCKDOWN_INTEGRITY)에서 unsigned 모듈 차단 | Secure Boot 환경에서 MOK(Machine Owner Key)으로 모듈 서명 필수 |
| CFI | Clang CFI가 간접 호출 대상을 타입 기반으로 검증 | 패치 함수의 타입이 원본과 정확히 일치해야 CFI 검증 통과 |
| W^X | CONFIG_STRICT_KERNEL_RWX로 코드 영역 쓰기 금지 | ftrace는 text_poke로 임시 매핑(Mapping) 생성하여 수정 후 복원 |
| IBT | x86 CET(Control-flow Enforcement Technology)의 간접 분기 추적 | 커널 6.2+에서 livepatch가 IBT 호환, ENDBR64 명령어 자동 처리 |
| FineIBT | kCFI + 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
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
klp_func 구조체 정의, 심볼 이름 매칭, old_sympos 결정 등이 필요하지만, kpatch-build는 이 과정을 모두 자동화합니다. 특히 create-diff-object 도구가 변경된 함수만 정확히 추출하고, 해당 함수가 참조하는 외부 심볼에 대한 relocation도 자동으로 처리합니다. 커널 보안 팀에서도 CVE 긴급 패치 시 kpatch-build를 활용합니다.
- 데이터 구조 변경 불가: 구조체 레이아웃이 변경되는 패치는 생성할 수 없습니다 (livepatch 자체의 제한)
- 전체 커널 빌드 필요: 패치 전/후로 커널을 두 번 빌드하므로 시간과 디스크 공간이 필요합니다
- 인라인 함수: 컴파일러가 인라인한 함수의 변경은 호출자를 모두 패치해야 하므로 패치 범위가 커질 수 있습니다
- 디버그 심볼: 커널 debuginfo 패키지가 반드시 필요합니다 (
vmlinuxwithDEBUG_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 발견부터 전체 플릿 배포까지 전 과정을 보여줍니다.
배포 자동화 도구:
- 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는 긴급 임시 조치, 재부팅이 최종 해결
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 정지)과 달리, 시스템 전체를 멈추지 않으면서도 의미론적 일관성을 보장합니다.
일관성 모델의 핵심 동작을 커널 코드 수준에서 살펴보겠습니다.
/*
* 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);
}
- 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 return | exit_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를 변경하여 함수 호출 자체를 리다이렉트할 수 있습니다.
/*
* 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_IPMODIFY | regs->ip 수정 권한 | 함수 리다이렉트 핵심 |
FTRACE_OPS_FL_SAVE_REGS | 전체 레지스터(Register) 저장 | IP 수정을 위해 필수 |
FTRACE_OPS_FL_DYNAMIC | 동적 할당된 ops | 모듈 로드 시 생성 |
FTRACE_OPS_FL_PERMANENT | 해제 불가 | Livepatch 미사용 |
FTRACE_OPS_FL_IPMODIFY ops만 등록할 수 있습니다. 이미 다른 도구(예: kretprobe)가 IPMODIFY를 사용 중이면 livepatch 등록이 -EEXIST로 실패합니다. kprobes는 IPMODIFY를 사용하지 않으므로 공존 가능하지만, kretprobe는 리턴 주소를 수정하므로 reliable stacktrace에 영향을 줄 수 있어 주의가 필요합니다.
섀도우 변수(Shadow Variable)
Livepatch는 함수만 교체할 수 있고 데이터 구조는 변경할 수 없습니다. 그런데 패치된 함수에서 원본 구조체에 없는 추가 필드가 필요한 경우가 빈번합니다. klp_shadow_* API는 이 문제를 해결합니다. 기존 커널 객체에 "그림자" 데이터를 해시 테이블로 연결하여, 구조체 레이아웃을 변경하지 않고도 추가 상태를 관리할 수 있습니다.
구조체 확장이 필요한 실전 패턴을 살펴보겠습니다. 원본 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");
}
- 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_ATOMIC | shadow data 포인터 또는 NULL |
klp_shadow_get_or_alloc() | 조회 후 없으면 생성 (원자적) | GFP_KERNEL / GFP_ATOMIC | shadow 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를 해제하지 않으면 영구 메모리 누수. 패치 대상 함수의 해제 경로도 패치하여
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 수명주기 핵심 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을 조합한 완성도 높은 모듈을 만드는 것이 목표입니다.
다중 함수 패치와 콜백, 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()가-EINVALMODULE_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"
자주 발생하는 오류와 해결법
| 오류 코드 | 발생 시점 | 원인 | 해결 방법 |
|---|---|---|---|
-EINVAL | klp_enable_patch() | MODULE_INFO(livepatch, "Y") 누락 | 모듈 소스에 매크로 추가 |
-ENODEV | klp_init_func() | 심볼을 /proc/kallsyms에서 찾을 수 없음 | 심볼 이름 확인, old_sympos 설정 |
-EBUSY | klp_enable_patch() | 이미 진행 중인 전환이 있음 | 전환 완료 후 재시도 |
-EEXIST | register_ftrace_function() | 대상 함수에 이미 IPMODIFY ops 존재 | kretprobe 등 충돌 도구 확인 |
-ENOMEM | 다양 | 메모리 할당 실패 | 시스템 메모리 상태 확인 |
-EAGAIN | klp_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 등을 활용하여 진단할 수 있습니다. 특히 전환이 완료되지 않는 경우 블로킹 태스크를 신속하게 식별하는 것이 핵심입니다.
# === 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 호출이 제거됩니다. - 벤치마크: 프로덕션 적용 전 대표 워크로드로 반드시 성능 비교 테스트를 수행합니다.
커널 라이브 패칭 솔루션 비교
리눅스 커널 라이브 패칭 분야에는 여러 솔루션이 존재합니다. 각각의 역사, 기술 접근 방식, 장단점을 비교하여 환경에 맞는 도구를 선택할 수 있습니다.
| 항목 | Ksplice | kpatch | kGraft | Livepatch (mainline) |
|---|---|---|---|---|
| 개발사 | MIT → Oracle | Red Hat | SUSE | 커널 커뮤니티 |
| 도입 시기 | 2008 | 2014 | 2014 | 2015 (v4.0) |
| 일관성 모델 | stop_machine | stop_machine + ftrace | per-task lazy | per-task + stack check |
| 전환 방식 | 전 CPU 정지 후 교체 | 전 CPU 정지 후 교체 | 태스크별 점진 전환 | 태스크별 + 스택 검증 |
| 시스템 영향 | 짧은 정지 시간 발생 | 짧은 정지 시간 발생 | 정지 없음 | 정지 없음 |
| 안전성 | 높음 (동시 교체) | 높음 (동시 교체) | 중간 (스택 미검사) | 높음 (스택 검증) |
| 전환 지연 | 없음 (강제) | 없음 (강제) | 가능 (장기 sleep) | 가능 (장기 sleep) |
| 라이선스 | 상용 (Oracle) | 오픈소스 (GPLv2) | 오픈소스 (GPLv2) | 오픈소스 (GPLv2) |
| 현재 상태 | Oracle Linux 전용 | 사용자 도구 유지 | mainline 통합 | 공식 프레임워크 |
| 자동 빌드 | ksplice-create | kpatch-build | 직접 작성 | 직접 / kpatch-build |
| shadow 변수 | 미지원 | 미지원 (직접 구현) | 미지원 | klp_shadow_* API |
| atomic replace | 미지원 | 미지원 | 미지원 | replace=true |
| 콜백 | 제한적 | 제한적 | 미지원 | pre/post patch/unpatch |
| 아키텍처 | x86_64 | x86_64 | x86_64, s390 | x86_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 + Livepatch | diff에서 자동 모듈 생성, 빠른 개발 |
| 디버깅용 임시 패치 | 수동 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 환경에서의 결정적 전환 시간 |
live-patching@vger.kernel.org 메일링 리스트에서 진행됩니다. 커널 소스의 Documentation/livepatch/ 디렉터리에 공식 문서가 있으며, kernel/livepatch/에 프레임워크 구현, lib/livepatch/에 셀프테스트 모듈이 포함되어 있습니다.
참고 자료
- Livepatch — The Linux Kernel documentation — 커널 공식 문서로, livepatch 프레임워크의 아키텍처, consistency model, API 사용법을 종합적으로 설명합니다
- Shadow Variables — The Linux Kernel documentation — 라이브패치에서 구조체 확장을 위한 shadow 변수 메커니즘 공식 문서입니다
- Livepatch Callbacks — The Linux Kernel documentation — pre/post-patch, pre/post-unpatch 콜백의 사용법과 실행 순서를 설명합니다
- Livepatch Module ELF Format — The Linux Kernel documentation — 라이브패치 모듈의 ELF 섹션 구조와 재배치(Relocation) 형식을 다룹니다
- kernel/livepatch/ — Bootlin Elixir — livepatch 프레임워크의 핵심 구현 소스 코드를 직접 탐색할 수 있습니다
- include/linux/livepatch.h — Bootlin Elixir — klp_patch, klp_object, klp_func 등 주요 구조체와 API 선언 헤더입니다
- kpatch — GitHub (dynup/kpatch) — Red Hat이 개발한 커널 라이브패치 도구로, 소스 diff 기반으로 라이브패치 모듈을 자동 생성합니다
- kpatch Patch Author Guide — GitHub — kpatch로 라이브패치를 작성할 때의 제약 사항, 패턴, 모범 사례를 안내합니다
- The initial kpatch submission (LWN, 2014) — kpatch의 최초 메인라인 제출과 설계 원리에 대한 기술 분석 기사입니다
- A rough patch for live patching (LWN, 2015) — kpatch와 kGraft의 통합 과정, 메인라인 livepatch 프레임워크 탄생 배경을 다룹니다
- Hybrid consistency for live kernel patching (LWN, 2015) — immediate/lazy consistency model의 하이브리드 설계와 스택 검사(Stack Checking) 메커니즘을 설명합니다
- Atomic live kernel patching (LWN, 2019) — atomic replace 기능의 도입 배경과 누적 패치 모델의 기술적 세부 사항을 분석합니다
- Per-object live patching (LWN, 2021) — 커널 모듈별 라이브패치 적용과 per-object 콜백 확장에 대한 논의입니다
- Applying patches with kernel live patching — Red Hat (RHEL 9) — RHEL 환경에서 kpatch 기반 커널 라이브패치를 적용하고 관리하는 공식 가이드입니다
- Kernel Live Patching with KLP — SUSE (SLES 15 SP5) — SUSE Linux Enterprise에서 kGraft 기반 라이브패치를 운영하는 방법을 설명합니다
- Canonical Livepatch Service — Ubuntu — Ubuntu/Canonical의 상용 라이브패치 서비스로, 자동 보안 패치 적용 방식을 소개합니다
- ftrace — Function Tracer — The Linux Kernel documentation — livepatch의 핵심 기반 기술인 ftrace의 동작 원리와 사용법을 설명합니다
- Live Kernel Patching: An Overview (Linux Plumbers Conference, 2014) — kpatch와 kGraft 개발자들의 라이브패치 프레임워크 통합 논의 발표입니다
- kpatch: Have Your Security and Eat It Too (Red Hat Summit, 2014) — kpatch의 설계 철학과 실전 적용 사례를 소개하는 Red Hat 발표입니다
관련 문서
커널 Livepatch와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.