土法炼钢兴趣小组的算法知识备份

【eBPF 内核实现深度拆解】与验证器共舞:常见拒绝模式与编程约束

文章导航

分类入口
kernelebpf
标签入口
#ebpf#bpf-verifier#verifier-patterns#xdp#bpf-programming#debugging

目录

Verifier 返回 "invalid access to packet, off=4 size=4, R1(id=1,off=4,r=0)"。你盯着这行看了三分钟,确认 R1 在指令 4 时确实是合法的 packet pointer。但 verifier 说它 r=0——范围为零,意味着已证明的可访问区域不存在。你明明写了 if (data + 4 <= data_end) 检查,为什么 verifier 没有记录 R1 的范围?

本文是第 02-03 篇理论知识的实战落地方案。每个模式包含三个要素:(1) 错误代码的最小复现,(2) verifier 日志示例(标注内核版本;以下日志为典型场景经删减的代表性输出),(3) 正确写法及其为什么能通过。覆盖 18 种最常见的 verifier 拒绝模式,最后给出系统化的排障方法论。读完后,你可以用 verifier 日志的精确措辞反推根因,而不是盲猜。

一、排障方法论:先学会读日志

在逐条展开模式之前,先建立系统化的排障流程。verifier 日志的结构是从顶向下的指令列表,但排障的正确方式是自底向上——先找到最后一条错误行,再向上追溯相关寄存器的状态变化。

# 正确方式:从底部找错误(Linux 6.6,以下输出经删减)
bpftool prog load my_prog.o /sys/fs/bpf/test type xdp log_level 2 2>&1 | tail -30

# 然后根据错误行中的指令索引追溯寄存器状态
# 例如错误在指令 14,查看指令 0-14 的寄存器跟踪

日志解读模板:

14: (61) r1 = *(u32 *)(r2 + 8)
R2 invalid mem access 'scalar'
processed 14 insns (limit 1000000)

解读: 1. 指令 14 试图从 [R2 + 8] 加载 4 字节。 2. R2 的类型是 scalar(即 SCALAR_VALUE),不是指针类型。 3. 原因可能:R2 在之前的路径中失去了指针类型(例如做了 R2 += R3 而未经过界检查),或者从未被赋予指针值。

以下 18 种模式按出现频率和对新手困惑度排序。

二、NULL 指针解引用与 map 查找

模式 1:bpf_map_lookup_elem 返回值未判空

错误代码

SEC("xdp")
int broken_prog(struct xdp_md *ctx)
{
    __u32 *value;
    
    value = bpf_map_lookup_elem(&my_map, &key);
    __u32 x = *value;  // 如果 value == NULL 则非法
    return x ? XDP_PASS : XDP_DROP;
}

verifier 日志(Linux 6.6):

5: (85) call bpf_map_lookup_elem#1
6: (61) r1 = *(u32 *)(r0 + 0)
R0 invalid mem access 'map_value_or_null'
processed 6 insns

R0 在 helper 调用后类型为 PTR_TO_MAP_VALUE_OR_NULL。直接解引用被拒绝。

正确写法

SEC("xdp")
int correct_prog(struct xdp_md *ctx)
{
    __u32 *value;
    
    value = bpf_map_lookup_elem(&my_map, &key);
    if (!value)                       // NULL 检查是必须的
        return XDP_DROP;
    
    __u32 x = *value;                 // 现在 value 是 PTR_TO_MAP_VALUE
    return x ? XDP_PASS : XDP_DROP;
}

verifier 在 if (!value) 的 fallthrough 路径中,将 R0 从 PTR_TO_MAP_VALUE_OR_NULL 转换为 PTR_TO_MAP_VALUE(因为 NULL 情况已排除)。在分支路径中,R0 被标记为 SCALAR_VALUE(值=0,用于 NULL 返回处理)。

三、边界检查与数据包访问

模式 2:XDP 数据包访问前未做边界检查

错误代码

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

    __u32 first_word = *(__u32 *)data;  // 未检查 data + 4 <= data_end
    return XDP_PASS;
}

verifier 日志

3: (61) r1 = *(u32 *)(r6 + 0)
R6 invalid mem access 'packet'
R6 offset is outside of the packet
processed 3 insns

或更具体的:

invalid access to packet, off=0 size=4, R6(id=0,off=0,r=0)
R6 pointer arithmetic on PTR_TO_PACKET prohibited

