c - 尝试从 TLS 数据包中提取 SNI 时出现 BPF 验证错误

标签 c linux-kernel ebpf xdp-bpf

我正在尝试从 XDP 程序中的 TLS hello 数据包的 SNI 扩展中获取服务器名称。当我尝试加载它时,我从 BPF 验证程序中收到以下错误: pkt 指针和具有无限最小值的寄存器之间的数学运算是不允许的

struct server_name {
    char server_name[256];
};

struct extension {
    __u16 type;
    __u16 len;
} __attribute__((packed));

struct sni_extension {
    __u16 list_len;
    __u8 type;
    __u16 len;
} __attribute__((packed));

#define SERVER_NAME_EXTENSION 0

SEC("xdp")
int collect_ips_prog(struct xdp_md *ctx) {
    char *data_end = (char *)(long)ctx->data_end;
    char *data = (char *)(long)ctx->data;

    if (data_end < (data + sizeof(__u16))) {
        goto end;
    }

    __u16 extension_method_len = __bpf_htons(*(__u16 *) data);

    data += sizeof(__u16);

    for(int i = 0; i < extension_method_len; i += sizeof(struct extension)) { // A
        if (data_end < (data + sizeof(struct extension))) {
            goto end;
        }

        struct extension *ext = (struct extension *) data;

        data += sizeof(struct extension);

        if (ext->type == SERVER_NAME_EXTENSION) {
            struct server_name sn;

            if (data_end < (data + sizeof(struct sni_extension))) {
                goto end;
            }

            struct sni_extension *sni = (struct sni_extension *) data;

            data += sizeof(struct sni_extension);

            __u16 server_name_len = __bpf_htons(sni->len);

            for(int sn_idx = 0; sn_idx < server_name_len; sn_idx++) {
                if (data_end < data + sn_idx) {
                    goto end;
                }

                if (sn.server_name + sizeof(struct server_name) < sn.server_name + sn_idx) {
                    goto end;
                }

                sn.server_name[sn_idx] = data[sn_idx];
            }

            sn.server_name[server_name_len] = 0;
            goto end;
        }

        volatile int ext_len = __bpf_htons(ext->len);

        if (ext_len < 0) {
            goto end;
        }

        data += ext_len;
        i += ext_len; // B
    } // C

end:
    return XDP_PASS;
}

忽略 data 没有指向 TLS 数据包扩展长度字段的开头;我没有包含进入该字段的代码,因为上面的代码足以重现我所看到的问题。

当我尝试加载这个程序时出现这个错误:

