openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch

top_tony
发布于 2022-8-3 17:27
浏览
0收藏

什么是 livepatch
Livepatch 即内核热补丁,通常在系统不可重启的情况下,用于修复内核以及内核模块的函数 bug。简单地说,livepatch 将待修复函数的开头几条指令替换为特定的跳转指令,让其跳转至修复函数中,这样该函数每次被调用,都会自动执行替换后的函数,达到修复函数的效果。

  openEuler 上的 livepatch 与 linux 主线上的实现略有不同,主要是 openEuler 上采用的方法是直接修改指令,而 linux 主线上采用的方法是基于 ftrace 实现跳转。Linux 社区主线采用的 ftrace 方案,新函数运行前必须先跳转至ftrace_caller中,并且通过klp_ftrace_handler将 ip 值修改为新函数的地址,最后才能执行新函数的指令。整个流程相对来说比较冗长,每次运行都需要跳转至ftrace_caller中查找klp_ftrace_handler,效率较低。而 openEuler 上的方案,在运行时直接跳转至新函数,无需经过中转,效率较高。

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区

 

livepatch 全局框架
Livepatch 主要涉及到内核的 livepatch 模块、用户态 kpatch 工具以及制作生成的热补丁 ko 三个大块,各个部分的关系如下图所示。

 

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区
 

在编译内核后,需要提供热补丁 patch,编译时的.config,编译后的 vmlinux 以及内核源码目录给 kpatch 工具,由 kpatch 工具中的kpatch-build脚本生成对应的热补丁 ko。热补丁 ko 中会包含数据以及新函数的指令,当热补丁 ko 插入内核后,会经过 module 处理,再到 livepatch 进行初始化。当命令行输入激活指令后,livepatch 会将老函数的前几条指令进行替换,跳转至热补丁 ko 中的新函数上,从而实现老函数的修复动作。此后,每次调用老函数,都会经过前几条跳转指令跳转至新函数上执行。

livepatch 内部框架
热补丁内部主要暴露的直接接口有klp_register_patch和klp_unregister_patch,分别用于注册和卸载热补丁。在热补丁 ko 的 init 函数和 exit 函数中会对应地进行调用。热补丁内部还有两个非直接暴露,但是可以通过命令调用的接口__klp_enable_patch和__klp_disable_patch,分别用于激活以及去使能热补丁。

如下图所示,热补丁注册过程中会执行的操作大致包含以下几个部分:符号重定向、jump_label 初始化以及 hook 函数执行。热补丁激活以及去使能过程中会执行栈检查以及指令替换的操作。当热补丁卸载时,需要执行 hook 函数以及资源回收。

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区

热补丁函数管理
Livepatch 允许对单个函数打多个 patch,也允许单个 patch 中对多个函数进行修改。基于此,需要对大量的热补丁函数进行规范化管理。

如下图所示,在内核有一个全局链表管理所有的已经激活的 livepatch 函数,横向管理的数据结构是klp_func_node,表示不同的热补丁函数,此链表上的所有函数可以乱序地去使能(比如先 disable C 函数再 disable A 函数)。纵向管理的数据结构是klp_func,表示同一个函数的多个热补丁版本,此链表上的多个版本,只允许去使能最后一个(去使能后从链表摘除),并且生效的永远是链表上的最后一个(比如 A 函数激活了四个补丁,分别是 A1、A2、A3、A4,此时生效的是 A4,当去使能时,只能去使能 A4,并且去使能后 A4 从链表摘除,并且 A3 生效)。

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区

livepatch 实现流程
如下图所示,初始目标是将内核函数 func A 通过一个 patch 的修改变为 func A’。

 

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区


首先需要将 vmlinux、.config 文件以及对应的 patch 提供给 kpatch 工具,用以生成 livepatch.ko 文件。如下图所示,livepatch.ko 文件中包含三大部分,第一部分是.klp.rela.xxx段,用于存储需要重定向的符号;第二个部分是数据段,主要用于存储klp_patch、klp_object、klp_func等数据;第三个部分是代码段,除了热补丁调用相关的代码之外,最主要的就是 func A’函数,也就是打上热补丁后的新函数。

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区

接下来需要将生成的 livepatch.ko 插入内核中,如下图所示,livepatch.ko 插入内核之后,会通过module_init模块,将 livepatch.ko 中包含的内核中的EXPORT_SYMBOL重定向。然后通过 ko 中的 init 函数,调用 livepatch 模块中的klp_register_patch接口。在此过程中,livepatch 将klp_patch数据结构挂在全局的klp_patches链表上管理起来,并且执行符号重定向(对象是.klp.rela.xxx段中的符号,这些符号都不是EXPORT_SYMBOL)、函数长度校验(若函数长度小于需要修改的指令条数,则不允许打热补丁)、jump_label 初始化(此处涉及到地址跳转,需要刷新以保证正确性)、load_hook 函数执行。

openEuler Kernel 特性解读 | 内核在线修复神器 – livepatch-鸿蒙开发者社区

