Skip to content

Latest commit

 

History

History
1891 lines (1451 loc) · 60.3 KB

File metadata and controls

1891 lines (1451 loc) · 60.3 KB

Linux 内核热补丁(livepatch)深度解析

基于 Linux Kernel 源码深度分析 路径:kernel/livepatch/ | include/linux/livepatch.h 分析基准版本:Linux 6.x mainline


目录

  1. 热补丁概述与设计背景
  2. 历史演进:kpatch / kGraft / livepatch
  3. 核心数据结构
  4. 函数替换机制(ftrace trampoline)
  5. 一致性模型(Consistency Model)
  6. 补丁应用完整流程
  7. Shadow 变量
  8. 累积补丁(Cumulative Patch)
  9. sysfs 接口
  10. 与 kexec/kdump 的交互
  11. 完整示例:编写一个 livepatch 模块
  12. 限制与注意事项
  13. kernel/livepatch/ 目录文件一览

1. 热补丁概述与设计背景

1.1 问题背景

生产环境的 Linux 服务器一旦发现内核安全漏洞,传统修复流程如下:

发现漏洞
  |
  v
编译新内核(数分钟~数十分钟)
  |
  v
安排维护窗口(协调业务方、通知用户)
  |
  v
重启系统(内核 panic 风险 + 几分钟停机)
  |
  v
验证修复(回归测试)

对于需要 99.999% 可用性(每年停机 < 5.26 分钟)的关键业务,哪怕计划重启也代价极高。金融交易系统、电信基础设施、大型数据库集群等场景尤为如此。

热补丁(livepatch)的目标:在不重启内核、不中断业务的前提下,安全地将有漏洞的内核函数替换为修复版本。

1.2 livepatch 与 kexec 的对比

技术 停机时间 安全性 实现复杂度
传统重启 分钟级 最高
kexec(快速重启) 秒级~分钟级
livepatch 零停机 需要一致性保证

kexec 只是缩短了 BIOS/UEFI 初始化时间,仍然需要完整的内核重新启动流程,进程全部终止。livepatch 则完全不需要重启:内核继续运行,所有进程保持状态。

1.3 核心约束

livepatch 面临一个根本性约束:在替换函数的瞬间,该函数可能正在被其他 CPU 上的任务执行。粗暴地修改函数代码可能导致:

  • 崩溃(指令流被破坏到一半)
  • 数据不一致(部分任务看到新函数,部分看到旧函数,共享状态语义不同)
  • 死锁(新旧函数对锁的使用方式不同)

这就是一致性模型(Consistency Model)的核心问题,也是 kpatch、kGraft 和最终 livepatch 设计的最大分歧所在。


2. 历史演进:kpatch / kGraft / livepatch

2.1 kpatch(Red Hat,2014)

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,在高负载场景下可能长时间阻塞,造成可感知的延迟抖动(虽然不是停机,但对实时系统有影响)。

2.2 kGraft(SUSE,2014)

SUSE 开发的 kGraft 采用了完全不同的思路——不停机,而是让新旧代码共存,通过任务迁移逐步切换:

                  kGraft 一致性机制
                  =================

时刻 T0: 应用补丁
  - 新补丁函数注册到 ftrace
  - 每个任务打上 "待迁移" 标记

时刻 T1~Tn: 逐步迁移
  - 任务每次从内核返回到用户空间时
  - 检查自己是否还在旧函数的调用链上
  - 安全则切换到 "已迁移" 状态

时刻 Tm: 所有任务迁移完成
  - 旧函数完全无人使用,可以清理

kGraft 的关键创新:per-task patch state。每个任务独立迁移,中间状态期间 klp_ftrace_handler 根据当前任务的 patch_state 决定路由到旧函数还是新函数。

2.3 mainline livepatch(2014~2015)

Linux mainline 的 livepatch 子系统综合了两者的优势:

  • 借鉴 kpatch 的 ftrace-based 函数替换机制
  • 借鉴 kGraft 的 per-task 迁移方案
  • 结合可靠栈回溯(reliable stack trace)来加速迁移

最初的 mainline livepatch(v3.19,2015)只支持 x86,且没有完整的一致性模型——func->transitiontask->patch_state 字段是后来(v4.12,2017)由 Josh Poimboeuf 加入的。

2.4 一致性模型演进时间线

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 支持等

3. 核心数据结构

3.1 总体层次关系

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);

3.2 struct klp_func

定义位于 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 中
};

patchedtransition 两个字段共同定义了函数的 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(完成)

3.3 struct klp_object

定义位于 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;
}

3.4 struct klp_patch

定义位于 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 完全卸载
};

enabledtransition 状态的关系:

               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() --> 模块卸载

3.5 struct klp_state

定义位于 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 回调中检查兼容性)

3.6 struct klp_callbacks

定义位于 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 专用资源

