原子替换与累积补丁

热补丁(livepatches)之间可能存在依赖关系。如果多个补丁需要对同一个(或多个)函数进行不同的修改,那么我们就需要定义补丁安装的顺序。并且,任何较新的热补丁的函数实现必须构建在较旧的补丁之上。

这可能会成为维护上的噩梦。尤其是当多个补丁以不同的方式修改了同一个函数时。

一种优雅的解决方案是引入名为“原子替换”(Atomic Replace)的功能。它允许创建所谓的“累积补丁”(Cumulative Patches)。它们包含了来自所有旧热补丁的所需修改,并在一次过渡中完全替换它们。

用法

可以通过在 struct klp_patch 中设置 “replace” 标志来启用原子替换,例如

static struct klp_patch patch = {
        .mod = THIS_MODULE,
        .objs = objs,
        .replace = true,
};

然后,所有进程都会被迁移到仅使用新补丁的代码。一旦过渡完成,所有旧补丁都会自动禁用。

Ftrace 处理程序会从不再由新累积补丁修改的函数中透明地移除。

因此,热补丁作者可能只需要维护一个累积补丁的源代码。这有助于在添加或删除各种修复或功能时保持补丁的一致性。

过渡完成后,用户可以在系统上仅保留最后一个安装的补丁。这有助于清楚地查看当前实际使用的代码。此外,热补丁可以被视为修改内核行为的“普通”模块。唯一的区别在于它可以在运行时更新,而不会破坏其功能。

功能

原子替换允许:

  • 在升级其他函数的同时,原子化地还原之前补丁中的某些函数。

  • 消除由于不再修补的函数的核心重定向(core redirection)而可能导致的性能影响。

  • 减少用户对热补丁之间依赖关系的困惑。

限制:

  • 一旦操作完成,就没有直接的方法可以撤销它并原子化地恢复被替换的补丁。

    一个好的做法是在任何发布的热补丁中设置 .replace 标志。这样,重新添加旧的热补丁就等同于降级到该补丁。只要热补丁没有在 (un)patching 回调或 module_init()module_exit() 函数中进行额外的修改,这样做是安全的,详见下文。

    还要注意,只有在过渡没有被强制执行时,被替换的补丁才能被移除并重新加载。

  • 仅执行来自_新_累积热补丁的 (un)patching 回调。来自被替换补丁的所有回调都将被忽略。

    换句话说,累积补丁负责执行正确替换任何旧补丁所需的任何操作。

    因此,用旧的补丁替换较新的累积补丁可能会有危险。旧的热补丁可能无法提供必要的回调。

    在某些场景下,这可能被视为一种限制。但在许多其他场景中,它使工作变得更容易。只有新的累积热补丁知道添加/删除了哪些修复/功能,以及平稳过渡需要哪些特殊操作。

    无论如何,如果调用所有已启用补丁的回调,考虑各种回调的顺序及其相互作用将是一场噩梦。

  • 没有对影子变量(shadow variables)的特殊处理。热补丁作者必须制定自己的规则,以将它们从一个累积补丁传递到另一个累积补丁。特别要注意的是,他们不应盲目地在 module_exit() 函数中移除它们。

    一个好的做法是在 post-unpatch 回调中移除影子变量。它仅在热补丁被正确禁用时才会被调用。