eBPF verifier常见错误整理 @龙蜥社区eBPF SIG

如今eBPF程序的编写,很多都是基于bcc或者bpftrace进行,也有开发者直接基于libbpf库进行,但是不管怎样,编写的xx.bpf.c程序,在加载到内核时,都必须经过内核的verifier校验器进行各种边界和内存检查,经常会碰到各种奇奇怪怪的 verifier 报错,导致 eBPF 程序加载失败。

有些错误,开发者可能要花费大量的时间去分析并修改程序,并祈祷程序能够加载成功。特别是在低版本的内核运行低版本Clang编译器编译的eBPF程序,错误提示非常糟糕,经常找不到出错点,这就大大增加了开发难度。

为此,本文梳理了一些常见的 eBPF verifier 报错,避免更多的人走弯路,写出能成功加载的 eBPF 程序。同时,本文通过讲解 eBPF verifier 检查原理及给出示例程序,来分析为什么 eBPF verifier 会报错,使读者能够知其然知其所以然,达到融会贯通。

简介

eBPF verifier 是一个位于内核的校验器,用于验证 eBPF 程序的安全性,保证 eBPF 程序不会破坏内核,导致内核崩溃。对于一个 eBPF 程序, verifier 会对其进行两次检查( first pass 和 second pass)(暂且这么翻译)。

  • 第一次检查通过 dfs 算法检查 eBPF 程序是否为有向无环图(DAG),也就是 eBPF 程序不能回跳,比如使用了 goto、for 循环、while 循环等则有可能导致第一次检查失败。
  • 第二次检查时,verifier 会遍历 eBPF 程序的每条指令,同时保存寄存器的类型、值域等状态信息。通过保存的寄存器状态信息,verifier 可以检查 eBPF 程序内存访问的安全性,比如数组是否越界、helper 函数参数类型是否匹配等等。

第一次检查

eBPF verifier 通过 dfs 算法检查程序是否为有向无环图(DAG)。在此阶段,以下几种情况的 eBPF 程序将会被 verifier 拒绝:

  • eBPF 程序指令数超过允许的最大值;
  • 存在环,即存在指令回跳;
  • 存在 unreachable 指令;
  • 非法 jump,如 jump 到 eBPF 程序范围之外。

其中,第 1 种场景在 4.19 版本内核很少遇见,因为从该版本开始,指令数限制为 1000000 条。第 3 和第 4 两种场景,大部分开发者很少遇见。因为开发者都是使用高级语言编程,由编译器负责生成相应指令,所以一般不会产生 unreachable 指令和非法 jump。但是如果直接使用 eBPF 指令来写 eBPF 程序则有可能遇到此类问题。

存在环

因为指令回跳会增加指令分析的复杂度,所以 verifier 直接禁止出现指令回跳。下面是一个使用 for 循环引入指令回跳的场景:

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
    int i;
    for (i=0;i<1000;i++)
        bpf_printk("%d\n", i);
    return 0;
}

上述eBPF代码在运行时,会报 back-edge from insn 12 to 2 错误。这个报错意思是 eBPF 程序的第 12 条指令跳转到第 2 条指令,形成会跳,即存在环。具体可以看下面的指令信息:

// bpftool
int tcp_sendmsg(struct pt_regs * ctx):
; int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
   0: (b7) r6 = 0
   1: (b7) r7 = 680997
; bpf_printk("%d\n", i);
   2: (63) *(u32 *)(r10 -4) = r7
   3: (bf) r1 = r10
; 
   4: (07) r1 += -4
; bpf_printk("%d\n", i);
   5: (b7) r2 = 4
   6: (bf) r3 = r6
   7: (85) call bpf_trace_printk#-57568
; for (i=0;i<1000;i++)
   8: (07) r6 += 1
   9: (bf) r1 = r6
  10: (67) r1 <<= 32
  11: (77) r1 >>= 32
; for (i=0;i<1000;i++)
  12: (55) if r1 != 0x3e8 goto pc-11
; int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
  13: (b7) r0 = 0
  14: (95) exit

对于该错误的一般解决方法是:在 for 循环前面添加 #pragma unroll,进行循环展开,避免指令回跳。

注意:在5.10内核版本是支持有限循环的,所以上述代码是可以在 5.10 内核正常运行。

第二次检查

verfier 第二次检查会遍历所有的分支,并记录寄存器状态。在此阶段,以下几种情况的 eBPF 程序将会被 verifier 拒绝:

  • 栈访问非法,如栈溢出、栈偏移为变量等;
  • helper 函数入参的参数类型不匹配;
  • 未做范围检查,可能导致内存访问越界;
  • 指针未对齐。

栈访问非法
栈访问也是开发者写代码经常碰到的问题。

1、栈限制512字节
因为 verifier 会保存栈内存的状态,所以栈的大小是有限的,目前是 512 字节。当栈内存大小超过 512 字节时,则会被 verifier 拒绝。

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
#define MAX_NUM (512/8)
    volatile u64 arr[MAX_NUM + 1] = {};
    arr[MAX_NUM] = 0xff;
    bpf_printk("%lld\n", arr[MAX_NUM]);
    return 0;
}

上述程序在编译阶段会报错:

Looks like the BPF stack limit of 512 bytes is exceeded. Please move large on stack variables into BPF per-cpu array map。

对于该错误,一般建议使用 map 来存储大数据。

注:因为是高级语言编程,所以这种容易检查出来的异常,编译器就直接报错了。如果直接使用 eBPF 指令,那么会由 verifier 拒绝程序的加载。

