前言
Linux 内核的 Livepatch(实时补丁)机制允许我们在不重启系统的情况下替换内核函数。但有一个非常有趣的问题:如果我们要给 Livepatch 自身的过渡检查函数 klp_try_complete_transition() 打补丁,这个过渡过程还能顺利完成吗?
答案是:能。而且它的实现非常巧妙,完全依赖通用机制,不需要任何特殊豁免。
问题场景
当我们执行 insmod livepatch.ko 加载一个热补丁时,内核会触发以下调用链:
c
insmod -> klp_enable_patch() -> klp_try_complete_transition()
klp_try_complete_transition() 的职责是遍历所有进程,检查它们的调用栈,确保没有任何进程正在执行即将被替换的旧函数。只有确认所有进程都安全后,它才会调用 klp_complete_transition() 完成过渡。
那么问题来了
如果这次热补丁恰好要替换 klp_try_complete_transition() 本身,那么在第一次检查时,insmod 进程的调用栈上必然存在这个函数的返回地址。
此时 klp_check_stack() 会在栈上匹配到旧函数的地址范围,返回 -EAGAIN,导致当前进程无法被标记为"已过渡"。
c
// 伪代码示意
for_each_process(task) {
ret = klp_check_stack(task, patch); // 检查栈上是否有旧函数
if (ret == -EAGAIN)
return -EAGAIN; // 当前进程(insmod)还没准备好
}
按照直觉,这似乎是"自己给自己做手术"的死锁------当前进程因为正在执行这个函数,所以无法完成过渡;而过渡完不成,这个函数又一直执行不下去。
巧妙的设计:异步重试
Livepatch 的解决方式非常优雅:它从不阻塞等待,而是立即返回,并安排 kworker 稍后重试。
在 klp_try_complete_transition() 发现还有进程未过渡时,它会执行:
c
err:
schedule_delayed_work(&klp_transition_work, round_jiffies_relative(HZ));
这行代码安排了一个延迟工作项(约 1 秒后由 kworker 执行),然后当前函数直接返回。
关键转折
klp_try_complete_transition() 是 void 函数,第一次调用失败后直接返回,上层 __klp_enable_patch() 也随之返回。此时:
insmod的系统调用路径已经走完;- 进程要么已经退出,要么已返回用户态;
- 无论哪种情况,它的内核栈上都不再保留
klp_try_complete_transition()的调用帧。
当 kworker 在后续轮询中再次执行 klp_try_complete_transition() 时,检查就能顺利通过,最终调用 klp_complete_transition() 完成过渡。
这不是特例,而是通用机制
这个"巧妙"之处,本质上是 Livepatch 一致性模型的通用表现:
| 机制 | 说明 |
|---|---|
| 栈检查 | 任何被打补丁的函数,只要某个进程正在执行它(出现在栈上),该进程就不能被切换 |
| 异步重试 | 通过 workqueue 周期性重试,直到所有进程都离开旧函数的调用栈 |
| 自举能力 | 因此 Livepatch 可以安全地给自己打补丁,无需特殊豁免逻辑 |
换句话说,Livepatch 不需要对 klp_try_complete_transition() 做任何特殊处理------它依靠"异步重试 + 栈检查"的通用机制,自然地解决了自举问题。
补充:新版内核的优化
在较新的内核版本中,除了 workqueue 的定时重试,Livepatch 还会通过 klp_send_signals() 向未过渡的进程发送"假信号":
- 对用户进程:调用
set_notify_signal(),促使其尽快到达检查点 - 对内核线程:调用
wake_up_state(),唤醒它继续执行
这加快了过渡速度,但核心逻辑不变------最终还是依赖进程自然离开旧函数的调用栈。
总结
Livepatch 的设计哲学非常清晰:不做特殊 case,只做强通用机制。
对 klp_try_complete_transition() 自身打补丁的场景,看似是一个"自举悖论",但实际上通过以下三步完美解决:
- 第一次检查 :
insmod栈上有该函数,返回-EAGAIN; - 异步重试:安排 kworker 1 秒后再次检查,当前调用栈释放;
- 后续检查 :
insmod已退出或离开该流程,检查通过,过渡完成。