最近,在上线 XDP 网关新版本的时候,触发了 v5.15 内核的一个 tailcall BUG,导致了一个很奇怪的问题:在一个 XDP 程序的 subprog 里,调用了 bpf_tail_call(),但是 tailcall 的目标程序并没有被执行,而是直接返回了。

在开发环境复现了之后,使用了 fentry 来调试这个问题,最终发现了问题的根源。

TL;DR 该问题由于在调用 subprog 前加载 tail_call_cnt 时,使用了错误的偏移量,导致了 subprog 使用了错误的 tail_call_cnt,从而导致了 bpf_tail_call() 的目标程序没有被执行。该问题已由 v5.19 内核 commit bpf, x86: Fix tail call count offset calculation on bpf2bpf call 修复了;且在较新的 v5.15 内核里,已移植了该 commit。

背景知识

想要调试该问题,需要比较多的背景知识,特别是 tailcall 和 fentry 的实现细节:

下文将展示使用 fentry 调试该 tailcall BUG 的过程。

问题复现

既然已经知道了问题的根因,使用 kprobe 来复现问题吧(复现起来方便一些):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// SPDX-License-Identifier: GPL-2.0 OR Apache-2.0
/* Copyright 2024 Leon Hwang */
#include "vmlinux.h"
#include "bpf_helpers.h"
#include "bpf_tracing.h"

/* make it look to compiler like value is read and written */
#define __sink(expr) asm volatile("" : "+g"(expr))

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u32);
} array_map SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u32);
} jmp_table SEC(".maps");

static __noinline int
my_tailcall(void *ctx)
{
    volatile int retval = 0;
    __sink(retval);
    void *map = (void *)(unsigned long)&jmp_table;
    __u32 slot = 0;

    bpf_tail_call_static(ctx, map, slot);

    bpf_printk("tailcaller, after bpf_tail_call(). should not print this log\n");

    return retval;
}

SEC("kprobe/tcp_connect")
int entry(struct pt_regs *ctx)
{
    // __u64 key = 0; /* Consume 8 bytes of stack space. */
    __u32 key = 0; /* Consume 4 bytes of stack space. */
    __u32 *value;

    value = bpf_map_lookup_elem(&array_map, &key);
    bpf_printk("tailcaller, before bpf_tail_call(): %d\n", value ? *value: 0);

    return my_tailcall(ctx);
}

SEC("kprobe/tcp_connect")
int tailcallee(struct pt_regs *ctx)
{
    bpf_printk("tailcallee, should print this log\n");
    return 0;
}

char __license[] SEC("license") = "GPL";

PS: 这段源代码在 tailcall issues 仓库里。

tailcall-issues 依赖 capstone-engine,所以需要先安装 capstone-engineapt install libcapstone-dev

然后,用 make 静态编译 libcapstone-dev


不过,还需要一个有问题的 v5.15 内核的 VM 环境:

  1. 使用 Ubuntu 22.04 的官方镜像创建一个 VM。
  2. 安装 bpftool: apt install -y linux-tools-$(uname -r)
  3. 查看内核版本:uname -r
  4. 降级内核到 v5.15.0-051500-generic: 到 v5.15 Mainline Test 下载 deb 包,然后使用 dpkg -i *.deb 安装;重启。
  5. 降级后,复用 bpftool: alias bpftool=/usr/lib/linux-tools-$(前任内核版本)/bpftool

将有问题的 bpf 程序跑起来后:

1
2
3
4
5
6
7
8
9
# ./tailcall-issues --check-invalid-offset --wait
2024/11/24 09:47:50 Current kernel has invalid offset issue
2024/11/24 09:47:50 Ctrl+C to stop ..

# echo "在另一个窗口里执行:"
# curl -s https://google.com/
# cat /sys/kernel/debug/tracing/trace_pipe
            curl-1464    [000] dN..1   368.031489: bpf_trace_printk: tailcaller, before bpf_tail_call(): 0
            curl-1464    [000] dN..1   368.031494: bpf_trace_printk: tailcaller, after bpf_tail_call(). should not print this log

