Live Patching

本页目录

函数入口切换了,正在执行的旧栈帧却不会跳进新实现。Livepatch 的核心是过渡期间的新旧逻辑兼容⁠,而不仅是重定向入口。

它适用于经过专门设计验证的修复,不是通用在线版本升级器;补丁仍要匹配目标构建和配置。

查阅目标阅读位置
何时允许切换任务函数入口、任务一致性与过渡
状态怎样继续兼容累积补丁、状态生命周期
怎样停止与验证禁用卸载、PREEMPT_RT 边界

函数入口与任务一致性

热补丁需要把任务迁移到一致的代码版本 准备新补丁与替换入口 解析目标函数,安装受控重定向 1 各任务到达可安全切换点 可靠栈检查、用户态边界等机制 2 过渡期间维护版本一致性 任务可能暂时处于不同补丁状态 3 全部转换完成 再处理相应旧补丁与状态生命周期 4 force 不是等待更快的安全模式;跳过一致性检查可能破坏正确性与卸载条件。

常见实现基于动态 ftrace 在函数入口重定向,livepatch handler 根据任务补丁状态等选择适用版本。正在执行旧函数的栈帧不会因为改了入口就自动跳到新实现中间。

内核使用一致性模型判断何时可以转换任务。可靠栈检查可识别是否仍在受影响调用链中,返回用户态等安全边界也帮助推进状态;长时间停留的内核线程或缺少可靠栈信息的路径可能阻碍完成。Livepatch

不能把实现概括为虚构的 IDLE、FORCE、IMMEDIATE 三个通用枚举模式。实际应围绕 enabled、transition、force 等接口状态和具体内核模型理解。

过渡完成以前,新旧版本可能共存

过渡状态补丁必须兼容什么
任务 A 已转新版本新逻辑访问共享状态
任务 B 仍在旧调用链旧布局、旧锁顺序仍可能被使用
所有相关任务完成过渡才能按完成条件处理后续阶段

共享数据格式和跨函数不变量,必须覆盖共存阶段,不能只针对最终状态设计。

若一个任务长期阻止过渡,应分析其栈和等待原因。辅助唤醒或信号不等于可以强迫任意正在执行的函数安全撤离。force 会绕过相应安全保障,并影响后续卸载能力,不能当作普通超时重试按钮。

同一个入口,为什么 A、B 暂时选择不同版本

假设补丁同时更新 update 与 lookup。A 睡眠时栈里仍有旧 update,B 的可靠睡眠栈不含这两个函数。A 暂时保留旧补丁状态,B 可以先切换;后续调用重定向仍要按各自任务状态选择。否则 A 可能在一条旧调用链内突然调用新 lookup,破坏两者共同维护的约定。

过渡期间按任务选择版本,旧栈帧仍需完成 任务 A:栈中仍有旧 update 暂留 v0,后续 lookup 也选 v0 任务 B:可靠栈没有受影响函数 可切至 v1,后续 lookup 选 v1 A 离开旧调用链并通过安全检查后,才能跟上 v1 入口重定向不改写正在执行的旧栈帧;两个任务仍可能同时访问共享数据。

下面的模型仅展示可靠睡眠栈检查和返回用户态边界。它假设 affected 已完整列出需要一起替换的函数,不模拟 ftrace、抢占、栈展开或内核状态位。

from dataclasses import dataclass

@dataclass
class Task:
    version: int
    stack: tuple
    sleeping: bool = True
    reliable: bool = True

    def try_stack_switch(self, affected, target):
        if not self.sleeping or not self.reliable:
            return False
        if any(function in affected for function in self.stack):
            return False
        self.version = target
        return True

    def return_to_user(self, target):
        self.stack = ()
        self.version = target  # 这个方法代表已经到达用户态返回边界

    def implementation(self, name):
        return f"{name}_v{self.version}"

affected = {"update", "lookup"}
a = Task(0, ("request", "update"))
b = Task(0, ("unrelated_wait",))
c = Task(0, (), reliable=False)
assert not a.try_stack_switch(affected, 1)
assert b.try_stack_switch(affected, 1)
assert not c.try_stack_switch(affected, 1)
assert a.implementation("lookup") == "lookup_v0"
assert b.implementation("lookup") == "lookup_v1"
assert not all(t.version == 1 for t in (a, b, c))
a.stack = ("request",)  # 旧 update 已返回,本例其他栈帧不受影响
assert a.try_stack_switch(affected, 1)
c.return_to_user(1)
assert all(t.version == 1 for t in (a, b, c))
assert not Task(0, (), sleeping=False).try_stack_switch(affected, 1)

c 的栈列表虽然为空,但“不可靠展开没看到目标函数”不等于“可靠证明目标函数不存在”。因此模型拒绝单凭该结果切换,而等待另一个已知安全边界。实际协议还要防止检查与任务继续执行之间出现竞争,不能由用户空间读一次 /proc/PID/stack 后自行改写任务补丁状态。Livepatch 一致性模型

这类逐任务规则也不自动解决跨任务的共享数据兼容。补丁作者还需检查 A 的旧逻辑与 B 的新逻辑同时运行时是否满足锁顺序、对象解释与调用契约。过渡完成是必要状态信息,不能代替这些语义条件。

