即使在访问前进行了检查,对数据包的访问也无效

Invalid access to packet even though check made before access

我从 eBPF 验证器得到 invalid access to packet,即使我在访问数据包中的字节之前执行检查。偏移量存储在 BPF_MAP_TYPE_ARRAY 中。循环迭代次数无关紧要,因为即使我进行一次迭代也会出现此问题。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct packet_context {
    __u16 pkt_offset;
};

struct bpf_map_def SEC("maps") context_table = {
   .type = BPF_MAP_TYPE_ARRAY,
   .key_size = sizeof(__u32),
   .value_size = sizeof(struct packet_context),
   .max_entries = 1,
};

SEC("xdp")
int collect_ips_prog(struct xdp_md *ctx) {
    char *data_end = (char *)(long)ctx->data_end;
    char *data = (char *)(long)ctx->data;
    __u32 index = 0;
    struct packet_context *pkt_ctx = (struct packet_context *) bpf_map_lookup_elem(&context_table, &index);

    if (pkt_ctx == NULL) {
        goto end;
    }

    __u32 length = 0;

    for (__u16 j = 0; j < 253; j++) {
        if (data_end < data + pkt_ctx->pkt_offset + j + 1) {
            goto end;
        }

        if (data[pkt_ctx->pkt_offset + j] == '\r') {
            break;
        }

        length++;
    }

    bpf_printk("%d", length);

end:
    return XDP_PASS;
}

这是错误。当 j = 0.

时,错误发生在 if (data[pkt_ctx->pkt_offset + j] == '\r') {
0: (61) r7 = *(u32 *)(r1 +0)
; char *data_end = (char *)(long)ctx->data_end;
1: (61) r8 = *(u32 *)(r1 +4)
2: (b7) r6 = 0
; __u32 index = 0;
3: (63) *(u32 *)(r10 -4) = r6
last_idx 3 first_idx 0
regs=40 stack=0 before 2: (b7) r6 = 0
4: (bf) r2 = r10
; 
5: (07) r2 += -4
; struct packet_context *pkt_ctx = (struct packet_context *) bpf_map_lookup_elem(&context_table, &index);
6: (18) r1 = 0xffff9790029a2e00
8: (85) call bpf_map_lookup_elem#1
; if (pkt_ctx == NULL) {
9: (15) if r0 == 0x0 goto pc+22
 R0_w=map_value(id=0,off=0,ks=4,vs=2,imm=0) R6_w=invP0 R7_w=pkt(id=0,off=0,r=0,imm=0) R8_w=pkt_end(id=0,off=0,imm=0) R10=fp0 fp-8=mmmm????
10: (69) r1 = *(u16 *)(r0 +0)
 R0_w=map_value(id=0,off=0,ks=4,vs=2,imm=0) R6_w=invP0 R7_w=pkt(id=0,off=0,r=0,imm=0) R8_w=pkt_end(id=0,off=0,imm=0) R10=fp0 fp-8=mmmm????
; for (__u16 j = 0; j < 253; j++) {
11: (0f) r7 += r1
last_idx 11 first_idx 0
regs=2 stack=0 before 10: (69) r1 = *(u16 *)(r0 +0)
12: (b7) r3 = 253
; if (data_end < data + pkt_ctx->pkt_offset + j + 1) {
13: (bf) r1 = r7
14: (0f) r1 += r6
15: (bf) r2 = r1
16: (07) r2 += 1
; if (data_end < data + pkt_ctx->pkt_offset + j + 1) {
17: (2d) if r2 > r8 goto pc+14
 R0=map_value(id=0,off=0,ks=4,vs=2,imm=0) R1_w=pkt(id=2,off=0,r=0,umax_value=65535,var_off=(0x0; 0xffff)) R2_w=pkt(id=2,off=1,r=0,umax_value=65535,var_off=(0x0; 0xffff)) R3=inv253 R6=invP0 R7=pkt(id=2,off=0,r=0,umax_value=65535,var_off=(0x0; 0xffff)) R8=pkt_end(id=0,off=0,imm=0) R10=fp0 fp-8=mmmm????
; if (data[pkt_ctx->pkt_offset + j] == '\r') {
18: (71) r1 = *(u8 *)(r1 +0)
invalid access to packet, off=0 size=1, R1(id=2,off=0,r=0)
R1 offset is outside of the packet
processed 18 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1

即使pkt_ctx->pkt_offset == 0xffff,访问前的if条件也应该阻止访问超出数据包前0xffff字节的字节。

我相信因为你的offset来自一个map,所以验证者不能直接用它来估计访问数据包的边界(R1的range

尝试在循环之前添加一个检查来限制偏移量:

    if (pkt_ctx->pkt_offset > 1500)
        goto end;

    for (__u16 j = 0; j < 253; j++) {
        ...

我在本地试过了,这似乎效果更好(验证者进入下一个问题,即如果要使用bpf_printk(),则需要将代码声明为GPL-compatible) .

TL;DR. 你打中了 a corner-case of the verifier. See 。正如@Qeole 所注意到的那样,在 pkt_ctx->pkt_offset 上添加边界检查将修复它。


验证者错误解释

13: (bf) r1 = r7
14: (0f) r1 += r6
15: (bf) r2 = r1
16: (07) r2 += 1
; if (data_end < data + pkt_ctx->pkt_offset + j + 1) {
17: (2d) if r2 > r8 goto pc+14
 R0=map_value(id=0,off=0,ks=4,vs=2,imm=0) R1_w=pkt(id=2,off=0,r=0,umax_value=65535,var_off=(0x0; 0xffff)) R2_w=pkt(id=2,off=1,r=0,umax_value=65535,var_off=(0x0; 0xffff)) R3=inv253 R6=invP0 R7=pkt(    id=2,off=0,r=0,umax_value=65535,var_off=(0x0; 0xffff)) R8=pkt_end(id=0,off=0,imm=0) R10=fp0 fp-8=mmmm????
; if (data[pkt_ctx->pkt_offset + j] == '\r') {
18: (71) r1 = *(u8 *)(r1 +0)
invalid access to packet, off=0 size=1, R1(id=2,off=0,r=0)
R1 offset is outside of the packet

验证者正在抱怨,因为数据包访问(insn.18)是out-of-bounds。这似乎出乎意料,因为您在访问之前检查了数据包的长度(insn.17)。

不幸的是,验证者甚至没有考虑边界检查,因为你正在点击 this condition。基本上,R2 的最大值太高(仅在第一次迭代时增加 1),验证者认为存在溢出风险。

R2 的最大值为 65535,其偏移量为 1(对于第一次迭代),因此两者之和高于 MAX_PACKET_OFF(65535)。考虑到这是一个溢出风险,来自 find_good_pkt_pointers 的验证者 returns 在更新所有数据包指针的边界之前。


解决方案

@Qeole 是正确的,添加边界检查会有所帮助,尽管不完全是因为他所说的原因。通过在 pkt_ctx->pkt_offset 上添加边界检查,R2 的最大值将减少,验证者将考虑数据包长度检查。