函数入口切换了,正在执行的旧栈帧却不会跳进新实现。Livepatch 的核心是过渡期间的新旧逻辑兼容,而不仅是重定向入口。
它适用于经过专门设计验证的修复,不是通用在线版本升级器;补丁仍要匹配目标构建和配置。
| 查阅目标 | 阅读位置 |
| 何时允许切换任务 | 函数入口、任务一致性与过渡 |
| 状态怎样继续兼容 | 累积补丁、状态生命周期 |
| 怎样停止与验证 | 禁用卸载、PREEMPT_RT 边界 |
函数入口与任务一致性
常见实现基于动态 ftrace 在函数入口重定向,livepatch handler 根据任务补丁状态等选择适用版本。正在执行旧函数的栈帧不会因为改了入口就自动跳到新实现中间。
内核使用一致性模型判断何时可以转换任务。可靠栈检查可识别是否仍在受影响调用链中,返回用户态等安全边界也帮助推进状态;长时间停留的内核线程或缺少可靠栈信息的路径可能阻碍完成。Livepatch
不能把实现概括为虚构的 IDLE、FORCE、IMMEDIATE 三个通用枚举模式。实际应围绕 enabled、transition、force 等接口状态和具体内核模型理解。
过渡完成以前,新旧版本可能共存
| 过渡状态 | 补丁必须兼容什么 |
| 任务 A 已转新版本 | 新逻辑访问共享状态 |
| 任务 B 仍在旧调用链 | 旧布局、旧锁顺序仍可能被使用 |
| 所有相关任务完成过渡 | 才能按完成条件处理后续阶段 |
共享数据格式和跨函数不变量,必须覆盖共存阶段,不能只针对最终状态设计。
若一个任务长期阻止过渡,应分析其栈和等待原因。辅助唤醒或信号不等于可以强迫任意正在执行的函数安全撤离。force 会绕过相应安全保障,并影响后续卸载能力,不能当作普通超时重试按钮。
同一个入口,为什么 A、B 暂时选择不同版本
假设补丁同时更新 update 与 lookup。A 睡眠时栈里仍有旧 update,B 的可靠睡眠栈不含这两个函数。A 暂时保留旧补丁状态,B 可以先切换;后续调用重定向仍要按各自任务状态选择。否则 A 可能在一条旧调用链内突然调用新 lookup,破坏两者共同维护的约定。
下面的模型仅展示可靠睡眠栈检查和返回用户态边界。它假设 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",) 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 的新逻辑同时运行时是否满足锁顺序、对象解释与调用契约。过渡完成是必要状态信息,不能代替这些语义条件。
让两个任务分别停在旧函数内或处于安全位置,再启用补丁。已经在栈上的旧调用不会被瞬间改写;反向切换和卸载也要等待相应边界。
正在呈现知识画面
目标版本变了,旧栈帧会立即跳过去吗?入口重定向不改写已经执行中的栈帧。过渡期间新旧任务版本可以共存;禁用要完成反向过渡,才能讨论补丁代码卸载。
{"id":"livepatch-task-transition","title":"目标版本变了,旧栈帧会立即跳过去吗?","summary":"入口重定向不改写已经执行中的栈帧。过渡期间新旧任务版本可以共存;禁用要完成反向过渡,才能讨论补丁代码卸载。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"enterA\" data-i18n=\"enterA\"></button><button id=\"returnA\" data-i18n=\"returnA\"></button></div><div class=\"actions\"><button id=\"enterB\" data-i18n=\"enterB\"></button><button id=\"returnB\" data-i18n=\"returnB\"></button></div><div class=\"actions\"><button id=\"enable\" data-i18n=\"enable\"></button><button id=\"disable\" data-i18n=\"disable\"></button></div><div class=\"track-row\"><span data-i18n=\"target\"></span><div id=\"target\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"tasks\"></span><div id=\"tasks\" class=\"track\"></div></div><div class=\"card\"><header data-i18n=\"status\"></header><div id=\"status\" class=\"readout\"></div></div><div class=\"actions\"><button id=\"unload\" data-i18n=\"unload\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#tasks{display:grid;grid-template-columns:1fr}#tasks .cell{min-height:65px}.readout{min-height:5.3em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let target=0,versions=[0,0],frames=[null,null],loaded=true;const name=v=>v?'new':'old';function settle(){for(let i=0;i<2;i++)if(frames[i]===null)versions[i]=target;}function draw(){settle();const transition=versions.some(v=>v!==target);cells('target',[{text:name(target),cls:target?'gold':'on'}]);cells('tasks',versions.map((v,i)=>({text:'AB'[i]+': '+name(v)+' → '+(frames[i]===null?'—':name(frames[i])),cls:v===target?'on':'gold'})));for(let i=0;i<2;i++){$('#enter'+'AB'[i]).disabled=frames[i]!==null;$('#return'+'AB'[i]).disabled=frames[i]===null;}$('#enable').disabled=!loaded||transition||target===1;$('#disable').disabled=!loaded||transition||target===0;$('#unload').disabled=!loaded||transition||target===1||frames.includes(1);set('status',t(!loaded?'unloaded':transition?'waiting':'stable'));set('explanation',t('note'));}for(let i=0;i<2;i++){$('#enter'+'AB'[i]).onclick=()=>{frames[i]=versions[i];draw();};$('#return'+'AB'[i]).onclick=()=>{frames[i]=null;draw();};}$('#enable').onclick=()=>{target=1;draw();};$('#disable').onclick=()=>{target=0;draw();};$('#unload').onclick=()=>{loaded=false;draw();};$('#reset').onclick=()=>{target=0;versions=[0,0];frames=[null,null];loaded=true;draw();};draw();","audio":false,"strings":{"disable":"请求禁用补丁","enable":"请求启用补丁","enterA":"A 进入目标函数","enterB":"B 进入目标函数","heading":"目标版本变了,旧栈帧会立即跳过去吗?","intro":"让任务 A/B 进入或退出受影响函数,再启用或禁用补丁。各任务保留自己的版本,旧调用链退出后才推进过渡。","note":"让 A 进入 old,再启用补丁:B 可先切到 new,A 的旧帧保持 old。A 返回后才结束这一轮过渡,不能靠修改目标把帧直接搬到新代码中间。","reset":"重置","returnA":"A 返回安全边界","returnB":"B 返回安全边界","scope":"两个任务、单补丁、无嵌套;采用保守栈模型:任何受影响帧退出后才切换任务,空栈任务立即采用目标。仅允许前一过渡完成后改变目标,不模拟 force、累积补丁或所有真实安全边界。卸载后可重置模型重载。","scopeLabel":"模型范围与约定","stable":"过渡完成;补丁代码仍加载","status":"过渡 / 代码寿命","target":"目标版本","tasks":"任务版本 → 当前栈帧版本","unload":"卸载已停用补丁","unloaded":"补丁代码已卸载","waiting":"过渡中;仍有旧任务状态"}}
累积补丁与 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 的 f | P2 的 g |
| P3 仅包含 f,替换旧集合 | P3 的 f | 原始 g |
| P3 同时包含所需 f、g 修复 | P3 的 f | P3 携带的 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 都不替代功能回归测试,相关方法见 调试与测试。