最近,在上线 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-engine
:apt install libcapstone-dev
。
然后,用 make
静态编译 libcapstone-dev
。
不过,还需要一个有问题的 v5.15 内核的 VM 环境:
- 使用 Ubuntu 22.04 的官方镜像创建一个 VM。
- 安装
bpftool
: apt install -y linux-tools-$(uname -r)
。
- 查看内核版本:
uname -r
。
- 降级内核到 v5.15.0-051500-generic: 到 v5.15 Mainline Test 下载 deb 包,然后使用
dpkg -i *.deb
安装;重启。
- 降级后,复用
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 个:
jmp_table
里没有对应的 bpf 程序。
- 运行时的
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_cnt
是 9b106db7
,而不是 0
。
然而,entry()
里的 tail_call_cnt
是 0
。
那么,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);
...
}
|
问题就出现在这里了:
- 在
emit_prologue
里 stack_depth
做了 round_up
处理。
- 在
do_jit
里 load tail_call_cnt
时,却没有做 round_up
处理。
- 在
emit_bpf_tail_call_indirect
和 emit_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_cnt
是 0x1868d3c
,对应 entry()
栈上未初始化的 4 个字节 0x1868d3c0
。
BUG 发生过程
先看一下 entry()
栈的内存分布:
1
2
3
4
5
6
7
|
+-------+ FP of entry
| ... |
| var |
| tcc |
| reg |
| reg |
+-------+ RSP of my_tailcall
|
将 var
和 tcc
展开来看:
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 的发生过程。