eBPF Talk: 手撕 verifier log 一例

文摘   2024-07-29 08:10   新加坡  

Ziv Chow 在微信群里问了个 verifier 的问题,一眼看上去肯定是 bpf 代码写得有问题,但是问题具体在哪里呢?

数组访问

如上图,crc16tab[key] 访问 crc16tab 数组时,在 bpf 里会转换成 bpf map 访问;因为 crc16tab 是一个长度为 256 的 u16 数组,需要消耗 512 字节的内存,因而无法保存在栈上。

所以,如果 verifier 判断寄存器的值没有有限最小值时,就会报上图中的错误。

可,明明已经 key & 0xFFif (key > 255 || key < 0) 了,为什么还会报错呢?

快速验证 crc16tab 数组访问是没问题的

static __always_inline
__u16 crc16(__u16 port)
{
    for (int i = 0; i < 6; i++) {
        port = (port << 8) ^ crc16tab[(port >> 10) & 0xff];
    }

    return port;
}

SEC("xdp")
int crc(struct xdp_md *ctx)
{
    struct ethhdr *eth = (typeof(eth)) ctx_ptr(ctxdata);
    struct iphdr *iph = (typeof(iph)) (eth + 1);
    struct tcphdr *tcph = (typeof(tcph)) (iph + 1);

    if ((void *)(tcph + 1) > ctx_ptr(ctx, data_end))
        return XDP_PASS;

    if (iph->protocol != IPPROTO_TCP || tcph->dest != bpf_htons(8080))
        return XDP_PASS;

    bpf_printk("crc16 of sport %d is %d\n", bpf_ntohs(tcph->source), crc16(tcph->source));

    return XDP_PASS;
}

跑起来后,可以看到输出:

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
          <idle>-0       [006] .Ns21 380503.312159: bpf_trace_printk: crc16 of sport 64654 is 61704
          <idle>-0       [006] ..s21 380504.676875: bpf_trace_printk: crc16 of sport 64659 is 40765

那么,到底哪里出错了呢?

问题出在 for 循环

找 Ziv Chow 要了一下完整的计算 CRC16 的代码,大概如下:

static __noinline
__u16 crc_16(__u64 port) 
{
    __u16 crc = 0;
    int key;

    for (int i = 0; i < 6; i++) {
        key = (int) ((crc >> 8) ^ (port >> i * 8 & 0xff)) & 0x00FF;
        if (key > 255 || key < 0) {
            return -1;
        }

        key = crc16tab[key];
        crc = (crc << 8) ^ key;
    }

    return crc;
}

static __always_inline
__u16 connection_crc_hash(__u32 src, __u16 src_port, __u32 dest, __u16 dest_port) 
{
    __u64 merge;
    __u16 crc;

    merge = ((__u64) src << 32) | dest;
    crc = crc_16(merge);
    merge = (__u64) (src_port) << 48|((__u64) crc << 32) | ((__u64) (dest_port) << 16) | ((__u64) (src_port));
    return crc_16(merge);
}

SEC("xdp")
int crc(struct xdp_md *ctx)
{
    struct ethhdr *eth = (typeof(eth)) ctx_ptr(ctxdata);
    struct iphdr *iph = (typeof(iph)) (eth + 1);
    struct tcphdr *tcph = (typeof(tcph)) (iph + 1);

    if ((void *)(tcph + 1) > ctx_ptr(ctx, data_end))
        return XDP_PASS;

    if (iph->protocol != IPPROTO_TCP || tcph->dest != bpf_htons(8080))
        return XDP_PASS;

    bpf_printk("crc16: %d\n", connection_crc_hash(iph->saddr, tcph->source, iph->daddr, tcph->dest));

    return XDP_PASS;
}

就是其中 key = crc16tab[key]; 这一行,导致 verifier 报错:

    ...
