eBPF verifier验证器与编译器inline内联

笔者在二月份编写eCapture的GoTls密钥捕获功能时,遇到了一个小bug,就是在UBUNTU 21.04的Linux上运行时,遇到eBPF verifier的报错,无法启动,笔者后来通过启用always inline解决了这个问题。

我觉得,这是一个比较好的编写eBPF的技巧案例,决定把详细排查过程、原因、修复方法一起分享出来,奈何事情比较忙,一直没时间,拖延了一个多月,这个周末,咬牙立Flag,整理出来,分享给大家。

报错详情

报错的eCapture代码是aa0b86ea,在ubuntu 20.04上,使用make nocore编译运行,则一切正常。若在ubuntu 21.04以及以后版本编译运行,则出现如下错误:

tls_2023/03/24 19:44:20 EBPFProbeGoTLS  BPF bytecode filename:user/bytecode/gotls_kern.o
tls_2023/03/24 19:44:21 EBPFProbeGoTLS  module run failed, [skip it]. error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted) , couldn't load eBPF programs, cs:&{map[events:PerfEventArray(keySize=0, valueSize=0, maxEntries=0, flags=0) gte_context:LRUHash(keySize=8, valueSize=4136, maxEntries=2048, flags=0) gte_context_gen:Array(keySize=4, valueSize=4136, maxEntries=1, flags=0)] map[gotls_text_register:0xc0003f6630 gotls_text_stack:0xc0003f66c0] 0xc000072f40 LittleEndian}

当然,把ubuntu 20.04上编译的版本拿到21.04上运行,也是如上错误。

关键信息

很显然,重点的错误是下面这句error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted)

eCapture是使用了gojue/ebpfmanager这个纯Go的ebpf类库来管理,里面是使用了 cilium/ebpf类库实现syscall eBPF调用。通过分析定位,在相应的代码处打日志,则会看到详细的报错信息,如下:

invalid argument
Validating gotls_text() func#1...
Arg#0 type PTR in gotls_text() is not supported yet.
processed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

继续跟进debug,确定报错信息来自cilium/ebpf类库中errors包内的323行,变量 logBuf ,当调用sys.ProgLoad加载eBPF程序时报错。

// internal/errors.go  #line 323
    var err2 error
    if !opts.LogDisabled && opts.LogLevel == 0 {
        logBuf = make([]byte, opts.LogSize)
        attr.LogLevel = LogLevelBranch
        attr.LogSize = uint32(len(logBuf))
        attr.LogBuf = sys.NewSlicePointer(logBuf)

        _, err2 = sys.ProgLoad(attr)
    }

重点的报错内容为Arg#0 type PTR in gotls_text() is not supported yet.,而这段报错是来自内核的BPF验证器。

在内核代码中,这段报错是来自 btf_prepare_func_args函数,功能为将函数的BTF转换为bpf_reg_state,无法转换BTF时,则返回EINVAL。即本次bug的现象。

该函数触发本次报错的代码如下:

/* Convert BTF function arguments into verifier types.
 * Only PTR_TO_CTX and SCALAR are supported atm.
 */
for (i = 0; i < nargs; i++) {
  t = btf_type_by_id(btf, args[i].type);
  while (btf_type_is_modifier(t))
    t = btf_type_by_id(btf, t->type);
  if (btf_type_is_int(t) || btf_type_is_enum(t)) {
    reg[i + 1].type = SCALAR_VALUE;
    continue;
  }
  if (btf_type_is_ptr(t) &&
      btf_get_prog_ctx_type(log, btf, t, prog_type, i)) {
    reg[i + 1].type = PTR_TO_CTX;
    continue;
  }
  bpf_log(log, "Arg#%d type %s in %s() is not supported yet.\n",
    i, btf_kind_str[BTF_INFO_KIND(t->info)], tname);
  return -EINVAL;
}

如注释内容,将BTF函数参数转换为验证器类型。目前仅支持PTR_TO_CTXSCALAR

问题在哪里?

很好理解,看gotls_text这个函数代码,Arg#0struct pt_regs *ctx,而这个参数实际上是PTR_TO_CTX这个类型,但依然没通过验证器的验证。

int gotls_text(struct pt_regs *ctx, bool is_register_abi) {
   // ...
}

SEC("uprobe/gotls_text_register")
int gotls_text_register(struct pt_regs *ctx) { 
  return gotls_text(ctx, true);
}

SEC("uprobe/gotls_text_stack")
int gotls_text_stack(struct pt_regs *ctx) {
  return gotls_text(ctx, false);
}

