linux 热补丁技术叫 livepatch 机制,可以在不重启内核的情况下应用补丁到内核中,常用于服务器环境,比如服务器处于生产环境不能随意重启,而为了应对某些临时发现漏洞修补,则可以通过该机制进行动态修复。

  • 依赖于 kprobe 和 ftrace 机制来实现 patch 注入。
  • 需要一致性模型

一致性模型:
一般 patch 修复不会改变一个函数的语义,只是为了添加一个 null 指针或者边界检查,添加一些内存屏障,或者给临界区加锁。还有一些复杂的 patch 会修改锁的顺序,修改变量,更新函数功能。所以需要一个一致性模型来解决这些问题。

kpatch 有下面的方式跟踪注入 patch 是安全的:

  • 第一种最有效方式:对睡眠任务进行堆栈检查,如果任务的堆栈上没有需要修改的函数,那么则是可以进行安全修复。大多数情况下,会在这时完成修补,否则周期性尝试检查并修补。这个能力需要 arch 提供 HAVE_RELIABLE_STACKTRACE 可靠栈支持。(目前仅 powerpc 和 x86 提供了该支持)
  • 第二种是内核从系统调用,用户空间 irq 和 信号返回到用户空间时,任务被切换,此时在以下两种情况有效:(1)修补是在受影响的函数上休眠的 i/o 绑定任务,这种可以通过 SIGSTOP 和 SIGCNT 信号强制退出后来进行补丁修补(2)修补是 cpu 绑定任务。如果任务是高度 cpu 绑定的,那么它将在下一次被 irq 中断时得以修补。
  • “swapper“ 任务,在进入 idle 前调用 klp_update_patch_state() ,此时可以进行修补。

如果 arch 没有 HAVE_RELIABLE_STACKTRACE 支持,则只能使用第二种修补方式。但无论怎样不支持 HAVE_RELIABLE_STACKTRACE 都会被认为是不完全受支持的 livepatch。

实现原理简述:

ftrace 原理详细分析这篇文章中 中知道:每个 tracer 实现 struct ftrace_ops 来在指定的 hook 点调用回调函数。livepatch 利用了这个 ftrace_ops,在 hook 点调用回调并调用相对应 patch 中的函数,并修改返回地址,实现一个函数的修补功能。

该过程可以分为load,enable,replace,disable,removing 五个步骤,其中 load 负责加载补丁,enable 负责注册 ftrace 等操作,replace 在合适时机调用 stop_machine 替换函数。

可以看到 livepatch 的修补单位是函数,其中对 patch 具有许多限制:

  • 只有可以被 trace 的函数才能被修补
    livepatch 需要动态 ftrace 支持,相对应的 notrace 的函数不能被修补,否则可能导致代码陷入无限循环。
  • 需要动态 ftrace 位于函数的最开始,也就是架构支持 -mfentry 编译选项(可以看 ftrace 原理详细分析 中的介绍),目前 x86 支持,arm64 不支持。
  • kretporbe 和 livepatch 是冲突的,因为他们实现都是基于在 hook 点修改返回值实现的,具体还是可以看 kprobe 原理详细分析 中对 kprobe 原理分析。

限制:
一些编写 patch 上的限制:
(1)补丁之间的交互
补丁之间可能存在潜在交互,所以建议补丁是以累计升级模式制作,即第二个补丁基于第一个补丁制作,并且第二个补丁是第一个补丁的累积升级。
(2)数据结构的修改
kpatch 修补的单位是函数,而不是数据结构,如果补丁对数据结构进行了修改,那一般会出问题,需要重新制作补丁,默认不允许修改数据结构,如果这样做了,kpatch-build 可能会抛出错误。
如果实在需要对数据结构中变量修改,可以使用 livepatch 提供的影子变量机制来分配一个变量使用。
(3)不能修补 __init 的函数
它们只会被调用一次,且可能涉及硬件初始化条件。
(4)头文件修改
头文件修改可能导致大范围数据产生变化,破坏数据结构。
(5)小心意外更改的函数
如内联函数,新补丁中的函数可能被编译器内联。
(6)静态局部变量
静态局部变量是全局变量,patch 中无法默认对其修改。
(7)移除代码
patch 中不要移除老的修复代码,只能新增代码,被移除的代码在 kpatch-build 作用下会被保留。
(8)禁止对 once 相关宏使用
类似的 pintk_once。。。等等会将变量存储在 __data_once 中,这是静态的,我们不能再去修改。