可以看到,“tailcallee, should print this log” 没有被打印出来,而且 “tailcaller, after bpf_tail_call(). should not print this log” 被打印出来了;说明了 tailcall 的目标程序没有被执行。

如果 entry 里使用 __u64 key,则不会触发该 BUG:

1
2
3
# cat /sys/kernel/debug/tracing/trace_pipe
            curl-1484    [001] d...1  1017.417270: bpf_trace_printk: tailcaller, before bpf_tail_call(): 0
            curl-1484    [001] d...1  1017.417308: bpf_trace_printk: tailcallee, should print this log

调试过程

为什么 bpf_tail_call() 没有执行呢?可能的原因有 2 个:

  1. jmp_table 里没有对应的 bpf 程序。
  2. 运行时的 tail_call_cnt >= 33, aka MAX_TAIL_CALL_CNT

在复现的 bpf 程序里,原因 1 是不可能发生的了。所以,原因 2 是如何发生的呢?

画图分析一下栈上的内存布局:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  +-------+ FP of entry
  |  ...  |
  |  tcc  |
  |  reg  |
  |  reg  |
  |  rip  | IP of entry
  |  rbp  | FP of entry
  +-------+ FP of my_tailcall
  |  ...  |
  |  tcc  |
  |  reg  |
  |  reg  |
  +-------+ RSP of my_tailcall

是否有办法读取到栈上的那 2 个 tail_call_cnt 呢?

有,使用 fentry 吧。

如果对 my_tailcall() subprog 直接使用 fentry,会发生 “观测者效应”:eBPF Talk: 一行代码两行泪

所以,可以在 bpf_tail_call() 后面加一个 subprog,然后对这个 subprog 使用 fentry

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static __noinline void
my_tailcall_inspect(void *ctx, void *map, __u32 slot)
{
    bpf_printk("my_tailcall_inspect, ctx: %016llx, map: %016llx, slot: %u\n", ctx, map, slot);
}

static __noinline int
my_tailcall(void *ctx)
{
    void *map = (void *)(unsigned long)&jmp_table;
    volatile int retval = 0;
    __u32 slot = 0;

    __sink(retval);

    bpf_tail_call_static(ctx, map, slot);
    my_tailcall_inspect(ctx, map, slot);

    bpf_printk("tailcaller, after bpf_tail_call(). should not print this log\n");

    return retval;
}

因此避免 “观测者效应” 的发生,并且能够读取到 my_tailcall() subprog 栈上的 tail_call_cnt

my_tailcall_inspect() 使用 fentry,栈的内存布局、以及 fentry bpf 程序如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/* With current inspect function, the stack layout is:
 *
 * +-------+ FP of entry
 * |  ...  |
 * |  tcc  |
 * |  reg  |
 * |  reg  |
 * |  rip  | IP of entry
 * |  rbp  | FP of entry
 * +-------+ FP of my_tailcall
 * |  ...  |
 * |  tcc  |
 * |  reg  |
 * |  reg  |
 * |  rip  | IP of my_tailcall
 * |  rip  | IP of my_tailcall_inspect
 * |  rbp  | FP of my_tailcall
 * +-------+ FP of trampoline
 * |  ...  |
 * |  arg  |
 * |  arg  |
 * |  arg  | <- ctx of tailcall_inspect
 * |  rip  | IP of trampoline
 * |  rbp  | FP of trampoline
 * +-------+ FP of tailcall_inspect
 * |  ...  |
 * +-------+ RSP of tailcall_inspect
 */

struct tramp_stack {
    __u64 fp;
    __u64 rip;
    __u64 args[3];
};

struct my_tailcall_stack {
    __u64 fp;
    __u64 rip[2];
    __u64 regs[2];
    __u64 tcc;
};

struct fentry_stack {
    __u64 fp;
    __u64 rip;
    __u64 regs[2];
    __u64 tcc;
};