2、栈偏移仅支持常量
当访问栈时采用变量偏移,会导致无法推测寄存器的状态。所以 4.19 版本只支持常量偏移。下面是使用变量偏移的错误示例:

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
    u64 volatile arr[16] = {};
    arr[bpf_ktime_get_ns() & 0xf] = 0;
    return 0;
}

当执行该程序时,会报如下错误:

variable stack access var_off=(0x0; 0x78) off=-128 size=8-

对于该错误的一般解决方法是将 arr 数组保存到 map 里面。

注意:5.10内核已经支持变量类型的栈偏移。

3、helper 函数参数类型不匹配
verfier 会检查 eBPF 程序中所调用 helper 函数的参数类型,比如 bpf_map_lookup_elem helper 函数的参数类型约束定义如下:

const struct bpf_func_proto bpf_map_lookup_elem_proto = {
  .func    = bpf_map_lookup_elem,
  .gpl_only  = false,
  .pkt_access  = true,
  .ret_type  = RET_PTR_TO_MAP_VALUE_OR_NULL,
  .arg1_type  = ARG_CONST_MAP_PTR, /* const argument used as pointer to bpf_map */
  .arg2_type  = ARG_PTR_TO_MAP_KEY, /* pointer to stack used as map key */
};

所以对于 bpf_map_lookup_elem helper 函数来说,其参数 2 类型约束为 ARG_PTR_TO_MAP_KEY,其表示指向栈的指针,即第二个参数的值必须存储在栈上。下面是一个错误的示例:

struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct sock *);
    __type(value, struct sockmap_val);
    __uint(max_entries, 1024);
} sockmap SEC(".maps");

struct sockmap_val
{
    int nothing;
};

SEC("tracepoint/tcp/tcp_rcv_space_adjust")
int tp__tcp_rcv_space_adjust(struct trace_event_raw_tcp_event_sk *ctx)
{
    struct sockmap_val *sv = bpf_map_lookup_elem(&sockmap, &ctx->skaddr);
    if (sv)
        bpf_printk("%d\n", sv->nothing);
    return 0;
}

该程序会报错:R2 type=ctx expected=fp, pkt, pkt_meta, map_value。因为此时传的参数 ctx->skaddrctx 类型参数,非 fp 类型。对于该问题的一般解决方法是:定义一个栈变量,将 ctx->skaddr 的值存在栈上,即 u64 skaddr = ctx->skaddr 。

4、未做范围检查
范围检查主要是用来判断内存访问是否越界。

struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, int);
    __type(value, int);
    __uint(max_entries, 1024);
} indexmap SEC(".maps");

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
    int map_key = 0;
    int map_val = 0;
    int array[10] = {};
    
    int *map_val_ptr = bpf_map_look_up(&indexmap, &map_key);
    if (map_val_ptr)
        map_val = *map_val_ptr;
    
    bpf_printk("array[%d] = %d\n", map_val, array[map_val]);
    return 0;
}
char LICENSE[] SEC("license") = "GPL";

上述程序会报错:math between fp pointer and register with unbounded min value is not allowed。一般解决方法是:在内存访问时,进行范围检查。

经验总结

虽然 eBPF 程序可以采用 c 语言编写,但是相比于 c 语言的非安全性,eBPF 则通过严格的检查来保证 eBPF 程序安全,与此同时也引入了大量的约束条件。

另外在写eBPF程序时,也会遇到其他的错误,比如不能去trace类似这样的函数,ip_rcv_finish_core.isra.16,它会作为perf trace事件的一个event name,而由于带了"."特殊符号,内核会检查不通过,导致运行失败。该问题在高版本内核已修复。

还有一种错误是运行时libbpf未能加载有效的btf文件,如下图所示。造成这个问题的原因是/boot 路径下的btf默认只支持elf格式。解决这类问题的方法是升级libbpf,高版本libbpf中支持两种格式,也可以通过libbpf参数指定btf路径来解决。

通过以上具体的例子介绍了常见的 verifier 报错,那么编写eBPF程序有哪些需要注意的地方呢?以下为通过实战开发经验,总结出来的一些tips(仅供参考,可能内核版本及libbpf版本不一样结果有所差异,如果需要可以自行再验证一下):

  • 两个指针不能做算术运算
  • 如果寄存器没有被写过,那么也不能读
  • call 返回时R1-R5被设置为不可读写,R0保存返回值
  • R6-R9 在call 执行中,值不会改变(保存到栈)
  • load/store 寄存器一定要是有效类型(PTR_TO_CTX,PTR_TO_MAP, PTR_TO_STACK)
  • 根据指针类型(PTR_TO_CTX,PTR_TO_MAP, PTR_TO_STACK),对访问的大小/对齐/边界进行校验
  • 只有先向对应堆栈写入数据,才能从堆栈读数据
  • 如果一个函数能够被eBPF访问,会严格检查传递的参数
  • 创建map时,没有使用的变量需要置0, value_size不能大于1<<(KMALLOC_SHIFT_MAX-1)
  • array map的元素总大小不能超过U32_MAX,也受进程RLIMIT_MEMLOCK的限制
  • && 要同一个类型,ptr && ptr , int && int
  • bpf_probe_read 的dst需要是栈空间
  • bpf_get_current_comm的dst需要是栈空间
  • bpf_map_update_elem参数都要是栈空间
  • 要对bpf_map_lookup_elem的返回做检测
  • map地址空间可以随意访问,但是其他内核空间地址,需要用bpf_probe_read

展望

即使开发者再怎么注意,也难免出现BUG和编译错误,如果当运行时出错提示如果能够更精确一些,明确告知异常代码在哪一行,那将是非常方便的。前面这些tips和错误解决方法都是经过不断的开发实践得来的经验总结,平时大家在编码时多认真review,最主要的还是多写多去用方可写出高效代码。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芯光未来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值