还有其他一些限制:可以参考 https://github.com/dynup/kpatch/blob/master/doc/patch-author-guide.md

总结下来 livepatch 机制的限制很多,对应架构支持也不相同(至少 arm64 支持目前很差,只支持最基本 livepatch 功能,上游目前有对 arm64 进行支持开发。。。),不过 kpatch 项目中可以检测架构是否支持 livepatch,如果不支持,kpatch 会在自己项目中的 kpatch/core 路径中单独编译出一个 kpatch.ko(而不是用内核自己 build 的kpatch.ko)来为架构提供热补丁支持。内核实现 livepatch 对架构,编译器等均有一些要求,比如编译器为架构提供可靠栈回溯,支持动态 ftrace regs 等,否则对应架构很难实现 livepatch,目前有 x86,powerpc,s390 能在内核直接支持 livepatch。而排除可靠栈回溯的特性也能去实现 livepatch,不过会得到更多的限制,包括对需要制作的 patch 也提出许多限制。目前 kpatch 项目中外部实现的 livepatch 也还没有对 arm64 提供支持(arm64相关支持开发在这儿:Arm64 support, including @surajjs95 & @t-msn changes #1302)。
不过上述 pull request 提到的 patch 并未被 merge 到主线,一直处于解决一些 bug 的阶段,直到现在该 pull request 也是 open 状态,其中 kpatch 的对应 arm64 分支:GitHub - swine/kpatch at arm64

简单示例:替换 /proc/version 版本字符串
拉取最新的 GitHub - dynup/kpatch: kpatch - live kernel patching,并在 x86 环境演示,执行命令如下:

make BUILDMOD=yes
make BUILDMOD=yes install

CACHEDIR=/home/xxxxx/build_dir kpatch-build -r kernel-4.18.0.el8.src.rpm  -t vmlinux kpatch/examples/proc-version.patch  -R


[root@localhost xx]# kpatch load livepatch-proc-version.ko 
loading patch module: livepatch-proc-version.ko
waiting (up to 15 seconds) for patch transition to complete...
transition complete (2 seconds)
[root@localhost xx]# cat /proc/version 
kpatch Linux version 4.18.0.el8.x86_64 (mockbuild@kojid_x86_bj) (gcc version 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)) #1 SMP Mon Jul 17 00:45:01 EDT 2023
[root@localhost xx]# kpatch unload livepatch-proc-version.ko 
disabling patch module: livepatch_proc_version
waiting (up to 15 seconds) for patch transition to complete...
transition complete (2 seconds)
unloading patch module: livepatch_proc_version
[root@localhost xx]# cat /proc/version 
Linux version 4.18.0.el8.x86_64 (mockbuild@kojid_x86_bj) (gcc version 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)) #1 SMP Mon Jul 17 00:45:01 EDT 2023
[root@localhost xx]# 

示例中的kpatch/examples/proc-version.patch如下:

From 64aff1ab8f9a9f5df06c998be73d4981b77e480d Mon Sep 17 00:00:00 2001
From: Joe Lawrence <joe.lawrence@redhat.com>
Date: Mon, 7 Nov 2022 08:21:58 -0500
Subject: [PATCH] kpatch: modify /proc/version output
Content-type: text/plain

This is a simple kpatch example that modifies version_proc_show() so
that the output of /proc/version will be prefixed by "kpatch ".

Signed-off-by: Joe Lawrence <joe.lawrence@redhat.com>
---
 fs/proc/version.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fs/proc/version.c b/fs/proc/version.c
index 02e3c3cd4a9a..957faeea8f5c 100644
--- a/fs/proc/version.c
+++ b/fs/proc/version.c
@@ -9,6 +9,7 @@
 
 static int version_proc_show(struct seq_file *m, void *v)
 {
+	seq_printf(m, "kpatch ");
 	seq_printf(m, linux_proc_banner,
 		utsname()->sysname,
 		utsname()->release,
-- 
2.26.3

主要是修改了Linux version 中Linux version 4.18.0.el8.x86_64Linux字段,修改成了kpatch字符串,从打印中可以看到补丁应用成功,卸载补丁后恢复原来的字符串。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