SEC("fentry/my_tailcall_inspect")
int BPF_PROG(tailcall_inspect, void *tgt_ctx, void *prog_array, __u32 index)
{
    struct bpf_array *arr = (struct bpf_array *) prog_array;
    struct my_tailcall_stack my_tailcall;
    struct fentry_stack fentry;
    struct tramp_stack tramp;
    struct bpf_prog *prog;
    __u32 prog_id;
    __u64 fp;

    asm volatile ("%[fp] = r10" : [fp] "+r"(fp) :);
    bpf_probe_read_kernel(&prog, sizeof(prog), (const void *) (arr->ptrs + index));
    BPF_CORE_READ_INTO(&prog_id, prog, aux, id);

    bpf_probe_read_kernel(&tramp, sizeof(tramp), (const void *) fp);
    bpf_probe_read_kernel(&my_tailcall, sizeof(my_tailcall), (const void *) tramp.fp);
    bpf_probe_read_kernel(&fentry, sizeof(fentry), (const void *) my_tailcall.fp);

    bpf_printk("tailcall_inspect: ctx=%016llx prog_array=%016llx index=%d\n",
               tgt_ctx, prog_array, index);
    bpf_printk("tailcall_inspect: prog=%016llx prog_id=%d\n", prog, prog_id);
    bpf_printk("tailcall_inspect: trampoline: rip=%016llx args=%016llx %016llx\n",
               tramp.rip, tramp.args[0], tramp.args[1]);
    bpf_printk("tailcall_inspect: my_tailcall: rip=%016llx %016llx regs=%016llx\n",
               my_tailcall.rip[0], my_tailcall.rip[1], my_tailcall.regs[0]);
    bpf_printk("tailcall_inspect: my_tailcall: tcc=%016llx\n", my_tailcall.tcc);
    bpf_printk("tailcall_inspect: fentry: rip=%016llx tcc=%016llx\n",
               fentry.rip, fentry.tcc);

    return 0;
}

PS: 源代码在 learn-by-example tailcall-inspect

其中,通过 asm volatile ("%[fp] = r10" : [fp] "+r"(fp) :) 读取当前栈帧的 FP。

在 bpf 的 ISA 里,r10 是 FP 寄存器。


如何确认该栈回溯的方式是正确的呢?

通过栈上的 IP 确认一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ./tailcall-inspect --prog 25
2024/11/24 12:27:30 Tracing my_tailcall_inspect ..
2024/11/24 12:27:30 cat /sys/kernel/debug/tracing/trace_pipe to see the output
2024/11/24 12:27:30 Press Ctrl+C to stop

# curl -s https://google.com/
# cat /sys/kernel/debug/tracing/trace_pipe
            curl-1361    [002] d...1   285.054433: bpf_trace_printk: tailcaller, before bpf_tail_call(): 0
            curl-1361    [002] d...2   285.054460: bpf_trace_printk: tailcall_inspect: ctx=ffffb31d8365fbe0 prog_array=ffff8bcb02baa800 index=0
            curl-1361    [002] d...2   285.054461: bpf_trace_printk: tailcall_inspect: prog=ffffb31d8067f000 prog_id=26
            curl-1361    [002] d...2   285.054462: bpf_trace_printk: tailcall_inspect: trampoline: rip=ffffffffc09a203d args=ffffb31d8365fbe0 ffffb31d8365fbe0
            curl-1361    [002] d...2   285.054463: bpf_trace_printk: tailcall_inspect: my_tailcall: rip=ffffffffc097f1fd ffffffffc097d7da regs=0000000000000000
            curl-1361    [002] d...2   285.054463: bpf_trace_printk: tailcall_inspect: my_tailcall: tcc=9b106db700000000
            curl-1361    [002] d...2   285.054464: bpf_trace_printk: tailcall_inspect: fentry: rip=ffffffffc097b182 tcc=0000000000000000
            curl-1361    [002] d...1   285.054465: bpf_trace_printk: my_tailcall_inspect, ctx: ffffb31d8365fbe0, map: ffff8bcb02baa800, slot: 0
            curl-1361    [002] d...1   285.054465: bpf_trace_printk: tailcaller, after bpf_tail_call(). should not print this log

直接看 struct my_tailcall_stack 里读取到的 2 个 IP 吧:

  • ffffffffc097f1fd: 是 my_tailcall() 的 IP。
  • ffffffffc097d7da: 是 my_tailcall_inspect() 的 IP。