根因分析:verifier 在 check_packet_access() 中强制要求:任何 PTR_TO_PACKET 的解引用必须在一次条件跳转的范围内,该条件跳转建立了 ptr + size <= PTR_TO_PACKET_END 的 fact。没有这个 guard,PTR_TO_PACKETrange 字段保持为 0。

正确写法

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

    if (data + sizeof(__u32) > data_end)      // 必须显式检查
        return XDP_DROP;

    __u32 first_word = *(__u32 *)data;        // 现在 data 的 range ≥ 4
    return XDP_PASS;
}

verifier 在 check_cond_jmp_op() 处理 data + 4 > data_end 的 fallthrough 路径时,更新 data 指针的 rangedata_end - data - 4(至少 0),并使 data 成为有效的可解引用指针。

关键规则:边界检查必须紧邻在访问之前。如果两条指令之间有任何其他指令改变了 datadata_end,检查失效。

模式 3:条件跳转中的比较顺序错误

错误代码

// 错误:使用了 if (data_end > data + 4) 而非 if (data + 4 > data_end)
if (data_end >= data + sizeof(struct ethhdr))
    // verifier 不接受这种比较顺序

verifier 日志

R1 invalid mem access 'packet'
processed 2 insns

根因:verifier 的模式匹配是单向的——它只识别 ptr + offset > PTR_TO_PACKET_ENDptr + offset >= PTR_TO_PACKET_END 这种形式。写成 data_end > data + offsetdata + offset <= data_end 可能不被识别,因为 verifier 的 reg_set_min_max() 只能对 BPF_JEQ/JGT/JGE 做单向推导。

正确写法:始终使用 if (data + offset > data_end) goto drop; 形式。

模式 4:可变长度读取

错误代码

int offset = compute_some_value();
void *ptr = data + offset;            // offset 是变量,不是常量
__u16 val = *(__u16 *)ptr;            // verifier 拒绝

verifier 日志

invalid variable-offset packet access, R2(id=2,off=0,r=0)
variable packet access is not allowed

根因:verifier 需要能够静态证明 ptr 满足 ptr + size <= data_end。如果 offset 是一个变量(verifier 不知道其精确值),这个证明就不可能。

正确写法

int offset = compute_some_value();       // offset 是变量
if (offset + sizeof(__u16) > MAX_OFFSET) // 需要将 offset 约束到已知上限
    return XDP_DROP;

void *ptr = data + offset;               // offset 现在 ∈ [0, MAX_OFFSET - 2]
if (ptr + sizeof(__u16) > data_end)      // 再验证实际可用空间
    return XDP_DROP;

__u16 val = *(__u16 *)ptr;

注意需要双层检查:(1) 变量偏移必须约束在已知的上界内;(2) 实际的内存访问必须通过 data_end 验证。两层缺一不可。

四、指针算术与标量限制

模式 5:在标量上做指针算术

错误代码

__u64 addr = bpf_get_current_pid_tgid();  // 返回 scalar
void *ptr = (void *)addr;                  // 不允许标量到指针的转换

verifier 日志

R1 type=scalar expected=ptr

或:

pointer arithmetic on PTR_TO_STACK prohibited
R1 min value is outside of the stack bounds

根因:BPF 程序不允许将标量值强制转换为指针。这是故意为之的安全限制——如果程序可以任意构造指针,verifier 的静态分析会被完全绕过。

正确做法:你不能从标量值制造指针,但有合法的替代方案:

// 替代方案 1:使用 bpf_probe_read_kernel 读取任意内核地址(如果可用)
// 替代方案 2:将数据存在 BPF map 中,用 key/value 而非直接指针访问
// 替代方案 3:使用 kfunc 获取合法指针(如 bpf_task_from_pid)

模式 6:指针算术超出已知范围

错误代码

struct my_value *val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
    return 0;

val += 2;  // 可能超出 map value 的范围

verifier 日志

invalid access to map value, val+=2
R1 offset is outside of the map value

根因PTR_TO_MAP_VALUE 的合法偏移受限于 map value 的大小。如果 map 定义的 value size 是 8 字节,val + 2 就指向了偏移 16 处(2 * sizeof(struct my_value)),超出了 8 字节的限制。

正确写法

struct my_value *val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
    return 0;