3.7 运行时辅助结构 klp_ops

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 来执行

4. 函数替换机制(ftrace trampoline)

4.1 ftrace 基础回顾

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 回调

4.2 klp_patch_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_IPMODIFYkernel/livepatch/patch.c:191)告诉 ftrace 这个回调会修改指令指针(IP),ftrace 会为此分配一个专用的 trampoline,并在调用回调后使用修改后的 IP 跳转,而非返回原函数。

4.3 klp_ftrace_handler() 详解

这是整个 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);
}

4.4 函数调用流程对比

                  无 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() 必须是一个完整的独立函数,有自己完整的栈帧。

4.5 FTRACE_OPS_FL_IPMODIFY 的唯一性限制

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() 或调试工具意外禁用。

4.6 取消 patch:klp_unpatch_func()

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;
}

5. 一致性模型(Consistency Model)

5.1 为什么需要一致性模型

仅有 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 版本,不存在混用状态

5.2 per-task patch_state

每个 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_PENDINGinclude/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 还未更新到目标状态,需要在合适的时机(系统调用返回、调度切换等)进行迁移。

5.3 迁移触发点

任务 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();
}

5.4 klp_try_switch_task() 栈检查

对于睡眠中的任务(不在运行,可以安全检查其栈),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;
}

5.5 klp_check_stack_func() 检查栈帧

核心的栈检查逻辑位于 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_STACKTRACECONFIG_STACKTRACEinclude/linux/livepatch.h:189):

static inline bool klp_have_reliable_stack(void)
{
    return IS_ENABLED(CONFIG_STACKTRACE) &&
           IS_ENABLED(CONFIG_HAVE_RELIABLE_STACKTRACE);
}

5.6 迁移超时与强制迁移

如果某些任务长时间无法完成迁移(例如,一个内核线程一直睡眠在被 patch 的函数上),klp_try_complete_transition() 会定期重试,并在超时后发送信号:

kernel/livepatch/transition.c:23

#define SIGNALS_TIMEOUT 15

klp_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 标记会被设置,表明此次迁移是非安全的(可能跳过了正在使用旧函数的任务)。

5.7 transition 状态机完整图

                     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 稳定生效

6. 补丁应用完整流程

6.1 klp_enable_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);
}

6.2 klp_init_object_loaded() 符号解析

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);
    }
}

6.3 klp_find_object_symbol() 符号查找

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 表示第一个出现的)。

6.4 __klp_enable_patch() 内部流程

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;
}

6.5 补丁叠加(多补丁共存)

当两个 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(上一个版本)

7. Shadow 变量

7.1 问题背景

livepatch 只能替换函数,不能修改数据结构。但有时修复一个 bug 需要给某个对象附加新字段。例如:

// 原来的结构(已经在内核中运行)
struct net_device {
    ...
    // 没有 my_new_field
};

// 修复需要给每个 net_device 增加一个状态字段
// 但不能修改已有结构体!

Shadow 变量(kernel/livepatch/shadow.c)提供了一种"附加字段"的机制:通过一个全局 hashtable,以 <obj_ptr, id> 为 key,将额外数据与任意内核对象关联起来。

7.2 内部数据结构

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 为哈希键(父对象地址)。

7.3 klp_shadow_get() 查找

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 读侧,无锁查找,高性能。

7.4 klp_shadow_alloc() 分配

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

7.5 klp_shadow_free() 释放

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);
}

7.6 Shadow 变量典型使用模式

// 定义 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);
}

7.7 Shadow 变量的 hashtable 布局图

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

8. 累积补丁(Cumulative Patch)

8.1 问题场景

随着 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)解决了这个问题。

8.2 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]

8.3 NOP 函数的作用

累积 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 unlockpatch.c:118):

if (func->nop)
    goto unlock; // 不修改 IP,让原函数执行

这样 patch_3 不需要重复包含 bar() 的修复代码,而是通过 NOP 保持 bar() 的旧修复版本的行为回退到原始函数。

实际上,当 patch_3 replace 完成后,patch_2(含 bar 的修复)被卸载,NOP 也清理掉,bar() 回到原函数——这意味着累积 patch 必须包含所有之前 patch 的修复内容,否则安全修复会丢失。

8.4 klp_state 兼容性检查

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

8.5 累积补丁时序图

时间轴
  |
  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 模块可以卸载

9. sysfs 接口

9.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(无属性,仅目录)

9.2 enabled 属性