使用 bpflbr 确认一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# ./bpflbr -p 25 -d
; bpf/invalid-offset.c:48:0 int entry(struct pt_regs *ctx)
0xffffffffc097b100: 0f 1f 44 00 00      nopl    (%rax, %rax)
0xffffffffc097b105: 31 c0               xorl    %eax, %eax
0xffffffffc097b107: 55                  pushq   %rbp
0xffffffffc097b108: 48 89 e5            movq    %rsp, %rbp
0xffffffffc097b10b: 48 81 ec 08 00 00 00    subq    $8, %rsp
0xffffffffc097b112: 50                  pushq   %rax
0xffffffffc097b113: 53                  pushq   %rbx
0xffffffffc097b114: 41 55               pushq   %r13
...
; bpf/invalid-offset.c:57:12 return my_tailcall(ctx);
0xffffffffc097b173: 48 89 df            movq    %rbx, %rdi
0xffffffffc097b176: 48 8b 85 f4 ff ff ff    movq    -0xc(%rbp), %rax
0xffffffffc097b17d: e8 ea 25 00 00      callq   0xffffffffc097d76c  ; my_tailcall+0x0 bpf/invalid-offset.c:31 [bpf]
; bpf/invalid-offset.c:57:5 return my_tailcall(ctx);
0xffffffffc097b182: 41 5d               popq    %r13
0xffffffffc097b184: 5b                  popq    %rbx
0xffffffffc097b185: c9                  leave
0xffffffffc097b186: c3                  retq

; bpf/invalid-offset.c:31:0 my_tailcall(void *ctx)
0xffffffffc097d76c: 0f 1f 44 00 00      nopl    (%rax, %rax)
0xffffffffc097d771: 66 90               nop
0xffffffffc097d773: 55                  pushq   %rbp
0xffffffffc097d774: 48 89 e5            movq    %rsp, %rbp
0xffffffffc097d777: 48 81 ec 08 00 00 00    subq    $8, %rsp
0xffffffffc097d77e: 50                  pushq   %rax
0xffffffffc097d77f: 53                  pushq   %rbx
0xffffffffc097d780: 41 55               pushq   %r13
...
0xffffffffc097d7c6: e9 c0 0a 02 00      jmp 0xffffffffc099e28b  ; tailcallee+0xb bpf/invalid-offset.c:63 [bpf]
; bpf/invalid-offset.c:40:5 my_tailcall_inspect(ctx, map, slot);
0xffffffffc097d7cb: 48 89 df            movq    %rbx, %rdi
0xffffffffc097d7ce: 48 8b 85 f4 ff ff ff    movq    -0xc(%rbp), %rax
0xffffffffc097d7d5: e8 1e 1a 00 00      callq   0xffffffffc097f1f8  ; my_tailcall_inspect+0x0 bpf/invalid-offset.c:25 [bpf]
; bpf/invalid-offset.c:42:5 bpf_printk("tailcaller, after bpf_tail_call(). should not print this log\n");
0xffffffffc097d7da: 48 bf 5b f1 d9 0d cb 8b ff ff   movabsq $0xffff8bcb0dd9f15b, %rdi
0xffffffffc097d7e4: be 3e 00 00 00      movl    $0x3e, %esi
0xffffffffc097d7e9: 48 8b 85 f4 ff ff ff    movq    -0xc(%rbp), %rax
0xffffffffc097d7f0: e8 8b 8d 87 da      callq   0xffffffff9b1f6580  ; bpf_trace_printk+0x0
; bpf/invalid-offset.c:44:5 return retval;
0xffffffffc097d7f5: 8b 45 fc            movl    -4(%rbp), %eax
0xffffffffc097d7f8: 41 5d               popq    %r13
0xffffffffc097d7fa: 5b                  popq    %rbx
0xffffffffc097d7fb: c9                  leave
0xffffffffc097d7fc: c3                  retq