显然,内核编译的 btf_vmlinux是不正确的,eBPF验证器无法确定上下文指针的类型,并将struct pt_regs *ctx作为PTR,而非真正的PTR_TO_CTX

btf_vmlinux为什么损坏,我不知道。继续排查下去会花费我很多时间、精力。所以,暂时跳过查找根因这个方向,转向跳过验证器方法。

不过,在笔者今天写这篇文章时,发现cilium社区这两天也遇到这个问题了,在bpf: inline test functions with ctx as input #24662 这个issue里,开发者也认为这是内核编译时遇到错误了,大概是paholegcc 的问题。

探索解决思路

正确的解决办法应该是定位出btf_vmlinux损坏的原因,但这事会特别复杂、特别漫长,涉及UBUNTU的内核编译问题排查,重现成本会很高。为此,我想尝试找到规避这个问题的方法,比如不进入BTF的BPF verifier验证器。

为什么会被eBPF verifier验证

继续通过内核函数 btf_prepare_func_args 继续跟进的话,会看到是do_check_subprogs()函数在验证所有ebpf progs。

/* Verify all global functions in a BPF program one by one based on their BTF.
 * All global functions must pass verification. Otherwise the whole program is rejected.
 * Consider:
 * int bar(int);
 * int foo(int f)
 * {
 *    return bar(f);
 * }
 * int bar(int b)
 * {
 *    ...
 * }
 * foo() will be verified first for R1=any_scalar_value. During verification it
 * will be assumed that bar() already verified successfully and call to bar()
 * from foo() will be checked for type match only. Later bar() will be verified
 * independently to check that it's safe for R1=any_scalar_value.
 */
static int do_check_subprogs(struct bpf_verifier_env *env) {
  // ...
}

正如函数的注释所写,根据BTF对BPF程序中的所有全局函数逐个进行验证。所有全局函数都必须通过验证,否则整个程序将被拒绝。

在验证过程中,会先验证foo(),验证时会假定bar()已经通过了验证,并且只会检查从foo()调用bar()的类型匹配性。之后,会单独验证bar(),以检查当R1=any_scalar_value时是否安全。

回到我的代码写法,即gotls_text_register调用了gotls_text函数,所以,gotls_text函数也会被eBPF 验证器再验证一次。

灵机一动

既然是独立的函数会被验证,那么我不拆分函数呗,但考虑到gotls_text函数是公用部分,编写上还是要剥离出来,降低维护成本。 显然,用编译器inline内联呗。

为此,我在ubuntu 20.04、21.04上分别验证了no_inlineinline两种模式的情况,并导出BPF字节码文件的汇编码逐一验证。

no_inline

ubuntu 20.04编译

没有自动inline,但不报错,可正常运行。

把程序放到21.04上,则报错

tls_2023/03/27 16:18:57 EBPFProbeGoTLS  module run failed, [skip it]. error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted) , couldn't load eBPF programs,

ubuntu 21.04编译

没有自动inline,但报错,运行失败

tls_2023/03/27 16:18:57 EBPFProbeGoTLS  module run failed, [skip it]. error:error:program gotls_text_register: load program: invalid argument: Arg#0 type PTR in gotls_text() is not supported yet. (2 line(s) omitted) , couldn't load eBPF programs,

将21.04上编译的版本,运行报错,但放到20.04上运行,则正常。

将20.04上编译、运行正常的程序,放到21.04上,也依旧会报错。

可以确定是内核BPF验证器增加了相关验证。

[bpf-next,3/6] bpf: Introduce function-by-function verification

inline

关闭inline内联

gotls_text 关闭用inline内联后,可以看到反汇编后的机器码:

0000000000000248 <gotls_text>:
      73:   bf 16 00 00 00 00 00 00 r6 = r1
      74:   15 02 1f 00 00 00 00 00 if r2 == 0 goto +31 <LBB3_2>
      75:   b7 01 00 00 28 00 00 00 r1 = 40
      76:   bf 63 00 00 00 00 00 00 r3 = r6
      // ...
     104:   85 00 00 00 71 00 00 00 call 113
     105:   05 00 1f 00 00 00 00 00 goto +31 <LBB3_3>

gotls_text_register调用者的反汇编:

Disassembly of section uprobe/gotls_text_register:

0000000000000000 <gotls_text_register>:
       0:   b7 02 00 00 01 00 00 00 r2 = 1
       1:   85 10 00 00 ff ff ff ff call -1
       2:   b7 00 00 00 00 00 00 00 r0 = 0
       3:   95 00 00 00 00 00 00 00 exit

编译器自动inline内联