// val++ 是允许的(移动一个完整的结构体宽度),但必须在范围内
// val += 2 被接受仅当 sizeof(struct my_value) * 2 <= value_size
// 如果不确定,使用数组:
struct my_value *val_array = bpf_map_lookup_elem(&array_map, &index);

五、循环与复杂度限制

模式 7:无界循环

错误代码

for (int i = 0; i < max_value; i++) {  // max_value 来自 map 或输入
    process_packet(data, i);
}

verifier 日志

back-edge from insn 8 to 3
infinite loop detected

或更常见的:

BPF program is too large. Processed 1000001 insn

根因:verifier 在 check_cfg() 阶段检测到回边(back-edge)。如果循环的迭代次数不能静态确定,verifier 在 do_check() 中会一直展开直到达到 1M 指令上限。

正确写法(几种选项)

// 选项 1:编译时常量上界 + unroll
#pragma clang loop unroll(full)
for (int i = 0; i < 10; i++) {
    process_item(i);
}

// 选项 2:编译时已知的常量上限
#define MAX_ITEMS 64
for (int i = 0; i < MAX_ITEMS && i < actual_count; i++) {
    process_item(i);
}

// 选项 3:有界循环 + bpf_for_each_map_elem 辅助
bpf_for_each_map_elem(&my_map, callback_fn, NULL, 0);

模式 8:程序过大

verifier 日志

BPF program is too large. Processed 1000000 insn

或:

BPF program is too complex

根因:程序的指令数、分支数或循环深度超过了 verifier 的复杂度上限。常见于含有多个嵌套条件的大程序。

缓解策略

// 策略 1:尾调用(tail call)将逻辑拆分
SEC("tc/0")
int program_part1(struct __sk_buff *skb)
{
    // 处理一部分逻辑
    bpf_tail_call(skb, &jmp_table, 1);  // 跳到 program_part2
    return TC_ACT_OK;
}

// 策略 2:减少嵌套条件深度
// 策略 3:使用 bpf_for_each_map_elem 代替手动循环
// 策略 4:使用 per-CPU 变量缓存中间结果

六、Helper 函数参数类型不匹配

模式 9:helper 参数类型错误

错误代码

struct __sk_buff *skb = (struct __sk_buff *)ctx;
// bpf_skb_store_bytes 第 4 个参数要求 ARG_PTR_TO_PACKET
// 但传入了 NULL
bpf_skb_store_bytes(skb, 0, NULL, 4, 0);

verifier 日志

R3 type=scalar expected=ptr_to_packet

根因:每个 helper 函数的参数类型在 bpf_func_proto 中定义。verifier 在 check_func_arg() 中对每个参数做类型匹配。

正确写法:查阅 helper 文档或头文件,确保参数类型正确。常见 helper 参数类型:

类型宏 含义
ARG_PTR_TO_MAP_KEY map key 指针
ARG_PTR_TO_MAP_VALUE map value 指针
ARG_PTR_TO_CTX 程序上下文
ARG_PTR_TO_MEM 通用内存指针
ARG_PTR_TO_PACKET 数据包指针
ARG_CONST_SIZE 常量大小
ARG_ANYTHING 任意值

模式 10:helper 不可用于当前程序类型

错误代码

SEC("xdp")
int xdp_prog(struct xdp_md *ctx)
{
    // bpf_skb_load_bytes 只能用于 SKB 上下文,不能用于 XDP
    char buf[16];
    bpf_skb_load_bytes(ctx, 0, buf, 16);
}

verifier 日志

unknown func bpf_skb_load_bytes#26

根因:XDP 程序上下文中没有 __sk_buff,所以所有 SKB 相关的 helper 都不可用。

七、Map 操作约束

模式 11:写入只读 map

错误代码

// 使用 BPF_F_RDONLY 创建的 map
bpf_map_update_elem(&readonly_map, &key, &value, BPF_ANY);

verifier 日志

R1 type=map_ptr expected=map_ptr
write to read-only map is prohibited

正确做法:只读 map 只能通过 bpf_map_lookup_elem 读取。写入需要 BPF_F_WRONLYBPF_F_RDWR 标志。

模式 12:原子操作的对齐

错误代码

__u64 *val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
    return 0;

__sync_fetch_and_add(val, 1);  // 需要 8-byte 对齐