读取(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 模块。

9.3 transition 属性

只读(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)。

9.4 force 属性

只写(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(),因为强制迁移后旧函数可能仍被某些任务引用(虽然概率极低),保持引用计数防止模块被卸载。

9.5 stack_order 属性

只读(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 开始),越大越新。

9.6 常用管理命令

# 查看所有活跃 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

10. 与 kexec/kdump 的交互

10.1 livepatch 对 kdump 分析的影响

当内核 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 期间),使得 "谁在用旧函数,谁在用新函数" 难以判断。

10.2 livepatch 模块信息写入 vmcore

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  # 反汇编新函数

10.3 kexec 快速重启场景

当系统需要紧急修复(livepatch 无法处理的情况)时,kexec 可与 livepatch 共存:

正常运行(有 livepatch 激活)
         |
         | kexec -l new_kernel + kexec -e
         v
新内核加载(热重启,无 BIOS 初始化)
         |
         v
新内核正常启动(无 livepatch,从干净状态开始)

重要:kexec 切换时,正在进行的 livepatch transition 会被强制中断,新内核启动后不继承任何 livepatch 状态。

10.4 klp_patches 链表在 kdump 中的读取

使用 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"

10.5 livepatch 对实时性的影响

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)有可感知的性能影响。


11. 完整示例:编写一个 livepatch 模块

11.1 场景描述

假设内核函数 vfs_read() 存在一个边界检查漏洞,需要 livepatch 修复。

11.2 模块代码结构

// 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);

11.3 Makefile

# 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

11.4 带 callback 的进阶示例

// 带 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,
        },
    },
    { }
};

11.5 累积 patch 示例

// 累积 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
};

11.6 符号解析失败的排查

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

11.7 模块加载、状态检查、卸载流程

# 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_read

12. 限制与注意事项

12.1 架构支持

livepatch 依赖 CONFIG_HAVE_RELIABLE_STACKTRACE 进行任务迁移。截至分析时:

架构 ftrace 支持 可靠栈回溯 livepatch 完整支持
x86_64 是(objtool 辅助)
arm64
powerpc 是(部分限制)
s390
arm(32位) 部分(transition 可能不完成)
MIPS 部分
riscv 部分 有限

无可靠栈回溯时,任务只能在系统调用返回或 klp_send_signals() 触发后被动迁移,transition 可能需要很长时间。

12.2 函数替换限制

不能 patch 的函数类型

  1. 内联函数:已被编译器内联到调用点,没有独立的函数体
  2. 不可 trace 的函数__notrace_func/notrace 修饰)
  3. 自身就是 ftrace infrastructure 的函数(防止递归)
  4. 极短函数(short functions):ftrace 需要在函数入口 5 字节处有 nop slot,太短的函数放不下
  5. __init 函数:已在初始化完成后释放

不能在 patch 函数中做的事

  • 直接调用同名旧函数(死递归)
  • 使用栈上的大型局部变量(ftrace trampoline 的栈空间有限制)
  • 修改调用约定(函数签名必须与原函数完全一致)

12.3 数据结构修改限制

livepatch 不能

  • 添加/删除/修改内核数据结构(struct 的布局)
  • 添加新的全局变量(除非通过 patch 模块导出)
  • 添加新的 EXPORT_SYMBOL 符号

可以通过 shadow 变量来绕过数据结构限制(见第 7 章)。

12.4 并发安全

patch 函数的编写者需要自行保证:

  • 新函数本身是线程安全的
  • 新函数与旧函数在 transition 期间对共享状态的访问是安全的(因为 transition 期间两者可能并发执行)
  • shadow 变量的访问需要调用者自行加锁

12.5 强制 transition 的风险

echo 1 > .../force 的后果:

被强制跳过的任务可能正在 old 函数中执行
   |
   v
该任务的 patch_state 被强制设为 PATCHED
   |
   v
下次该任务进入被 patch 函数时,使用 new 函数
   |
   v
但在此之前(old 函数未返回),old 函数和 new 函数
可能对同一数据结构做不兼容的操作 -> 未定义行为

因此 force 应该作为最后手段,且 patch->forced = true 后系统完整性不再被保证。

12.6 模块卸载同步

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->finishklp_kobj_release_patch()(kobject 最后一次引用释放时)中触发 complete(),确保 patch 数据结构完全释放后才允许模块卸载。


13. kernel/livepatch/ 目录文件一览

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 版本的数据状态维护                              |
|                                                                  |
+------------------------------------------------------------------+

关键设计决策

  1. 不停机:通过 ftrace 动态替换,无需 stop_machine
  2. per-task 迁移:每个任务独立切换,避免全局停顿
  3. 栈检查:通过可靠栈回溯确保任务不在被 patch 函数中才迁移
  4. RCU 保护func_stack 的修改使用 RCU,handler 可以无锁读取
  5. 内存屏障:在 func->transition 写入和 func_stack 修改之间、klp_target_state 写入和 TIF_PATCH_PENDING 设置之间均有 smp_wmb(),确保 handler 看到一致状态

这套机制使得 Linux 内核能够在生产环境中安全地进行不停机安全补丁,成为现代 Linux 发行版企业支持的核心能力之一。


由 Claude Code 分析生成