kernel/livepatch 内核热补丁(Livepatch)机制与原理详解

kernel/livepatch 内核热补丁(Livepatch)机制与原理详解

源码路径rk3588/kernel-6.1/kernel/livepatch/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)

该目录实现 Kernel Live Patching(内核热补丁) 核心框架。在不重启系统、不卸载内核模块的前提下,通过 ftrace 动态跳转 将已运行内核中被 patch 的函数调用重定向到补丁模块中的新实现,并配合 一致性模型(Consistency Model) 保证所有任务安全迁移到新/旧代码。

定位:生产环境安全修复内核 bug 的基础设施;补丁以 livepatch 模块 形式加载,由管理员通过 sysfs 或模块 init 启用。


目录


一、源码目录结构

1.1 编译依赖(Makefile)

1
2
3
obj-$(CONFIG_LIVEPATCH) += livepatch.o

livepatch-objs := core.o patch.o shadow.o state.o transition.o
配置项 说明
CONFIG_LIVEPATCH 启用内核热补丁核心
CONFIG_HAVE_LIVEPATCH 架构声明支持 livepatch(ARM64 未 select
CONFIG_DYNAMIC_FTRACE_WITH_REGSCONFIG_DYNAMIC_FTRACE_WITH_ARGS ftrace 需支持寄存器/参数保存
CONFIG_KALLSYMS_ALL 完整符号表(含未导出符号)
CONFIG_HAVE_RELIABLE_STACKTRACE 可靠栈回溯(一致性模型加速路径)
!CONFIG_TRIM_UNUSED_KSYMS 不可裁剪未使用 ksym

Kconfig 依赖链:

1
2
3
4
5
6
7
LIVEPATCH
├── DYNAMIC_FTRACE_WITH_REGS || DYNAMIC_FTRACE_WITH_ARGS
├── MODULES
├── SYSFS
├── KALLSYMS_ALL
├── HAVE_LIVEPATCH ← 架构能力
└── !TRIM_UNUSED_KSYMS

1.2 源文件

文件 行数 功能
core.c 1291 补丁生命周期、符号查找、KLP 重定位、sysfs、模块钩子
transition.c 666 一致性模型:任务 patch_state、栈检查、过渡完成
patch.c 289 ftrace 注册/注销、klp_ftrace_handler 跳转
shadow.c 299 Shadow 变量:RCU 哈希表 <obj, id> → 数据
state.c 119 klp_state 版本兼容、累积补丁检查
core.h 59 内部 API、回调封装、patch 遍历宏
patch.h 35 patch/unpatch 对象声明
transition.h 16 过渡函数声明
state.h 9 state 兼容检查声明

相关头文件

文件 功能
include/linux/livepatch.h 公共 API、klp_patch/klp_object/klp_func 定义
kernel/entry/common.c 返回用户态时 klp_update_patch_state()
kernel/sched/idle.c idle 循环中 klp_update_patch_state()
Documentation/livepatch/ 官方文档与示例

二、整体架构

2.1 设计思想

Livepatch 不修改内核 text 段,而是在函数入口通过 ftrace mcount 钩子 拦截调用,将指令指针重定向到补丁模块中的 new_func。多个补丁叠加时,同一函数维护 func_stack 栈,栈顶为当前生效补丁。

1
2
3
4
5
6
7
补丁模块加载
→ klp_enable_patch()
→ 符号解析 + KLP 重定位
→ klp_patch_object() → register_ftrace_function()
→ klp_init_transition() + klp_start_transition()
→ 所有任务迁移 patch_state
→ klp_complete_transition()

2.2 模块关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────┐
│ Livepatch 补丁模块 │
│ struct klp_patch { objs[], states[], replace } │
│ └── klp_object (vmlinux 或 module) │
│ └── klp_func { old_name → new_func } │
└──────────────────────────┬──────────────────────────────────┘
│ klp_enable_patch()

┌─────────────────────────────────────────────────────────────┐
│ kernel/livepatch/ 核心 │
│ core.c ── 生命周期、符号、sysfs、模块钩子 │
│ patch.c ── ftrace 注册 + klp_ftrace_handler 跳转 │
│ transition.c ── 一致性模型、任务迁移 │
│ shadow.c ── 补丁私有数据(Shadow 变量) │
│ state.c ── 累积补丁状态版本兼容 │
└──────────────────────────┬──────────────────────────────────┘
│ ftrace

┌─────────────────────────────────────────────────────────────┐
│ arch/arm64/kernel/ftrace.c │
│ DYNAMIC_FTRACE_WITH_REGS → ftrace_regs + IP 修改 │
└─────────────────────────────────────────────────────────────┘

2.3 与相关子系统对比

机制 方式 是否需要重启
Livepatch ftrace 跳转 + 一致性模型
kpatch / kgraft 早期独立实现,已并入 livepatch
模块 reload 卸载/重载 .ko 通常需停止使用者
kexec 新内核启动 近似重启

三、核心数据结构

定义于 include/linux/livepatch.h

3.1 klp_func — 函数补丁项

1
2
3
4
5
6
7
8
9
10
11
struct klp_func {
const char *old_name; /* 被 patch 的函数名 */
void *new_func; /* 补丁实现地址 */
unsigned long old_sympos; /* 同名符号第 N 个(0=须唯一) */
void *old_func; /* 运行时解析的原函数地址 */
unsigned long old_size, new_size;
bool nop; /* replace 模式下的占位 NOP */
bool patched; /* 已加入 klp_ops */
bool transition; /* 过渡期中 */
struct list_head stack_node; /* klp_ops.func_stack 节点 */
};

patching 状态机

patched transition 含义
0 0 未 patch
0 1 未 patch,过渡初始态
1 1 已 patch,部分任务可见
1 0 已 patch,全部任务可见

unpatch 时按相反顺序迁移。

3.2 klp_object — 内核对象(vmlinux 或模块)

1
2
3
4
5
6
7
8
struct klp_object {
const char *name; /* NULL = vmlinux */
struct klp_func *funcs;
struct klp_callbacks callbacks; /* pre/post patch/unpatch */
struct module *mod; /* 运行时关联模块 */
bool dynamic; /* replace 模式动态 NOP 对象 */
bool patched;
};

3.3 klp_patch — 补丁顶层

1
2
3
4
5
6
7
8
struct klp_patch {
struct module *mod; /* livepatch 模块本身 */
struct klp_object *objs;
struct klp_state *states; /* 系统状态版本(累积补丁用) */
bool replace; /* 替换所有已有补丁 */
bool enabled;
bool forced; /* 强制过渡(可能破坏一致性) */
};

3.4 任务 patch 状态

每个 task_struct 有:

1
2
int patch_state;   /* KLP_UNDEFINED / KLP_UNPATCHED / KLP_PATCHED */
/* TIF_PATCH_PENDING 线程标志:需在安全点更新 patch_state */

全局过渡变量(transition.c):

1
2
struct klp_patch *klp_transition_patch;
static int klp_target_state; /* 目标状态 */

四、ftrace 跳转机制(patch.c)

4.1 klp_ops 与 func_stack

每个被 patch 的 原函数地址 对应一个 klp_ops

1
2
3
4
5
struct klp_ops {
struct list_head node;
struct ftrace_ops fops;
struct list_head func_stack; /* klp_func.stack_node,栈顶=当前生效 */
};

全局链表 klp_ops 管理所有已注册 ops。

4.2 klp_patch_func() 流程

1
2
3
4
5
6
7
8
9
1. klp_find_ops(old_func) — 是否已有 ops
2. 若无:
a. ftrace_location(old_func) 获取 ftrace 插桩点
b. 分配 klp_ops,设置 fops:
func = klp_ftrace_handler
flags = DYNAMIC | IPMODIFY | PERMANENT [| SAVE_REGS]
c. ftrace_set_filter_ip() + register_ftrace_function()
3. list_add_rcu(&func->stack_node, &ops->func_stack) /* 栈顶 */
4. func->patched = true

4.3 klp_ftrace_handler() — 核心跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void notrace klp_ftrace_handler(unsigned long ip,
unsigned long parent_ip,
struct ftrace_ops *fops,
struct ftrace_regs *fregs)
{
func = list_first_or_null_rcu(&ops->func_stack, ...);

if (unlikely(func->transition)) {
patch_state = current->patch_state;
if (patch_state == KLP_UNPATCHED) {
/* 过渡中仍用旧版:取 stack 中下一层 */
func = list_entry_rcu(func->stack_node.next, ...);
if (&func->stack_node == &ops->func_stack)
return; /* 无旧版,执行原函数 */
}
}

if (func->nop)
return; /* NOP:不修改 IP,执行原函数 */

ftrace_instruction_pointer_set(fregs, (unsigned long)func->new_func);
}

要点

  • 非过渡期:栈顶 new_func 直接生效
  • 过渡期:根据 current->patch_state 选择新/旧实现
  • NOP 函数new_func == old_func,handler 直接返回
  • 使用 ftrace_test_recursion_trylock 保证与 klp_synchronize_transition() 的 RCU 语义兼容

4.4 klp_unpatch_func()

  • func_stack 仅一项:注销 ftrace、删除 ops
  • 否则:仅从 stack 移除该 func(RCU 延迟释放)

五、补丁启用/禁用流程(core.c)

5.1 klp_enable_patch() — 入口

补丁模块在 module_init() 中调用:

1
2
3
4
5
6
7
8
9
int klp_enable_patch(struct klp_patch *patch)
{
/* 1. 校验:livepatch 模块标记、objs 非空 */
/* 2. 警告:无可靠栈时过渡可能永不完成 */
/* 3. klp_is_patch_compatible() — 累积补丁版本检查 */
/* 4. try_module_get() */
/* 5. klp_init_patch_early() + klp_init_patch() — sysfs、符号 */
/* 6. __klp_enable_patch() */
}

5.2 __klp_enable_patch()

1
2
3
4
5
6
7
8
9
10
11
klp_init_transition(patch, KLP_PATCHED)
→ 所有 task->patch_state = KLP_UNPATCHED(初始态)
→ 所有 func->transition = true
smp_wmb()
对每个已加载 object:
klp_pre_patch_callback()
klp_patch_object() ← ftrace 注册
klp_start_transition()
→ 设置 TIF_PATCH_PENDING
patch->enabled = true
klp_try_complete_transition()

5.3 __klp_disable_patch()

1
2
3
4
5
6
7
klp_init_transition(patch, KLP_UNPATCHED)
→ 初始态 = KLP_PATCHED
klp_pre_unpatch_callback()
klp_start_transition()
patch->enabled = false
klp_try_complete_transition()
→ 完成后 klp_unpatch_objects()

5.4 replace 模式(累积补丁)

patch->replace = true 时:

  1. klp_add_nops() — 为旧补丁所有函数创建 NOP 占位
  2. 启用新补丁后,klp_complete_transition() 中:
    • klp_unpatch_replaced_patches() — 注销旧补丁 ftrace
    • klp_discard_nops() — 移除动态 NOP

NOP 保证过渡期间 ftrace handler 始终有合法 stack 条目。

5.5 全局锁 klp_mutex

粗粒度互斥锁,保护所有 klp 数据结构。例外(无锁访问):

  • klp_ftrace_handler() — ftrace 上下文
  • klp_update_patch_state() — 任务安全点

六、一致性模型与过渡(transition.c)

6.1 问题背景

patch 生效后,可能仍有任务 栈上持有旧函数帧正在旧代码中执行。直接移除旧代码会导致 use-after-free 类崩溃。一致性模型确保:所有任务迁移到目标 patch_state 后才完成过渡

6.2 迁移路径

路径 触发点 条件
栈检查切换 klp_try_complete_transition() klp_have_reliable_stack()
内核退出切换 exit_to_user_mode_loop() TIF_PATCH_PENDING
idle 切换 do_idle() 循环 klp_patch_pending(current)
信号唤醒 klp_send_signals() 延迟工作重试超时
强制过渡 sysfs force=1 管理员承担风险

6.3 klp_update_patch_state()

安全点(返回用户态、idle 循环)调用:

1
2
3
4
5
6
7
void klp_update_patch_state(struct task_struct *task)
{
preempt_disable_notrace();
if (test_and_clear_tsk_thread_flag(task, TIF_PATCH_PENDING))
task->patch_state = READ_ONCE(klp_target_state);
preempt_enable_notrace();
}

kernel/entry/common.c 集成:

1
2
if (ti_work & _TIF_PATCH_PENDING)
klp_update_patch_state(current);

6.4 栈检查 klp_check_stack()

对 sleeping 任务保存可靠栈回溯,检查是否包含:

  • patching:旧函数或上一层 patched 函数地址范围
  • unpatching:新函数地址范围

若命中 → 任务暂不能切换,延迟重试。

6.5 klp_synchronize_transition()

RCU 非 watching 路径(如 user_exit() 之前),不能用普通 synchronize_rcu()

1
2
3
4
static void klp_synchronize_transition(void)
{
schedule_on_each_cpu(klp_sync); /* 强制所有 CPU schedule */
}

6.6 过渡完成 klp_complete_transition()

1
2
3
4
5
6
1. replace 模式:unpatch 旧补丁 + discard NOPs
2. unpatch 目标:klp_unpatch_objects() + synchronize
3. 所有 func->transition = false
4. 所有 task->patch_state = KLP_UNDEFINED
5. post_patch / post_unpatch 回调
6. klp_transition_patch = NULL

6.7 反向过渡与强制

  • klp_reverse_transition():sysfs enabled 在过渡中写反值,取消进行中的 enable/disable
  • klp_force_transition():对所有任务强制 klp_update_patch_state()可能破坏一致性,标记 patch->forced

七、Shadow 变量(shadow.c)

补丁常需为已有内核对象附加 私有扩展数据(如替换后的状态),Shadow 变量提供 <obj, id>data 映射。

7.1 数据结构

1
2
3
4
5
6
7
8
9
struct klp_shadow {
struct hlist_node node;
void *obj;
unsigned long id;
char data[]; /* 柔性数组 */
};

static DEFINE_HASHTABLE(klp_shadow_hash, 12); /* RCU 读 */
static DEFINE_SPINLOCK(klp_shadow_lock); /* 写保护 */

7.2 API

函数 说明
klp_shadow_get(obj, id) RCU 读,获取已有 shadow
klp_shadow_alloc(obj, id, size, gfp, ctor, data) 分配;重复则 WARN 返回 NULL
klp_shadow_get_or_alloc(...) 存在则返回,否则分配
klp_shadow_free(obj, id, dtor) 删除并 kfree_rcu
klp_shadow_free_all(id, dtor) 按 id 批量删除

并发:哈希表 RCU 读与 klp_shadow_free 并发安全;shadow 数据本身的互斥由调用者负责。


八、系统状态与累积补丁(state.c)

8.1 klp_state

1
2
3
4
5
struct klp_state {
unsigned long id; /* 非零自定义标识 */
unsigned int version; /* 状态版本号 */
void *data;
};

用于描述补丁修改的 系统级语义状态(如数据结构布局变更)。

8.2 兼容性检查

1
bool klp_is_patch_compatible(struct klp_patch *patch)

规则:

补丁类型 要求
replace(累积) 必须处理所有已有 patch 修改过的 state,且 version >= 旧版
非累积 可叠加,允许 touch 已有 state

辅助 API:

  • klp_get_state(patch, id) — 当前补丁的 state
  • klp_get_prev_state(id) — 过渡期间查已安装补丁的最新 state

九、符号解析与 KLP 重定位

9.1 符号查找 klp_find_object_symbol()

通过 kallsyms_on_each_symbol()module_kallsyms_on_each_symbol() 查找:

  • 支持 old_sympos 消歧(同名符号第 N 个)
  • sympos == 0 要求符号唯一,否则报错

9.2 KLP 重定位段

补丁模块 ELF 含特殊段:

段名格式 用途
.klp.rela.vmlinux.* 引用 vmlinux 未导出符号
.klp.rela.{module}.* 引用目标模块符号

符号名格式:.klp.sym.{objname}.{symname},{sympos}

两阶段应用

  1. klp 模块加载时:解析 vmlinux 重定位(klp_apply_section_relocs
  2. 目标模块加载时:解析 module 重定位(klp_init_object_loaded

这支持 late module patching(先加载补丁,后加载被 patch 模块)。


十、Sysfs 管理接口

挂载于 /sys/kernel/livepatch/

1
2
3
4
5
6
7
8
/sys/kernel/livepatch/
└── <patch_name>/
├── enabled (RW) 0=disable / 过渡中 reverse
├── transition (RO) 是否处于过渡
├── force (WO) 强制完成过渡
└── <object>/ vmlinux 或模块名
├── patched (RO)
└── <func,sympos>/ (函数目录)
属性 行为
enabled=0 触发 disable(若未在过渡)
enabled=1 过渡中 reverse;已 disable 的 patch 不可 re-enable
force=1 仅过渡中有效,强制所有任务切换

十一、动态模块补丁

11.1 klp_module_coming()

模块 MODULE_STATE_COMING 时:

1
2
3
4
mod->klp_alive = true
对每个 patch 中匹配 obj:
klp_init_object_loaded() → klp_patch_object()
若 patch 不在过渡中 → 立即 post_patch

失败则 拒绝加载模块

11.2 klp_module_going()

模块卸载/加载失败时:

1
2
mod->klp_alive = false
klp_cleanup_module_patches_limited() — unpatch + 释放符号

11.3 fork 继承 klp_copy_process()

子进程继承父进程 patch_stateTIF_PATCH_PENDING,由 tasklist_lock 与过渡操作串行化。


十二、RK3588/ARM64 平台说明

12.1 架构支持现状(Linux 6.1)

能力 ARM64/RK3588 说明
HAVE_DYNAMIC_FTRACE arch/arm64/Kconfig select
HAVE_DYNAMIC_FTRACE_WITH_REGS GCC/Clang 支持时 select
HAVE_LIVEPATCH 未 select(x86_64/s390/powerpc 有)
HAVE_RELIABLE_STACKTRACE 未 select(需 objtool/ORC 等)

因此 RK3588 默认 defconfig 下 CONFIG_LIVEPATCH 不可选;即使手动开启,也缺少架构官方 livepatch 支持与可靠栈,一致性模型主要依赖 内核退出/idle 切换,过渡可能缓慢或需 force

12.2 ftrace 与 IP 修改

ARM64 在 CONFIG_DYNAMIC_FTRACE_WITH_REGS 下,klp_ftrace_handler 通过 ftrace_instruction_pointer_set(fregs, new_func) 修改 pc,实现跳转。相关实现在:

  • arch/arm64/kernel/ftrace.c
  • arch/arm64/kernel/entry-ftrace.S

12.3 生产建议

  1. RK3588 嵌入式场景通常 不启用 livepatch;安全修复更常用 OTA 内核升级
  2. 若需实验:手动添加 select HAVE_LIVEPATCH 并满足全部 Kconfig 依赖,充分测试过渡与模块 patch
  3. 参考 samples/livepatch/lib/livepatch/ 测试模块

十三、完整启用/禁用时序

13.1 启用时序

各任务transition.cpatch.ccore.c补丁模块各任务transition.cpatch.ccore.c补丁模块loop[直到全部任务迁移]klp_enable_patch()klp_init_patch() 符号/sysfsklp_init_transition(KLP_PATCHED)patch_state = UNPATCHEDfunc->transition = truepre_patch_callback()klp_patch_object() / ftrace 注册klp_start_transition()TIF_PATCH_PENDING = 1klp_try_complete_transition()栈检查 或 等待安全点klp_update_patch_state()klp_complete_transition()post_patch_callback()

13.2 函数调用路径(运行时)

1
2
3
4
5
6
调用被 patch 函数
→ ftrace 入口 (mcount)
→ klp_ftrace_handler()
├── transition && patch_state==UNPATCHED → 旧实现
└── 否则 → ftrace_instruction_pointer_set(new_func)
→ 执行 new_func(补丁模块代码)

13.3 禁用时序(概要)

与启用对称:KLP_UNPATCHED 为目标态,初始态 KLP_PATCHED,完成后 klp_unpatch_objects() 注销 ftrace。


十四、总结

组件 职责
core.c 补丁生命周期、符号/KLP 重定位、sysfs、模块 coming/going
patch.c ftrace ops 管理、运行时 IP 重定向、func_stack 多层补丁
transition.c 一致性模型:任务状态机、栈检查、安全点切换、强制/反向过渡
shadow.c 补丁私有 per-object 数据
state.c 累积补丁 state 版本兼容

Livepatch 的核心难点不在「跳转」(ftrace 已成熟),而在 一致性模型:保证任意时刻每个任务执行的是与其 patch_state 一致的代码版本。RK3588/ARM64 在 Linux 6.1 上 未官方声明 HAVE_LIVEPATCH,部署前需评估架构支持与测试覆盖。


附录:源文件清单

文件 行数
kernel/livepatch/core.c 1291
kernel/livepatch/transition.c 666
kernel/livepatch/patch.c 289
kernel/livepatch/shadow.c 299
kernel/livepatch/state.c 119
kernel/livepatch/core.h 59
kernel/livepatch/patch.h 35
kernel/livepatch/transition.h 16
kernel/livepatch/state.h 9
kernel/livepatch/Kconfig 21
kernel/livepatch/Makefile 5
合计 ~2807

关联源码(目录外)

文件 行数(约) 说明
include/linux/livepatch.h 257 公共 API
kernel/entry/common.c 用户态返回路径
kernel/sched/idle.c idle 任务切换
arch/arm64/kernel/ftrace.c ARM64 ftrace 实现

文章互动

阅读 --

留言

0 条留言

正在加载留言…