; bpf/invalid-offset.c:25:0 my_tailcall_inspect(void *ctx, void *map, __u32 slot)
0xffffffffc097f1f8: e8 03 2e 02 00      callq   0xffffffffc09a2000  ; bpf_trampoline_107374182403_0+0x0
0xffffffffc097f1fd: 66 90               nop
0xffffffffc097f1ff: 55                  pushq   %rbp
0xffffffffc097f200: 48 89 e5            movq    %rsp, %rbp
0xffffffffc097f203: 48 89 fa            movq    %rdi, %rdx
; bpf/invalid-offset.c:27:5 bpf_printk("my_tailcall_inspect, ctx: %016llx, map: %016llx, slot: %u\n", ctx, map, slot);
0xffffffffc097f206: 48 bf 99 f1 d9 0d cb 8b ff ff   movabsq $0xffff8bcb0dd9f199, %rdi
0xffffffffc097f210: be 3b 00 00 00      movl    $0x3b, %esi
0xffffffffc097f215: 48 b9 00 a8 ba 02 cb 8b ff ff   movabsq $0xffff8bcb02baa800, %rcx
0xffffffffc097f21f: 45 31 c0            xorl    %r8d, %r8d
0xffffffffc097f222: e8 59 73 87 da      callq   0xffffffff9b1f6580  ; bpf_trace_printk+0x0
; bpf/invalid-offset.c:28:1 }
0xffffffffc097f227: c9                  leave
0xffffffffc097f228: c3                  retq
  • ffffffffc097f1fd: 是 my_tailcall() 的 IP:
    • 0xffffffffc097f1f8: e8 03 2e 02 00 callq 0xffffffffc09a2000 ; bpf_trampoline_107374182403_0+0x0
    • 0xffffffffc097f1fd: 66 90 nop
  • ffffffffc097d7da: 是 my_tailcall_inspect() 的 IP。
    • 0xffffffffc097d7d5: e8 1e 1a 00 00 callq 0xffffffffc097f1f8 ; my_tailcall_inspect+0x0 bpf/invalid-offset.c:25 [bpf]
    • 0xffffffffc097d7da: 48 bf 5b f1 d9 0d cb 8b ff ff movabsq $0xffff8bcb0dd9f15b, %rdi

通过对比栈上的 IP 信息,确认了该栈回溯的方式是正确的。


从上面的输出可以看到,my_tailcall() 里的 tail_call_cnt9b106db7,而不是 0

然而,entry() 里的 tail_call_cnt0

那么,my_tailcall() 里的 tail_call_cnt 是怎么来的呢?

灵机一动,看一下对应的源代码吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// https://elixir.bootlin.com/linux/v5.15/source/arch/x86/net/bpf_jit_comp.c
/*
 * Emit x86-64 prologue code for BPF program.
 * bpf_tail_call helper will skip the first X86_TAIL_CALL_OFFSET bytes
 * while jumping to another program
 */
static void emit_prologue(u8 **pprog, u32 stack_depth, bool ebpf_from_cbpf,
                          bool tail_call_reachable, bool is_subprog)
{
    u8 *prog = *pprog;

    ...
    /* sub rsp, rounded_stack_depth */
    if (stack_depth)
        EMIT3_off32(0x48, 0x81, 0xEC, round_up(stack_depth, 8));
    if (tail_call_reachable)
        EMIT1(0x50);         /* push rax */
    *pprog = prog;
}

static int do_jit(struct bpf_prog *bpf_prog, int *addrs, u8 *image,
                  int oldproglen, struct jit_context *ctx, bool jmp_padding)
{
    case BPF_JMP | BPF_CALL:
    func = (u8 *) __bpf_call_base + imm32;
    if (tail_call_reachable) {
        EMIT3_off32(0x48, 0x8B, 0x85,
                    -(bpf_prog->aux->stack_depth + 8));
        if (!imm32 || emit_call(&prog, func, image + addrs[i - 1] + 7))
            return -EINVAL;
    } else {
        if (!imm32 || emit_call(&prog, func, image + addrs[i - 1]))
            return -EINVAL;
   }
   break;
}

static void emit_bpf_tail_call_indirect(u8 **pprog, bool *callee_regs_used,
                                        u32 stack_depth)
{
    int tcc_off = -4 - round_up(stack_depth, 8);
    ...
}

