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_PACKET 的
range 字段保持为 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 指针的 range 为
data_end - data - 4(至少 0),并使 data
成为有效的可解引用指针。
关键规则:边界检查必须紧邻在访问之前。如果两条指令之间有任何其他指令改变了
data 或 data_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_END 或
ptr + offset >= PTR_TO_PACKET_END
这种形式。写成 data_end > data + offset 或
data + 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_WRONLY 或 BPF_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->data 和
ctx->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 篇)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器
eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。
【eBPF 内核实现深度拆解】验证器核心算法:抽象解释、状态跟踪与路径裁剪
深入 verifier 的静态分析引擎——寄存器状态 reg_state 的类型/值域表示、栈状态 stack_state 的初始化标记、explore_state 的 DFS 搜索、states_equal 的等价判定、precision tracking——这是整个系列最难也最核心的一篇。
【eBPF 内核实现深度拆解】Helper 函数子系统:注册、类型检查与参数传递
从 bpf_func_proto 结构体出发,讲解 helper 函数的注册机制(BPF_CALL_n 宏链)、参数类型编码(ARG_PTR_TO_MAP_KEY 等枚举)、返回值策略,以及 verifier 在 check_helper_call() 中对每个参数的类型与边界检查。