如何解决加载 eBPF 文件对象时出现“R0 invalid mem access 'inv'”错误

pa5*_*nh0 2 jit clang linux-kernel bpf

我试图在内核中加载一个 eBPF 对象libbpf,但没有成功,出现标题中指定的错误。但让我展示一下我的 BPF*_kern.c是多么简单。

SEC("entry_point_prog")
int entry_point(struct xdp_md *ctx)
{
    int act = XDP_DROP;
    int rc, i = 0;
    struct global_vars *globals;
    struct ip_addr addr = {};
    struct some_key key = {};
    void *temp;

    globals = bpf_map_lookup_elem(&globals_map, &i);
    if (!globals)
        return XDP_ABORTED;

    rc = some_inlined_func(ctx, &key);

    addr = key.dst_ip;
    temp = bpf_map_lookup_elem(&some_map, &addr);

    switch(rc)
    {
    case 0:
        if(temp)
        {
            // no rocket science here ...
        } else
            act = XDP_PASS;
        break;
    default:
        break;
    }

    return act;  // this gives the error
    //return XDP_<whatever>;  // this works fine
}
Run Code Online (Sandbox Code Playgroud)

更准确地说,libbpf错误日志如下:

105: (bf) r4 = r0
106: (07) r4 += 8
107: (b7) r8 = 1
108: (2d) if r4 > r3 goto pc+4
 R0=inv40 R1=inv0 R2=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R3=pkt_end(id=0,off=0,imm=0) R4=inv48 R5=inv512 R6=inv1 R7=inv17 R8=inv1 R10=fp0,call_-1 fp-16=0 fp-32=0 fp-40=0
109: (69) r3 = *(u16 *)(r0 +2)
R0 invalid mem access 'inv'
Run Code Online (Sandbox Code Playgroud)

我真的没有看到任何问题。我的意思是,这是如此简单,但它却失败了。为什么这不起作用?我缺少什么?要么验证者疯了,要么我做了一些非常愚蠢的事情。

pa5*_*nh0 8

好吧,所以,经过 3 天,更准确地说 3 x 8 小时 = 24 小时,值得寻找代码,我想我终于找到了痒痒的问题。

问题始终存在some_inlined_func(),它比挑战更棘手。我在这里写下一个代码模板来解释这个问题,这样其他人就可以看到,并希望能少花 24 小时的时间来头痛;我为此经历了地狱,所以保持专注。

__alwais_inline static
int some_inlined_func(struct xdp_md *ctx, /* other non important args */)
{
    if (!ctx)
        return AN_ERROR_CODE;

    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth;
    struct iphdr *ipv4_hdr = NULL;
    struct ipv6hdr *ipv6_hdr = NULL;
    struct udphdr *udph;
    uint16_t ethertype;

    eth = (struct ethhdr *)data;
    if (eth + 1 > data_end)
        return AN_ERROR_CODE;

    ethertype = __constant_ntohs(eth->h_proto);
    if (ethertype == ETH_P_IP)
    {
        ipv4_hdr = (void *)eth + ETH_HLEN;
        if (ipv4_hdr + 1 > data_end)
            return AN_ERROR_CODE;

        // stuff non related to the issue ...
    } else if (ethertype == ETH_P_IPV6)
    {
        ipv6_hdr = (void *)eth + ETH_HLEN;
        if (ipv6_hdr + 1 > data_end)
            return AN_ERROR_CODE;

        // stuff non related to the issue ...
    } else
        return A_RET_CODE_1;

    /* here's the problem, but ... */
    udph = (ipv4_hdr) ? ((void *)ipv4_hdr + sizeof(*ipv4_hdr)) :
            ((void *)ipv6_hdr + sizeof(*ipv6_hdr));
    if (udph + 1 > data_end)
        return AN_ERROR_CODE;

    /* it actually breaks HERE, when dereferencing 'udph' */
    uint16_t dst_port = __constant_ntohs(udph->dest);

    // blablabla other stuff here unrelated to the problem ...

    return A_RET_CODE_2;
}
Run Code Online (Sandbox Code Playgroud)

那么,为什么会在此时破裂呢?我认为这是因为验证者假设ipv6_hdr可能是NULL,这是完全错误的,因为如果执行到达该点,那只是因为 或ipv4_hdripv6_hdr被设置(即,如果既不是 IPv4 也不是 IPv6 的情况,则执行会在此点之前终止)。因此,显然验证者无法推断出这一点。然而,有一个问题,如果ipv6_hdr显式检查also的有效性是很高兴的,如下所示:

if (ipv4_hdr)
    udph = (void *)ipv4_hdr + sizeof(*ipv4_hdr);
else if (ipv6_hdr)
    udph = (void *)ipv6_hdr + sizeof(*ipv6_hdr);
else return A_RET_CODE_1;  // this is redundant
Run Code Online (Sandbox Code Playgroud)

如果我们这样做,它也会起作用:

// "(ethertype == ETH_P_IP)" instead of "(ipv4_hdr)"
udph = (ethertype == ETH_P_IP) ? ((void *)ipv4_hdr + sizeof(*ipv4_hdr)) :
        ((void *)ipv6_hdr + sizeof(*ipv6_hdr));
Run Code Online (Sandbox Code Playgroud)

所以,在我看来,这里的验证器有些奇怪,因为它不够聪明(也许也不需要?),无法意识到如果它达到了这一点,那只是因为ctx引用了 IPv4 或 IPv6 数据包。

所有这一切如何解释return act;内部的抱怨entry_point()?很简单,请耐心等待。没有some_inlined_func()改变ctx,并且它的剩余参数也没有被使用entry_point()。因此,在返回 的情况下act,因为它取决于some_inlined_func()结果,some_inlined_func()所以会被执行,验证者此时会抱怨。但是,如果返回XDP_<whatever>, 作为switch-case主体,并且some_inlined_func(), 都不会改变entry_point()程序/函数的内部状态,编译器(使用 O2)足够聪明,能够意识到为some_inlined_func()和 整个程序生成汇编是没有意义的。 switch-case(这就是这里的 O2 优化)。因此,总而言之,如果返回XDP_<whatever>,验证者很高兴,因为问题实际上在于some_inlined_func(),但实际生成的 BPF 组件没有任何此类内容,因此验证者没有检查,some_inlined_func()因为第一个中没有任何问题地方。说得通?

这种 BPF“限制”已知吗?有没有任何文件说明这些已知的限制?因为我没有找到。