You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

BPF Map/skb->cb存储包偏移报错:原因分析与解决方法

BPF程序存储包偏移时触发越界错误的问题解决

问题背景

  • 需求:将包内找到的偏移值传递给后续尾调用
  • 问题现象:尝试将偏移值存入**BPF Map(方法1)**或skb->cb字段(方法2)时,BPF验证器报错offset is outside of the packet;移除存储代码后程序可正常加载。后续按建议在循环条件添加+1,仍出现同类报错。
  • 疑问:
    1. 为何添加存储逻辑会触发越界报错?
    2. 如何正确存储该偏移值?

示例代码

#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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 15:10:26