概述
CTyunOS 使用LibcarePlus 来实现用户态热补丁,LibcarePlus 是一个用户态进程热补丁框架,可以在不重启进程的情况下对 Linux 系统上运行的目标进程进行热补丁操作。热补丁可以应用于 CVE 漏洞修复,也可以应用于不中断应用服务的紧急 bug 修复。
软硬件要求
在 CTyunOS 上使用 LibcarePlus,需要满足一定的软硬件要求:
- 当前仅支持 x86_64/ aarch64 体系架构。
- 理论上LibcarePlus 可以在任何支持安装 libunwind、 elfutils 以及 binutils 的 Linux 发行版系统上运行。但目前仅在 CTyunOS 对外发行的版本进行了验证。
注意事项和约束
使用 LibcarePlus,需遵循以下热补丁规范和约束:
- 仅支持对 C 语言编写的代码,不支持汇编语言等。
- 仅支持用户态程序,不支持动态库打补丁。
- 代码文件名必须符合 C 语言标识符命名规范:由字母(A-Z,a-z)、数字 (0-9)、下划线“_”组成;并且首字符不能是数字,但可以是字母或者下划线;不能包含“-”、“$”等特殊符号。
- 不支持增量补丁,即必须卸载原有补丁才能加载第二个补丁。
- 不支持补丁自动加载。
- 不支持补丁查询。
- 被打热补丁的目标函数的出参和入参不能增加和删除。
- 静态函数补丁受限于系统中能找到该函数的符号表。
- 动态库热补丁只能对调用这个动态库的进程打补丁。
- 以下场景不支持热补丁:
- 死循环函数、不退出函数、inline 函数、初始化函数、NMI 中断处理函数
- 替换全局变量
- 修改头文件
- 数据结构成员变化(新增、删除、修改)
- 动态库、静态函数、静态变量
- 修改全局变量、TLS 变量、RCU 变量
- 修改包含 LINE , FILE 等gcc编译宏的 C 文件
- 修改 intel 矢量汇编指令
安装 LibcarePlus
安装软件依赖
LibcarePlus 运行依赖于 libunwind、 elfutils 和 binutils,在配置了 yum 源的 openEuler 系统上,可以参考如下命令安装 LibcarePlus 的依赖软件。
[root@localhost ~]# yum install -y binutils elfutils elfutils-libelf-devel libunwind-devel
安装 LibcarePlus
[root@localhost ~]# install libcareplus -y
查看安装是否成功:
[root@localhost ~]# libcare-ctl -help
usage: libcare-ctl [options] <cmd> [args]
Options:
-v - verbose mode
-h - this message
Commands:
patch - apply patch to a user-space process
unpatch- unapply patch from a user-space process
info - show info on applied patches
制作 LibcarePlus 热补丁
概述
LibcarePlus 支持如下方式制作热补丁:
- 手动制作
- 通过脚本制作
手动制作热补丁的过程繁琐,对于代码量较大的工程,例如QEMU,手动制作热补丁极其困难。建议使用 LibcarePlus 自带脚本一键式地生成热补丁文件。
制作热补丁需要安装libcareplus-devel包
[root@localhost ~]# sudo yum install -y libcareplus-devel
手动制作
本节以原文件 foo.c 和补丁文件 bar.c 为例,给出手动制作热补丁的指导。
-
准备 C 语言编写的原文件和补丁文件。例如原文件 demo.c 和补丁文件 demo_patch.c 。
// demo.c #include <stdio.h> #include <time.h> #include <unistd.h> void print_hello(void) { printf("Hello CTyunOS!\n"); } int main(void) { while (1) { print_hello(); sleep(1); } }
// demo_patch.c #include <stdio.h> #include <time.h> #include <unistd.h> void print_hello(void) { printf("Hello CTyunOS being patched!\n"); } int main(void) { while (1) { print_hello(); sleep(1); } }
-
编译得到原文件和补丁文件的汇编文件 demo.s 和 demo_patch.s,参考命令如下:
[root@localhost demo]# gcc -S demo.c [root@localhost demo]# gcc -S demo_patch.c [root@localhost demo]# ls demo.c demo_patch.c demo_patch.s demo.s
-
使用 kpatch_gensrc 对比 foo.s 和 bar.s 差异,生成包含原文件的汇编内容和差异内容的 foobar.s,参考命令如下:
[root@localhost demo]# sed -i 's/demo_patch.c/demo.c/' demo_patch.s [root@localhost demo]# kpatch_gensrc --os=rhel6 -i demo.s -i demo_patch.s -o demodiff.s --force-global
由于 kpatch_gensrc 默认对同一 C 语言原文件进行对比,所以对比前需要使用 sed 命令将补丁汇编文件 bar.s 中的 bar.c 改为原文件名称 foo.c。随后调用 kpatch_gensrc,指定输入文件为 demo.s 与 demo_patch.s,输出文件为 demodiff.s。
-
编译原文件的汇编文件 demo.s 和生成的汇编文件 demodiff .s,得到可执行文件 demo和 demodiff ,参考命令如下:
[root@localhost demo]# gcc -o demo demo.s [root@localhost demo]# gcc -o demodiff demodiff.s
-
利用 kpatch_strip 去除可执行程序 foo 和 foobar 的相同内容,保留制作热补丁所需要的内容:
[root@localhost demo]# kpatch_strip --strip demodiff demodiff.stripped [root@localhost demo]# kpatch_strip --rel-fixup demo demodiff.stripped [root@localhost demo]# strip --strip-unneeded demodiff.stripped [root@localhost demo]# kpatch_strip --undo-link demo demodiff.stripped
上述命令中的各参数含义为:
- --strip 用于去除 demodiff 中对于补丁制作无用的 section;
- --rel-fixup 用于修复补丁内所访问的变量以及函数的地址;
- strip --strip-unneeded 用于去除对于热补丁重定位操作无用的符号信息;
- --undo-link 用于将补丁内符号的地址从绝对地址更改为相对地址。
-
制作热补丁文件。
通过以上操作,已经得到了热补丁制作所需的主要内容。接下来需要使用 kpatch_make 将原可执行文件的 Build ID 以及 kpatch_strip 的输出文件 foobar.stripped 作为参数传递给 kpatch_make,最终生成热补丁文件,参考命令如下:[root@localhost demo]# str=$(readelf -n demo | grep 'Build ID') [root@localhost demo]# substr=${str##* } [root@localhost demo]# kpatch_make -b $substr demodiff.stripped -o demo.kpatch -i 0001 [root@localhost demo]# ls demo demo.c demodiff demodiff.s demodiff.stripped demo.kpatch demo_patch.c demo_patch.s demo.s
至此,就得到了最终的热补丁文件 demo.kpatch。
通过脚本制作
本节介绍如何利用 LibcarePlus 自带的 libcare-patch-make 脚本制作热补丁文件,仍以原文件 foo.c 和补丁文件 bar.c 为例。
- 利用 diff 命令生成 foo.c 和 bar.c 的对比文件,命令如下所示:
[root@localhost demo]# diff -up demo.c demo_patch.c > demo.diff
demo.diff 文件内容如下所示:
--- demo.c 2024-05-20 17:10:31.990354799 +0800
+++ demo_diff.c 2024-05-20 17:12:20.647351315 +0800
@@ -4,7 +4,7 @@
void print_hello(void)
{
- printf("Hello CTyunOS!\n");
+ printf("Hello CTyunOS, Patched!\n");
}
int main(void)
- 编写编译 demo.c 的 MakeFile 文件,具体如下所示:
all: demo foo: demo.c $(CC) -o $@ $< clean: rm -f demo install: demo mkdir $$DESTDIR || : cp demo $$DESTDIR
- 编写好 MakeFile 之后,直接调用 libcare-patch-make 即可。若 libcare-patch-make 询问选择哪个文件进行打补丁操作,输入原文件名即可,具体如下所示:
[root@localhost demo]# libcare-patch-make --clean demo.diff -i 0001 rm -f demo BUILDING ORIGINAL CODE /usr/bin/libcare-cc demo.c -o demo KPCC_DEBUG=1 # debug output enabled KPATCH_STAGE="original" KPCCREAL="/usr/bin/gcc" KPATCH_PREFIX=".kpatch_" KPATCH_ASM_DIR="(null)" # action is build-multiple # 1 input files: # input[1] = "demo.c" KPCC_REMOVE_ARGS="-g" KPCC_PATCH_ARGS="--force-gotpcrel;--os=rhel6" "/usr/bin/gcc" "-S" "demo.c" "-o" "./.kpatch_demo.c.original.s" "/usr/bin/gcc" "./.kpatch_demo.c.original.s" "-o" "demo" INSTALLING ORIGINAL OBJECTS INTO /home/demo/lpmake mkdir $DESTDIR || : cp demo $DESTDIR applying demo.diff... can't find file to patch at input line 3 Perhaps you used the wrong -p or --strip option? The text leading up to this was: -------------------------- |--- demo.c 2024-07-20 17:10:31.990354799 +0800 |+++ demo_diff.c 2024-07-20 17:12:20.647351315 +0800 -------------------------- File to patch: demo.c patching file demo.c grep: config-host.mak: No such file or directory replace QEMU_LDFLAGS to ' -Wl,-q' BUILDING PATCHED CODE /usr/bin/libcare-cc demo.c -o demo KPCC_DEBUG=1 # debug output enabled KPATCH_STAGE="patched" KPCCREAL="/usr/bin/gcc" KPATCH_PREFIX=".kpatch_" KPATCH_ASM_DIR="(null)" # action is build-multiple # 1 input files: # input[1] = "demo.c" KPCC_REMOVE_ARGS="-g" KPCC_APPEND_ARGS="-Wl,-q" KPCC_PATCH_ARGS="--force-gotpcrel;--os=rhel6" "/usr/bin/gcc" "-Wl,-q" "-S" "demo.c" "-o" "./.kpatch_demo.c.patched.s" "/usr/bin/kpatch_gensrc" "--force-gotpcrel" "--os=rhel6" "-i" "./.kpatch_demo.c.original.s" "-i" "./.kpatch_demo.c.patched.s" "-o" "./.kpatch_demo.c.s.tmp" mv "./.kpatch_demo.c.s.tmp" "./.kpatch_demo.c.s" "/usr/bin/gcc" "./.kpatch_demo.c.s" "-Wl,-q" "-o" "demo" INSTALLING PATCHED OBJECTS INTO /home/demo/.lpmaketmp/patched mkdir $DESTDIR || : cp demo $DESTDIR recover QEMU_LDFLAGS to '' MAKING PATCHES Fixing up relocation puts@GLIBC_2.17+0 Fixing up relocation print_hello+0 patch for /home/demo/lpmake/demo is in /home/demo/patchroot/b05a8052ad67630ca0cac907477d57fa1fdf8eb2.kpatch
执行成功之后,输出显示:热补丁文件位于当前目录的 patchroot 目录下,可执行文件则在 lpmake 目录下。脚本生成的热补丁文件默认是采用 Build ID 作为热补丁文件的文件名。
应用 LibcarePlus 热补丁
本节以原文件 foo.c 和补丁文件 bar.c 为例,介绍 LibcarePlus 热补丁的应用指导。
前期准备
应用 LibcarePlus 热补丁之前,需要提前准备好原可执行程序 foo、以及热补丁文件 foo.kpatch。
加载热补丁
本节介绍应用 LibcarePlus 热补丁的具体流程。
- 首先在第一个 shell 窗口运行需要打补丁的可执行程序,如下所示:
[root@localhost ~]# ./lpmake/demo Hello CTyunOS! Hello CTyunOS! Hello CTyunOS!
- 随后在第二个 shell 窗口运行 libcare-ctl 应用热补丁,命令如下所示:
libcare-ctl -v patch -p $(pidof demo) ./bfd0275b31041dc9efdcd65e4328b57c0355020b.kpatch
- 若此时热补丁应用成功,第二个 shell 窗口会有如下输出:
1 patch hunk(s) have been successfully applied to PID '7051'
- 而第一个 shell 窗口内运行的目标进程则会出现如下输出:
Hello CTyunOS! Hello CTyunOS! Hello CTyunOS being patched! Hello CTyunOS being patched!
实际的呈现图如下:
运行补丁前
运行补丁后
卸载热补丁
本节介绍卸载 LibcarePlus 热补丁的具体流程。
- 在第二个 shell 窗口执行如下命令:
[root@localhost ~]# libcare-ctl unpatch -p $(pidof foo)
- 此时若热补丁卸载成功,第二个 shell 窗口会有如下输出:
1 patch hunk(s) were successfully cancelled from PID '10999'
- 第一个 shell 窗口内运行的目标进程则会出现如下输出:
Hello CTyunOS being patched! Hello CTyunOS being patched! Hello CTyunOS! Hello CTyunOS!
实际的呈现图如下:
卸载补丁前
卸载补丁后