Linux 内核热补丁实战:用 kpatch 与 livepatch 不重启给运行中的内核打安全补丁

为什么服务器不能随便重启:内核漏洞修复的两难

做个人站长久了,一定会遇到这个场景:某天早上看到安全公告,说 Linux 内核里出了一个高危漏洞(比如某个本地提权、容器逃逸,或者网络协议栈的远程拒绝服务),修复它的唯一办法就是升级内核。问题来了——升级内核必须重启服务器才生效,而重启意味着网站中断。对于一个跑着稳定业务、有实时访问的站点,几分钟的 downtime 可能就是收入损失和 SEO 波动。

传统的应对只有三条路,每条都不好走:

  • 硬扛不重启:内核漏洞一直挂着,等于把服务器门敞开,等哪天被人扫到就晚了。
  • 午夜维护窗口重启:要提前通知、要盯着业务恢复、要承担重启后起不来的风险,运维成本高。
  • 加一台机器做滚动重启:对个人站长来说是过度设计,成本翻倍。

其实还有第四条路,而且它正是为「不想重启又要修内核」这个痛点生的——内核热补丁(Live Patching)。它能在内核正常运行、业务完全不中断的情况下,把有漏洞的函数替换成打过补丁的版本。本文就把主流的两套实现 kpatch 和 kGraft(内核主线叫 livepatch)讲透,从原理、选型、安装、应用到验证和回滚,一次说清,附可直接照抄的操作步骤。

一、内核热补丁到底改了什么:函数级替换的原理

要理解热补丁,先要破除一个误解:它并不是给你换了一整个新内核。内核热补丁的粒度是「函数」,它只替换出问题的那几个函数,其余部分完全不动。

核心机制叫 ftrace 的函数重定向。内核里有大量函数调用,编译时如果开启了 ftrace 的 mcount 桩,每个函数入口会留下一个可以挂钩子的位置。热补丁工具利用这个位置,把通往旧函数的调用点(call site)动态改写成跳转到新函数的入口。整个过程是原子的:用 stop_machine 机制短暂冻结所有 CPU,把调用点安全改写,再恢复运行。因为冻结时间极短(毫秒级),业务感知不到。

更关键的是「一致性模型」:如果一个进程正在旧函数里执行到一半,热补丁不能直接把旧代码从内存里抹掉。所以它会做一次「backtrace 检查」——确认没有任何任务还在旧函数里活动之后,才认为补丁「最终生效」(transition complete)。这就是为什么 kpatch list 会区分 enabled(调用点已改)和 active(旧代码已可安全回收)两种状态。

Linux 内核主线在 4.0 版本合并了 livepatch 框架,把「如何打补丁」标准化了。上层的 kpatch 和 kGraft,本质上都是这个框架的客户端。

二、kpatch 与 kGraft 的选型:先看你的发行版

两套工具解决同一个问题,但走的路不同,选择标准其实很简单——跟着你的发行版走。

kpatch:红帽系(RHEL/CentOS/Rocky/Alma)的官方方案

kpatch 由红帽开发,是 RHEL 系内核热补丁的事实标准。它的补丁以 .ko 内核模块形式存在,加载即生效。红帽还提供商业的热补丁订阅服务(kpatch-stream),能直接下发官方为 CVE 制作的热补丁,你连编译都不用做。

它的模型是「累积式」的:如果你先后打了 A、B 两个热补丁,之后想只打 A 不打 B 是不行的——热补丁按顺序堆叠,卸载也是从最后一个往回卸。

kGraft:SUSE 系的方案,后被主线吸收

kGraft 是 SUSE 主导的实现在 openSUSE/SLES 上原生可用。它跟 kpatch 的一个关键区别在于一致性处理策略(per-task 迁移 vs 全局 stop_machine),SUSE 侧重在补丁生效过程中对正在运行任务的处理更平滑。对普通用户而言,两者的使用体验差别不大。

通用结论

  • RHEL/CentOS/Rocky/Alma → 用 kpatch。
  • SUSE/openSUSE → 用 kGraft(命令叫 klp)。
  • Debian/Ubuntu → 没有官方的 livepatch 产品线(Canonical 的 Livepatch 是 Ubuntu Pro 付费服务),源码级可用但需要自己编译,个人站长要谨慎评估。

本文以最通用的 kpatch 为主线演示,它也是国内 VPS 里最常遇到的场景(大量站长用 Rocky/Alma)。

三、实战:从零构建并打上第一个内核热补丁

假设我们的场景是:手上有一台 Rocky Linux 9 的 VPS,内核出了个 CVE,官方代码仓库还没出正式 RPM,我们想自己做一个热补丁先顶上。目标函数就用一个最简单的例子——修改 /proc/sys/kernel/ 里某个被写死的逻辑,或者干脆改一个打印语句来验证机制跑通。

步骤 1:准备环境

热补丁编译需要内核源码树和编译工具链,缺一不可:

# 安装 kpatch 构建工具链
dnf install -y kpatch kpatch-build

# 安装与当前运行内核完全匹配的源码和头文件
dnf install -y kernel-devel-$(uname -r) kernel-source-$(uname -r)

# 确认版本严格一致,不一致编译会失败
uname -r
rpm -q kernel-devel

这里有个几乎人人会踩的坑:kernel-devel 的版本必须和你当前 uname -r 完全一致。如果你刚 dnf update 过,运行中的内核可能是旧的,而 kernel-devel 已经装成新的了。这时候要么 dnf install kernel-devel-$(uname -r) 精确指定,要么先重启让运行内核跟上。

