第一百九十八章:实时补丁
大乘期涉及内核源码:
一
林小源离开 KVM 的工坊,来到一座灯火通明的医院。医院的走廊里弥漫着消毒水的气味,墙上贴满了各种告示——"CVE-2024-XXXX 已修复"、"安全补丁已部署"。
"你来得正好。"一个穿白大褂的中年医师从手术室里走出来,摘下口罩。他的面容疲惫但专注,手里拿着一块刻着函数名的金属片。
"你是……livepatch?"
"实时补丁。"医师说,"服务器不能停机,但安全漏洞必须修复。我的工作就是在不重启系统的情况下,替换有问题的函数。"
他把金属片递给林小源。那金属片的正面刻着 original_func,背面刻着 patched_func。
"原理很简单。"医师说,"每个内核函数的入口都有一个跳转点。我在跳转点上插入一条指令,让调用跳转到补丁函数,而不是原始函数。用的是 ftrace 的机制——ftrace 已经在函数入口埋了钩子,我只是借用一下。"
/*
* 实时补丁 (Live Patching):
*
* 为什么需要:
* 服务器不能重启
* 安全漏洞需要及时修复
* 减少停机时间
*
* 技术:
* kpatch (Red Hat)
* kGraft (SUSE)
* livepatch (内核主线)
*
* 原理:
* 替换函数指针
* 使用 ftrace 机制
* 在函数入口处跳转
*
* 约束:
* 不能修改数据结构
* 不能改变函数签名
* 需要兼容性
*
* 工具:
* kpatch-build — 构建补丁
* kpatch load — 加载补丁
*
* 安全性:
* 补丁签名
* 验证补丁来源
*/
/* 模拟函数补丁 */
typedef int (*func_ptr)(int);
/* 原始函数 */
int original_func(int x) {
return x * 2;
}
/* 补丁函数 */
int patched_func(int x) {
return x * 3;
}
/* 模拟函数指针表 */
func_ptr func_table[2] = {
original_func,
NULL
};
printf("=== 实时补丁 — 不重启更新 ===\n\n");
printf("实时补丁在不重启的情况下修复内核:\n\n");
printf("--- 原始函数 ---\n");
printf("func_table[0](5) = %d\n", func_table[0](5));
printf("\n--- 应用补丁 ---\n");
printf("替换函数指针:\n");
func_table[0] = patched_func;
printf("func_table[0](5) = %d\n", func_table[0](5));
printf("\n--- 实时补丁技术 ---\n");
printf("kpatch (Red Hat):\n");
printf(" 构建补丁模块\n");
printf(" 替换函数\n\n");
printf("kGraft (SUSE):\n");
printf(" per-task 策略\n");
printf(" 逐步迁移\n\n");
printf("livepatch (内核主线):\n");
printf(" 统一框架\n");
printf(" 融合 kpatch 和 kGraft\n\n");
printf("--- 原理 ---\n");
printf("使用 ftrace 机制:\n");
printf(" 在函数入口处跳转\n");
printf(" 跳转到补丁函数\n\n");
printf("实现:\n");
printf(" 注册 ftrace 回调\n");
printf(" 修改函数入口\n");
printf(" 跳转到新函数\n\n");
printf("--- 约束 ---\n");
printf("1. 不能修改数据结构:\n");
printf(" 数据结构已存在\n\n");
printf("2. 不能改变函数签名:\n");
printf(" 调用者期望相同接口\n\n");
printf("3. 需要兼容性:\n");
printf(" 新旧函数行为兼容\n\n");
printf("--- 使用方法 ---\n");
printf("1. 构建补丁:\n");
printf(" kpatch-build patch.patch\n\n");
printf("2. 加载补丁:\n");
printf(" kpatch load patch.ko\n\n");
printf("3. 查看状态:\n");
printf(" kpatch list\n\n");
printf("--- 安全性 ---\n");
printf("补丁签名:\n");
printf(" 验证补丁来源\n\n");
printf("模块验证:\n");
printf(" 检查模块完整性\n");#include <stdio.h>
#include <string.h>
/*
* 实时补丁 (Live Patching):
*
* 为什么需要:
* 服务器不能重启
* 安全漏洞需要及时修复
* 减少停机时间
*
* 技术:
* kpatch (Red Hat)
* kGraft (SUSE)
* livepatch (内核主线)
*
* 原理:
* 替换函数指针
* 使用 ftrace 机制
* 在函数入口处跳转
*
* 约束:
* 不能修改数据结构
* 不能改变函数签名
* 需要兼容性
*
* 工具:
* kpatch-build — 构建补丁
* kpatch load — 加载补丁
*
* 安全性:
* 补丁签名
* 验证补丁来源
*/
/* 模拟函数补丁 */
typedef int (*func_ptr)(int);
/* 原始函数 */
int original_func(int x) {
return x * 2;
}
/* 补丁函数 */
int patched_func(int x) {
return x * 3;
}
/* 模拟函数指针表 */
func_ptr func_table[2] = {
original_func,
NULL
};
int main() {
printf("=== 实时补丁 — 不重启更新 ===\n\n");
printf("实时补丁在不重启的情况下修复内核:\n\n");
printf("--- 原始函数 ---\n");
printf("func_table[0](5) = %d\n", func_table[0](5));
printf("\n--- 应用补丁 ---\n");
printf("替换函数指针:\n");
func_table[0] = patched_func;
printf("func_table[0](5) = %d\n", func_table[0](5));
printf("\n--- 实时补丁技术 ---\n");
printf("kpatch (Red Hat):\n");
printf(" 构建补丁模块\n");
printf(" 替换函数\n\n");
printf("kGraft (SUSE):\n");
printf(" per-task 策略\n");
printf(" 逐步迁移\n\n");
printf("livepatch (内核主线):\n");
printf(" 统一框架\n");
printf(" 融合 kpatch 和 kGraft\n\n");
printf("--- 原理 ---\n");
printf("使用 ftrace 机制:\n");
printf(" 在函数入口处跳转\n");
printf(" 跳转到补丁函数\n\n");
printf("实现:\n");
printf(" 注册 ftrace 回调\n");
printf(" 修改函数入口\n");
printf(" 跳转到新函数\n\n");
printf("--- 约束 ---\n");
printf("1. 不能修改数据结构:\n");
printf(" 数据结构已存在\n\n");
printf("2. 不能改变函数签名:\n");
printf(" 调用者期望相同接口\n\n");
printf("3. 需要兼容性:\n");
printf(" 新旧函数行为兼容\n\n");
printf("--- 使用方法 ---\n");
printf("1. 构建补丁:\n");
printf(" kpatch-build patch.patch\n\n");
printf("2. 加载补丁:\n");
printf(" kpatch load patch.ko\n\n");
printf("3. 查看状态:\n");
printf(" kpatch list\n\n");
printf("--- 安全性 ---\n");
printf("补丁签名:\n");
printf(" 验证补丁来源\n\n");
printf("模块验证:\n");
printf(" 检查模块完整性\n");
return 0;
}二
医师领着林小源走进手术室。手术台上躺着一个内核模块,模块的表面有一个小小的裂缝——一个安全漏洞。
"看好了。"医师拿起手术刀,"我不动原始代码。我只是在函数入口处插入一条跳转指令。"
他把手术刀伸向模块表面,在裂缝旁边精确地切了一个小口。然后他把那块刻着 patched_func 的金属片嵌入切口中。金属片的边缘与模块表面完美贴合,裂缝被覆盖了。
"用 ftrace 机制。"医师说,"ftrace 已经在每个函数入口埋了一个 NOP 指令。我把它改成跳转指令,跳转到补丁函数。原始函数还在那里,只是不被调用了。"
"不需要修改原始代码?"
"不需要。"医师收起手术刀,"原始函数的代码不变,只是入口被重定向了。这就是实时补丁的优雅之处——最小侵入,最大效果。"
三
林小源注意到手术室的墙上挂着一张长长的清单,上面列着各种限制。
"约束。"医师说,"实时补丁不是万能的。"
他指着清单:"第一,不能修改数据结构。数据结构已经在内存中了,你不能改变它的布局。第二,不能改变函数签名。调用者期望 func(int) 返回 int,你不能改成 func(int, int)。第三,新旧函数的行为必须兼容——你不能把一个返回成功值的函数改成返回错误值。"
"所以只能修复简单的 bug?"
"对。"医师点头,"复杂的修改——添加新字段、改变逻辑——还是得重启。但安全漏洞往往是简单的逻辑错误,实时补丁刚好够用。"
他从口袋里掏出一块小印章:"还有安全性。补丁必须签名,验证来源。你不能让随便一个人往内核里注入代码。kpatch-build 构建补丁,kpatch load 加载补丁,kpatch list 查看状态。每一步都要验证。"
林小源看着那张清单,明白了约束的意义——不是限制,是保障。没有这些约束,实时补丁本身就会成为安全漏洞。
道藏笔记
内核启示
实时补丁在不重启的情况下修复内核。
技术:
- kpatch (Red Hat)
- kGraft (SUSE)
- livepatch (内核主线)
原理:
- 使用 ftrace 机制
- 在函数入口处跳转
- 跳转到补丁函数
约束:
- 不能修改数据结构
- 不能改变函数签名
- 需要兼容性
安全性:
- 补丁签名
- 模块验证
实时补丁是修复——不重启,不停机。
实时补丁之试
服务器不能重启但要替换有问题的内核函数时,本章主线机制叫什么?