` 自己打热补丁)
前言Linux 内核的 Livepatch实时补丁机制允许我们在不重启系统的情况下替换内核函数。但有一个非常有趣的问题如果我们要给 Livepatch 自身的过渡检查函数klp_try_complete_transition()打补丁这个过渡过程还能顺利完成吗答案是能。而且它的实现非常巧妙完全依赖通用机制不需要任何特殊豁免。问题场景当我们执行insmod livepatch.ko加载一个热补丁时内核会触发以下调用链insmod-klp_enable_patch()-klp_try_complete_transition()klp_try_complete_transition()的职责是遍历所有进程检查它们的调用栈确保没有任何进程正在执行即将被替换的旧函数。只有确认所有进程都安全后它才会调用klp_complete_transition()完成过渡。那么问题来了如果这次热补丁恰好要替换klp_try_complete_transition()本身那么在第一次检查时insmod进程的调用栈上必然存在这个函数的返回地址。此时klp_check_stack()会在栈上匹配到旧函数的地址范围返回-EAGAIN导致当前进程无法被标记为已过渡。// 伪代码示意for_each_process(task){retklp_check_stack(task,patch);// 检查栈上是否有旧函数if(ret-EAGAIN)return-EAGAIN;// 当前进程insmod还没准备好}按照直觉这似乎是自己给自己做手术的死锁——当前进程因为正在执行这个函数所以无法完成过渡而过渡完不成这个函数又一直执行不下去。巧妙的设计异步重试Livepatch 的解决方式非常优雅它从不阻塞等待而是立即返回并安排 kworker 稍后重试。在klp_try_complete_transition()发现还有进程未过渡时它会执行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已退出或离开该流程检查通过过渡完成。