让两个任务分别停在旧函数内或处于安全位置,再启用补丁。已经在栈上的旧调用不会被瞬间改写;反向切换和卸载也要等待相应边界。

正在呈现知识画面
目标版本变了,旧栈帧会立即跳过去吗?

入口重定向不改写已经执行中的栈帧。过渡期间新旧任务版本可以共存;禁用要完成反向过渡,才能讨论补丁代码卸载。

累积补丁与 atomic replace

多个独立补丁叠加会增加顺序和兼容性复杂度。累积补丁可以包含一组已有修复,使用 atomic replace 在一致性过渡中替代先前补丁集合。

这里的 atomic 不表示所有 CPU 在同一个瞬间改写全部指令,也不提供数据库事务式的任意数据回滚。它指向补丁集合替换的相应机制,仍需完成任务转换并维护系统状态。Livepatch 文档索引

新累积补丁没有列出 g,意味着什么

设原始内核提供 f、g;P1 修复 f,P2 修复 g。现在用带 replace 语义的 P3 替换旧集合,但 P3 只包含 f 的新实现。过渡完成后,g 会回到原始实现,而不是自动保留 P2 的修复。

已完成的配置f 使用什么g 使用什么
P1 与 P2 生效P1 的 fP2 的 g
P3 仅包含 f,替换旧集合P3 的 f原始 g
P3 同时包含所需 f、g 修复P3 的 fP3 携带的 g
base = {"f": "base_f", "g": "base_g"}
p1 = {"f": "fix_f1"}
p2 = {"g": "fix_g1"}
p3 = {"f": "fix_f2"}
old_active = base | p1 | p2
replacement = base | p3
assert old_active["g"] == "fix_g1"
assert replacement == {"f": "fix_f2", "g": "base_g"}
p3_cumulative = {"f": "fix_f2", "g": "fix_g1"}
assert (base | p3_cumulative)["g"] == "fix_g1"
# 只比较完成后的函数集合,不模拟过渡或回调。
disabled_replacement = base.copy()
assert disabled_replacement != old_active

所以累积补丁要包含所有仍需保留的修复。禁用新补丁并不自动把旧补丁集合原子地恢复回来;需要回退时,必须准备与当前状态兼容的替换方案。

此外,atomic replace 主要执行新累积补丁的相关回调,不会按想象顺序替每个被替代补丁执行清理。新补丁需要接管必要的共享状态;旧模块退出时也不能盲目释放新补丁仍在使用的 shadow variables。Atomic Replace 与累积补丁

有状态修复需要显式生命周期设计

不能直接把正在使用的结构体布局换成任意新布局,旧对象仍按原布局存在。但“所有有状态补丁都不可能”也不准确:shadow variables、回调和 system-state tracking 可支持经过设计的状态变化。

补丁需要处理首次启用、替代旧补丁、撤销过渡、禁用及失败清理。klp_state 的 id/version 用于跟踪兼容状态,不会自动理解业务对象如何转换。系统状态变化

字段宽度不变,也可能需要状态迁移

假设旧对象的 timeout=100 表示 100 ms,新代码想把同一个整数改成以 μs 为单位。即使结构体大小、字段偏移和函数签名全部不变,两套代码对同一位模式的解释仍不同:

对象内容与读取者实际解释与原本 100 ms 的差异
旧对象值 100,由新代码按 μs 读取0.1 ms缩短 1000 倍
新代码写入 100000,旧代码按 ms 读取100 s延长 1000 倍

仅等所有任务完成代码过渡,也不会把已有对象中的 100 自动变成 100000。过渡期间可以保留旧字段作为共同的权威表示,让新代码在边界转换;要更换持久表示,则需要兼容版本标识、对象更新协议和合适的转换时机。额外状态工具提供存放与跟踪机制,不替作者决定转换顺序或回退语义。Livepatch 系统状态变化

替换函数时,保留调用方的契约也要成立

函数签名与调用约定要与保留的调用方兼容。修复涉及参数、对象生命周期或跨模块调用时,范围可能远大于替换一个函数,不能仅凭能重定位就认为安全。

禁用与卸载不是同一步

常规补丁可以请求禁用并完成反向过渡,满足条件后再卸载相关模块;“livepatch 模块永远不能卸载”过度概括。仍被使用、过渡未完成、强制过渡等状态会限制卸载,必须遵循实际接口条件。

只读观察可以从 /sys/kernel/livepatch 下各补丁目录的 enabled 与 transition 属性开始。不存在该目录或条目,可能是未启用支持或未加载补丁,不能据此推断系统是否修复某个漏洞;发行版也可能通过常规内核更新修复。

PREEMPT_RT 是另一个问题

PREEMPT_RT 主要降低不可抢占区间等导致的调度延迟,涉及线程化中断、可睡眠锁及优先级继承等机制。它不是 livepatch 的一种模式,也不能把所有关抢占操作都概括成替换为 rt_mutex。实时内核机制

现代主线已提供 PREEMPT_RT 配置能力,架构支持与驱动约束仍需核对当前目标。不要保留“6.12 接近合并完成”这种已过期时间描述,也不要无来源地承诺某个小版本解决了全部 livepatch/RT 交互。

验证热补丁要覆盖目标工作负载、过渡完成、禁用及状态兼容路径。构建成功或 sysfs 显示 enabled 都不替代功能回归测试,相关方法见 调试与测试。