基于 Linux Kernel 源码深度分析 路径:
kernel/livepatch/|include/linux/livepatch.h分析基准版本:Linux 6.x mainline
- 热补丁概述与设计背景
- 历史演进:kpatch / kGraft / livepatch
- 核心数据结构
- 函数替换机制(ftrace trampoline)
- 一致性模型(Consistency Model)
- 补丁应用完整流程
- Shadow 变量
- 累积补丁(Cumulative Patch)
- sysfs 接口
- 与 kexec/kdump 的交互
- 完整示例:编写一个 livepatch 模块
- 限制与注意事项
kernel/livepatch/目录文件一览
生产环境的 Linux 服务器一旦发现内核安全漏洞,传统修复流程如下:
发现漏洞
|
v
编译新内核(数分钟~数十分钟)
|
v
安排维护窗口(协调业务方、通知用户)
|
v
重启系统(内核 panic 风险 + 几分钟停机)
|
v
验证修复(回归测试)
对于需要 99.999% 可用性(每年停机 < 5.26 分钟)的关键业务,哪怕计划重启也代价极高。金融交易系统、电信基础设施、大型数据库集群等场景尤为如此。
热补丁(livepatch)的目标:在不重启内核、不中断业务的前提下,安全地将有漏洞的内核函数替换为修复版本。
| 技术 | 停机时间 | 安全性 | 实现复杂度 |
|---|---|---|---|
| 传统重启 | 分钟级 | 最高 | 低 |
| kexec(快速重启) | 秒级~分钟级 | 高 | 中 |
| livepatch | 零停机 | 需要一致性保证 | 高 |
kexec 只是缩短了 BIOS/UEFI 初始化时间,仍然需要完整的内核重新启动流程,进程全部终止。livepatch 则完全不需要重启:内核继续运行,所有进程保持状态。
livepatch 面临一个根本性约束:在替换函数的瞬间,该函数可能正在被其他 CPU 上的任务执行。粗暴地修改函数代码可能导致:
- 崩溃(指令流被破坏到一半)
- 数据不一致(部分任务看到新函数,部分看到旧函数,共享状态语义不同)
- 死锁(新旧函数对锁的使用方式不同)
这就是一致性模型(Consistency Model)的核心问题,也是 kpatch、kGraft 和最终 livepatch 设计的最大分歧所在。
kpatch 是 Red Hat 工程师开发的最早的实用 Linux 热补丁方案,核心思路:
kpatch 核心机制
===============
原始函数 foo(): 补丁函数 foo_new():
+------------------+ +------------------+
| CALL SITE | | 修复后的代码 |
| ftrace nop slot |----->| 直接替换 IP |
| ... | | ... |
+------------------+ +------------------+
使用 ftrace 的 -pg 入口点修改 IP 寄存器
kpatch 的一致性保证:使用 stop_machine() 停止所有 CPU,检查所有 CPU 的调用栈不包含被 patch 的函数,然后完成 patch。
这种方法有个重大缺陷:stop_machine() 需要抢占所有 CPU,在高负载场景下可能长时间阻塞,造成可感知的延迟抖动(虽然不是停机,但对实时系统有影响)。
SUSE 开发的 kGraft 采用了完全不同的思路——不停机,而是让新旧代码共存,通过任务迁移逐步切换:
kGraft 一致性机制
=================
时刻 T0: 应用补丁
- 新补丁函数注册到 ftrace
- 每个任务打上 "待迁移" 标记
时刻 T1~Tn: 逐步迁移
- 任务每次从内核返回到用户空间时
- 检查自己是否还在旧函数的调用链上
- 安全则切换到 "已迁移" 状态
时刻 Tm: 所有任务迁移完成
- 旧函数完全无人使用,可以清理
kGraft 的关键创新:per-task patch state。每个任务独立迁移,中间状态期间 klp_ftrace_handler 根据当前任务的 patch_state 决定路由到旧函数还是新函数。
Linux mainline 的 livepatch 子系统综合了两者的优势:
- 借鉴 kpatch 的 ftrace-based 函数替换机制
- 借鉴 kGraft 的 per-task 迁移方案
- 结合可靠栈回溯(reliable stack trace)来加速迁移
最初的 mainline livepatch(v3.19,2015)只支持 x86,且没有完整的一致性模型——func->transition 和 task->patch_state 字段是后来(v4.12,2017)由 Josh Poimboeuf 加入的。
2014-01 kpatch 发布(stop_machine 方案)
2014-02 kGraft 发布(per-task 迁移方案)
2014-11 Linux 3.18: CONFIG_LIVEPATCH 配置项合入(无一致性模型)
2015-02 Linux 3.19: 初版 livepatch 合入(基于 ftrace,仅 x86)
2017-07 Linux 4.12: 完整一致性模型合入(per-task + 栈检查)
- klp_try_switch_task()
- KLP_TRANSITION_* 状态机
- TIF_PATCH_PENDING 线程标志
2018-10 Linux 4.20: 累积补丁(replace=true)支持
2019-05 Linux 5.1: klp_state 系统状态管理 API
2021+ 持续改进:arm64 支持、powerpc 支持等
klp_patch (一个补丁模块)
|
+-- klp_object[] (被 patch 的内核对象:vmlinux 或某个 .ko)
|
+-- klp_func[] (具体被替换的函数)
|
+-- klp_callbacks (补丁前后的回调钩子)
|
+-- klp_state[] (该补丁修改的系统状态记录)
全局 klp_patches 链表持有所有活跃的 klp_patch:
kernel/livepatch/core.c:45
LIST_HEAD(klp_patches);
运行时还有一个关键全局结构 klp_ops,每个被 patch 的函数对应一个 klp_ops,其中的 func_stack 链表记录了对这个函数的所有历史 patch 层叠:
kernel/livepatch/patch.c:23
static LIST_HEAD(klp_ops);
定义位于 include/linux/livepatch.h:57:
struct klp_func {
/* external(模块作者填写) */
const char *old_name; // 被 patch 的原函数名,如 "vfs_read"
void *new_func; // 新函数指针(patch 代码)
unsigned long old_sympos; // 重名符号时指定第几个(0=唯一,>0=第N个)
/* internal(内核管理) */
void *old_func; // 运行时解析出的原函数地址
struct kobject kobj; // sysfs 节点
struct list_head node; // 挂在 klp_object.func_list
struct list_head stack_node; // 挂在 klp_ops.func_stack(补丁叠加栈)
unsigned long old_size, new_size; // 函数大小(用于栈检查范围判断)
bool nop; // 是否是 NOP(累积 patch 用于撤回前版本)
bool patched; // 是否已加入 klp_ops.func_stack
bool transition; // 是否正在 transition 中
};patched 和 transition 两个字段共同定义了函数的 patch 状态机(include/linux/livepatch.h:43):
打 patch 方向:
patched=0 transition=0 -> unpatched(初始)
patched=0 transition=1 -> unpatched,transition 刚开始
patched=1 transition=1 -> patched,对部分任务可见
patched=1 transition=0 -> patched,对所有任务可见(完成)
卸载 patch 方向(反序):
patched=1 transition=0 -> patched,所有任务看到
patched=1 transition=1 -> patched,对部分任务可见
patched=0 transition=1 -> unpatched,transition 快结束
patched=0 transition=0 -> unpatched(完成)
定义位于 include/linux/livepatch.h:94:
struct klp_object {
/* external */
const char *name; // NULL 表示 vmlinux;非 NULL 表示模块名
struct klp_func *funcs; // 静态 func 数组(NULL 终止)
struct klp_callbacks callbacks; // pre/post patch/unpatch 钩子
/* internal */
struct kobject kobj;
struct list_head func_list;// 动态 func 链表(运行时构建)
struct list_head node; // 挂在 klp_patch.obj_list
struct module *mod; // 对应的 module 指针(vmlinux 为 NULL)
bool dynamic; // 是否是动态分配的(NOP 对象)
bool patched; // 该 object 的 func 是否已注册到 ftrace
};name == NULL 意味着 patch 针对 vmlinux 本身。判断函数:
kernel/livepatch/core.c:49
static bool klp_is_module(struct klp_object *obj)
{
return obj->name;
}定义位于 include/linux/livepatch.h:135:
struct klp_patch {
/* external */
struct module *mod; // 指向 livepatch 模块自身
struct klp_object *objs; // 静态 object 数组
struct klp_state *states; // 系统状态数组(可选)
bool replace; // true = 累积补丁(替换所有已有 patch)
/* internal */
struct list_head list; // 挂在全局 klp_patches
struct kobject kobj; // sysfs 节点 /sys/kernel/livepatch/<name>/
struct list_head obj_list; // 动态 object 链表
bool enabled; // patch 是否已 enable
bool forced; // 是否经历过强制 transition
struct work_struct free_work; // 异步释放用
struct completion finish; // 等待 patch 完全卸载
};enabled 与 transition 状态的关系:
klp_patch 状态机
================
[disabled]
|
| klp_enable_patch()
v
[enabled, in transition] <----> klp_reverse_transition() 可逆转
|
| 所有任务迁移完成
v
[enabled, stable]
|
| echo 0 > /sys/.../enabled
v
[disabled, in transition]
|
| 所有任务迁移完成
v
[disabled] --> klp_free_patch_async() --> 模块卸载
定义位于 include/linux/livepatch.h:115:
struct klp_state {
unsigned long id; // 系统状态的唯一标识(非零)
unsigned int version; // 版本号(累积 patch 需要 >= 旧版本)
void *data; // 用户自定义数据指针
};klp_state 是为了解决累积补丁场景下的兼容性问题而引入的。如果一个系统状态(比如某个内核子系统的内部数据结构布局)被多个补丁修改过,后续补丁必须声明自己能处理之前所有版本的状态,否则 klp_enable_patch 会拒绝加载。
关键 API(kernel/livepatch/state.c):
klp_get_state(patch, id):获取当前 patch 中某个 id 对应的状态klp_get_prev_state(id):获取已安装的 patch 中最新的同 id 状态(用于 transition 回调中检查兼容性)
定义位于 include/linux/livepatch_external.h:45:
struct klp_callbacks {
klp_pre_patch_t pre_patch; // patch 安装前调用
klp_post_patch_t post_patch; // patch 安装后调用
klp_pre_unpatch_t pre_unpatch; // patch 卸载前调用
klp_post_unpatch_t post_unpatch;// patch 卸载后调用
bool post_unpatch_enabled; // post_unpatch 是否生效
};这些回调用于维护 patch 周期中的附加操作,例如:
pre_patch:分配新版本函数需要的资源post_patch:记录 patch 已生效pre_unpatch:通知其他子系统 patch 即将移除post_unpatch:释放 patch 专用资源
klp_ops 是 livepatch 内部使用的结构,不暴露给 patch 作者,定义在 kernel/livepatch/patch.h:
struct klp_ops {
struct list_head node; // 挂在全局 klp_ops 链表
struct list_head func_stack; // 该原函数上的 patch 叠加栈(最新在头部)
struct ftrace_ops fops; // 注册给 ftrace 的操作
};
func_stack 是实现补丁叠加(多个 patch 都 patch 了同一个函数)的核心:
func_stack(最新 patch 在链表头部):
[patch_B::func_new] -> [patch_A::func_new] -> (list head = klp_ops)
klp_ftrace_handler() 选取链表第一个有效的 func 来执行
Linux 内核编译时若开启 CONFIG_DYNAMIC_FTRACE,每个可 trace 的函数入口处都会有一条 nop 指令(或 call __fentry__,arch 相关)。运行时可以将此 nop 替换为跳转到 ftrace trampoline,从而在函数入口处进行拦截。
ftrace 拦截点
=============
原始函数入口(无 patch):
func_foo:
nop ; ftrace 占位指令(编译期生成)
push %rbp
...
注册了 ftrace_ops 后:
func_foo:
call __fentry__ ; 跳转到 ftrace infrastructure
push %rbp
...
ftrace infrastructure 会依次调用所有注册的 ftrace_ops.func 回调
当 klp_patch_object() 对某个函数执行 patch 时,核心逻辑在 kernel/livepatch/patch.c:160:
static int klp_patch_func(struct klp_func *func)
{
struct klp_ops *ops;
int ret;
ops = klp_find_ops(func->old_func); // 查找是否已有 klp_ops
if (!ops) {
// 首次 patch 此函数:分配新的 klp_ops,注册 ftrace
unsigned long ftrace_loc;
ftrace_loc = ftrace_location((unsigned long)func->old_func);
// 找到函数入口处的 ftrace 拦截点地址
ops = kzalloc_obj(*ops);
ops->fops.func = klp_ftrace_handler; // 设置回调
ops->fops.flags = FTRACE_OPS_FL_DYNAMIC |
FTRACE_OPS_FL_IPMODIFY | // 关键:允许修改 IP
FTRACE_OPS_FL_PERMANENT;
list_add(&ops->node, &klp_ops);
INIT_LIST_HEAD(&ops->func_stack);
list_add_rcu(&func->stack_node, &ops->func_stack);
ftrace_set_filter_ip(&ops->fops, ftrace_loc, 0, 0); // 设置过滤 IP
register_ftrace_function(&ops->fops); // 注册
} else {
// 已有 patch:直接加入 func_stack 头部
list_add_rcu(&func->stack_node, &ops->func_stack);
}
func->patched = true;
return 0;
}关键标志 FTRACE_OPS_FL_IPMODIFY(kernel/livepatch/patch.c:191)告诉 ftrace 这个回调会修改指令指针(IP),ftrace 会为此分配一个专用的 trampoline,并在调用回调后使用修改后的 IP 跳转,而非返回原函数。
这是整个 livepatch 的核心热路径函数,位于 kernel/livepatch/patch.c:40:
static void notrace 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;
int bit;
ops = container_of(fops, struct klp_ops, fops);
// 防止递归(ftrace 自身也可能被 patch)
bit = ftrace_test_recursion_trylock(ip, parent_ip);
if (WARN_ON_ONCE(bit < 0))
return;
// 取 func_stack 最顶层(最新 patch)的函数
func = list_first_or_null_rcu(&ops->func_stack, struct klp_func,
stack_node);
smp_rmb(); // 确保读取 func->transition 的顺序
if (unlikely(func->transition)) {
// 正在 transition 期间:根据当前任务的 patch_state 决定路由
smp_rmb(); // 确保读 func->transition 和 current->patch_state 有序
patch_state = current->patch_state;
if (patch_state == KLP_TRANSITION_UNPATCHED) {
// 该任务还未迁移:使用旧版本(func_stack 中的下一个)
func = list_entry_rcu(func->stack_node.next,
struct klp_func, stack_node);
if (&func->stack_node == &ops->func_stack)
goto unlock; // 没有旧版本,执行原函数
}
// patch_state == KLP_TRANSITION_PATCHED:使用新版本(当前 func)
}
// NOP 函数:什么都不做,让执行流回到原函数
if (func->nop)
goto unlock;
// 修改指令指针,跳转到新函数
ftrace_regs_set_instruction_pointer(fregs, (unsigned long)func->new_func);
unlock:
ftrace_test_recursion_unlock(bit);
} 无 patch 时的调用流程
====================
调用方
|
| CALL foo
v
foo():
nop ; ftrace 占位,无操作
push rbp
... (原始代码) ...
ret
|
v
调用方(返回)
有 patch 时的调用流程
====================
调用方
|
| CALL foo
v
foo():
CALL __fentry__
|
v
ftrace trampoline
|
v
klp_ftrace_handler()
|
+-- 检查 func->transition
|
+-- 读 current->patch_state
|
+-- 决定:执行 foo_new 还是 foo_old
|
v
ftrace_regs_set_instruction_pointer(fregs, foo_new)
|
v
trampoline 跳转到 foo_new()
|
v
foo_new():
... (修复后的代码) ...
ret
|
v
调用方(返回)
注意:foo() 的栈帧 prologue(push rbp 等)从未执行,因为 IP 在 __fentry__ 点就被重定向了。这意味着 foo_new() 必须是一个完整的独立函数,有自己完整的栈帧。
kernel/livepatch/patch.c:187 设置的标志:
ops->fops.flags = FTRACE_OPS_FL_DYNAMIC |
#ifndef CONFIG_HAVE_DYNAMIC_FTRACE_WITH_ARGS
FTRACE_OPS_FL_SAVE_REGS |
#endif
FTRACE_OPS_FL_IPMODIFY |
FTRACE_OPS_FL_PERMANENT;FTRACE_OPS_FL_IPMODIFY 有一个重要限制:每个函数只能有一个 IPMODIFY 类型的 ftrace_ops。这正是为什么当同一函数被多个 livepatch 覆盖时,livepatch 使用 func_stack 在同一个 klp_ops 中管理——只注册一个 ftrace_ops,在 handler 内部通过栈逻辑决定执行哪个版本。
FTRACE_OPS_FL_PERMANENT 则确保此 ftrace_ops 不能被 ftrace_kill() 或调试工具意外禁用。
kernel/livepatch/patch.c:127:
static void klp_unpatch_func(struct klp_func *func)
{
struct klp_ops *ops = klp_find_ops(func->old_func);
if (list_is_singular(&ops->func_stack)) {
// func_stack 只剩这一个:注销 ftrace,释放 ops
unsigned long ftrace_loc = ftrace_location((unsigned long)func->old_func);
unregister_ftrace_function(&ops->fops);
ftrace_set_filter_ip(&ops->fops, ftrace_loc, 1, 0);
list_del_rcu(&func->stack_node);
list_del(&ops->node);
kfree(ops);
} else {
// 还有其他 patch:只从 func_stack 移除自己
list_del_rcu(&func->stack_node);
}
func->patched = false;
}仅有 ftrace 替换并不够。考虑以下场景:
时刻 T0: 函数 foo() 正在 CPU0 上执行(old 版本)
时刻 T1: 管理员安装 livepatch,new_foo() 生效
时刻 T2: CPU1 上一个新任务调用 foo(),使用 new_foo()
时刻 T3: CPU0 上的 foo() 仍在执行 old 代码
问题:
- old_foo() 和 new_foo() 可能对某个共享数据结构的语义理解不同
- 例如 old_foo() 认为某个字段是 int,new_foo() 改为 u64
- 此时两个函数同时操作该字段 -> 数据损坏
一致性的定义:在任意时刻,系统中所有任务对被 patch 函数的调用,要么全部使用 old 版本,要么全部使用 new 版本,不存在混用状态。
每个 task_struct 有一个 patch_state 字段,记录该任务当前处于哪个版本:
include/linux/livepatch.h:22
#define KLP_TRANSITION_IDLE -1 // 无 patch 进行中
#define KLP_TRANSITION_UNPATCHED 0 // 使用旧版本
#define KLP_TRANSITION_PATCHED 1 // 使用新版本
还有一个线程标志 TIF_PATCH_PENDING(include/linux/livepatch.h:186):
static inline bool klp_patch_pending(struct task_struct *task)
{
return test_tsk_thread_flag(task, TIF_PATCH_PENDING);
}当 TIF_PATCH_PENDING 置位时,表示该任务的 patch_state 还未更新到目标状态,需要在合适的时机(系统调用返回、调度切换等)进行迁移。
任务 patch_state 的更新通过以下几个路径触发:
patch_state 迁移触发点
======================
1. 系统调用返回(kernel_exit):
arch_exit_to_user_mode_prepare() -> klp_update_patch_state()
2. 调度切换(schedule()):
__klp_sched_try_switch() <- static_key 保护,仅 transition 期间激活
3. klp_try_complete_transition() 主动扫描:
遍历所有任务,对睡眠中的任务尝试栈检查后直接切换
4. 新进程 fork:
klp_copy_process() 继承父进程的 patch_state
klp_update_patch_state() 的实现(kernel/livepatch/transition.c:175):
void klp_update_patch_state(struct task_struct *task)
{
preempt_disable_notrace();
// test_and_clear 同时充当内存屏障(smp_rmb)
if (test_and_clear_tsk_thread_flag(task, TIF_PATCH_PENDING))
task->patch_state = READ_ONCE(klp_target_state);
preempt_enable_notrace();
}对于睡眠中的任务(不在运行,可以安全检查其栈),klp_try_switch_task() 会主动尝试迁移(kernel/livepatch/transition.c:305):
static bool klp_try_switch_task(struct task_struct *task)
{
const char *old_name;
int ret;
// 已经是目标状态,无需操作
if (task->patch_state == klp_target_state)
return true;
// 没有可靠栈回溯支持:只能靠被动迁移(系统调用返回)
if (!klp_have_reliable_stack())
return false;
// 对 current 或停止的任务进行栈检查
if (task == current)
ret = klp_check_and_switch_task(current, &old_name);
else
ret = task_call_func(task, klp_check_and_switch_task, &old_name);
switch (ret) {
case 0: // 成功切换
case -EBUSY: // 任务正在另一个 CPU 上运行
case -EINVAL: // 栈不可靠(unwinding 失败)
case -EADDRINUSE: // 任务正睡眠在被 patch 的函数上
...
}
return !ret;
}核心的栈检查逻辑位于 kernel/livepatch/transition.c:205:
static int klp_check_stack_func(struct klp_func *func,
unsigned long *entries,
unsigned int nr_entries)
{
unsigned long func_addr, func_size, address;
int i;
if (klp_target_state == KLP_TRANSITION_UNPATCHED) {
// 正在卸载:检查 new_func 是否在栈上
func_addr = (unsigned long)func->new_func;
func_size = func->new_size;
} else {
// 正在安装:检查 old_func(或上一个 patch 版本)是否在栈上
ops = klp_find_ops(func->old_func);
if (list_is_singular(&ops->func_stack)) {
func_addr = (unsigned long)func->old_func;
func_size = func->old_size;
} else {
// 存在上一个 patch 版本
struct klp_func *prev = list_next_entry(func, stack_node);
func_addr = (unsigned long)prev->new_func;
func_size = prev->new_size;
}
}
// 检查栈上所有地址是否落在该函数范围内
for (i = 0; i < nr_entries; i++) {
address = entries[i];
if (address >= func_addr && address < func_addr + func_size)
return -EAGAIN; // 函数在栈上,不能迁移
}
return 0; // 安全,可以迁移
}可靠栈回溯使用 stack_trace_save_tsk_reliable(),依赖 CONFIG_HAVE_RELIABLE_STACKTRACE 和 CONFIG_STACKTRACE(include/linux/livepatch.h:189):
static inline bool klp_have_reliable_stack(void)
{
return IS_ENABLED(CONFIG_STACKTRACE) &&
IS_ENABLED(CONFIG_HAVE_RELIABLE_STACKTRACE);
}如果某些任务长时间无法完成迁移(例如,一个内核线程一直睡眠在被 patch 的函数上),klp_try_complete_transition() 会定期重试,并在超时后发送信号:
kernel/livepatch/transition.c:23:
#define SIGNALS_TIMEOUT 15klp_send_signals()(kernel/livepatch/transition.c:387)在每次 SIGNALS_TIMEOUT 轮检查失败后触发:
static void klp_send_signals(void)
{
...
for_each_process_thread(g, task) {
if (!klp_patch_pending(task))
continue;
if (task->flags & PF_KTHREAD) {
// 内核线程:发送 TASK_INTERRUPTIBLE 唤醒
wake_up_state(task, TASK_INTERRUPTIBLE);
} else {
// 用户进程:发送假信号,强制返回用户空间(触发迁移)
set_notify_signal(task);
}
}
}如果上述方法都无效,管理员可以通过 sysfs 强制迁移:
echo 1 > /sys/kernel/livepatch/<patch>/force这会调用 klp_force_transition(),强制将所有未迁移任务切换到目标状态,但 patch->forced = true 标记会被设置,表明此次迁移是非安全的(可能跳过了正在使用旧函数的任务)。
klp_init_transition(patch, PATCHED)
|
v
+---------------+---------------+
| 所有 task 设为 UNPATCHED |
| 所有 func->transition = true |
+---------------+---------------+
|
klp_start_transition()
|
v
+---------------+---------------+
| 所有 task 设 TIF_PATCH_PENDING |
| klp_resched_enable() |
+---------------+---------------+
|
klp_try_complete_transition()
|
+---------------+---------------+
| 遍历所有 task |
| klp_try_switch_task() -> 成功 |
| 则 task->patch_state=PATCHED |
+---------------+---------------+
|
+--------------+--------------+
| |
所有任务完成 有任务未完成
| |
v v
klp_complete_transition() schedule_delayed_work(1s)
| |
func->transition=false 下一轮重试
klp_synchronize_transition() +
task->patch_state=IDLE klp_send_signals() (超时后)
|
v
patch 稳定生效
入口函数 klp_enable_patch()(kernel/livepatch/core.c:1108):
int klp_enable_patch(struct klp_patch *patch)
{
// 1. 参数合法性检查
if (!patch || !patch->mod || !patch->objs)
return -EINVAL;
// 2. 检查 patch 模块是否有 MODULE_INFO(livepatch, "Y") 标记
if (!is_livepatch_module(patch->mod))
return -EINVAL;
// 3. 检查 livepatch 子系统是否已初始化(/sys/kernel/livepatch 存在)
if (!klp_initialized())
return -ENODEV;
// 4. 可靠栈回溯警告(架构不支持时 transition 可能永远不完成)
if (!klp_have_reliable_stack())
pr_warn("This architecture doesn't have support for...");
mutex_lock(&klp_mutex);
// 5. 检查 state 兼容性(klp_state 版本号)
if (!klp_is_patch_compatible(patch))
return -EINVAL;
// 6. 增加 patch 模块的引用计数(防止 patch 未卸载时模块被移除)
try_module_get(patch->mod);
// 7. 初始化 klp_patch 数据结构(early,不依赖符号解析)
klp_init_patch_early(patch);
// 8. 符号解析 + sysfs 节点创建
klp_init_patch(patch);
// -> 如果是 replace 模式,先调用 klp_add_nops()
// -> klp_init_object() 对每个 object
// -> klp_find_object_module()(找到 module 指针)
// -> klp_init_func()(创建 sysfs 节点)
// -> 如果 object 已加载:klp_init_object_loaded()
// -> klp_find_object_symbol()(kallsyms 符号解析)
// -> kallsyms_lookup_size_offset()(获取函数大小)
// 9. 真正激活 patch
__klp_enable_patch(patch);
mutex_unlock(&klp_mutex);
}kernel/livepatch/core.c:866,这是 patch 能正确工作的关键步骤:
static int klp_init_object_loaded(struct klp_patch *patch,
struct klp_object *obj)
{
struct klp_func *func;
int ret;
// 对模块内的符号,先应用 klp 专用的 ELF 重定位节
if (klp_is_module(obj)) {
ret = klp_apply_object_relocs(patch, obj);
}
klp_for_each_func(obj, func) {
// 通过 kallsyms 解析原函数地址
ret = klp_find_object_symbol(obj->name,
func->old_name,
func->old_sympos,
(unsigned long *)&func->old_func);
// 获取原函数大小(用于栈检查范围判断)
kallsyms_lookup_size_offset((unsigned long)func->old_func,
&func->old_size, NULL);
// NOP 函数的 new_func == old_func(回到原函数)
if (func->nop)
func->new_func = func->old_func;
// 获取新函数大小
kallsyms_lookup_size_offset((unsigned long)func->new_func,
&func->new_size, NULL);
}
}kernel/livepatch/core.c:160:
static int klp_find_object_symbol(const char *objname, const char *name,
unsigned long sympos, unsigned long *addr)
{
struct klp_find_arg args = {
.name = name,
.addr = 0,
.count = 0,
.pos = sympos,
};
if (objname)
// 模块符号:搜索特定模块的 kallsyms
module_kallsyms_on_each_symbol(objname, klp_find_callback, &args);
else
// vmlinux 符号:搜索全局 kallsyms
kallsyms_on_each_match_symbol(klp_match_callback, name, &args);
// 验证结果:sympos=0 要求唯一,sympos>0 要求精确匹配第 N 个
if (args.addr == 0)
pr_err("symbol '%s' not found in symbol table\n", name);
else if (args.count > 1 && sympos == 0)
pr_err("unresolvable ambiguity for symbol '%s'\n", name);
else {
*addr = args.addr;
return 0;
}
return -EINVAL;
}old_sympos 字段专门解决内核中存在同名函数的情况(例如两个模块都定义了同名 static 函数时,old_sympos=1 表示第一个出现的)。
kernel/livepatch/core.c:1040,在 klp_enable_patch() 的锁保护下调用:
static int __klp_enable_patch(struct klp_patch *patch)
{
// 1. 初始化 transition 状态机(目标:PATCHED)
klp_init_transition(patch, KLP_TRANSITION_PATCHED);
// -> klp_transition_patch = patch
// -> klp_target_state = KLP_TRANSITION_PATCHED
// -> 所有 task->patch_state = KLP_TRANSITION_UNPATCHED
// -> 所有 func->transition = true
// -> smp_wmb()
smp_wmb(); // 确保 func->transition 写入先于 ops->func_stack 写入
// 2. 对每个已加载的 object,调用 pre_patch 回调并注册 ftrace
klp_for_each_object(patch, obj) {
if (!klp_is_object_loaded(obj))
continue;
klp_pre_patch_callback(obj); // 用户回调
klp_patch_object(obj); // 注册 ftrace,加入 func_stack
// -> klp_patch_func() 对每个 func
}
// 3. 开始 transition(设置 TIF_PATCH_PENDING)
klp_start_transition();
patch->enabled = true;
// 4. 立即尝试一次 transition(快速路径:当前没有任务在被 patch 的函数上)
klp_try_complete_transition();
return 0;
}当两个 livepatch 补丁都 patch 了同一个函数时,func_stack 管理叠加关系:
场景:patch_A 先安装,patch_B 后安装(均 patch 了 foo())
klp_ops for foo():
func_stack: [patch_B::new_foo] -> [patch_A::new_foo] -> (head)
klp_ftrace_handler() 行为:
transition 完成后:总是执行 patch_B::new_foo(最新版本)
transition 期间:
task->patch_state == PATCHED -> 执行 patch_B::new_foo
task->patch_state == UNPATCHED -> 执行 patch_A::new_foo(上一个版本)
livepatch 只能替换函数,不能修改数据结构。但有时修复一个 bug 需要给某个对象附加新字段。例如:
// 原来的结构(已经在内核中运行)
struct net_device {
...
// 没有 my_new_field
};
// 修复需要给每个 net_device 增加一个状态字段
// 但不能修改已有结构体!Shadow 变量(kernel/livepatch/shadow.c)提供了一种"附加字段"的机制:通过一个全局 hashtable,以 <obj_ptr, id> 为 key,将额外数据与任意内核对象关联起来。
kernel/livepatch/shadow.c:54:
struct klp_shadow {
struct hlist_node node; // hashtable 节点
struct rcu_head rcu_head; // RCU 安全释放
void *obj; // 父对象指针(作为 key 的一部分)
unsigned long id; // 数据标识符(作为 key 的一部分)
char data[]; // 柔性数组:实际存储的数据
};全局 hashtable(kernel/livepatch/shadow.c:38):
static DEFINE_HASHTABLE(klp_shadow_hash, 12); // 2^12 = 4096 个桶
static DEFINE_SPINLOCK(klp_shadow_lock);哈希桶数为 4096,以 (unsigned long)obj 为哈希键(父对象地址)。
kernel/livepatch/shadow.c:83:
void *klp_shadow_get(void *obj, unsigned long id)
{
struct klp_shadow *shadow;
rcu_read_lock();
hash_for_each_possible_rcu(klp_shadow_hash, shadow, node,
(unsigned long)obj) {
if (klp_shadow_match(shadow, obj, id)) {
rcu_read_unlock();
return shadow->data; // 返回数据指针,不返回结构体本身
}
}
rcu_read_unlock();
return NULL;
}RCU 读侧,无锁查找,高性能。
kernel/livepatch/shadow.c:196:
void *klp_shadow_alloc(void *obj, unsigned long id,
size_t size, gfp_t gfp_flags,
klp_shadow_ctor_t ctor, void *ctor_data)
{
return __klp_shadow_get_or_alloc(obj, id, size, gfp_flags,
ctor, ctor_data, true);
// warn_on_exist=true:如果已存在则 WARN 并返回 NULL
}内部实现 __klp_shadow_get_or_alloc()(kernel/livepatch/shadow.c:104)采用双重检查锁:
1. RCU 无锁检查是否已存在(快速路径)
2. kzalloc 分配新 shadow(在锁外,避免持锁期间分配内存)
3. spin_lock 后再次检查(防止并发创建)
4. 调用 ctor 初始化数据(在锁内,保证 ctor 仅调用一次)
5. hash_add_rcu 插入 hashtable
kernel/livepatch/shadow.c:253:
void klp_shadow_free(void *obj, unsigned long id, klp_shadow_dtor_t dtor)
{
struct klp_shadow *shadow;
unsigned long flags;
spin_lock_irqsave(&klp_shadow_lock, flags);
hash_for_each_possible(klp_shadow_hash, shadow, node, (unsigned long)obj) {
if (klp_shadow_match(shadow, obj, id)) {
hash_del_rcu(&shadow->node);
if (dtor)
dtor(shadow->obj, shadow->data); // 析构回调
kfree_rcu(shadow, rcu_head); // RCU 延迟释放
break;
}
}
spin_unlock_irqrestore(&klp_shadow_lock, flags);
}// 定义 shadow 变量 ID(避免与其他 patch 冲突)
#define MY_SHADOW_ID 0x12345678UL
// 在 pre_patch 回调中分配
static int my_pre_patch(struct klp_object *obj)
{
struct net_device *dev;
// 遍历所有已有 net_device,为每个分配 shadow 变量
for_each_netdev(&init_net, dev) {
struct my_extra *extra;
extra = klp_shadow_alloc(dev, MY_SHADOW_ID,
sizeof(*extra), GFP_KERNEL,
NULL, NULL);
if (!extra)
return -ENOMEM;
extra->state = INITIAL_STATE;
}
return 0;
}
// 在 patch 后的新函数中使用
void patched_dev_func(struct net_device *dev)
{
struct my_extra *extra = klp_shadow_get(dev, MY_SHADOW_ID);
if (extra)
extra->state = RUNNING;
// ...
}
// 在 post_unpatch 回调中释放
static void my_post_unpatch(struct klp_object *obj)
{
struct net_device *dev;
for_each_netdev(&init_net, dev)
klp_shadow_free(dev, MY_SHADOW_ID, NULL);
}klp_shadow_hash(2^12 桶):
bucket[hash(obj_A)] -> [klp_shadow{obj=obj_A, id=1, data=[...]}]
-> [klp_shadow{obj=obj_A, id=2, data=[...]}]
bucket[hash(obj_B)] -> [klp_shadow{obj=obj_B, id=1, data=[...]}]
bucket[hash(obj_C)] -> [klp_shadow{obj=obj_C, id=5, data=[...]}]
-> ...
查找:hash_for_each_possible_rcu(hash, obj_ptr)
对每个候选节点检查 shadow->obj == obj && shadow->id == id
随着 CVE 不断发现,系统上可能累积多个 livepatch:
patch_1: 修复 CVE-2023-001
patch_2: 修复 CVE-2023-002 (同时包含 CVE-2023-001 的修复)
patch_3: 修复 CVE-2023-003 (同时包含以上所有修复)
此时 func_stack 有三层,每次调用都要走 handler 的 transition 逻辑,且管理复杂。累积补丁(replace=true)解决了这个问题。
当 klp_patch.replace = true 时,新 patch 在完成 transition 后会替换(而非叠加)所有已有 patch,最终系统上只剩一个活跃 patch。
安装前:
klp_patches: [patch_1] -> [patch_2]
func_stack of foo: [patch_2::foo_new] -> [patch_1::foo_new]
安装 patch_3(replace=true):
transition 期间:
func_stack of foo: [patch_3::foo_new] -> [patch_2::foo_new] -> [patch_1::foo_new]
transition 完成后:
klp_unpatch_replaced_patches(patch_3) 卸载 patch_1, patch_2
klp_discard_nops(patch_3) 清理 NOP 函数
最终 func_stack: [patch_3::foo_new]
最终 klp_patches: [patch_3]
累积 patch 中可能不覆盖前一个 patch 的所有函数。例如 patch_2 只修复了 bar(),patch_3 只修复了 foo()。如果简单地替换 patch_2,bar() 的修复就丢失了。
解决方案:klp_add_nops()(kernel/livepatch/core.c:615)为 patch_3 中缺少的函数自动创建 NOP 条目:
// NOP 函数:new_func == old_func
if (func->nop)
func->new_func = func->old_func; // 见 klp_init_object_loaded()NOP 函数在 klp_ftrace_handler() 中直接 goto unlock(patch.c:118):
if (func->nop)
goto unlock; // 不修改 IP,让原函数执行这样 patch_3 不需要重复包含 bar() 的修复代码,而是通过 NOP 保持 bar() 的旧修复版本的行为回退到原始函数。
实际上,当 patch_3 replace 完成后,patch_2(含 bar 的修复)被卸载,NOP 也清理掉,bar() 回到原函数——这意味着累积 patch 必须包含所有之前 patch 的修复内容,否则安全修复会丢失。
kernel/livepatch/state.c:98:
static bool klp_is_state_compatible(struct klp_patch *patch,
struct klp_state *old_state)
{
struct klp_state *state = klp_get_state(patch, old_state->id);
// 累积 patch 必须处理所有已修改的 state
if (!state)
return !patch->replace;
// 版本号只能增加,不能降低
return state->version >= old_state->version;
}如果系统上已有一个 patch 将某 state 改为 version=2,新的累积 patch 必须声明 version>=2,否则 klp_enable_patch() 返回 -EINVAL。
时间轴
|
T0: patch_1 安装完成(修复 foo_v1, bar_v1)
|
T1: patch_2 安装(replace=true,修复 foo_v2, bar_v2)
| klp_add_nops() 为未覆盖函数(如有)创建 NOP
| klp_init_transition(patch_2, PATCHED)
| 所有任务标记 TIF_PATCH_PENDING
|
T2: 任务逐步迁移到 PATCHED 状态
| func_stack: [patch_2::foo_v2] -> [patch_1::foo_v1]
| PATCHED 任务调用 foo_v2,UNPATCHED 任务调用 foo_v1
|
T3: 所有任务迁移完成
| klp_complete_transition()
| -> klp_unpatch_replaced_patches(patch_2)
| 清理 patch_1 的 ftrace 注册
| -> klp_discard_nops(patch_2)
| 清理 NOP 函数
| 最终:只剩 patch_2,func_stack: [patch_2::foo_v2]
|
T4: patch_1 模块可以卸载
kernel/livepatch/core.c:348-358 注释给出了完整布局:
/sys/kernel/livepatch/
└── <patch_name>/ # 对应一个 klp_patch
├── enabled # RW:0/1,禁用/激活补丁
├── transition # RO:0/1,是否正在 transition
├── force # WO:写 1 强制完成 transition
├── replace # RO:0/1,是否是累积补丁
├── stack_order # RO:补丁在叠加栈中的位置
└── <object_name>/ # 对应一个 klp_object(vmlinux 或模块名)
├── patched # RO:0/1,该 object 是否已 patch
└── <func_name,sympos> # 对应一个 klp_func(无属性,仅目录)
读取(core.c:404):
static ssize_t enabled_show(...)
{
patch = container_of(kobj, struct klp_patch, kobj);
return sysfs_emit(buf, "%d\n", patch->enabled);
}写入(core.c:361):
static ssize_t enabled_store(...)
{
// 若该 patch 正在 transition:允许反转(reverse_transition)
if (patch == klp_transition_patch)
klp_reverse_transition();
// 若 enabled=true 且写 0:禁用该 patch
else if (!enabled)
ret = __klp_disable_patch(patch);
// 否则(尝试重新 enable 已 disabled patch):不允许
else
ret = -EINVAL;
}注意:已经 disabled 的 patch 不能通过写 enabled=1 重新激活。若要重新应用必须重新加载 patch 模块。
只读(core.c:413):
static ssize_t transition_show(...)
{
patch = container_of(kobj, struct klp_patch, kobj);
return sysfs_emit(buf, "%d\n", patch == klp_transition_patch);
}当 transition == 1 时,表示该 patch 正在进行任务迁移过程,不能对其他 patch 进行 enable/disable 操作(返回 -EBUSY)。
只写(core.c:422):
static ssize_t force_store(...)
{
patch = container_of(kobj, struct klp_patch, kobj);
// 只能对正在 transition 的 patch 使用
if (patch != klp_transition_patch)
return -EINVAL;
klp_force_transition();
// 强制所有未迁移任务切换到目标 patch_state
// patch->forced = true(标记为非安全迁移)
}forced=true 的后果:klp_free_patch_finish() 不会调用 module_put(),因为强制迁移后旧函数可能仍被某些任务引用(虽然概率极低),保持引用计数防止模块被卸载。
只读(core.c:460):
static ssize_t stack_order_show(...)
{
int stack_order = 0;
klp_for_each_patch(patch) {
stack_order++;
if (patch == this_patch)
break;
}
return sysfs_emit(buf, "%d\n", stack_order);
}表示该 patch 在全局 klp_patches 链表中的位置(从 1 开始),越大越新。
# 查看所有活跃 livepatch
ls /sys/kernel/livepatch/
# 查看某 patch 状态
cat /sys/kernel/livepatch/livepatch_cve_2023_001/enabled
cat /sys/kernel/livepatch/livepatch_cve_2023_001/transition
# 等待 transition 完成(轮询)
while [ "$(cat /sys/kernel/livepatch/*/transition)" = "1" ]; do
sleep 1
done
# 禁用某 patch
echo 0 > /sys/kernel/livepatch/livepatch_cve_2023_001/enabled
# 强制完成(危险!仅在确认无任务卡在被 patch 函数上时使用)
echo 1 > /sys/kernel/livepatch/livepatch_cve_2023_001/force
# 查看补丁叠加顺序
cat /sys/kernel/livepatch/*/stack_order当内核 panic 且已安装 livepatch 时,崩溃转储(vmcore/kdump)包含的是运行时内存快照,其中函数代码是被 patch 修改过的版本。这对事后分析带来若干挑战:
符号解析偏移问题:
原函数 foo 地址:0xffffffff81234567
patch 后,ftrace 将 foo 的入口改为跳转到 trampoline
trampoline 地址:0xffffffff82abcdef(动态分配)
新函数 foo_new 地址:在 livepatch 模块文本段
崩溃时 PC 可能指向 foo_new,调试器需要知道:
1. foo_new 属于哪个 livepatch 模块
2. livepatch 模块的符号表(KASLR 偏移后的实际地址)
任务状态不一致:崩溃时不同 CPU 上的任务 patch_state 可能不同(处于 transition 期间),使得 "谁在用旧函数,谁在用新函数" 难以判断。
Linux 内核通过 /proc/vmcore_dmesg 等机制在 kdump 内核启动时访问崩溃内核的内存。livepatch 模块作为普通内核模块,其信息记录在:
/proc/modules -> 可以通过 crash 工具的 mod 命令加载
/sys/module/<name>/ -> 模块基址、参数等
crash> mod -s livepatch_cve_2023_001 /path/to/livepatch_cve_2023_001.ko
crash> dis foo_new # 反汇编新函数
当系统需要紧急修复(livepatch 无法处理的情况)时,kexec 可与 livepatch 共存:
正常运行(有 livepatch 激活)
|
| kexec -l new_kernel + kexec -e
v
新内核加载(热重启,无 BIOS 初始化)
|
v
新内核正常启动(无 livepatch,从干净状态开始)
重要:kexec 切换时,正在进行的 livepatch transition 会被强制中断,新内核启动后不继承任何 livepatch 状态。
使用 crash 工具分析 vmcore 时,可以通过以下方式检查 livepatch 状态:
# crash 命令
crash> p klp_patches
crash> list klp_patch.list -s klp_patch.mod -l klp_patches
# 查看 transition 状态
crash> p klp_transition_patch
crash> p klp_target_state
# 查看所有任务的 patch_state
crash> foreach task "p task.patch_state"
transition 期间的开销:
| 操作 | 开销来源 | 量级 |
|---|---|---|
klp_init_transition() |
遍历所有任务设置状态 | O(N_tasks),持 tasklist_lock |
klp_try_complete_transition() |
遍历所有任务做栈检查 | O(N_tasks * stack_depth) |
klp_synchronize_transition() |
schedule_on_each_cpu | O(N_CPUs),类似 synchronize_rcu |
klp_ftrace_handler() |
每次函数调用额外检查 | O(1),但有 smp_rmb() 开销 |
transition 期间(通常数秒到数十秒),每次被 patch 函数的调用都会多一次 current->patch_state 读取和可能的 smp_rmb(),对高频调用函数(如 vfs_read)有可感知的性能影响。
假设内核函数 vfs_read() 存在一个边界检查漏洞,需要 livepatch 修复。
// livepatch_fix_vfs_read.c
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/fs.h>
#include <linux/uio.h>
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y"); // 必须:标记为 livepatch 模块
/* ===== 新版本函数 ===== */
/* 修复后的 vfs_read 替代函数
*
* 注意:函数签名必须与原函数完全一致
* 原函数:ssize_t vfs_read(struct file *file, char __user *buf,
* size_t count, loff_t *pos)
*/
static ssize_t patched_vfs_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
// 新增的边界检查
if (unlikely(count > MAX_RW_COUNT))
return -EINVAL;
// 调用原始实现(通过 kallsyms 解析)
// 注意:不能直接调用 vfs_read(),那会导致无限递归!
// 实际场景中这里会包含完整的修复后逻辑,不调用原函数
// 或者通过 klp_func.old_func 调用原始函数(需要 patch 代码支持)
return 0; // 简化示例
}
/* ===== livepatch 数据结构定义 ===== */
static struct klp_func fix_funcs[] = {
{
.old_name = "vfs_read", // 被 patch 的函数名
.new_func = patched_vfs_read, // 新函数指针
.old_sympos = 0, // 0 = 要求符号唯一
},
{ } // 数组终止符(所有字段为 0)
};
static struct klp_object fix_objs[] = {
{
.name = NULL, // NULL = vmlinux
.funcs = fix_funcs,
},
{ } // 数组终止符
};
static struct klp_patch fix_patch = {
.mod = THIS_MODULE,
.objs = fix_objs,
// .replace = false, // 默认:叠加模式
};
/* ===== 模块初始化 ===== */
static int __init fix_init(void)
{
// klp_enable_patch() 是唯一需要调用的 API
// 它会完成:符号解析、sysfs 创建、ftrace 注册、transition 启动
return klp_enable_patch(&fix_patch);
}
static void __exit fix_exit(void)
{
// livepatch 模块不需要做任何清理
// disable 是通过 sysfs 触发的,模块卸载会等待 patch->finish
}
module_init(fix_init);
module_exit(fix_exit);# Makefile
obj-m := livepatch_fix_vfs_read.o
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean// 带 pre/post 回调的示例:patch 需要分配额外资源
static int my_pre_patch_callback(struct klp_object *obj)
{
pr_info("pre-patch callback called\n");
// 在这里分配 patch 需要的资源(shadow 变量等)
return 0; // 返回非 0 将取消整个 patch
}
static void my_post_patch_callback(struct klp_object *obj)
{
pr_info("post-patch callback called, patch active\n");
// transition 完成后的通知
}
static void my_pre_unpatch_callback(struct klp_object *obj)
{
pr_info("pre-unpatch callback called\n");
}
static void my_post_unpatch_callback(struct klp_object *obj)
{
pr_info("post-unpatch callback called\n");
// 在这里释放 shadow 变量等资源
}
static struct klp_object fix_objs[] = {
{
.name = NULL,
.funcs = fix_funcs,
.callbacks = {
.pre_patch = my_pre_patch_callback,
.post_patch = my_post_patch_callback,
.pre_unpatch = my_pre_unpatch_callback,
.post_unpatch = my_post_unpatch_callback,
},
},
{ }
};// 累积 patch:替换之前所有 patch
static struct klp_state my_states[] = {
{
.id = 1, // 系统状态 ID(与前一个 patch 对应)
.version = 2, // 新版本(必须 >= 前一个 patch 声明的版本)
},
{ } // 终止符
};
static struct klp_patch cumulative_patch = {
.mod = THIS_MODULE,
.objs = fix_objs,
.states = my_states,
.replace = true, // 启用累积模式:替换所有已有 patch
};当 klp_enable_patch() 返回 -EINVAL 且 dmesg 显示符号未找到时:
# 检查符号是否存在
grep vfs_read /proc/kallsyms
# 如果有重名符号,查看位置
grep " vfs_read$" /proc/kallsyms | awk '{print NR, $0}'
# 然后设置 .old_sympos = 1 或 2 等
# 检查模块是否正确标记
modinfo livepatch_fix_vfs_read.ko | grep livepatch
# 应输出:livepatch: Y# 1. 加载 patch 模块(触发 module_init -> klp_enable_patch)
insmod livepatch_fix_vfs_read.ko
# 2. 检查 patch 状态
dmesg | tail -20
# 期望看到:
# livepatch: enabling patch 'livepatch_fix_vfs_read'
# livepatch: 'livepatch_fix_vfs_read': starting patching transition
# livepatch: 'livepatch_fix_vfs_read': patching complete
# 3. 查看 sysfs 状态
ls /sys/kernel/livepatch/livepatch_fix_vfs_read/
cat /sys/kernel/livepatch/livepatch_fix_vfs_read/enabled # 1
cat /sys/kernel/livepatch/livepatch_fix_vfs_read/transition # 0(完成后)
# 4. 禁用 patch(触发 unpatching transition)
echo 0 > /sys/kernel/livepatch/livepatch_fix_vfs_read/enabled
# 5. 等待 transition 完成
while [ "$(cat /sys/kernel/livepatch/livepatch_fix_vfs_read/transition)" = "1" ]; do
sleep 1
done
# 6. 卸载模块
rmmod livepatch_fix_vfs_readlivepatch 依赖 CONFIG_HAVE_RELIABLE_STACKTRACE 进行任务迁移。截至分析时:
| 架构 | ftrace 支持 | 可靠栈回溯 | livepatch 完整支持 |
|---|---|---|---|
| x86_64 | 是 | 是(objtool 辅助) | 是 |
| arm64 | 是 | 是 | 是 |
| powerpc | 是 | 是 | 是(部分限制) |
| s390 | 是 | 是 | 是 |
| arm(32位) | 是 | 否 | 部分(transition 可能不完成) |
| MIPS | 是 | 否 | 部分 |
| riscv | 部分 | 否 | 有限 |
无可靠栈回溯时,任务只能在系统调用返回或 klp_send_signals() 触发后被动迁移,transition 可能需要很长时间。
不能 patch 的函数类型:
- 内联函数:已被编译器内联到调用点,没有独立的函数体
- 不可 trace 的函数(
__notrace_func/notrace修饰) - 自身就是 ftrace infrastructure 的函数(防止递归)
- 极短函数(short functions):ftrace 需要在函数入口 5 字节处有 nop slot,太短的函数放不下
__init函数:已在初始化完成后释放
不能在 patch 函数中做的事:
- 直接调用同名旧函数(死递归)
- 使用栈上的大型局部变量(ftrace trampoline 的栈空间有限制)
- 修改调用约定(函数签名必须与原函数完全一致)
livepatch 不能:
- 添加/删除/修改内核数据结构(
struct的布局) - 添加新的全局变量(除非通过 patch 模块导出)
- 添加新的
EXPORT_SYMBOL符号
可以通过 shadow 变量来绕过数据结构限制(见第 7 章)。
patch 函数的编写者需要自行保证:
- 新函数本身是线程安全的
- 新函数与旧函数在 transition 期间对共享状态的访问是安全的(因为 transition 期间两者可能并发执行)
- shadow 变量的访问需要调用者自行加锁
echo 1 > .../force 的后果:
被强制跳过的任务可能正在 old 函数中执行
|
v
该任务的 patch_state 被强制设为 PATCHED
|
v
下次该任务进入被 patch 函数时,使用 new 函数
|
v
但在此之前(old 函数未返回),old 函数和 new 函数
可能对同一数据结构做不兼容的操作 -> 未定义行为
因此 force 应该作为最后手段,且 patch->forced = true 后系统完整性不再被保证。
klp_free_patch_finish() (kernel/livepatch/core.c:754):
static void klp_free_patch_finish(struct klp_patch *patch)
{
kobject_put(&patch->kobj);
wait_for_completion(&patch->finish); // 等待 kobject 完全释放
if (!patch->forced)
module_put(patch->mod); // 释放模块引用计数
}patch->finish 在 klp_kobj_release_patch()(kobject 最后一次引用释放时)中触发 complete(),确保 patch 数据结构完全释放后才允许模块卸载。
kernel/livepatch/
├── core.c klp_enable_patch() 主入口;sysfs 接口;符号解析;对象/函数初始化
├── patch.c klp_ftrace_handler();klp_patch_func();klp_unpatch_func();klp_ops 管理
├── transition.c 一致性模型实现;klp_try_switch_task();klp_check_stack();
│ klp_init_transition();klp_start_transition();klp_try_complete_transition()
├── shadow.c shadow 变量(klp_shadow_get/alloc/free);全局 hashtable
├── state.c 系统状态管理(klp_get_state / klp_get_prev_state / klp_is_patch_compatible)
├── core.h 内部声明;klp_mutex;klp_patches;klp_is_object_loaded() 等
├── patch.h struct klp_ops 定义;klp_find_ops();klp_patch_object();klp_unpatch_objects()
├── transition.h klp_transition_patch;klp_init_transition();klp_try_complete_transition() 声明
└── state.h klp_is_patch_compatible() 声明
以及相关头文件:
include/linux/
├── livepatch.h 公开 API:struct klp_func/object/patch/state;klp_enable_patch();
│ klp_shadow_*();KLP_TRANSITION_* 常量
├── livepatch_external.h 外部工具接口:struct klp_callbacks/func_ext/object_ext;KLP_*_PREFIX
└── livepatch_sched.h 调度相关:klp_sched_try_switch_key;__klp_sched_try_switch()
Linux livepatch 是一个精心设计的系统,其核心在于三个层面的协作:
+------------------------------------------------------------------+
| livepatch 三层架构 |
+------------------------------------------------------------------+
| |
| 1. 函数替换层(patch.c) |
| ftrace IPMODIFY + klp_ops.func_stack |
| 实现:任意函数调用 -> trampoline -> 新函数 |
| |
| 2. 一致性保证层(transition.c) |
| per-task patch_state + TIF_PATCH_PENDING |
| 实现:新旧函数并存期间,每个任务看到一致的视图 |
| |
| 3. 状态管理层(shadow.c / state.c) |
| hashtable shadow 变量 + klp_state 版本兼容 |
| 实现:跨 patch 版本的数据状态维护 |
| |
+------------------------------------------------------------------+
关键设计决策:
- 不停机:通过 ftrace 动态替换,无需
stop_machine - per-task 迁移:每个任务独立切换,避免全局停顿
- 栈检查:通过可靠栈回溯确保任务不在被 patch 函数中才迁移
- RCU 保护:
func_stack的修改使用 RCU,handler 可以无锁读取 - 内存屏障:在
func->transition写入和func_stack修改之间、klp_target_state写入和TIF_PATCH_PENDING设置之间均有smp_wmb(),确保 handler 看到一致状态
这套机制使得 Linux 内核能够在生产环境中安全地进行不停机安全补丁,成为现代 Linux 发行版企业支持的核心能力之一。
由 Claude Code 分析生成