gotls_text 内函数较小时,则被clang inline,比如注释掉函数后面几行。

int gotls_text(struct pt_regs *ctx, bool is_register_abi) {
    s32 record_type, len;
    const char *str;
    void * record_type_ptr;
    void * len_ptr;
    record_type_ptr = (void *)go_get_argument(ctx, is_register_abi, 2);
    bpf_probe_read_kernel(&record_type, sizeof(record_type), (void *)&record_type_ptr);
    str = (void *)go_get_argument(ctx, is_register_abi, 3);
    len_ptr = (void *)go_get_argument(ctx, is_register_abi, 4);
    bpf_probe_read_kernel(&len, sizeof(len), (void *)&len_ptr);

    debug_bpf_printk("gotls_text record_type:%d\n", record_type);
    if (record_type != recordTypeApplicationData) {
        return 0;
    }
    // ... 以下代码都注视掉
    return 0;
}

从BPF 汇编可以看到,虽然gotls_text函数依然存在,但是gotls_text_register已经内联了他的内容。

llvm-objdump -d gotls_kern.o

gotls_kern.o:   file format elf64-bpf

Disassembly of section .text:

0000000000000248 <gotls_text>:
      73:   bf 16 00 00 00 00 00 00 r6 = r1
      74:   15 02 1e 00 00 00 00 00 if r2 == 0 goto +30 <LBB3_2>
      75:   b7 01 00 00 28 00 00 00 r1 = 40
      76:   bf 63 00 00 00 00 00 00 r3 = r6
      77:   0f 13 00 00 00 00 00 00 r3 += r1
      78:   bf a1 00 00 00 00 00 00 r1 = r10
      79:   07 01 00 00 f8 ff ff ff r1 += -8
      80:   b7 02 00 00 08 00 00 00 r2 = 8
      81:   85 00 00 00 71 00 00 00 call 113
      82:   79 a1 f8 ff 00 00 00 00 r1 = *(u64 *)(r10 - 8)
      ...
Disassembly of section uprobe/gotls_text_register:

0000000000000000 <gotls_text_register>:
       0:   bf 16 00 00 00 00 00 00 r6 = r1
       1:   b7 01 00 00 28 00 00 00 r1 = 40
       2:   bf 63 00 00 00 00 00 00 r3 = r6
       3:   0f 13 00 00 00 00 00 00 r3 += r1
       4:   bf a1 00 00 00 00 00 00 r1 = r10
       5:   07 01 00 00 f8 ff ff ff r1 += -8
       6:   b7 02 00 00 08 00 00 00 r2 = 8
       7:   85 00 00 00 71 00 00 00 call 113
       8:   79 a1 f8 ff 00 00 00 00 r1 = *(u64 *)(r10 - 8)
       9:   7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
      10:   bf a1 00 00 00 00 00 00 r1 = r10
      11:   07 01 00 00 f4 ff ff ff r1 += -12
      12:   bf a3 00 00 00 00 00 00 r3 = r10
      ...
手动inline

可以通过__always_inline 关键字,手动让编译器对这块代码进行内联。

static __always_inline int gotls_text(struct pt_regs *ctx, bool is_register_abi) {
    s32 record_type, len;
    const char *str;
  // ...
}

最终的反汇编情况,与 自动inline一样,并且可以支持更多行数的代码。

解决方法

经过前面的验证,解决办法就是__always_inline关键字启用编译器内联,跳过eBPF verifier的验证。

总结

经过笔者的验证,可以看出几个问题

  • ubuntu 20.04的5.4内核中,eBPF verifier未启用 对被调用子函数的参数类型判断。
  • ubuntu 21.04的5.11内核中,eBPF verifier启用了对被调用子函数的参数类型判断。
  • ubuntu 21.04的5.11内核中,btf_vmlinux文件有问题,大概编译内核时遇到错误了。
  • clang会对较短的子函数进行自动inline内联,具体的行数阈值我也不清楚。

编译器内联inline能很好的规避eBPF verifier的检查,建议默认开启。

其他

另外,笔者在解决这个问题时,也搜到几个类似的案例,虽然报错信息、排查思路不一样,但都是通过启用__always_inline来解决的,供参考。

Always Use always_inline In BPF Programs

CI: ebpf unit tests fail with Ubuntu 20.04.3 kernel 5.11 #24051

[译] LLVM eBPF 汇编编程(2020)

知识共享许可协议CFC4N的博客CFC4N 创作,采用 署名—非商业性使用—相同方式共享 4.0 进行许可。基于https://www.cnxct.com上的作品创作。转载请注明转自:eBPF verifier验证器与编译器inline内联