19: R0=pkt(id=0,off=2,r=6,imm=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
; __u16 ext_len = __bpf_htons(ext->len);
19: (71) r6 = *(u8 *)(r0 +2)
20: R0=pkt(id=0,off=2,r=6,imm=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
20: (71) r0 = *(u8 *)(r0 +3)
21: R0_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
21: (67) r0 <<= 8
22: R0_w=inv(id=0,umax_value=65280,var_off=(0x0; 0xff00)) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
22: (4f) r0 |= r6
23: R0_w=inv(id=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
23: (dc) r0 = be16 r0
24: R0_w=inv(id=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
; if (data_end < (data + ext_len)) {
24: (0f) r5 += r0
last_idx 24 first_idx 12
regs=1 stack=0 before 23: (dc) r0 = be16 r0
regs=1 stack=0 before 22: (4f) r0 |= r6
regs=41 stack=0 before 21: (67) r0 <<= 8
regs=41 stack=0 before 20: (71) r0 = *(u8 *)(r0 +3)
regs=40 stack=0 before 19: (71) r6 = *(u8 *)(r0 +2)
math between pkt pointer and register with unbounded min value is not allowed
processed 24 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1  

如果我注释掉 A、B 和 C 点,即外部 for 循环,程序将成功加载。如果我评论 if (ext->type == SERVER_NAME_EXTENSION) { block ,程序加载成功。所以我很困惑错误到底在哪里。总的来说,我对 BPF 验证器的经验是,注释掉不相关的部分会影响其他代码部分。

我正在使用 Go library 加载这个程序,但如果使用 xdp-loader 加载程序,我会得到同样的错误:xdp-loader load -m skb -vv -s collect_ips enp0s8 dist/collect_ips.o

当我根据 pchaigno 的回答更新代码时,出现了这个错误:

0: (61) r2 = *(u32 *)(r1 +4)
; char *data = (char *)(long)ctx->data;
1: (61) r1 = *(u32 *)(r1 +0)
; if (data_end < (data + EXTENSION_METHODS_LEN_FIELD_SIZE)) {
2: (bf) r0 = r1
3: (07) r0 += 2
; if (data_end < (data + EXTENSION_METHODS_LEN_FIELD_SIZE)) {
4: (2d) if r0 > r2 goto pc+37
 R0_w=pkt(id=0,off=2,r=2,imm=0) R1_w=pkt(id=0,off=0,r=2,imm=0) R2_w=pkt_end(id=0,off=0,imm=0) R10=fp0
; __u16 extension_methods_len = __bpf_htons(*(__u16 *) data);
5: (69) r1 = *(u16 *)(r1 +0)
; for(int i = 0; i < extension_methods_len; i += sizeof(struct extension)) {
6: (15) if r1 == 0x0 goto pc+35
 R0_w=pkt(id=0,off=2,r=2,imm=0) R1_w=inv(id=0,umax_value=65535,var_off=(0x0; 0xffff)) R2_w=pkt_end(id=0,off=0,imm=0) R10=fp0
7: (dc) r1 = be16 r1
8: (b7) r3 = 0
9: (18) r4 = 0x400000000
; if (data_end < (data + sizeof(struct extension))) {
11: (bf) r5 = r0
12: (07) r5 += 4
; if (data_end < (data + sizeof(struct extension))) {
13: (2d) if r5 > r2 goto pc+28
 R0=pkt(id=0,off=2,r=6,imm=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R10=fp0
; if (ext->type == SERVER_NAME_EXTENSION) {
14: (71) r6 = *(u8 *)(r0 +0)
15: (71) r7 = *(u8 *)(r0 +1)
16: (67) r7 <<= 8
17: (4f) r7 |= r6
; if (ext->type == SERVER_NAME_EXTENSION) {
18: (15) if r7 == 0x0 goto pc+23
 R0=pkt(id=0,off=2,r=6,imm=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
; volatile int ext_len = __bpf_htons(ext->len);
19: (71) r6 = *(u8 *)(r0 +2)
20: (71) r0 = *(u8 *)(r0 +3)
21: (67) r0 <<= 8
22: (4f) r0 |= r6
23: (dc) r0 = be16 r0
; volatile int ext_len = __bpf_htons(ext->len);
24: (63) *(u32 *)(r10 -4) = r0
; if (ext_len < 0) {
25: (61) r0 = *(u32 *)(r10 -4)
26: (67) r0 <<= 32
27: (c7) r0 s>>= 32
; if (ext_len < 0) {
28: (65) if r0 s> 0xffffffff goto pc+1

from 28 to 30: R0=inv(id=0,umax_value=2147483647,var_off=(0x0; 0xffffffff)) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5=pkt(id=0,off=6,r=6,imm=0) R6=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7=inv(id=0) R10=fp0 fp-8=mmmm????
; data += ext_len;
30: (61) r0 = *(u32 *)(r10 -4)
; i += ext_len;
31: (61) r6 = *(u32 *)(r10 -4)
; i += ext_len;
32: (0f) r3 += r6
; for(int i = 0; i < extension_methods_len; i += sizeof(struct extension)) {
33: (67) r3 <<= 32
34: (0f) r3 += r4
35: (c7) r3 s>>= 32
; for(int i = 0; i < extension_methods_len; i += sizeof(struct extension)) {
36: (7d) if r3 s>= r1 goto pc+5
 R0=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv(id=0,smin_value=-2147483648,smax_value=2147483647) R4=inv17179869184 R5=pkt(id=0,off=6,r=6,imm=0) R6=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R7=inv(id=0) R10=fp0 fp-8=mmmm????
; 
37: (67) r0 <<= 32
38: (c7) r0 s>>= 32
39: (0f) r5 += r0
last_idx 39 first_idx 36
regs=1 stack=0 before 38: (c7) r0 s>>= 32
regs=1 stack=0 before 37: (67) r0 <<= 32
regs=1 stack=0 before 36: (7d) if r3 s>= r1 goto pc+5
 R0_rw=invP(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R1_r=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3_rw=inv(id=0,smin_value=-2147483648,smax_value=2147483647) R4=inv17179869184 R5_r=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R7=inv(id=0) R10=fp0 fp-8=mmmm????
parent didn't have regs=1 stack=0 marks
last_idx 35 first_idx 28
regs=1 stack=0 before 35: (c7) r3 s>>= 32
regs=1 stack=0 before 34: (0f) r3 += r4
regs=1 stack=0 before 33: (67) r3 <<= 32
regs=1 stack=0 before 32: (0f) r3 += r6
regs=1 stack=0 before 31: (61) r6 = *(u32 *)(r10 -4)
regs=1 stack=0 before 30: (61) r0 = *(u32 *)(r10 -4)
value -2147483648 makes pkt pointer be out of bounds
processed 41 insns (limit 1000000) max_states_per_insn 0 total_states 3 peak_states 3 mark_read 3 

最佳答案

TL;DR. 从验证者的角度来看,ext_len 因其计算方式而不受限制。为了允许您将此值添加到数据包指针,您需要添加一个新的绑定(bind)检查。请参阅下面的完整说明。


验证者错误的解释

22: R0_w=inv(id=0,umax_value=65280,var_off=(0x0; 0xff00)) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0;     0xff)) R7_w=inv(id=0) R10=fp0
22: (4f) r0 |= r6
23: R0_w=inv(id=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
23: (dc) r0 = be16 r0
24: R0_w=inv(id=0) R1=inv(id=0) R2=pkt_end(id=0,off=0,imm=0) R3=inv0 R4=inv17179869184 R5_w=pkt(id=0,off=6,r=6,imm=0) R6_w=inv(id=0,umax_value=255,var_off=(0x0; 0xff)) R7_w=inv(id=0) R10=fp0
; if (data_end < (data + ext_len)) {
24: (0f) r5 += r0
[...]
math between pkt pointer and register with unbounded min value is not allowed

验证器拒绝该程序,因为它看到一个无限寄存器 (R0) 添加到一个保存数据包指针的寄存器 (R5)。特别是它要求 R0 有一个最小值添加到数据包指针。否则,您可以从数据包指针中减去任何值并读取任意内核内存。

为什么 R0 是无界的?

在指令 22 (r0 |= r6) 之前,R0 有界限 (umax_value=65280,var_off=(0x0; 0xff00)) . R6 也是如此。不幸的是,验证者似乎无法在逻辑 OR 之后跟踪这些界限,并丢失了它们。较新的内核版本可能能够更好地跟踪这一点。

为什么添加 0 最小界限不起作用?

@Qeole 在评论中建议添加对 0 的最小边界检查:

if (ext_len < 0)
    goto end;

这可能行不通,因为 ext_len 的类型为 __u16(即 unsigned short),因此编译器可能会优化检查负号。

为什么添加上限检查有效?

添加上限检查(例如 30000)是有效的,因为 the verifier can deduce a lower signed bound来自无符号上限检查。

如何解决?

解决此问题的最佳方法可能是按照 Quentin 的建议添加下限检查。但是,您需要对 ext_len 变量进行签名,以便编译器不会优化绑定(bind)检查。

关于c - 尝试从 TLS 数据包中提取 SNI 时出现 BPF 验证错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70750259/

相关文章:

c++ - 如果你给一个指针加1,它是真的加1,还是加4或32?

c - 保护内核和用户空间之间的共享内存段

linux - 为什么系统 CPU 时间 (% sy) 很高?

c - 如何从内核空间确定 PID 位于哪些 namespace 中?

linux-kernel - 无法以非 root 身份运行 bpf 程序

ebpf - 对于attach_xdp,标志是否控制模式?

c - 将数据发送到 C 中的函数时数据表的更改

c - 帮助我为 "Trusted"应用程序编写最简单的程序

C语言写模式的正确方法

linux-kernel - Linux 驱动程序 phys_mem_access_prot 未定义