Как решить ошибку «inv» недопустимого доступа к памяти R0 при загрузке файлового объекта eBPF

Я пытаюсь загрузить объект 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
}

Точнее, журнал ошибок 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'

Я действительно не вижу здесь никакой проблемы. Я имею в виду, это так просто, и все же это ломается. Почему это не должно работать? Что мне не хватает? Либо верификатор сошел с ума, либо я делаю что-то очень глупое.


person pa5h1nh0    schedule 03.11.2018    source источник
comment
Не могли бы вы попробовать с типом возврата u64?   -  person pchaigno    schedule 04.11.2018
comment
Я не мог воспроизвести с минимальным примером. Похоже, что в какой-то момент программа пытается сохранить в r3 значение, хранящееся в *(r0 + 2), в то время, когда r0 является скалярным значением. r0 обычно используется в качестве возвращаемого значения, используете ли вы какой-либо помощник BPF? Или, поскольку проблема связана с act, вы так или иначе обновляете act в остальной части вашей программы?   -  person Qeole    schedule 04.11.2018
comment
@pchaigno Я пробовал с u64, ничего не изменилось :( ... @Qeole act обновляется только в этой ветке else... Я обновляю код, чтобы вы могли лучше понять, что происходит   -  person pa5h1nh0    schedule 04.11.2018
comment
Плохо, тип возврата int в порядке! Я тоже не смог воспроизвести баг. Если вы попытаетесь запустить точный код, который вы нам дали, с пустым телом if, у вас все еще будет ошибка? Какие версии ядра и LLVM вы используете?   -  person pchaigno    schedule 04.11.2018
comment
ядро 4.18.10 ... clang и llc в версии 8.0.0, которая на данный момент еще не вышла, поэтому она нестабильна. Может ли это быть проблема?   -  person pa5h1nh0    schedule 04.11.2018
comment
Недавно у меня была проблема с clang-8, вы можете попробовать с -7 или -6.0, если можете. Кроме этого, я не вижу ничего плохого в программе.   -  person Qeole    schedule 04.11.2018
comment
Пробовал с llvm-7, точно такая же ошибка. Я действительно понятия не имею, что теперь делать :(   -  person pa5h1nh0    schedule 04.11.2018
comment
Не могли бы вы опубликовать полный код (полный код ядра C + код пользовательского пространства)? Возможно, XDP_DROP определен странно или что-то пытается напрямую записать в стек. Учитывая ошибку, ее будет сложно отлаживать без всех деталей. (Если есть части, которые вы хотите сохранить в тайне, вы можете удалить их и убедиться, что они не влияют на ошибку.)   -  person pchaigno    schedule 05.11.2018


Ответы (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(), потому что не было никаких в первую очередь. Имеет смысл?

Известно ли такое "ограничение" БНФ? Есть ли вообще какой-либо документ, в котором указаны такие известные ограничения? Потому что я ничего не нашел.

person pa5h1nh0    schedule 05.11.2018
comment
Привет, рад видеть, что ты нашел проблему! Я бы не стал рассматривать это как «ограничение», и нет, я не знаю ни одного подобного документа. По сути, мы должны помнить, что верификатор вообще не работает с some_inlined_func(), он никогда не знает об этой функции, а просто рассуждает о наборе инструкций байт-кода eBPF. Если компилятор оптимизировал вашу функцию, верификатор не сможет угадать, что вы намеревались сделать вместо этого... Так что в этом случае вы можете попытаться скомпилировать с -O0 и проанализировать различия и работать, например, с. llvm-objdump -d или -S, чтобы посмотреть на сгенерированный байт-код. - person Qeole; 05.11.2018