커널 Livepatch
커널 Livepatch를 무중단 보안 패치와 운영 리스크 최소화 관점에서 심층 분석합니다. klp_patch/klp_object/klp_func 구조와 ftrace 기반 함수 교체 메커니즘, consistency model과 태스크 전환 보장, shadow variable 활용과 데이터 호환성 관리, atomic replace 전략, 패치 적용·롤백 절차, 배포 자동화와 검증 파이프라인, 충돌 가능 모듈/심볼 의존성 점검, 실서비스에서의 실패 대응과 모니터링 포인트까지 실전 운영 지침을 다룹니다.
핵심 요약
- 재현 우선 — 증상보다 재현 조건 고정이 먼저입니다.
- 도구 선택 — 로그, 트레이스, 코어덤프 용도를 분리합니다.
- 증거 보존 — 원인 추적 전 관측 데이터를 먼저 확보합니다.
- 오버헤드 관리 — 추적 범위를 최소화해 왜곡을 줄입니다.
- 사후 검증 — 수정 후 동일 조건에서 재발 여부를 확인합니다.
단계별 이해
- 재현 시나리오 정의
입력 조건과 타이밍을 고정합니다. - 관측 포인트 배치
핵심 함수/이벤트만 선별 추적합니다. - 원인 축소
가설을 하나씩 배제하며 범위를 줄입니다. - 수정 검증
회귀 테스트와 운영 지표를 함께 확인합니다.
커널 Livepatch
Livepatch(커널 라이브 패칭)는 시스템 재부팅 없이 실행 중인 커널의 함수를 동적으로 교체하는 기술입니다. 보안 취약점 긴급 수정, 고가용성 서버 운영, 장기 실행 워크로드 보호 등에 활용됩니다. 리눅스 커널 4.0부터 CONFIG_LIVEPATCH로 공식 지원합니다.
Livepatch 아키텍처
Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능을 기반으로 동작합니다. 패치 대상 함수의 진입점에 ftrace 훅을 설치하고, 함수 호출 시 instruction pointer(IP)를 새 함수로 리다이렉트합니다.
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를 동기화합니다. 그 후 나머지 바이트를 수정하고, 마지막으로 첫 바이트를 최종 값으로 교체합니다. 이 과정은 원자적이어서 다른 CPU가 반쯤 수정된 명령어를 실행하는 것을 방지합니다. INT3을 실행한 CPU는 bp_patching_in_progress를 확인하고 올바른 명령어로 에뮬레이션합니다.
커널 CONFIG 옵션
# 필수 옵션
CONFIG_LIVEPATCH=y # Livepatch 프레임워크 활성화
CONFIG_FTRACE=y # ftrace 기반 (필수 의존성)
CONFIG_DYNAMIC_FTRACE=y # 동적 ftrace (필수 의존성)
CONFIG_MODULES=y # 모듈 지원 (패치를 모듈로 로드)
CONFIG_KALLSYMS_ALL=y # 전체 심볼 해석 (권장)
# 스택 검증 관련 (안전한 전환에 필수)
CONFIG_HAVE_RELIABLE_STACKTRACE=y # 신뢰성 있는 스택 트레이스
CONFIG_STACKTRACE=y # 스택 트레이스 지원
CONFIG_FRAME_POINTER=y # 또는 ORC unwinder (x86_64)
CONFIG_UNWINDER_ORC=y # ORC unwinder (더 정확하고 성능 좋음)
# 아키텍처 지원 확인
# x86_64, s390, ppc64le에서 완전 지원
# arm64는 커널 6.x부터 지원
Livepatch API 핵심 구조체
#include <linux/livepatch.h>
/**
* klp_func - 패치할 개별 함수 정의
* @old_name: 교체할 원본 함수의 심볼 이름
* @new_func: 대체할 새 함수의 포인터
* @old_sympos: 동명 함수가 여러 개일 때 위치 (0=유일, 1=첫 번째, ...)
* @nop: true이면 NOP 패치 (atomic replace에서 활용)
*/
struct klp_func {
const char *old_name;
void *new_func;
unsigned long old_sympos;
bool nop;
/* 내부 필드 (커널이 관리) */
unsigned long old_func; /* 런타임에 해석됨 */
struct kobject kobj;
struct list_head node;
struct list_head stack_node;
unsigned long old_size, new_size;
bool patched;
bool transition;
};
/**
* klp_object - 패치 대상 오브젝트 (vmlinux 또는 모듈)
* @name: 대상 모듈 이름 (NULL이면 vmlinux)
* @funcs: 이 오브젝트에서 패치할 함수 배열 (sentinel 종료)
*/
struct klp_object {
const char *name;
struct klp_func *funcs;
/* 내부 필드 */
struct kobject kobj;
struct list_head func_list;
struct list_head node;
struct module *mod;
bool dynamic;
bool patched;
};
/**
* klp_patch - 라이브 패치 최상위 구조체
* @mod: 패치 모듈 자신
* @objs: 패치 대상 오브젝트 배열 (sentinel 종료)
* @replace: true이면 이전 패치들을 모두 대체 (atomic replace)
*/
struct klp_patch {
struct module *mod;
struct klp_object *objs;
bool replace;
/* 내부 필드 */
struct list_head list;
struct kobject kobj;
bool enabled;
bool forced;
struct work_struct free_work;
struct completion finish;
};
기본 Livepatch 모듈 작성
/*
* livepatch-sample.c - 커널 함수를 런타임에 교체하는 예제
* cmdline_proc_show()를 패치하여 /proc/cmdline 출력을 변경
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/seq_file.h>
/* 새 함수: 원본과 동일한 시그니처 필수 */
static int livepatch_cmdline_proc_show(struct seq_file *m, void *v)
{
seq_printf(m, "%s [LIVEPATCHED]\\n", saved_command_line);
return 0;
}
/* 패치할 함수 목록 (sentinel으로 빈 항목 필수) */
static struct klp_func funcs[] = {
{
.old_name = "cmdline_proc_show",
.new_func = livepatch_cmdline_proc_show,
},
{ } /* sentinel */
};
/* 패치 대상 오브젝트: NULL name = vmlinux */
static struct klp_object objs[] = {
{
.name = NULL, /* vmlinux (커널 코어) */
.funcs = funcs,
},
{ } /* sentinel */
};
static struct klp_patch patch = {
.mod = THIS_MODULE,
.objs = objs,
};
/* 모듈 로드 시 패치 활성화 */
static int __init livepatch_init(void)
{
return klp_enable_patch(&patch);
}
/* 모듈 언로드 시 자동 복원 (커널이 처리) */
static void __exit livepatch_exit(void)
{
/* klp_unpatch는 모듈 제거 시 커널이 자동 호출 */
}
module_init(livepatch_init);
module_exit(livepatch_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y"); /* 필수: livepatch 모듈 표시 */
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Livepatch sample: cmdline_proc_show");
klp_enable_patch() 호출이 -EINVAL로 실패합니다. 반드시 포함해야 합니다.
Livepatch 모듈 Makefile
# Makefile for livepatch module
obj-m += livepatch-sample.o
# 커널 빌드 디렉터리
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
모듈 함수 패치
vmlinux뿐만 아니라 로드된 커널 모듈의 함수도 패치할 수 있습니다. klp_object.name에 대상 모듈 이름을 지정합니다.
/* ext4 모듈의 함수를 패치하는 예제 */
static int patched_ext4_file_write_iter(
struct kiocb *iocb, struct iov_iter *from)
{
/* 원본 함수 호출 전 추가 검증 로직 */
struct inode *inode = file_inode(iocb->ki_filp);
if (iov_iter_count(from) > (1ULL << 30)) {
pr_warn_ratelimited("ext4: unusually large write %zu on ino %lu\\n",
iov_iter_count(from), inode->i_ino);
}
/* klp_shadow 또는 직접 심볼 참조로 원본 호출 가능 */
return ext4_file_write_iter(iocb, from);
}
static struct klp_func ext4_funcs[] = {
{
.old_name = "ext4_file_write_iter",
.new_func = patched_ext4_file_write_iter,
},
{ }
};
static struct klp_object objs[] = {
{
.name = "ext4", /* 대상 모듈 이름 */
.funcs = ext4_funcs,
},
{ }
};
ext4)이 아직 로드되지 않은 상태에서도 livepatch 모듈을 로드할 수 있습니다. 대상 모듈이 나중에 로드되면 커널이 자동으로 패치를 적용합니다. 대상 모듈이 언로드되면 패치도 자동으로 비활성화됩니다.
Shadow Variable (klp_shadow)
패치 함수에서 원본 구조체에 없는 추가 데이터를 저장해야 할 때 klp_shadow_* API를 사용합니다. 이는 원본 객체에 "그림자" 데이터를 연결하는 해시 테이블 기반 메커니즘입니다.
#include <linux/livepatch.h>
#define SV_LEAK_TRACKER 1 /* shadow variable ID (패치별 고유) */
struct leak_info {
u64 alloc_time;
unsigned long alloc_site;
size_t size;
};
/* 생성자: shadow variable 할당 시 호출 */
static int leak_info_ctor(void *obj, void *shadow_data, void *ctor_data)
{
struct leak_info *info = shadow_data;
info->alloc_time = ktime_get_ns();
info->alloc_site = (unsigned long)ctor_data;
info->size = 0;
return 0;
}
/* 패치 함수에서 shadow variable 사용 */
static void *patched_kmalloc(size_t size, gfp_t flags)
{
void *ptr = kmalloc(size, flags);
if (ptr) {
struct leak_info *info;
/* shadow variable 생성 및 연결 */
info = klp_shadow_get_or_alloc(
ptr, /* 연결할 원본 객체 */
SV_LEAK_TRACKER, /* shadow variable ID */
sizeof(*info), /* shadow data 크기 */
GFP_KERNEL,
leak_info_ctor, /* 생성자 */
(void *)_RET_IP_ /* 생성자에 전달할 데이터 */
);
if (info)
info->size = size;
}
return ptr;
}
static void patched_kfree(const void *ptr)
{
if (ptr) {
struct leak_info *info;
/* shadow variable 조회 */
info = klp_shadow_get(ptr, SV_LEAK_TRACKER);
if (info) {
pr_debug("free: obj=%p size=%zu alive=%llu ns\\n",
ptr, info->size,
ktime_get_ns() - info->alloc_time);
/* shadow variable 해제 */
klp_shadow_free(ptr, SV_LEAK_TRACKER, NULL);
}
}
kfree(ptr);
}
| API | 설명 |
|---|---|
klp_shadow_get(obj, id) | 기존 shadow variable 조회 (없으면 NULL) |
klp_shadow_alloc(obj, id, size, gfp, ctor, ctor_data) | 새 shadow variable 생성 (이미 존재하면 NULL) |
klp_shadow_get_or_alloc(obj, id, size, gfp, ctor, ctor_data) | 조회 후 없으면 생성 |
klp_shadow_free(obj, id, dtor) | 특정 shadow variable 해제 |
klp_shadow_free_all(id, dtor) | 특정 ID의 모든 shadow variable 일괄 해제 |
Atomic Replace (누적 패치 관리)
여러 livepatch가 순차적으로 적용되면 패치 스택이 누적됩니다. replace 플래그를 사용하면 기존 모든 패치를 단일 패치로 원자적으로 대체할 수 있어 패치 관리가 간소화됩니다.
/* Atomic replace 패치: 기존 모든 livepatch를 대체 */
static struct klp_patch cumulative_patch = {
.mod = THIS_MODULE,
.objs = objs,
.replace = true, /* 핵심: 이전 패치 모두 대체 */
};
/*
* replace=true의 동작:
*
* 1. 이 패치가 활성화되면 기존 모든 livepatch가 비활성화됨
* 2. 이 패치에 포함되지 않은 함수는 원본으로 복원됨
* 3. 전환은 per-task consistency model을 따름
*
* 사용 시나리오:
* - 보안 패치 v1 적용 후 v2로 업그레이드
* - 여러 개별 패치를 하나의 누적 패치로 통합
* - 패치 완전 제거 (빈 패치에 replace=true)
*/
replace=true를 설정한 패치를 적용하면 됩니다. 이렇게 하면 기존 모든 패치가 원자적으로 비활성화됩니다.
동명 함수 처리 (old_sympos)
커널에는 static 함수 중 이름이 같은 것들이 다른 파일에 존재할 수 있습니다. old_sympos로 어떤 함수를 패치할지 지정합니다.
/*
* 예: 커널 내에 cleanup()이라는 static 함수가 3개 존재
*
* kallsyms에서 확인:
* $ grep ' cleanup$' /proc/kallsyms
* ffffffff81234560 t cleanup [drivers/foo/bar.c] ← sympos=1
* ffffffff81234780 t cleanup [drivers/baz/qux.c] ← sympos=2
* ffffffff81234900 t cleanup [net/core/sock.c] ← sympos=3
*/
static struct klp_func funcs[] = {
{
.old_name = "cleanup",
.new_func = patched_cleanup,
.old_sympos = 2, /* 두 번째 cleanup (drivers/baz/qux.c) */
},
{ }
};
/*
* old_sympos 값:
* 0 = 유일한 심볼 (동명이 있으면 에러)
* 1 = kallsyms 순서로 첫 번째
* N = kallsyms 순서로 N번째
*/
sysfs 인터페이스와 운영
# Livepatch 모듈 로드 (패치 자동 활성화)
insmod livepatch-sample.ko
# sysfs에서 패치 상태 확인
ls /sys/kernel/livepatch/
# livepatch_sample/
# 패치 활성화 상태
cat /sys/kernel/livepatch/livepatch_sample/enabled
# 1
# 전환(transition) 진행 중인지 확인
cat /sys/kernel/livepatch/livepatch_sample/transition
# 0 (완료) / 1 (진행 중)
# 강제 전환 (전환이 멈춘 경우 — 주의: 일관성 보장 안 됨)
echo 1 > /sys/kernel/livepatch/livepatch_sample/force
# 패치 비활성화 (원본 함수로 복원 전환 시작)
echo 0 > /sys/kernel/livepatch/livepatch_sample/enabled
# 비활성화 후 모듈 제거
rmmod livepatch-sample
# 개별 함수 패치 상태 확인
cat /sys/kernel/livepatch/livepatch_sample/vmlinux/cmdline_proc_show/transition
# 전환 상태 모니터링: 아직 old universe에 있는 태스크 확인
for pid in /proc/[0-9]*; do
patch_state=$(cat "$pid/patch_state" 2>/dev/null)
if [ "$patch_state" = "-1" ]; then
comm=$(cat "$pid/comm" 2>/dev/null)
echo "Blocking: PID=$(basename $pid) COMM=$comm"
fi
done
# /proc/<pid>/patch_state 값:
# -1 = old universe (전환 대기 중)
# 0 = undefined (전환 진행 안 됨)
# 1 = new universe (전환 완료)
전환 메커니즘 상세
Livepatch 전환은 각 태스크가 안전한 지점에 도달할 때까지 대기하는 과정입니다. 안전한 지점이란 패치 대상 함수가 해당 태스크의 스택에 존재하지 않는 상태를 의미합니다.
- 전환 흐름 (kernel/livepatch/transition.c):
- klp_enable_patch()
- → klp_init_patch() : 패치 구조체 초기화, sysfs 등록
- → klp_try_enable_patch() : ftrace 훅 설치
- → klp_start_transition() : 전환 시작
- → 모든 태스크를 TIF_PATCH_PENDING 플래그 설정
- → klp_target_state = KLP_PATCHED
- 각 태스크 (스케줄러에서):
- schedule()
- → __schedule()
- → klp_update_patch_state(current)
- → klp_try_switch_task(current)
- → klp_check_stack() : reliable stacktrace로 스택 검사
- → 패치 대상 함수가 스택에 없으면:
- → 태스크를 new universe로 전환
- → TIF_PATCH_PENDING 해제
- → 스택에 있으면:
- → 다음 스케줄링까지 대기
- 모든 태스크 전환 완료:
- klp_complete_transition()
- → klp_unpatch_replaced_patches() (replace=true일 때)
- → klp_clear_object_relocations()
- → 전환 완료
- TASK_IDLE 상태 CPU: idle 루프의 스택이 간단하여 보통 빠르게 전환
- 장기 sleep (TASK_UNINTERRUPTIBLE): 패치 대상 함수 내부에서 I/O 대기 중일 때
- RT 태스크: 선점이 억제된 상태에서 패치 대상 함수 실행 중
- kthread:
cond_resched()를 드물게 호출하는 커널 스레드
/proc/<pid>/patch_state로 블로킹 태스크를 식별하고, 시그널 전송이나 force 전환을 고려합니다.
Livepatch 상태 전이 다이어그램
Livepatch 시스템은 여러 상태를 거쳐 전환됩니다. 아래 다이어그램은 패치 로드부터 완전 전환까지 전체 흐름을 보여줍니다.
상태 확인 명령어:
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인 프로세스 식별 → /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는 함수만 교체하며 데이터 구조는 변경하지 않습니다.
- 인라인 함수: 컴파일러가 인라인한 함수는 패치할 수 없습니다.
noinline속성이 필요합니다. - 컴파일러 최적화: 패치 모듈과 원본 커널은 동일한 컴파일러 버전과 최적화 수준으로 빌드해야 합니다.
- 시맨틱 변경 범위: 호출자가 기대하는 함수의 의미론(side effect, 에러 코드 등)을 유지해야 합니다.
# Livepatch 적용 전후 검증 스크립트
# 1. 커널 Livepatch 지원 확인
if [ ! -d /sys/kernel/livepatch ]; then
echo "ERROR: CONFIG_LIVEPATCH not enabled"
exit 1
fi
# 2. 현재 적용된 패치 목록
echo "=== Active Livepatches ==="
for patch in /sys/kernel/livepatch/*; do
[ -d "$patch" ] || continue
name=$(basename "$patch")
enabled=$(cat "$patch/enabled")
transition=$(cat "$patch/transition")
echo " $name: enabled=$enabled transition=$transition"
# 패치된 함수 목록
for obj in "$patch"/*/; do
[ -d "$obj" ] || continue
obj_name=$(basename "$obj")
for func in "$obj"/*/; do
[ -d "$func" ] || continue
func_name=$(basename "$func")
echo " [$obj_name] $func_name"
done
done
done
# 3. tainted 상태 확인 (Livepatch 적용 시 K 플래그 설정)
tainted=$(cat /proc/sys/kernel/tainted)
if [ $((tainted & 32768)) -ne 0 ]; then
echo "Kernel tainted: TAINT_LIVEPATCH (K)"
fi
# 4. 전환 블로킹 태스크 확인
echo "=== Blocking Tasks ==="
blocking=0
for pid in /proc/[0-9]*; do
state=$(cat "$pid/patch_state" 2>/dev/null)
if [ "$state" = "-1" ]; then
comm=$(cat "$pid/comm" 2>/dev/null)
wchan=$(cat "$pid/wchan" 2>/dev/null)
echo " PID=$(basename $pid) COMM=$comm WCHAN=$wchan"
blocking=$((blocking + 1))
fi
done
echo "Total blocking: $blocking"
안전성 검증 의사결정 트리
Livepatch를 프로덕션에 적용하기 전 반드시 안전성 검증을 수행해야 합니다. 아래 의사결정 트리는 체계적인 검증 절차를 안내합니다.
절대 Livepatch를 사용하면 안 되는 경우:
- 함수 시그니처 변경이 필요한 경우 (인자 개수/타입 변경)
- 전역 구조체 레이아웃 변경이 필요한 경우
- 초기화 코드 변경 (부팅 시 한 번만 실행되는 코드)
- 인라인 함수 수정 (noinline 추가가 불가능한 경우)
- 데이터 섹션 변경 (.data, .bss, .rodata)
- 테스트 없이 프로덕션 직행
이런 경우 커널 재부팅이 유일한 안전한 방법입니다.
Livepatch 역사: kGraft vs kpatch
| 항목 | 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") 누락 | 모듈 소스에 해당 매크로 추가 |
| 전환이 완료되지 않음 | 태스크가 패치 대상 함수 내 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 필수, 인라인 어셈블리에서 깨질 수 있음 |
| 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로 임시 매핑 생성하여 수정 후 복원 |
| IBT | x86 CET(Control-flow Enforcement Technology)의 간접 분기 추적 | 커널 6.2+에서 livepatch가 IBT 호환, ENDBR64 명령어 자동 처리 |
| FineIBT | kCFI + IBT 조합으로 간접 호출 시 해시 검증 | 패치 함수의 프로토타입이 원본과 동일해야 해시가 일치 |
# === Secure Boot 환경에서 livepatch 모듈 서명 ===
# 1. 커널 빌드 시 생성된 키로 서명 (개발 환경)
/usr/src/linux/scripts/sign-file sha256 \
/usr/src/linux/certs/signing_key.pem \
/usr/src/linux/certs/signing_key.x509 \
livepatch-sample.ko
# 2. 서명 확인
modinfo livepatch-sample.ko | grep sig
# sig_id: PKCS#7
# signer: Build time autogenerated kernel key
# sig_hashalgo: sha256
# 3. Secure Boot + MOK 환경 (배포판)
# MOK 키 생성
openssl req -new -x509 -newkey rsa:2048 \
-keyout MOK.priv -outform DER -out MOK.der \
-nodes -days 36500 -subj "/CN=Livepatch Signing Key/"
# MOK 등록 (재부팅 시 UEFI에서 승인 필요)
sudo mokutil --import MOK.der
# MOK 키로 모듈 서명
/usr/src/linux/scripts/sign-file sha256 \
MOK.priv MOK.der livepatch-sample.ko
# 4. 서명 검증
sudo keyctl list %:.builtin_trusted_keys
sudo keyctl list %:.secondary_trusted_keys
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")은 반드시 포함해야 합니다.
일관성 모델 심화
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 경계: 시스템 콜 진입/리턴 시
klp_update_patch_state()호출 - idle 진입: CPU idle 루프에서 자동 전환
- signal 처리: 시그널 전달 시
klp_update_patch_state()트리거
PREEMPT_NONE 커널에서는 cond_resched()를 거의 호출하지 않는 커널 스레드가 전환을 장시간 지연시킬 수 있습니다. 이런 경우 /proc/PID/patch_state가 -1로 남아 있으며, 시그널 전송이나 force 전환을 검토해야 합니다.
| 전환 트리거 | 호출 경로 | 조건 |
|---|---|---|
schedule() | __schedule() → klp_update_patch_state() | 태스크가 CPU를 양보할 때 |
cond_resched() | __cond_resched() → schedule() | 선점 가능 지점 |
| syscall return | exit_to_user_mode_loop() | 사용자 공간 복귀 시 |
| signal 전달 | get_signal() → klp_update_patch_state() | pending signal 처리 시 |
| idle 진입 | do_idle() → klp_update_patch_state() | CPU idle 시 |
| 새 태스크 생성 | copy_process() | fork/clone 시 부모 상태 상속 |
ftrace 기반 패칭 메커니즘 심화
Livepatch는 ftrace의 FTRACE_OPS_FL_IPMODIFY 기능에 전적으로 의존합니다. 이 플래그가 설정된 ftrace ops는 대상 함수의 instruction pointer(IP)를 수정할 수 있는 특별한 권한을 갖습니다. 일반 ftrace 트레이서와 달리 IP를 변경하여 함수 호출 자체를 리다이렉트할 수 있습니다.
/*
* 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 | 전체 레지스터 저장 | 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 플래그: 인터럽트 컨텍스트에서는
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 |
라이브패치 수명주기
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로 빌드되어야 함
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로 강제 전환 (주의: 일관성 보장 안 됨)
커널 라이브 패칭 솔루션 비교
리눅스 커널 라이브 패칭 분야에는 여러 솔루션이 존재합니다. 각각의 역사, 기술 접근 방식, 장단점을 비교하여 환경에 맞는 도구를 선택할 수 있습니다.
| 항목 | 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와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.