BPF Map/skb->cb存储包偏移报错:原因分析与解决方法
BPF程序存储包偏移时触发越界错误的问题解决
问题背景
- 需求:将包内找到的偏移值传递给后续尾调用
- 问题现象:尝试将偏移值存入**BPF Map(方法1)**或
skb->cb字段(方法2)时,BPF验证器报错offset is outside of the packet;移除存储代码后程序可正常加载。后续按建议在循环条件添加+1,仍出现同类报错。 - 疑问:
- 为何添加存储逻辑会触发越界报错?
- 如何正确存储该偏移值?
示例代码
#include <vmlinux.h> #include <bpf/bpf_endian.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __type(key, __u32); __type(value, __u32); __uint(max_entries, 100); } state_vars SEC(".maps"); SEC("tc") int tc_ingress(struct __sk_buff *ctx) { void *data_end = (void *)(__u64)ctx->data_end; void *data = (void *)(__u64)ctx->data; u32 data_len = data_end - data; u32 rn = 0; u32 rn_idx = 0; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { char c = *(char*)(data + rn); if (c == '\r') { rn_idx = rn; break; } } /// Method 1 // u32 var_idx = 0; // bpf_map_update_elem(&state_vars, &var_idx, &rn_idx, BPF_ANY); /// Method 2 // ctx->cb[0] = rn_idx; return TC_ACT_OK; }
错误信息
方法1(存储到BPF Map)
; void *data = (void *)(__u64)ctx->data; 0: (61) r2 = *(u32 *)(r1 +76) ; void *data_end = (void *)(__u64)ctx->data_end; 1: (61) r3 = *(u32 *)(r1 +80) 2: (b7) r1 = 0 ; u32 rn_idx = 0; 3: (63) *(u32 *)(r10 -4) = r1 last_idx 3 first_idx 0 regs=2 stack=0 before 2: (b7) r1 = 0 ; u32 data_len = data_end - data; 4: (bf) r4 = r3 5: (1f) r4 -= r2 6: (67) r4 <<= 32 7: (77) r4 >>= 32 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { 8: (15) if r4 == 0x0 goto pc+10 R1_w=invP0 R2_w=pkt(id=0,off=0,r=0,imm=0) R3_w=pkt_end(id=0,off=0,imm=0) R4_w=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R10=fp0 fp-8=0000???? ; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { 9: (bf) r5 = r2 10: (0f) r5 += r1 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { 11: (3d) if r5 >= r3 goto pc+7 R1_w=invP0 R2_w=pkt(id=0,off=0,r=0,imm=0) R3_w=pkt_end(id=0,off=0,imm=0) R4_w=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R5_w=pkt(id=0,off=0,r=0,imm=0) R10=fp0 fp-8=0000???? ; char c = *(char*)(data + rn); 12: (71) r5 = *(u8 *)(r5 +0) invalid access to packet, off=0 size=1, R5(id=0,off=0,r=0) R5 offset is outside of the packet processed 13 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
方法2(存储到skb->cb)
; void *data = (void *)(__u64)ctx->data; 1: (61) r3 = *(u32 *)(r1 +76) ; void *data_end = (void *)(__u64)ctx->data_end; 2: (61) r4 = *(u32 *)(r1 +80) ; u32 data_len = data_end - data; 3: (bf) r5 = r4 4: (1f) r5 -= r3 5: (bf) r0 = r5 6: (67) r0 <<= 32 7: (77) r0 >>= 32 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { 8: (15) if r0 == 0x0 goto pc+21 R0_w=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R1=ctx(id=0,off=0,imm=0) R2_w=inv0 R3_w=pkt(id=0,off=0,r=0,imm=0) R4_w=pkt_end(id=0,off=0,imm=0) R5_w=inv(id=0) R10=fp0 9: (b7) r2 = 0 10: (b7) r0 = 0 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { 11: (bf) r6 = r3 12: (0f) r6 += r0 last_idx 12 first_idx 0 regs=1 stack=0 before 11: (bf) r6 = r3 regs=1 stack=0 before 10: (b7) r0 = 0 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn < data_end; rn++) { 13: (3d) if r6 >= r4 goto pc+16 R0_w=invP0 R1=ctx(id=0,off=0,imm=0) R2_w=inv0 R3_w=pkt(id=0,off=0,r=0,imm=0) R4_w=pkt_end(id=0,off=0,imm=0) R5_w=inv(id=0) R6_w=pkt(id=0,off=0,r=0,imm=0) R10=fp0 ; char c = *(char*)(data + rn); 14: (71) r6 = *(u8 *)(r6 +0) invalid access to packet, off=0 size=1, R6(id=0,off=0,r=0) R6 offset is outside of the packet processed 15 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
2022.11.1更新(添加+1后仍报错)
; int tc_ingress(struct __sk_buff *ctx) 0: (b7) r2 = 0 ; void *data = (void *)(__u64)ctx->data; 1: (61) r3 = *(u32 *)(r1 +76) ; void *data_end = (void *)(__u64)ctx->data_end; 2: (61) r4 = *(u32 *)(r1 +80) ; u32 data_len = data_end - data; 3: (bf) r5 = r4 4: (1f) r5 -= r3 5: (bf) r0 = r5 6: (67) r0 <<= 32 7: (77) r0 >>= 32 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn + 1 < data_end; rn++) { 8: (15) if r0 == 0x0 goto pc+23 R0_w=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff)) R1=ctx(id=0,off=0,imm=0) R2_w=inv0 R3_w=pkt(id=0,off=0,r=0,imm=0) R4_w=pkt_end(id=0,off=0,imm=0) R5_w=inv(id=0) R10=fp0 9: (b7) r2 = 0 10: (b7) r0 = 0 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn + 1 < data_end; rn++) { 11: (bf) r6 = r3 12: (0f) r6 += r0 last_idx 12 first_idx 0 regs=1 stack=0 before 11: (bf) r6 = r3 regs=1 stack=0 before 10: (b7) r0 = 0 13: (bf) r7 = r6 14: (07) r7 += 1 ; for (rn = 0; rn < 1000 && rn < data_len && data + rn + 1 < data_end; rn++) { 15: (3d) if r7 >= r4 goto pc+16 R0_w=invP0 R1=ctx(id=0,off=0,imm=0) R2_w=inv0 R3_w=pkt(id=0,off=0,r=0,imm=0) R4_w=pkt_end(id=0,off=0,imm=0) R5_w=inv(id=0) R6_w=pkt(id=0,off=0,r=0,imm=0) R7_w=pkt(id=0,off=1,r=0,imm=0) R10=fp0 ; char c = *(char*)(data + rn); 16: (71) r6 = *(u8 *)(r6 +0) invalid access to packet, off=0 size=1, R6(id=0,off=0,r=0) R6 offset is outside of the packet processed 17 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
问题分析与解决方案
1. 报错原因
BPF验证器的寄存器状态跟踪逻辑非常严格,当添加Map更新或skb->cb修改代码后,验证器对代码路径的分析会发生变化,导致它无法正确识别循环中data + rn的边界检查有效性。即使你写了合法的边界条件,验证器也可能因为代码逻辑的组合变化,误判data + rn可能超出data_end范围,从而触发越界错误。
2. 正确的存储方式与代码修复
核心修复思路
- 用BPF提供的安全数据读取函数替代直接指针访问,让验证器明确识别访问的安全性;
- 简化边界检查逻辑,让验证器更容易跟踪寄存器的合法范围。
修复后的代码
#include <vmlinux.h> #include <bpf/bpf_endian.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __type(key, __u32); __type(value, __u32); __uint(max_entries, 100); } state_vars SEC(".maps"); SEC("tc") int tc_ingress(struct __sk_buff *ctx) { void *data_end = (void *)(__u64)ctx->data_end; void *data = (void *)(__u64)ctx->data; u32 rn = 0; u32 rn_idx = 0; char c; // 拆分边界检查,确保每次读取1字节时不会越界 for (rn = 0; rn < 1000; rn++) { // 检查当前偏移+1是否在包数据范围内,保证能安全读取1字节 if (data + rn + 1 > data_end) { break; } // 使用bpf_skb_load_bytes安全读取包数据,验证器可识别其合法性 if (bpf_skb_load_bytes(ctx, rn, &c, sizeof(c)) != 0) { break; } if (c == '\r') { rn_idx = rn; break; } } // 方法1:存储到BPF Map u32 var_idx = 0; bpf_map_update_elem(&state_vars, &var_idx, &rn_idx, BPF_ANY); // 方法2:存储到skb->cb字段 ctx->cb[0] = rn_idx; return TC_ACT_OK; }
关键修复点说明
- 替换直接指针访问为
bpf_skb_load_bytes:这个函数是BPF官方提供的安全读取skb数据的接口,验证器能直接识别其边界检查逻辑,不会误判越界; - 拆分边界检查条件:把多条件的循环判断拆分成独立的if语句,让验证器更容易跟踪
data + rn的合法范围,避免复杂逻辑导致的分析失效。
内容的提问来源于stack exchange,提问作者HypoGump
相关产品推荐
相关产品推荐