步骤 2:准备源码补丁

kpatch 需要一个标准的 diff/patch 格式源码补丁。假设我们要修的函数在源码文件 fs/example.c 里,先准备一份改动前后的 diff:

--- a/fs/example.c
+++ b/fs/example.c
@@ -42,7 +42,7 @@ static int example_func(void)
     /* 有漏洞的判断:返回值被忽略 */
-    ret = check_something();
+    if (check_something() < 0)
+        return -EINVAL;
     return 0;
 }

注意代码块里的 < 是必须保留的语法,写的时候直接用符号即可。

步骤 3:编译热补丁

把上面的 diff 存成 fix.patch,然后跑编译。kpatch 会对比原内核和打过补丁的源码,只提取变化的函数生成模块:

kpatch-build -s /usr/src/kernels/$(uname -r) \
             -t vmlinux \
             fix.patch
# 输出成功后会得到一个 .ko 文件,例如 livepatch-fix-example.ko

编译过程通常需要几分钟,会消耗不少 CPU 和内存。个人 VPS 如果只有 1 核 1G,建议本地编译好再传到服务器,或者临时升级配置。

四、加载、查看与验证热补丁

热补丁编译出来的就是一个内核模块,加载它即可生效:

# 先 dry-run 检查兼容性,不真正加载
kpatch load livepatch-fix-example.ko

# 查看当前所有热补丁及状态
kpatch list

# 典型输出:
# Loaded patch modules:
# livepatch_fix_example [enabled]
#
# Installed patch modules:
# livepatch-fix-example.ko (4.18.0-xxx)

这里要理解两个状态词的含义,否则很容易误判「补丁没生效」:

  • enabled:调用点已被改写,新代码已经在跑了,补丁实际已经在起作用。
  • active:所有旧代码引用都已安全回收,可以卸载了。从 enabled 到 active 可能要等一段时间,因为要给正在旧函数里执行的任务跑完的机会。

系统层面的统一视图可以用 sysfs 查看:

cat /sys/kernel/livepatch/livepatch_fix_example/enabled
cat /sys/kernel/livepatch/livepatch_fix_example/transition
cat /proc/sys/kernel/ftrace_enabled

验证补丁真的生效,最可靠的办法是构造触发条件,观察行为是否与修复后一致。不要只看 kpatch list 说 enabled 就下结论——状态位和实际路径都改对了,才算真正修复。

五、卸载与回滚:留好退路

如果补丁打上去导致异常(比如某个服务莫名挂了),要能快速回滚:

# 卸载指定热补丁
kpatch unload livepatch-fix-example.ko

# 或者卸载所有
kpatch unload --all

由于热补丁是累积堆叠的,卸载必须从最后一个开始。如果你打了 A、B,想卸 A 留下 B,是不允许的——必须先把 B 卸了。规划补丁顺序时要想到这一点,别把关键修复压在栈底。

别忘了把加载做成开机自启,否则下次重启后热补丁就没了(虽然重启本身会让正式内核补丁生效,但在正式内核升级到位之前这段时间你是裸奔的):

# 启用服务,让已加载的补丁在每次启动时自动恢复
systemctl enable --now kpatch.service
kpatch list   # 确认服务里登记了补丁

六、五个必须知道的限制和坑

  • 只能改函数,不能改数据结构布局。如果漏洞修复涉及结构体字段增减,热补丁做不了,必须等正式内核。这是最大的限制。
  • 内联函数和内联展开的函数无法热补丁。编译器把函数内联进调用方之后,就没有独立的函数入口可以挂钩了。
  • 静态函数、标记 __init 的函数、以及很多编译期常量化处理过的代码,改不了。构建时 kpatch 会直接报错告诉你哪些函数不支持。
  • 和第三方内核模块冲突。如果你加载了 DKMS 编译的驱动(比如某些 RAID、虚拟化增强模块),热补丁改动了它们依赖的内核符号,可能导致模块加载失败或系统不稳定。热补丁前先 lsmod 盘一遍。
  • 热补丁是权宜之计,不是终点。它让你在下一个维护窗口到达之前不至于裸奔,但正式内核 RPM 出来后,还是要安排一次正常重启升级,把热补丁卸掉。

七、给个人站长的落地建议

把上面的内容收束成一套可执行的工作流:

  • 建立「内核 CVE 监控」习惯。订阅发行版的安全公告邮件,或用 dnf updateinfo list security 定期扫一遍,明确哪些漏洞需要重启、哪些有热补丁可用。
  • 优先用官方热补丁。能订阅 RHEL 的 kpatch-stream 就直接用官方编译好的,别自己折腾源码编译,稳定性差别很大。
  • 自己编译只用在「官方还没有、漏洞又很急」的场景。而且一定要先在测试机上验证过再上生产。
  • 无论打不打热补丁,都要保留正常的重启维护窗口。热补丁是缓冲,不是免除重启的理由。
  • 打好补丁立刻记录。用 kpatch list 的输出存档,配合变更日志,下次排查问题时有据可查。

内核热补丁这个能力,过去是大型运维团队的专属,现在随着发行版把它标准化,个人站长也能用上了。它的价值不在于让你永远不重启,而在于把「要不要为了一个安全补丁牺牲业务连续性」这个非此即彼的选择题,变成了一道可以两全的题。掌握它,你的服务器在安全响应上就多了一张可打的牌。

Last modification:October 14th, 2026 at 08:24 pm

Leave a Comment