(取消)补丁回调

热补丁(取消)补丁回调为热补丁模块提供了一种机制,以便在内核对象被(取消)打补丁时执行回调函数。它们可以被认为是一个强大的功能,它扩展了热补丁的能力,包括:

  • 对全局数据的安全更新

  • 对 init 和 probe 函数的“补丁”

  • 修补其他无法修补的代码(例如,汇编代码)

在大多数情况下,(取消)补丁回调需要与内存屏障和内核同步原语(如互斥锁/自旋锁,甚至stop_machine())一起使用,以避免并发问题。

1. 动机

回调不同于现有的内核设施

  • 禁用和重新启用补丁时,模块的 init/exit 代码不会运行。

  • 模块通知器无法阻止要打补丁的模块加载。

回调是 klp_object 结构的一部分,它们的实现特定于该 klp_object。其他热补丁对象可能会或可能不会被打补丁,而与目标 klp_object 的当前状态无关。

2. 回调类型

可以为以下热补丁操作注册回调

  • 预补丁
    • 在 klp_object 被打补丁之前

  • 后补丁
    • 在 klp_object 被打补丁并在所有任务中处于活动状态之后

  • 预取消补丁
    • 在 klp_object 被取消打补丁(即,补丁代码处于活动状态)之前,用于清理后补丁回调资源

  • 后取消补丁
    • 在 klp_object 被取消打补丁之后,所有代码都已恢复,并且没有任务正在运行补丁代码,用于清理预补丁回调资源

3. 工作原理

每个回调都是可选的,省略一个回调并不妨碍指定任何其他回调。但是,热补丁核心对称地执行处理程序:预补丁回调具有后取消补丁对应项,而后补丁回调具有预取消补丁对应项。仅当执行了相应的补丁回调时,才会执行取消补丁回调。典型的用例是将获取和配置资源的补丁处理程序与拆卸和释放这些相同资源的取消补丁处理程序配对。

仅当加载了其宿主 klp_object 时,才会执行回调。对于内核中的 vmlinux 目标,这意味着当启用/禁用热补丁时,回调将始终执行。对于补丁目标内核模块,仅当加载了目标模块时,才会执行回调。当(取消)加载模块目标时,仅当启用热补丁模块时,其回调才会执行。

如果指定了预补丁回调,则预期返回一个状态码(0 表示成功,-ERRNO 表示错误)。错误状态码向热补丁核心指示,当前 klp_object 的修补不安全,应停止当前的修补请求。(当未提供预补丁回调时,则假定转换是安全的。)如果预补丁回调返回失败,则内核的模块加载器将:

  • 如果热补丁在目标代码之后加载,则拒绝加载热补丁。

  • 如果热补丁已成功加载,则拒绝加载模块。

如果由于预补丁回调失败或任何其他原因而导致对象未能修补,则不会为给定的 klp_object 执行任何后补丁、预取消补丁或后取消补丁回调。

如果补丁转换被反转,则不会运行任何预取消补丁处理程序(这遵循前面提到的对称性——仅当执行了相应的后补丁回调时,才会出现预取消补丁回调)。

如果对象确实成功打上了补丁,但由于某种原因(例如,如果另一个对象未能打补丁)从未开始补丁转换,则只会调用后取消补丁回调。

4. 用例

可以在 samples/livepatch/ 目录中找到演示回调 API 的示例热补丁模块。这些示例已修改为在 kselftests 中使用,可以在 lib/livepatch 目录中找到。

全局数据更新

预补丁回调可用于更新全局变量。例如,commit 75ff39ccc1bd(“tcp:使挑战 ack 不那么可预测”)更改了一个全局 sysctl,并修补了 tcp_send_challenge_ack() 函数。

在这种情况下,如果我们非常偏执,那么在后补丁回调完成修补修补数据可能是有意义的,以便 tcp_send_challenge_ack() 可以首先更改为使用 READ_ONCE 读取 sysctl_tcp_challenge_ack_limit。

__init 和 probe 函数补丁支持

尽管 __init 和 probe 函数不能直接进行热补丁,但可以通过预/后补丁回调来实现类似的更新。

commit 48900cb6af42(“virtio-net:删除 NETIF_F_FRAGLIST”)更改了 virtnet_probe() 初始化其驱动程序的 net_device 功能的方式。预/后补丁回调可以迭代所有此类设备,对其 hw_features 值进行类似的更改。(可能需要相应地更新该值的客户端函数。)