如果 val 不是 8-byte 对齐(例如 map value 的第一个字段是 __u32),verifier 拒绝。

verifier 日志

misaligned stack access off=... size=8

或:

BPF_ATOMIC stores into R1 off=0 is not allowed

八、栈使用约束

模式 13:栈大小超出

错误代码

SEC("xdp")
int large_stack(struct xdp_md *ctx)
{
    char large_buffer[400];  // 分配 400 字节
    char another_buffer[200];  // 又分配 200 字节 → 600 > 512
    ...
}

verifier 日志

combined stack size of 4 calls is 600. Too large

或:

stack depth 600 exceeds limit 512

根因:BPF 程序的栈空间固定为 512 字节(MAX_BPF_STACK)。这包括所有函数调用栈帧的总和(对于包含子程序的程序)。

缓解策略

// 策略 1:使用 per-CPU array map 存储大缓冲区
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, char[1024]);
} big_buffer SEC(".maps");

// 策略 2:拆分子程序
// 策略 3:使用 bpf_dynptr 动态分配(如果可用)

模式 14:未初始化栈读取

错误代码

char buf[16];
__u32 val = *(__u32 *)buf;  // buf 未初始化,verifier 拒绝

verifier 日志

invalid read from stack off -16+0 size 4

根因:栈槽初始化为 STACK_INVALID。必须先写入才能读取。

正确写法

char buf[16] = {0};  // 写入零值,标记为 STACK_ZERO
// 或显式写入
*(__u32 *)buf = 0;
__u32 val = *(__u32 *)buf;  // 现在允许

九、返回值与程序出口

模式 15:返回值不在有效范围

对于 XDP 程序,返回值必须是 XDP_ABORTED/XDP_DROP/XDP_PASS/XDP_TX/XDP_REDIRECT 之一。如果 verifier 不能证明返回值在这些值之内,程序被拒绝。

SEC("xdp")
int bad_return(struct xdp_md *ctx)
{
    return bpf_get_prandom_u32() % 3;  // 返回值未知
}

verifier 日志

At program exit the register R0 has smin=-1 smax=2 should have been in [0, 3]

正确写法:确保出口处 R0 在所有可达路径上的值都是合法返回值。

十、额外模式

模式 16:不正确的上下文访问

XDP 程序的 ctx->datactx->data_end 必须按特定方式加载:

// 正确方式
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;

// 错误方式:直接解引用 ctx 字段
void *data = ctx->data;  // verifier 可能不接受

模式 17:spinlock 持有期间调用 helper

持有 bpf_spin_lock 期间不能调用任何 helper:

bpf_spin_lock(&lock);
bpf_map_update_elem(...);  // 错误:持有锁时不能调用 helper
bpf_spin_unlock(&lock);

verifier 日志

calling kernel function bpf_map_update_elem is not allowed when holding a lock

模式 18:类型不匹配的 map 间接访问

BPF 程序在 map value 中存储了辅助指针(存储为 __u64),试图直接读取为指针类型:

__u64 stored_ptr = val->saved_ptr;  // scalar 类型
struct task_struct *task = (struct task_struct *)stored_ptr;
// verifier 拒绝:scalar 不能转换为指针

正确的替代方案:使用 bpf_kptr_xchg 和 kptr 机制(5.x+ 内核),或通过 bpf_task_from_pid 等 kfunc 重新获取合法指针。

十一、排障方法论

当 verifier 拒绝程序时,按以下步骤系统化排查:

步骤 1:从日志底部找到具体错误行

# 获取完整 verifier 日志
bpftool prog load my_prog.o /sys/fs/bpf/test \
    type xdp \
    log_level 2 \
    log_buf verifier.log 2>&1

# 从尾部开始看
tail -20 verifier.log

步骤 2:识别错误类型

错误关键词 对应模式 检查方向
invalid mem access 'map_value_or_null' 模式 1 map_lookup_elem 后未判空
invalid mem access 'packet' 模式 2-3 边界检查缺失或顺序错误
variable packet access 模式 4 偏移不是常量
type=scalar expected=ptr 模式 9 helper 参数类型错误
stack depth 模式 13 栈变量过大
invalid read from stack 模式 14 读取未初始化栈变量
unknown func 模式 10 helper 不可用于此程序类型
processed 1000000 insn 模式 7-8 复杂度超限

