如何解决加载 eBPF 文件时的 "R0 invalid mem access 'inv'" 错误 object

How to solve the "R0 invalid mem access 'inv'" error when loading an eBPF file object

我正在尝试使用 libbpf 在内核中加载 eBPF object,但没有成功,出现标题中指定的错误。但是让我展示一下我的 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
}

更准确地说,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'

我真的看不出这里有什么问题。我的意思是,这非常简单,但它会中断。为什么这行不通?我错过了什么?要么验证者疯了,要么我做的事情很蠢

好的,所以,经过 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;
}

那么,为什么它在那个时候坏了?我认为这是因为验证者假设 ipv6_hdr 可能是 NULL,这是完全错误的,因为如果执行到那个点,那只是因为 ipv4_hdripv6_hdr已设置(即,如果既不是 IPv4 也不是 IPv6,则执行在此点之前终止)。因此,显然,验证者无法推断出这一点。但是,有一个问题,如果 ipv6_hdr 的有效性也被显式检查,那就很高兴了,就像这样:

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

如果我们这样做也有效:

// "(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));

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

所有这些如何解释 entry_point() 中对 return act; 的抱怨?很简单,忍耐一下。 some_inlined_func() 未更改 ctx,其剩余参数也未被 entry_point() 使用。因此,在返回 act 的情况下,因为它取决于 some_inlined_func() 结果,所以 some_inlined_func() 被执行,此时验证者会抱怨。但是,如果返回 XDP_<whatever>,因为 switch-case 主体和 some_inlined_func() 都不会改变 entry_point() program/function 的内部状态,编译器(使用 O2)足够聪明,可以意识到为 some_inlined_func() 和整个 switch-case 生成汇编是没有意义的(这就是 O2 的优化)。因此,总而言之,在返回 XDP_<whatever> 的情况下,验证者很高兴,因为问题实际上出在 some_inlined_func() 但实际生成的 BPF 程序集没有任何东西,所以验证者没有检查 some_inlined_func() 因为一开始就没有。有道理吗?

这样的 BPF "limitation" 已知吗?是否有任何文件说明此类已知限制?因为我没有找到。