; crc = crc_16(merge);
31: (85) call pc+11
caller:
frame1: R6=scalar(id=2,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R7=scalar(id=1,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R10=fp0
callee:
frame2: R1=scalar() R2=scalar(id=2,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R3=scalar(smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff)) R4=scalar(id=1,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R10=fp0
43: frame2: R1=scalar() R2=scalar(id=2,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R3=scalar(smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff)) R4=scalar(id=1,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R10=fp0
; key = (int) ((crc >> 8) ^ (port >> i * 8 & 0xff)) & 0x00FF;
43: (bf) r3 = r1 ; frame2: R1=scalar(id=3) R3_w=scalar(id=3)
44: (57) r3 &= 255 ; frame2: R3_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
; key = crc16tab[key];
45: (67) r3 <<= 1 ; frame2: R3_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=510,var_off=(0x0; 0x1fe))
46: (18) r2 = 0xffff8f8b9a03051c ; frame2: R2_w=map_value(map=.rodata,ks=4,vs=524,off=12)
48: (18) r4 = 0xffff8f8b9a03051c ; frame2: R4_w=map_value(map=.rodata,ks=4,vs=524,off=12)
50: (0f) r4 += r3 ; frame2: R3_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=510,var_off=(0x0; 0x1fe)) R4_w=map_value(map=.rodata,ks=4,vs=524,off=12,smin=smin32=0,smax=umax=smax32=umax32=510,var_off=(0x0; 0x1fe))
51: (69) r3 = *(u16 *)(r4 +0) ; frame2: R3_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R4_w=map_value(map=.rodata,ks=4,vs=524,off=12,smin=smin32=0,smax=umax=smax32=umax32=510,var_off=(0x0; 0x1fe))
; key = (int) ((crc >> 8) ^ (port >> i * 8 & 0xff)) & 0x00FF;
52: (bf) r4 = r1 ; frame2: R1=scalar(id=3) R4_w=scalar(id=3)
53: (77) r4 >>= 8 ; frame2: R4_w=scalar(smin=0,smax=umax=0xffffffffffffff,var_off=(0x0; 0xffffffffffffff))
; key = (int) ((crc >> 8) ^ (port >> i * 8 & 0xff)) & 0x00FF;
54: (57) r4 &= 255 ; frame2: R4_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
55: (bf) r5 = r3 ; frame2: R3_w=scalar(id=4,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff)) R5_w=scalar(id=4,smin=smin32=0,smax=umax=smax32=umax32=0xffff,var_off=(0x0; 0xffff))
56: (77) r5 >>= 8 ; frame2: R5_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
57: (af) r4 ^= r5 ; frame2: R4_w=scalar() R5_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
; crc = (crc << 8) ^ key;
58: (67) r3 <<= 8 ; frame2: R3_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=0xffff00,var_off=(0x0; 0xffff00))
; key = crc16tab[key];
59: (67) r4 <<= 1 ; frame2: R4_w=scalar(smax=0x7ffffffffffffffe,umax=0xfffffffffffffffe,smax32=0x7ffffffe,umax32=0xfffffffe,var_off=(0x0; 0xfffffffffffffffe))
60: (18) r5 = 0xffff8f8b9a03051c ; frame2: R5_w=map_value(map=.rodata,ks=4,vs=524,off=12)
62: (0f) r5 += r4
math between map_value pointer and register with unbounded min value is not allowed
processed 41 insns (limit 1000000) max_states_per_insn 0 total_states 2 peak_states 2 mark_read 1

从下往上看,r4 ^= r5 破坏了 r4 的范围值,前面明明 r4 &= 255 做了范围检查。

再仔细分析,r5 += r4 是第二次循环中的 crc16tab 访问。而在第一次访问 crc16tab 前,r3 << 1r3 &= 255 之间却没有类似 r3 ^= r5 的操作。

这又是为什么呢?

crc_16() 的 verifier log 精简一下:

    r3 = r1                 ;; r3 = port
r3 &= 255 ;; r3 = port & 0xff, 因为 crc = 0 且 counter = 0,所以优化掉了不少地方
r3 <<= 1 ;; ???
r2 = 0xffff8f8b9a03051c ;; r2 = &crc16tab
r4 = 0xffff8f8b9a03051c ;; r4 = &crc16tab
r4 += r3 ;; r4 = &crc16tab + (port & 0xff) * 2
r3 = *(u16 *)(r4 +0) ;; r3 = crc16tab[r4]
r4 = r1 ;; r4 = port
r4 >>= 8 ;; r4 = port >> 8
r4 &= 255 ;; r4 = (port >> 8) & 0xff
r5 = r3 ;; r5 = r3, crc16tab 取出的值
r5 >>= 8 ;; r5 = r3 >> 8, 对应 crc >> 8
r4 ^= r5 ;; r4 = r4 ^ r5, 对应 (crc >> 8) ^ ((port >> 8) & 0xff)
r3 <<= 8 ;; r3 = r3 << 8, 对应 crc << 8
r4 <<= 1 ;; ???
r5 = 0xffff8f8b9a03051c ;; r5 = &crc16tab
r5 += r4 ;; r5 = &crc16tab + (r4 << 1)

噢噢,原来第一次的 r3 ^= r5 的操作被优化掉了。

所以,要解决这问题,只需要确保 &= 255^= r5 之后便可。

解法1: 禁止 for 循环复用寄存器

意即,每次循环都进行一次函数调用:

static __noinline
__u32 __crc(__u32 crc, __u64 port, int i)
{
    return (crc << 8) ^ crc16tab[((crc >> 8) ^ (port >> i * 8)) & 0xff];
}

static __always_inline
__u16 crc16(__u64 port)
{
    __u32 crc = 0;

    for (int i = 0; i < 6; i++)
        crc = __crc(crc, port, i);

    return crc;
}

解法2: 强行阻止编译器复用寄存器

/* make it look to compiler like value is read and written */
#define __sink(expr) asm volatile("" : "+g"(expr))

static __always_inline
__u32 __crc(__u32 crc, __u64 port, int i)
{
    __u64 key = (crc >> 8) ^ (port >> i * 8);
    __sink(key);
    return (crc << 8) ^ crc16tab[key & 0xff];
}

static __always_inline
__u16 crc16(__u64 port)
{
    __u32 crc = 0;

    for (int i = 0; i < 6; i++)
        crc = __crc(crc, port, i);

    return crc;
}

如此一来,便可省掉 6 次函数调用。

总结

过不了 verifier 的问题总是那么恼人,而且需要耗费大量时间去分析 verifier log,才能找到问题所在。

而想要解决这个问题,还需要对编译器有一定的理解,才能找到解决方案。

完整代码:learn-by-example xdp-crc[1]

参考资料
[1]

learn-by-example xdp-crc: https://github.com/Asphaltt/learn-by-example/tree/main/ebpf/xdp-crc

eBPF Talk
专注于 eBPF 技术,以及 Linux 网络上的 eBPF 技术应用