Как решить ошибку "R0 invalid mem access 'inv'" при загрузке объекта файла eBPF
Я пытаюсь загрузить объект eBPF в ядре с libbpf
, безуспешно получая ошибку, указанную в заголовке. Но позвольте мне показать, насколько прост мой БПФ *_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'
Я действительно не вижу здесь никаких проблем. Я имею в виду, это так просто, и все же это ломается. Почему это не должно работать? Что мне не хватает? Либо верификатор сошел с ума, либо я делаю что-то очень глупое.
1 ответ
Итак, через 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()
, не меняет внутреннее состояние entry_point()
программа / функция, компилятор (с O2) достаточно умен, чтобы понять, что нет смысла производить сборку для some_inlined_func()
и весь switch-case
(это оптимизация O2 здесь). Поэтому, чтобы сделать вывод, в случае возвращения XDP_<whatever>
верификатор был счастлив, так как проблема на самом деле заключается в some_inlined_func()
но фактически произведенная сборка BPF ничего такого не имеет, поэтому верификатор не проверил some_inlined_func()
потому что не было никого в первую очередь. Имеет смысл?
Известно ли такое "ограничение" БНФ? Есть ли какой-нибудь документ, в котором говорится о таких известных ограничениях? Потому что я не нашел ни одного.