背景
每天例行刷新 Github Issue 列表的时候,发现问题 The computer totally freezes on TestKfunc
,这个问题 报告了在运行 cilium/ebpf 库中的 TestKfunc 测试时,计算机会完全冻结,无法响应鼠标和键盘操作,只能通过强制关机来恢复。
问题描述
这个 issue (#1781) 报告了在运行 cilium/ebpf 库中的 TestKfunc 测试时,计算机会完全冻结,无法响应鼠标和键盘操作,只能通过强制关机来恢复。
环境信息
- 操作系统:Ubuntu 24.04.2 LTS
- 内核版本:6.8.0-45-generic
- Go 版本:go1.24.2 linux/amd64
- cilium/ebpf:main 分支
复现步骤
执行以下命令即可复现问题:
go test -exec sudo -run TestKfunc .
问题分析
根据 issue 提交者 @bogushevich 的分析,问题出在测试使用的 BPF 程序中,具体位于 testdata/kfunc.c 文件中:
__section("tc") int call_kfunc(void *ctx) {
char buf[1];
struct nf_conn *conn = bpf_skb_ct_lookup(ctx, (void *)buf, 0, (void *)buf, 0);
if (conn) {
bpf_ct_release(conn);
}
return 1;
}
核心问题在于 bpf_skb_ct_lookup
函数的调用。这个函数的参数配置不正确,特别是:
- 第四个参数
(void *)buf
被传递给了opts
参数 - 第五个参数
0
被传递给了opts_len
参数
内核源码分析
查看内核源码(net/netfilter/nf_conntrack_bpf.c),我们可以看到:
if (!opts || !bpf_tuple || opts->reserved[0] || opts->reserved[1] || opts_len != NF_BPF_CT_OPTS_SZ)
return ERR_PTR(-EINVAL);
当 opts_len
为 0 时,内核在检查 opts_len
是否等于 NF_BPF_CT_OPTS_SZ
之前,就先尝试读取 opts->reserved[0]
和 opts->reserved[1]
,这些读取操作可能会越界访问内存,导致内核崩溃。
临时解决方案
issue 提交者发现修改代码如下后,测试能够正常运行:
#define NF_BPF_CT_OPTS_SZ 12
__section("tc") int call_kfunc(void *ctx) {
char buf[1];
char opts[NF_BPF_CT_OPTS_SZ];
struct nf_conn *conn = bpf_skb_ct_lookup(ctx, (void *)buf, 0, (void *)opts, sizeof(opts));
if (conn) {
bpf_ct_release(conn);
}
return 1;
}
关键变化:
- 定义了
NF_BPF_CT_OPTS_SZ
常量为 12 - 创建了正确大小的
opts
缓冲区 - 传递了正确的缓冲区大小
sizeof(opts)
结论
这是一个严重的内核 bug,当 BPF 程序调用 bpf_skb_ct_lookup
函数并传递大小为 0 的 opts 参数时,会导致内核在访问 opts
结构体成员之前没有正确验证其大小,从而引发内存越界访问,最终导致系统冻结。
排查
复现问题
部署 kdump
安装必要工具
# 添加 dbgsym 仓库
sudo apt-get install ubuntu-dbgsym-keyring
echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse" | \
sudo tee /etc/apt/sources.list.d/ddebs.list
# 更新仓库
sudo apt-get update
# 安装 crash 工具和必要的调试符号
sudo apt install linux-headers-$(uname -r)
sudo apt install linux-tools-common linux-tools-generic
sudo apt install linux-image-$(uname -r)-dbgsym
sudo apt install crash kdump-tools
sudo apt install linux-image-6.8.0-59-generic-dbgsym
# 如果找不到 dbgsym 包,可能需要添加 dbgsym 仓库
sudo apt install linux-image-$(uname -r)-dbg
配置
在 /etc/default/kdump-tools 文件中添加以下行:
KDUMP_CMDLINE=`echo "root=$(findmnt -fno SOURCE /) $(cat /proc/cmdline)"`
检查
要应用 kdump 配置,需重启节点。节点恢复后,在计算节点上运行‘kdump-config status’命令以检查 Kdump 是否就绪,下方提供示例输出供参考:
# kdump-config status
current state : ready to kdump
分析 kdump 压缩转储文件
文件目录为:/var/crash/202505151113 ,根据文件信息,dump.202505151113
是一个 Flattened kdump 格式的压缩转储文件,系统信息如下:
- Linux 系统
- 内核版本:6.8.0-59-generic
- Ubuntu 发行版
- x86_64 架构
# 直接分析压缩转储文件
sudo crash /usr/lib/debug/boot/vmlinux-6.8.0-59-generic dump.202505151113
确认崩溃点
crash> bt
PID: 10718 TASK: ffff94dfc1392f40 CPU: 0 COMMAND: "ebpf.test"
#0 [ffffbb198d02f5a0] machine_kexec at ffffffffadab9ada
#1 [ffffbb198d02f608] __crash_kexec at ffffffffadc31713
#2 [ffffbb198d02f6d0] crash_kexec at ffffffffadc33164
#3 [ffffbb198d02f6e0] oops_end at ffffffffada5bb11
#4 [ffffbb198d02f708] page_fault_oops at ffffffffadad5500
#5 [ffffbb198d02f768] kernelmode_fixup_or_oops at ffffffffadad5669
#6 [ffffbb198d02f790] __bad_area_nosemaphore at ffffffffadad583d
#7 [ffffbb198d02f7e8] bad_area_nosemaphore at ffffffffadad5986
#8 [ffffbb198d02f7f8] do_kern_addr_fault at ffffffffadad5a2b
#9 [ffffbb198d02f820] exc_page_fault at ffffffffaec447f4
#10 [ffffbb198d02f850] asm_exc_page_fault at ffffffffaee00bc7
[exception RIP: bpf_test_run+315]
RIP: ffffffffae9af0cb RSP: ffffbb198d02f908 RFLAGS: 00010246
RAX: 0000000000000001 RBX: ffffbb19882b5000 RCX: 0000000000000000
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
RBP: ffffffffea02f9b8 R8: 0000000000000000 R9: 0000000000000000
R10: ffff94dfc027d400 R11: 0000000000000000 R12: 0000000000000001
R13: ffff94dfc274f000 R14: ffffbb198d02f9f8 R15: 0000000000000000
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
#11 [ffffbb198d02f9c0] bpf_prog_test_run_skb at ffffffffae9b0005
#12 [ffffbb198d02fa40] __sys_bpf at ffffffffadd1b19a
#13 [ffffbb198d02fb18] __x64_sys_bpf at ffffffffadd1bb7a
#14 [ffffbb198d02fb28] x64_sys_call at ffffffffada07248
#15 [ffffbb198d02fb38] do_syscall_64 at ffffffffaec3c86f
#16 [ffffbb198d02ff50] entry_SYSCALL_64_after_hwframe at ffffffffaee00130
RIP: 0000000000407bee RSP: 000000c002767b18 RFLAGS: 00000202
RAX: ffffffffffffffda RBX: 000000000000000a RCX: 0000000000407bee
RDX: 0000000000000050 RSI: 000000c002767cd8 RDI: 000000000000000a
RBP: 000000c002767b58 R8: 0000000000000000 R9: 0000000000000000
R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000111
R13: 000000c00325a240 R14: 000000c000007dc0 R15: 0000000000000000
ORIG_RAX: 0000000000000141 CS: 0033 SS: 002b
- 崩溃进程:
ebpf.test
(PID 10718) - 崩溃类型: 内核页面错误 (page fault)
- 崩溃位置:
bpf_test_run+315
- 崩溃堆栈: 经过
bpf_prog_test_run_skb
和__sys_bpf
系统调用
确认崩溃日志
查看内核日志中的崩溃信息:
crash> log | grep -A 20 -B 20 "Oops"
[ 1436.442779] BUG: unable to handle page fault for address: ffffffffea02f930
[ 1436.442825] #PF: supervisor read access in kernel mode
[ 1436.442839] #PF: error_code(0x0000) - not-present page
[ 1436.442846] PGD 4ca41067 P4D 4ca41067 PUD 4ca43067 PMD 0
[ 1436.442856] Oops: 0000 [#1] PREEMPT SMP NOPTI
[ 1436.442870] CPU: 0 PID: 10718 Comm: ebpf.test Kdump: loaded Not tainted 6.8.0-59-generic #61-Ubuntu
[ 1436.442883] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014
[ 1436.442896] RIP: 0010:bpf_test_run+0x13b/0x350
[ 1436.442907] Code: 00 48 89 85 78 ff ff ff eb 55 0f 1f 44 00 00 48 8b 53 30 48 8b b5 70 ff ff ff 4c 89 ef e8 ad 20 f9 ff 41 89 c4 66 90 45 89 26 <48> 8b bd 78 ff ff ff be 00 02 00 00 e8 24 fc 15 ff 4c 8b 45 80 8b
[ 1436.442935] RSP: 0018:ffffbb198d02f908 EFLAGS: 00010246
[ 1436.442945] RAX: 0000000000000001 RBX: ffffbb19882b5000 RCX: 0000000000000000
[ 1436.442989] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
[ 1436.443061] RBP: ffffffffea02f9b8 R08: 0000000000000000 R09: 0000000000000000
[ 1436.443120] R10: ffff94dfc027d400 R11: 0000000000000000 R12: 0000000000000001
[ 1436.443152] R13: ffff94dfc274f000 R14: ffffbb198d02f9f8 R15: 0000000000000000
[ 1436.443165] FS: 000070d1377fe6c0(0000) GS:ffff94e0f1600000(0000) knlGS:0000000000000000
[ 1436.443180] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1436.443201] CR2: ffffffffea02f930 CR3: 0000000106676004 CR4: 0000000000770ef0
[ 1436.443243] PKRU: 55555554
[ 1436.443313] Call Trace:
[ 1436.443395] <TASK>
[ 1436.443472] ? show_regs+0x6d/0x80
[ 1436.443495] ? __die+0x24/0x80
[ 1436.443514] ? page_fault_oops+0x99/0x1b0
[ 1436.443563] ? kernelmode_fixup_or_oops.isra.0+0x69/0x90
获取到关键错误信息:
[ 1436.442779] BUG: unable to handle page fault for address: ffffffffea02f930
[ 1436.442825] #PF: supervisor read access in kernel mode
[ 1436.442839] #PF: error_code(0x0000) - not-present page
确认了:
- 访问的无效地址:
ffffffffea02f930
- 该地址正是
RBP-0x88
(ffffffffea02f9b8-0x88
)
查找崩溃指令
查看崩溃指令:
crash> dis bpf_test_run+315
0xffffffffae9af0cb <bpf_test_run+315>: mov -0x88(%rbp),%rdi
发现崩溃在访问栈变量时发生。
查看周围指令上下文:
crash> dis bpf_test_run+300 20
0xffffffffae9af0bc <bpf_test_run+300>: mov %ebp,%edi
0xffffffffae9af0be <bpf_test_run+302>: call 0xffffffffae941170 <bpf_dispatcher_xdp_func>
0xffffffffae9af0c3 <bpf_test_run+307>: mov %eax,%r12d
0xffffffffae9af0c6 <bpf_test_run+310>: xchg %ax,%ax
0xffffffffae9af0c8 <bpf_test_run+312>: mov %r12d,(%r14)
0xffffffffae9af0cb <bpf_test_run+315>: mov -0x88(%rbp),%rdi # 崩溃点
0xffffffffae9af0d2 <bpf_test_run+322>: mov $0x200,%esi
0xffffffffae9af0d7 <bpf_test_run+327>: call 0xffffffffadb0ed00 <__local_bh_enable_ip>
0xffffffffae9af0dc <bpf_test_run+332>: mov -0x80(%rbp),%r8
0xffffffffae9af0e0 <bpf_test_run+336>: mov -0x94(%rbp),%edx
0xffffffffae9af0e6 <bpf_test_run+342>: lea -0x74(%rbp),%rcx
0xffffffffae9af0ea <bpf_test_run+346>: mov $0x1,%esi
0xffffffffae9af0ef <bpf_test_run+351>: lea -0x60(%rbp),%rdi
0xffffffffae9af0f3 <bpf_test_run+355>: call 0xffffffffae9aee60 <bpf_test_timer_continue>
0xffffffffae9af0f8 <bpf_test_run+360>: test %al,%al
0xffffffffae9af0fa <bpf_test_run+362>: je 0xffffffffae9af216 <bpf_test_run+646>
0xffffffffae9af100 <bpf_test_run+368>: lea -0x48(%rbp),%rax
0xffffffffae9af104 <bpf_test_run+372>: mov %rax,-0x70(%rbp)
0xffffffffae9af108 <bpf_test_run+376>: addl $0x200,%gs:0x51685275(%rip)
0xffffffffae9af113 <bpf_test_run+387>: test %r15b,%r15b
确定执行流程:
- 调用
bpf_dispatcher_xdp_func
执行 BPF 程序 - 程序返回并存储返回值
- 崩溃发生在准备调用
__local_bh_enable_ip
清理函数时
对照内核版本对应代码,交叉比对下崩溃点:
static int bpf_test_run(struct bpf_prog *prog, void *ctx, u32 repeat,
u32 *retval, u32 *time, bool xdp)
{
...
do {
run_ctx.prog_item = &item;
local_bh_disable();
if (xdp)
*retval = bpf_prog_run_xdp(prog, ctx);
else
*retval = bpf_prog_run(prog, ctx);
local_bh_enable();
} while (bpf_test_timer_continue(&t, 1, repeat, &ret, time));
...
}
根据上述分析,崩溃点在执行完毕 BPF 程序之后发生
确认 BPF 程序
查看进程打开的文件:
crash> files 10718
PID: 10718 TASK: ffff94dfc1392f40 CPU: 0 COMMAND: "ebpf.test"
ROOT: / CWD: /workload/ebpf
FD FILE DENTRY INODE TYPE PATH
0 ffff94dfc5986a00 ffff94dfc06a6300 ffff94dfc15e03b0 CHR /dev/null
1 ffff94dfc5986400 ffff94decaddda80 ffff94df188c8f00 FIFO
2 ffff94dfc5986400 ffff94decaddda80 ffff94df188c8f00 FIFO
3 ffff94dec11db800 ffff94e05f1c1cc0 ffff94dfc05f2080 UNKN [eventpoll]
4 ffff94dec11db600 ffff94df402e7180 ffff94e0ce5baa80 FIFO
5 ffff94dec11db500 ffff94df402e7180 ffff94e0ce5baa80 FIFO
7 ffff94ded16b3e00 ffff94e063c4bf00 ffff94dfc05f2080 UNKN bpf-prog
确认进程打开了 BPF 程序文件:
- 文件描述符 7 为 "bpf-prog" 类型
进一步查看文件结构:
crash> struct file ffff94ded16b3e00
struct file {
...
f_inode = 0xffff94dfc05f2080,
f_op = 0xffffffffaf0393c0 <bpf_prog_fops>, # 确定是 bpf prog
f_version = 0,
f_security = 0xffff94dfc7042240,
private_data = 0xffffbb19882b5000,
f_ep = 0x0,
f_mapping = 0xffff94dfc05f21f8,
f_wb_err = 0,
f_sb_err = 0
}
关键发现:
- 文件操作是
bpf_prog_fops
- private_data 指针 (0xffffbb19882b5000) 与 RBX 寄存器值匹配
确认 BPF 程序名称:
crash> p ((struct bpf_prog *)0xffffbb19882b5000)->aux->name
$1 = "call_kfunc\000\000\000\000\000"
确认 BPF 程序代码:
extern struct nf_conn *bpf_skb_ct_lookup(struct __sk_buff *, struct bpf_sock_tuple *, uint32_t, struct bpf_ct_opts *, uint32_t) __ksym;
extern void bpf_ct_release(struct nf_conn *) __ksym;
__section("tc") int call_kfunc(void *ctx) {
char buf[1];
struct nf_conn *conn = bpf_skb_ct_lookup(ctx, (void *)buf, 0, (void *)buf, 0);
if (conn) {
bpf_ct_release(conn);
}
return 1;
}
确认崩溃原因
查看可能的相关函数:
crash> sym bpf_skb_ct_lookup
ffffffffc1233740 (T) bpf_skb_ct_lookup [nf_conntrack]
crash> dis bpf_skb_ct_lookup
0xffffffffc1233760 <bpf_skb_ct_lookup+32>: call 0xffffffffc1233450 <__bpf_nf_ct_lookup>
0xffffffffc1233765 <bpf_skb_ct_lookup+37>: cmp $0xfffffffffffff000,%rax
0xffffffffc123376b <bpf_skb_ct_lookup+43>: ja 0xffffffffc123379a <bpf_skb_ct_lookup+90>
0xffffffffc123376d <bpf_skb_ct_lookup+45>: mov -0x8(%rbp),%rbx
关键发现在于参数检查顺序错误:
crash> dis 0xffffffffc1233450
# 先检查 opts 指针是否为 NULL
0xffffffffc1233499 <__bpf_nf_ct_lookup+73>: test %rcx,%rcx
0xffffffffc123349c <__bpf_nf_ct_lookup+76>: je 0xffffffffc1233554 <__bpf_nf_ct_lookup+260>
# 不等待验证大小,直接访问 opts->reserved[1] (偏移+10)
0xffffffffc12334b1 <__bpf_nf_ct_lookup+97>: cmpw $0x0,0xa(%rcx)
0xffffffffc12334b6 <__bpf_nf_ct_lookup+102>: mov %rcx,%rbx
0xffffffffc12334b9 <__bpf_nf_ct_lookup+105>: jne 0xffffffffc1233554 <__bpf_nf_ct_lookup+260>
# 先访问内存后,才检查 opts_len 是否为 12
0xffffffffc12334bf <__bpf_nf_ct_lookup+111>: cmp $0xc,%r8d
0xffffffffc12334c3 <__bpf_nf_ct_lookup+115>: jne 0xffffffffc1233554 <__bpf_nf_ct_lookup+260>
# 继续访问 opts->l4proto (偏移+8)
0xffffffffc12334cb <__bpf_nf_ct_lookup+123>: movzbl 0x8(%rcx),%edx
-
cmpw $0x0,0xa(%rcx)
:- 这是一个16位比较指令,比较内存中的一个字(word)值与0
%rcx
寄存器此时保存着opts
参数的地址0xa(%rcx)
表示访问%rcx
所指内存位置往后偏移10字节的内容- 这对应结构体中的
opts->reserved[1]
字段,按照源代码是一个16位保留字段
-
mov %rcx,%rbx
:- 简单地将
%rcx
寄存器的值(即opts
指针)复制到%rbx
寄存器 - 这是为了保存
opts
指针供后续代码使用,同时避免修改原始参数
- 简单地将
-
jne 0xffffffffc1233554
:- 这是条件跳转指令,基于前面比较的结果
- 如果
opts->reserved[1]
不等于0,则跳转到错误处理路径(地址 0xffffffffc1233554) - 该错误处理路径会返回
-EINVAL
(0xffffffffffffffea) 错误码
问题代码:
if (!opts || !bpf_tuple || opts->reserved[0] || opts->reserved[1] ||
opts_len != NF_BPF_CT_OPTS_SZ)
return ERR_PTR(-EINVAL)
问题所在:
这段代码的危险之处在于:它在第一条指令就直接访问了 opts
结构体偏移量为10字节的位置(opts->reserved[1]
),但直到第四条指令(cmp $0xc,%r8d
)才检查 opts_len
是否大于或等于12字节!
在我们的崩溃案例中:
opts
参数是一个只有1字节大小的小缓冲区opts_len
参数为0- 当执行到
cmpw $0x0,0xa(%rcx)
指令时,它试图读取buf+10
位置的内容 - 这已经远远超出了分配的1字节缓冲区,导致内存访问越界
- 导致页面错误和内核崩溃
这是一个典型的"先访问后验证"编程错误,正确的顺序应该是先验证 opts_len
是否足够,再访问结构体的任何成员。
修复代码:
if (!opts || !bpf_tuple)
return ERR_PTR(-EINVAL);
if (!(opts_len == NF_BPF_CT_OPTS_SZ || opts_len == 12))
return ERR_PTR(-EINVAL);
if (opts_len == NF_BPF_CT_OPTS_SZ) {
if (opts->reserved[0] || opts->reserved[1] || opts->reserved[2])
return ERR_PTR(-EINVAL);
} else {
if (opts->ct_zone_id)
return ERR_PTR(-EINVAL);
}
解决方案
- 修改 BPF 程序,为 opts 参数分配足够的内存:
#define NF_BPF_CT_OPTS_SZ 12
char opts[NF_BPF_CT_OPTS_SZ];
bpf_skb_ct_lookup(ctx, (void *)buf, 0, (void *)opts, sizeof(opts));
- 内核版本升级到 >= 6.11 ,修复 patch 为 lore.kernel.org/bpf/f3721e8…