梅开二度,再次向内核 bpf 子系统贡献 patch。

此次修复的是 x64 平台的一个 bug,该 bug 会导致 tailcall 陷入无限循环,最终导致宕机。

tailcall 无限循环

tailcall infinite loop

如上图,触发条件如下:

  1. 对 subprog 进行 fentry
  2. subprog 里的 tailcall 调用 prog。

其中,fentry 破坏了基于 RAX 的 tail_call_cnt 传递机制,导致 tail_call_cnt 未能传递到 subprog。从而,在 subprog 里进行 tailcall 时,MAX_TAIL_CALL_CNT 的判断就失效了。

修复 tailcall 无限循环

修复的思路就是:保证在 fentry 的 trampoline 里遵循 tail_call_cnt 传递机制。

确认 subprog 里有 tailcall

fentry 里的 trampoline 需要知道当前 trace 的 subprog 里有 tailcall,才能决定是否需要遵循 tail_call_cnt 传递机制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index dbba2b8060176..18e673c0ac159 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -19774,6 +19774,9 @@ static int check_attach_btf_id(struct bpf_verifier_env *env)
     if (!tr)
         return -ENOMEM;

+   if (tgt_prog && tgt_prog->aux->tail_call_reachable)
+       tr->flags = BPF_TRAMP_F_TAIL_CALL_CTX;
+
     prog->aux->dst_trampoline = tr;
     return 0;
 }

trampoline 里传递 tail_call_cnt

trampoline 里需要区分 fentryfexit 两种情况,分别进行处理。

 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
diff --git a/arch/x86/net/bpf_jit_comp.c b/arch/x86/net/bpf_jit_comp.c
index bcca1c9b9a027..2846c21d75bfa 100644
--- a/arch/x86/net/bpf_jit_comp.c
+++ b/arch/x86/net/bpf_jit_comp.c
@@ -1022,6 +1022,10 @@ static void emit_shiftx(u8 **pprog, u32 dst_reg, u8 src_reg, bool is64, u8 op)

 #define INSN_SZ_DIFF (((addrs[i] - addrs[i - 1]) - (prog - temp)))

+/* mov rax, qword ptr [rbp - rounded_stack_depth - 8] */
+#define RESTORE_TAIL_CALL_CNT(stack)               \
+   EMIT3_off32(0x48, 0x8B, 0x85, -round_up(stack, 8) - 8)
+
// ...
@@ -2404,6 +2406,7 @@ int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image, void *i
      *                     [ ...        ]
      *                     [ stack_arg2 ]
      * RBP - arg_stack_off [ stack_arg1 ]
+    * RSP                 [ tail_call_cnt ] BPF_TRAMP_F_TAIL_CALL_CTX
      */

     /* room for return value of orig_call or fentry prog */
@@ -2468,6 +2471,8 @@ int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image, void *i
     else
         /* sub rsp, stack_size */
         EMIT4(0x48, 0x83, 0xEC, stack_size);
+   if (flags & BPF_TRAMP_F_TAIL_CALL_CTX)
+       EMIT1(0x50);        /* push rax */
     /* mov QWORD PTR [rbp - rbx_off], rbx */
     emit_stx(&prog, BPF_DW, BPF_REG_FP, BPF_REG_6, -rbx_off);

@@ -2520,9 +2525,15 @@ int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image, void *i
         restore_regs(m, &prog, regs_off);
         save_args(m, &prog, arg_stack_off, true);

+       if (flags & BPF_TRAMP_F_TAIL_CALL_CTX)
+           /* Before calling the original function, restore the
+            * tail_call_cnt from stack to rax.
+            */
+           RESTORE_TAIL_CALL_CNT(stack_size);
+
         if (flags & BPF_TRAMP_F_ORIG_STACK) {
-           emit_ldx(&prog, BPF_DW, BPF_REG_0, BPF_REG_FP, 8);
-           EMIT2(0xff, 0xd0); /* call *rax */
+           emit_ldx(&prog, BPF_DW, BPF_REG_6, BPF_REG_FP, 8);
+           EMIT2(0xff, 0xd3); /* call *rbx */
         } else {
             /* call original function */
             if (emit_rsb_call(&prog, orig_call, prog)) {
@@ -2573,7 +2584,12 @@ int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image, void *i
             ret = -EINVAL;
             goto cleanup;
         }
-   }
+   } else if (flags & BPF_TRAMP_F_TAIL_CALL_CTX)
+       /* Before running the original function, restore the
+        * tail_call_cnt from stack to rax.
+        */
+       RESTORE_TAIL_CALL_CNT(stack_size);
+
     /* restore return value of orig_call or fentry prog back into RAX */
     if (save_ret)
         emit_ldx(&prog, BPF_DW, BPF_REG_0, BPF_REG_FP, -8);

新增 selftests

针对 fentry/fexit subprog、subprog 里有 tailcall 的情况,新增 2 个 selftest。

小结

该问题不仅仅出现在 x86 平台,也出现在 s390x 平台。

所以,在 s390x 平台也修复了该 BUG 后,向内核 bpf 社区提交的 patch 便被合并了。