步骤 3:向上追溯相关的寄存器状态

在错误行的上面,查找相关寄存器的状态变化。例如:

5: (85) call bpf_map_lookup_elem#1
6: (bf) r1 = r0                    ; R0=map_value_or_null(id=1,off=0,ks=4,vs=8)
7: (15) if r1 == 0x0 goto pc+2    ; 这里做了 NULL 检查但...
8: (61) r2 = *(u32 *)(r10 - 4)    ; 栈访问
9: (07) r1 += 0x2                 ; 指针算术改变了 R1...
10: (61) r1 = *(u32 *)(r1 + 0)    ; ☆ 错误!R1 类型已变成 scalar

步骤 4:修正并验证

修正代码后,使用以下命令验证:

# 使用 bpftool 直接在系统中加载验证
bpftool prog load my_prog.o /sys/fs/bpf/test \
    type xdp \
    log_level 1

# 如果通过,卸载
rm /sys/fs/bpf/test

十二、Mermaid:verifier 错误排障决策树

flowchart TD
    ERR["verifier 拒绝程序<br/>有日志输出"]

    BOTTOM["从日志底部找到错误行<br/>识别错误关键词"]

    MAP_NULL{"invalid mem<br/>map_value_or_null?"}
    PKT{"invalid mem<br/>packet?"}
    VAR_PKT{"variable<br/>packet access?"}
    SCALAR{"type=scalar<br/>expected=ptr?"}
    STACK{"stack depth<br/>exceeds?"}
    UNINIT{"invalid read<br/>from stack?"}
    COMPLEX{"processed<br/>1000000 insn?"}
    UNKNOWN{"unknown<br/>func?"}

    FIX1["在 map_lookup_elem 后<br/>添加 if (!ptr) 检查"]
    FIX2["在数据访问前添加<br/>if (data+size > data_end)"]
    FIX3["将偏移约束在<br/>已知常量上界内"]
    FIX4["检查 helper 参数类型<br/>确保传入正确指针"]
    FIX5["减少局部变量大小<br/>或使用 per-CPU map"]
    FIX6["初始化所有栈变量<br/>char buf[16] = {}"]
    FIX7["用尾调用拆分逻辑<br/>或用常量上界循环"]
    FIX8["此 helper 不支持<br/>当前程序类型"]

    RETRY["修正后重新加载<br/>bpftool prog load..."]
    PASS{"通过?"}
    DONE["完成 ✓"]

    ERR --> BOTTOM
    BOTTOM --> MAP_NULL
    BOTTOM --> PKT
    BOTTOM --> VAR_PKT
    BOTTOM --> SCALAR
    BOTTOM --> STACK
    BOTTOM --> UNINIT
    BOTTOM --> COMPLEX
    BOTTOM --> UNKNOWN

    MAP_NULL -->|是| FIX1
    PKT -->|是| FIX2
    VAR_PKT -->|是| FIX3
    SCALAR -->|是| FIX4
    STACK -->|是| FIX5
    UNINIT -->|是| FIX6
    COMPLEX -->|是| FIX7
    UNKNOWN -->|是| FIX8

    FIX1 --> RETRY
    FIX2 --> RETRY
    FIX3 --> RETRY
    FIX4 --> RETRY
    FIX5 --> RETRY
    FIX6 --> RETRY
    FIX7 --> RETRY
    FIX8 --> RETRY

    RETRY --> PASS
    PASS -->|是| DONE
    PASS -->|否| BOTTOM

十三、小结

verifier 的约束不是随机的为难——每一个拒绝模式都对应了内核安全模型中的一个具体攻击面。PTR_TO_MAP_VALUE_OR_NULL 阻止了空指针解引用;check_packet_access 阻止了越界包读取;scalar-to-pointer 的禁止防止了任意内核内存访问。

熟练掌握这 18 种模式后,verifier 日志从一个黑箱的”报错”变成了精确的静态分析输出——你可以通过错误路径中寄存器的类型和值域,反推程序逻辑的哪一步破坏了 verifier 的推导链。排障效率从”猜半小时”降到”看日志 30 秒”。


上一篇验证器核心算法:抽象解释、状态跟踪与路径裁剪(第 03 篇)

下一篇JIT 编译器后端:x86-64 与 ARM64 的 BPF→Native 翻译管线(第 05 篇)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器

eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。


By .