eBPF 问题排查-使用 crash 分析内核崩溃原因

0 阅读6分钟

背景

每天例行刷新 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 函数的调用。这个函数的参数配置不正确,特别是:

  1. 第四个参数 (void *)buf 被传递给了 opts 参数
  2. 第五个参数 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;
}

关键变化:

  1. 定义了 NF_BPF_CT_OPTS_SZ 常量为 12
  2. 创建了正确大小的 opts 缓冲区
  3. 传递了正确的缓冲区大小 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

确定执行流程:

  1. 调用 bpf_dispatcher_xdp_func 执行 BPF 程序
  2. 程序返回并存储返回值
  3. 崩溃发生在准备调用 __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
  1. cmpw $0x0,0xa(%rcx)

    • 这是一个16位比较指令,比较内存中的一个字(word)值与0
    • %rcx 寄存器此时保存着 opts 参数的地址
    • 0xa(%rcx) 表示访问 %rcx 所指内存位置往后偏移10字节的内容
    • 这对应结构体中的 opts->reserved[1] 字段,按照源代码是一个16位保留字段
  2. mov %rcx,%rbx

    • 简单地将 %rcx 寄存器的值(即 opts 指针)复制到 %rbx 寄存器
    • 这是为了保存 opts 指针供后续代码使用,同时避免修改原始参数
  3. 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);
	}

解决方案

  1. 修改 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));
  1. 内核版本升级到 >= 6.11 ,修复 patch 为 lore.kernel.org/bpf/f3721e8…