static void emit_bpf_tail_call_direct(struct bpf_jit_poke_descriptor *poke,
                                      u8 **pprog, int addr, u8 *image,
                                      bool *callee_regs_used, u32 stack_depth)
{
    int tcc_off = -4 - round_up(stack_depth, 8);
    ...
}

问题就出现在这里了:

  1. emit_prologuestack_depth 做了 round_up 处理。
  2. do_jit 里 load tail_call_cnt 时,却没有做 round_up 处理。
  3. emit_bpf_tail_call_indirectemit_bpf_tail_call_direct 里,tcc_off 也都做了 round_up 处理。

如果 stack_depth 不是 8 的倍数,那么 tail_call_cnt 就会被加载到错误的位置。

调整一下上面 tailcall_inspect 源代码,从 entry 栈上多读取 8 个字节;调整后的输出如下:

1
2
3
# cat /sys/kernel/debug/tracing/trace_pipe
            curl-1383    [003] d...2  1657.930226: bpf_trace_printk: tailcall_inspect: my_tailcall: tcc=1868d3c000000000
            curl-1383    [003] d...2  1657.930227: bpf_trace_printk: tailcall_inspect: fentry: rip=ffffffffc097b182 tcc=0000000000000000 var=000000001868d3c0

可以看出,my_tailcall() 里的 tail_call_cnt0x1868d3c,对应 entry() 栈上未初始化的 4 个字节 0x1868d3c0

BUG 发生过程

先看一下 entry() 栈的内存分布:

1
2
3
4
5
6
7
  +-------+ FP of entry
  |  ...  |
  |  var  |
  |  tcc  |
  |  reg  |
  |  reg  |
  +-------+ RSP of my_tailcall

vartcc 展开来看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  +------+ top of var
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  |  18  |
  |  68  |
  |  d3  |
  |  c0  |
  +------+ separator
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  +------+ bottom of tcc

已知,entry() 的真实 stack_depth 是 4,所以在 callq 前从栈上加载 tail_call_cnt 时:

  • 48 8b 85 f4 ff ff ff movq -0xc(%rbp), %rax

读取的栈内存是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  +------+ top of var
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  |  18  | <--+
  |  68  |    |
  |  d3  |    |
  |  c0  |    |
  +------+    | load tail_call_cnt
  |  00  |    |
  |  00  |    |
  |  00  |    |
  |  00  | <--+
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  +------+ bottom of tcc

接着,my_tailcall() 里将 tail_call_cnt push 到栈上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  +------+ top of tcc
  |  18  |
  |  68  |
  |  d3  |
  |  c0  |
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  +------+ bottom of tcc

接着,在 bpf_tail_call() 时,读取的 tail_call_cnt 是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  +------+ top of tcc
  |  18  | <--+
  |  68  |    | load these
  |  d3  |    |  4 bytes
  |  c0  | <--+
  |  00  |
  |  00  |
  |  00  |
  |  00  |
  +------+ bottom of tcc

因而,0x1868d3c 大于 MAX_TAIL_CALL_CNT,所以 bpf_tail_call() 的剩余部分就被跳过了,即使 bpf_tail_call() 的目标程序是存在的。

BUG 发生概率

因为真实生效的是一段未初始化的栈内存,所以这个 BUG 的发生概率较高,而不是 100% 发生。

BUG 规避办法

解决该 BUG 的首选方案是升级内核,不然就得想办法规避一下了。

首先,确认当前内核是否存在该问题、或者指定 bpf 程序是否存在该问题:

1
2
3
# ./tailcall-issues --check-invalid-offset --prog 25
2024/11/24 14:02:44 Current kernel has invalid offset issue
2024/11/24 14:02:44 BPF program (id=25 name=entry) has invalid offset issue

确认存在该问题后,可以通过以下方式规避:

  • stack_depth 调整为 8 的倍数:比如多消耗 4 个字节的栈空间。
    • volatile __u32 __pad = 0; __sink(__pad);

总结

通过 fentry,可以读取到栈上的 tail_call_cnt,从而用来调试 tailcall BUG。

然而,调试该 BUG 的前提是知道 tailcall 和 fentry 的实现细节,并且能正确地推断出栈上的内存布局和 BUG 的发生过程。