Livepatch¶
本文概述了内核热补丁(livepatching)的基本信息。
1. 动机¶
在许多情况下,用户不愿意重启系统。可能是因为他们的系统正在执行复杂的科学计算,或者在高峰期处于高负载状态。除了保持系统持续运行外,用户还希望拥有一个稳定且安全的系统。Livepatching 允许重定向函数调用,从而在无需重启系统的情况下修复关键函数,满足了用户的这两种需求。
2. Kprobes、Ftrace、Livepatching¶
Linux 内核中有多个机制与代码执行的重定向直接相关,即:内核探测(kernel probes)、函数跟踪(function tracing)和热补丁(livepatching)
内核探测(Kprobes)是最通用的。可以通过将断点指令放在任何指令的位置来重定向代码。
函数跟踪器(Function tracer)从靠近函数入口点的预定义位置调用代码。该位置由编译器使用 gcc 的 ‘-pg’ 选项生成。
Livepatching 通常需要在函数入口的最开始处重定向代码,此时函数参数或栈尚未以任何方式被修改。
这三种方法都需要在运行时修改现有代码。因此,它们需要相互感知并避免冲突。其中大部分问题是通过使用动态 ftrace 框架作为基础来解决的。当对函数入口进行探测时,Kprobe 会注册为一个 ftrace 处理程序,参见 CONFIG_KPROBES_ON_FTRACE. 此外,来自热补丁的替代函数是在自定义 ftrace 处理程序的帮助下被调用的。但也存在一些限制,见下文。
3. 一致性模型¶
函数存在是有原因的。它们接受一些输入参数,获取或释放锁,以确定的方式读取、处理甚至写入一些数据,并具有返回值。换句话说,每个函数都有一个明确的语义。
许多修复并没有改变被修改函数的语义。例如,它们添加空指针或边界检查,通过添加缺失的内存屏障来修复竞态条件,或者在临界区周围添加一些锁。这些更改大多是自包含的,且函数向系统其余部分呈现的行为保持不变。在这种情况下,可以逐个独立地更新这些函数。
但也有更复杂的修复。例如,一个补丁可能会同时更改多个函数中的加锁顺序。或者一个补丁可能会改变某些临时结构的含义并更新所有相关函数。在这种情况下,受影响的单元(线程、整个内核)需要同时开始使用这些函数的所有新版本。并且,只有在安全的时候才能进行切换,例如当受影响的锁被释放,或者此时没有数据存储在被修改的结构中时。
关于如何以安全的方式应用函数的理论相当复杂。其目的是定义一个所谓的一致性模型。它试图定义新实现可以使用的条件,从而使系统保持一致。
Livepatch 拥有一个结合了 kGraft 和 kpatch 特性的混合一致性模型:它使用了 kGraft 的按任务一致性(per-task consistency)和系统调用屏障切换,并结合了 kpatch 的栈回溯切换。此外,还有许多回退选项,使其非常灵活。
补丁是按任务(per-task)应用的,当任务被认为可以安全切换时进行切换。当启用补丁时,livepatch 会进入过渡状态,此时各个任务正向已打补丁状态收敛。通常这个过渡状态在几秒钟内即可完成。禁用补丁时也会发生相同的序列,只是任务从已打补丁状态向未打补丁状态收敛。
中断处理程序继承其所中断的任务的已打补丁状态。对于 fork 出的任务也是如此:子任务继承父任务的已打补丁状态。
Livepatch 使用几种互补的方法来确定何时可以安全地为任务打补丁
第一种也是最有效的方法是检查睡眠任务的栈。如果给定任务的栈上没有受影响的函数,则该任务会被打补丁。在大多数情况下,这会在第一次尝试时为大部分或所有任务打补丁。否则,它会定期继续尝试。此选项仅在架构具有可靠栈(HAVE_RELIABLE_STACKTRACE)时可用。
如果需要,第二种方法是内核退出切换(kernel exit switching)。当任务从系统调用、用户空间 IRQ 或信号返回到用户空间时,该任务会被切换。它在以下情况下很有用
为在受影响函数上睡眠的 I/O 密集型用户任务打补丁。在这种情况下,你必须发送 SIGSTOP 和 SIGCONT 来强制它退出内核并打补丁。
为 CPU 密集型用户任务打补丁。如果该任务高度依赖 CPU,那么它将在下次被 IRQ 中断时打补丁。
对于空闲的 “swapper” 任务,由于它们从不退出内核,因此它们在空闲循环(idle loop)中有一个
klp_update_patch_state()调用,允许它们在 CPU 进入空闲状态之前打补丁。(注意,目前对内核线程(kthreads)还没有这种方法。)
没有 HAVE_RELIABLE_STACKTRACE 的架构完全依赖于第二种方法。极有可能某些任务仍在使用旧版本的函数运行,直到该函数返回。在这种情况下,你必须向任务发送信号。这尤其适用于内核线程。它们可能不会被唤醒,需要被强制执行。有关更多信息,请参见下文。
除非我们能想出另一种为内核线程打补丁的方法,否则没有 HAVE_RELIABLE_STACKTRACE 的架构不被内核热补丁完全支持。
/sys/kernel/livepatch/<patch>/transition 文件显示补丁是否处于过渡状态。在给定的时间,只能有一个补丁处于过渡状态。如果有任何任务卡在初始补丁状态,补丁可能会无限期地保持在过渡状态。
在过渡进行期间,可以通过向 /sys/kernel/livepatch/<patch>/enabled 文件写入相反的值来逆转并有效取消过渡。然后,所有任务将尝试收敛回最初的补丁状态。
还有一个 /proc/<pid>/patch_state 文件,可用于确定哪些任务正在阻塞打补丁操作的完成。如果补丁处于过渡状态,该文件显示 0 表示任务未打补丁,显示 1 表示已打补丁。否则,如果没有补丁处于过渡状态,它显示 -1. 可以通过使用 SIGSTOP 和 SIGCONT 向任何阻塞过渡的任务发送信号,强制它们改变打补丁状态。不过这可能对系统有害。向所有剩余的阻塞任务发送伪信号(fake signal)是更好的替代方案。实际上并没有传递真正的信号(在挂起信号结构中没有数据)。任务被中断或唤醒,并被迫改变其打补丁状态。伪信号每 15 秒自动发送一次。
管理员还可以通过 /sys/kernel/livepatch/<patch>/force 属性来影响过渡。向其中写入 1 会清除所有任务的 TIF_PATCH_PENDING 标志,从而强制任务进入已打补丁状态。重要提示!force 属性适用于因阻塞任务导致过渡长时间卡住的情况。管理员应收集所有必要的数据(即此类阻塞任务的栈回溯),并请求补丁分发者的许可来强制过渡。未经授权的使用可能会对系统造成损害。这取决于补丁的性质、哪些函数被(取消)打补丁,以及阻塞任务在哪个函数中睡眠(此时 /proc/<pid>/stack 可能会有帮助)。当使用 force 功能时,补丁模块的移除(rmmod)将被永久禁用。无法保证没有任务在此类模块中睡眠。如果在一个循环中禁用和启用补丁模块,这意味着无限的引用计数。
此外,使用 force 还可能影响未来热补丁的应用,并对系统造成更大的损害。管理员应首先考虑简单地取消过渡(见上文)。如果使用了 force,则应计划重启,并且不再应用热补丁。
3.1 为新架构添加一致性模型支持¶
为新架构添加一致性模型支持有几种选择
添加 CONFIG_HAVE_RELIABLE_STACKTRACE. 这意味着移植 objtool,对于非 DWARF 解旋器(unwinders),还要确保栈回溯代码有一种方法来检测栈上的中断。
或者,确保每个内核线程(kthread)在安全位置都有一个对
klp_update_patch_state()的调用。内核线程通常处于一个重复执行某些动作的无限循环中。切换内核线程补丁状态的安全位置将是循环中的指定点,在该点没有获取任何锁,并且所有数据结构都处于定义良好的状态。当使用工作队列(workqueues)或 kthread worker API 时,这个位置是很清晰的。这些内核线程在一个通用循环中处理独立的动作。
对于具有自定义循环的内核线程,情况要复杂得多。在那里,必须针对每个具体情况仔细选择安全的位置。
在这种情况下,没有 HAVE_RELIABLE_STACKTRACE 的架构仍然可以使用一致性模型中不检查栈的部分
当用户任务跨越内核/用户空间边界时为其打补丁;以及
在其指定的补丁点为内核线程和空闲任务打补丁。
这个选项不如选项 1 好,因为它需要向用户任务发送信号并唤醒内核线程来为它们打补丁。但对于那些尚无可靠栈回溯的架构来说,它仍然可以作为一个很好的备选方案。
4. Livepatch 模块¶
Livepatches 使用内核模块进行分发,参见 samples/livepatch/livepatch-sample.c.
该模块包含了我们想要替换的函数的新实现。此外,它还定义了一些结构,描述原实现与新实现之间的关系。然后是当加载 livepatch 模块时使内核开始使用新代码的代码。还有在移除 livepatch 模块之前进行清理的代码。所有这些都将在接下来的章节中进行更详细的解释。
4.1. 新函数¶
函数的新版本通常只是从原始源码中复制过来的。一个好的做法是为名称添加一个前缀,以便它们可以与原函数区分开来,例如在栈回溯中。此外,它们可以声明为 static,因为它们不是被直接调用的,不需要全局可见性。
补丁只包含真正被修改的函数。但它们可能需要访问原始源文件中可能仅局部可访问的函数或数据。这可以通过生成的 livepatch 模块中的特殊重定位段来解决,详见 Livepatch module ELF format 了解更多详细信息。
4.2. 元数据¶
补丁由几个将信息分为三个级别的结构来描述
为每个被修补的函数定义了
struct klp_func. 它描述了特定函数的原实现与新实现之间的关系。该结构包含原函数的名称(作为字符串)。函数地址在运行时通过 kallsyms 查找。
然后它包含新函数的地址。这是通过直接赋值函数指针来定义的。注意,新函数通常定义在同一个源文件中。
作为一个可选参数,可以使用 kallsyms 数据库中的符号位置来消除同名函数的歧义。这不是数据库中的绝对位置,而是仅针对特定对象(vmlinux 或内核模块)找到该符号的顺序。请注意,kallsyms 允许根据对象名称搜索符号。
struct klp_object定义了同一对象中一组被修补的函数(struct klp_func)。其中对象要么是 vmlinux (NULL),要么是模块名称。该结构有助于将每个对象的函数分组并统一处理。请注意,被修补的模块可能会比补丁本身加载得晚,并且相关函数只有在它们可用时才能被修补。
struct klp_patch定义了一个被修补对象的数组(struct klp_object)。该结构一致且最终同步地处理所有被修补的函数。只有当找到所有被修补的符号时,整个补丁才会被应用。唯一的例外是尚未加载的对象(内核模块)中的符号。
有关如何按任务应用补丁的更多详细信息,请参见“一致性模型”一节。
5. Livepatch 生命周期¶
Livepatching 可以通过五个基本操作来描述:加载、启用、替换、禁用、移除。
其中替换和禁用操作是互斥的。对于给定的补丁,它们产生相同的结果,但对系统而言则不然。
5.1. 加载¶
唯一合理的方法是在加载 livepatch 内核模块时启用补丁。为此,必须在 module_init() 回调中调用 klp_enable_patch(). 主要有两个原因
首先,只有模块才能轻松访问相关的 struct klp_patch。
其次,当补丁无法启用时,可以利用错误码来拒绝加载模块。
5.2. 启用¶
通过从 module_init() 回调中调用 klp_enable_patch() 来启用 livepatch。在此阶段,系统将开始使用被修补函数的新实现。
首先,根据名称查找被修补函数的地址。应用“新函数”一节中提到的特殊重定向。在 /sys/kernel/livepatch/<name> 下创建相关的条目。当上述任何操作失败时,补丁将被拒绝。
其次,livepatch 进入过渡状态,此时任务正向已打补丁状态收敛。如果某个原函数是第一次被修补,则会创建一个函数特定的 struct klp_ops 并注册一个通用的 ftrace 处理程序[1]。/sys/kernel/livepatch/<name>/transition 中的值 ‘1’ 表示此阶段。有关此过程的更多信息,请参见“一致性模型”一节。
最后,一旦所有任务都已打补丁,“transition”值变为 ‘0’。
5.3. 替换¶
所有已启用的补丁可能会被设置了 .replace 标志的累积补丁所替换。
一旦新补丁启用并且“过渡”完成,与被替换补丁相关的所有函数(struct klp_func)将从相应的 struct klp_ops 中移除。此外,当新补丁未修改相关函数且 func_stack 列表变为空时,ftrace 处理程序将被注销,并且 struct klp_ops 将被释放。
有关更多详细信息,请参见 Atomic Replace & Cumulative Patches。
5.4. 禁用¶
可以通过向 /sys/kernel/livepatch/<name>/enabled 写入 ‘0’ 来禁用已启用的补丁。
首先,livepatch 进入过渡状态,此时任务正向未打补丁状态收敛。系统开始使用先前启用的补丁中的代码甚至是原代码。/sys/kernel/livepatch/<name>/transition 中的值 ‘1’ 表示此阶段。有关此过程的更多信息,请参见“一致性模型”一节。
其次,一旦所有任务都取消了补丁,“transition”值变为 ‘0’。与即将禁用的补丁相关的所有函数(struct klp_func)都将从相应的 struct klp_ops 中移除。当 func_stack 列表变为空时,ftrace 处理程序将被注销,并且 struct klp_ops 将被释放。
第三,销毁 sysfs 接口。
5.5. 移除¶
只有当模块提供的函数没有任何使用者时,模块移除才是安全的。这就是 force 功能永久禁用移除的原因。只有当系统成功过渡到新的补丁状态(已打补丁/未打补丁)且未被强制时,才能保证没有任务在旧代码中睡眠或运行。
6. Sysfs¶
有关已注册补丁的信息可以在 /sys/kernel/livepatch 下找到。可以通过向其中写入来启用和禁用补丁。
/sys/kernel/livepatch/<patch>/force 属性允许管理员影响打补丁操作。
有关更多详细信息,请参见 ABI file testing/sysfs-kernel-livepatch。
7. 限制¶
当前的 Livepatch 实现有几个限制
只有能够被跟踪的函数才能被修补。
Livepatch 基于动态 ftrace。特别是,实现 ftrace 或 livepatch ftrace 处理程序的函数不能被修补。否则,代码将陷入无限循环。通过将有问题的函数标记为 “notrace” 可以防止潜在的错误。
只有当动态 ftrace 位于函数的最开始时,Livepatch 才能可靠地工作。
函数需要在栈或函数参数被以任何方式修改之前进行重定向。例如,livepatch 要求在 x86_64 上使用 -fentry gcc 编译器选项。
一个例外是 PPC 移植。它使用相对寻址和 TOC。每个函数在调用 ftrace 处理程序之前必须处理 TOC 并保存 LR. 此操作在返回时必须恢复。幸运的是,通用的 ftrace 代码也有同样的问题,所有这些都在 ftrace 级别上处理。
使用 ftrace 框架的 Kretprobes 与被修补的函数冲突。
kretprobes 和 livepatches 都使用修改返回地址的 ftrace 处理程序。先到先得。当处理程序已被其中一个占用时,另一个探测或补丁将被拒绝。
当代码重定向到新实现时,原函数中的 Kprobes 将被忽略。
目前正在进行添加关于此情况警告的工作。