最后通过命令echo 1 > /sys/kernel/livepatch/xxxx/enable进行激活,自动调用 livepatch 中的__klp_enable_patch接口。在此过程中,livepatch 模块会陷入stop_machine中,将所有的 cpu 暂停工作,切换至 migration 线程上执行。其中只有 CPU 会执行热补丁激活的动作,其余 CPU 执行cpu_relax操作。在热补丁 CPU 上,会执行以下几个动作:一是进行 kprobe 检查,若是待修改的函数上有 kprobe 点则不允许使用热补丁修改,理由是同时存在指令修改,可能会互相覆盖,产生预料外的结果;二是进行栈检查,检查所有线程,保证待修改的函数都不在线程栈上;三是执行指令修改,将待修改函数的前几条指令修改为跳转指令,使其跳转至 livepatch.ko 中的新函数上。

livepatch 使用
内核热补丁功能需要使能以下这些 config:

CONFIG_HAVE_LIVEPATCH_WO_FTRACE=y
CONFIG_LIVEPATCH=y
CONFIG_LIVEPATCH_WO_FTRACE=y
CONFIG_LIVEPATCH_STOP_MACHINE_CONSISTENCY=y
CONFIG_LIVEPATCH_RESTRICT_KPROBE=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_DEBUG_INFO=y

制作热补丁 ko 有两种方式,第一种方式是通过编写模块代码的方式,可以参考内核源码的samples/livepatch/livepatch-sample.c文件。如下所示,编写的过程需要构造关键的数据结构,包括klp_func、klp_object、klp_patch。

static struct klp_func funcs[] = {
 {
  .old_name = "cmdline_proc_show",
  .new_func = livepatch_cmdline_proc_show,
 }, { }
};

static struct klp_object objs[] = {
 {
  /* name being NULL means vmlinux */
  .funcs = funcs,
  .hooks_load = hooks_load,
  .hooks_unload = hooks_unload,
 }, { }
};

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

然后就是热补丁函数,以及通过 init 函数调用klp_register_patch接口。

static int livepatch_cmdline_proc_show(struct seq_file *m, void *v)
{
 seq_printf(m, "%s\n", "this has been live patched");
 return 0;
}
static int livepatch_init(void)
{
 return klp_register_patch(&patch);
}

static void livepatch_exit(void)
{
 WARN_ON(klp_unregister_patch(&patch));
}

第二种方式是通过 kpatch 工具来制作(可以参考https://gitee.com/src-openeuler/kpatch),大致的流程如下:①安装kpatch工具:yum install kpatch kpatch-runtime;②准备好一个修改函数内容的patch;③export NO_PROFILING_CALLS=1;④设置对应的ARCH和CROSS_COMPILE(如果是在本机使用则无需设置);⑤在kpatch-build目录下执行:./kpatch-build -s <src dir> -v <src dir>/vmlinux <patch dir>/xxxx.patch --skip-gcc-check -c <src dir>/.config。

得到热补丁 ko 之后,使用比较简单,大致内容如下:

• 插入内核:insmod xxx.ko• 激活热补丁:echo 1 > /sys/kernel/livepatch/xxx/enabled• 去使能热补丁:echo 0 > /sys/kernel/livepatch/xxx/enabled• 卸载 ko:rmmod xxx• 查询当前热补丁状态:cat /proc/livepatch/state

livepatch 实例
① 准备好 patch。可用git format-patch来生成:git format-patch -1。注意:生成 patch 后记得回退该补丁;

--- a/kernel/cgroup/cgroup.c
+++ b/kernel/cgroup/cgroup.c
@@ -6204,6 +6204,7 @@ void cgroup_exit(struct task_struct *tsk)
  struct css_set *cset;
  int i;

+ printk("livepatch: out of cgroup\n");
  spin_lock_irq(&css_set_lock);

  WARN_ON_ONCE(list_empty(&tsk->cg_list));

② 使用 kpatch 工具制作生成 livepatch,-s 表示源码路径,-c 表示.config,-v 表示 vmlinux;

./kpatch-build -s /home/yeweihua/projects/hulk/hulk-5.10/ -c build/.config -v build/vmlinux --skip-gcc-check /home/yeweihua/projects/hulk/hulk-5.10/0001-test.patch

③ 将生成的热补丁插入内核;

insmod livepatch-0001-test.ko

④ 将热补丁激活;

echo 1 > /sys/kernel/livepatch/livepatch_0001_test/enabled

⑤ 验证热补丁是否生效(cgroup_exit函数只要进程退出便会执行);

/modules # ls
[  382.479847] livepatch: out of cgroup
livepatch-0001-test.ko

⑥ 去使能热补丁;

echo 0 > /sys/kernel/livepatch/livepatch_0001_test/enabled

⑦ 查询当前系统的热补丁状况;

/modules # cat /proc/livepatch/state
Index    Patch                              State
----------------------------------------------------
1        livepatch_0001_test                disabled
----------------------------------------------------

⑧ 卸载热补丁

rmmod livepatch-0001-test.ko

(文章转载自公众号:架构与思维)

标签
已于2022-8-3 17:27:33修改
收藏
回复
举报
回复
    相关推荐