好的,所以,经过 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_hdr
已经ipv6_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 数据包。
这一切如何解释return act;
内部的抱怨entry_point()
?很简单,请耐心等待。some_inlined_func()
没有改变,并且它的ctx
剩余参数也没有被entry_point()
. 因此,在返回的情况下act
,因为它取决于some_inlined_func()
结果,所以some_inlined_func()
会被执行,此时验证者会抱怨。但是,如果返回XDP_<whatever>
, 作为switch-case
主体,并且some_inlined_func()
, 都不会改变程序/函数的内部状态,编译器(使用 O2)足够聪明,可以意识到为整个entry_point()
程序生成程序集是没有意义的(这就是这里的 O2 优化)。因此,总结一下,在返回的情况下some_inlined_func()
switch-case
XDP_<whatever>
,验证者很高兴,因为问题实际上在于,some_inlined_func()
但实际生成的 BPF 程序集没有任何东西,所以验证者没有检查some_inlined_func()
,因为一开始就没有。说得通?
这种 BPF 的“限制”是否已知?是否有任何文件说明此类已知限制?因为我没有找到。