本文地址:https://www.ebpf.top/post/rust-bpf-evolution

先说结论。想用标准 Rust(BTreeMap、Box、dyn Trait、Drop、panic!())写 BPF 程序,函数调用这一层已经基本打通,最大的拦路虎是验证器:两个 Rust 写的 sched_ext 调度器里 13 个程序,在现有验证器上一个都加载不了;换上还在 RFC 阶段的新验证器后,能加载 6 个。指针访问、panic 时的资源清理和工具链也都还在补。

这篇解读依据 BPF 子系统维护者 Alexei Starovoitov 在 LPC 2026 eBPF track 上的报告 Rust-BPF updates,本号在《LPC 2026 eBPF 议程前瞻》里称它为整个 Track 的定调之作。材料是会议页面公开的 23 页演示稿,属于项目作者本人的进展汇报;文中“已合入”“评审中”等状态以演示稿为准,未另行核验最新上游状态。项目代码在 github.com/4ast/rust-bpf。

层面 现状 一句话
函数调用 已合入 参数、返回值、调用深度、栈空间、间接调用都放宽了
验证器 RFC,未发出 13 个复杂程序从 0 个可加载到 6 个
指针与内存 评审中 用 arena 让“数字形式的地址”也能安全访问
panic 清理 评审中(v8) 异常退出时也要执行 Drop
工具链 靠脚本补洞 不 fork rustc,但构建链里有三步自定义

1. 目标:rustc 接受的,验证器也接受

报告的目标只有一句话:

If rustc accepts it, the verifier accepts it.(如果 rustc 接受,验证器也应当接受。)

这是兼容性目标,不是放松安全检查。演示稿同页写明:会泄漏资源的程序不在接受之列,锁、引用、RCU 和 kfunc 参数类型仍要检查,过分的 unsafe 和内联汇编会被拒绝。rustc 在用户态帮程序员查错,验证器仍是内核里的安全边界。

演示用的例子是 scx_simple 调度器:统计数据放在 BTreeMap 里,计数器用 AtomicU64,调度策略通过 dyn Trait 切换。对 Rust 开发者这再普通不过,但编译成 BPF 后,虚表调用变成间接跳转,BTreeMap 查询展开成循环和字符串比较,unwrap() 还会带进 panic 路径。源码几行,验证起来却很重。

Rust 调度器示例:用 BTreeMap 保存统计数据,通过 dyn Trait 选择调度策略

图 1:用 Rust 写的 scx_simple 调度器(图源:Alexei Starovoitov《Rust-BPF updates》第 3 页,LPC 2026)

另一个前提是使用未经修改的上游 core 和 alloc,不维护 BPF 专用的删减版标准库。

2. 已就绪:函数调用这一层基本放开了

rustc 生成的代码经常超出 BPF 原有的调用限制,2026 年合入的几项改动把这些限制放宽了:

BPF 调用能力扩展:超过五个参数、十六字节值、十六层调用帧和两 KiB 栈

图 2:已合入的调用约定扩展(图源:Alexei Starovoitov《Rust-BPF updates》第 6 页,LPC 2026)

能力 变化 为什么需要
函数参数 5 个增至 5 个以上,多出的走栈 不必为 BPF 改写库函数签名
16 字节值 struct、i128 可按值传递和返回 rust-bpf 多处用到 i128
调用深度 8 层增至 16 层 scx_cosmos 的 panic 路径有 12 层
栈空间 512 字节增至 2 KiB,限 x86、arm64 cosmos_exit_task 要 544 字节
间接调用 新增 callx dyn Trait 要走虚表

callx 也有边界:调用目标必须是验证前就全部已知的静态子程序,不是任意跳转。思路很一致:让 BPF 去适应标准 Rust 库的编译结果,而不是让每个 crate 为 BPF 改写。

3. 卡点:验证器“算不过来”

调用通了,程序还是加载不了。scx_simple 和 scx_cosmos 里 13 个非平凡程序,在现有验证器上全部失败,报错都是同一句:

The sequence of 8193 jumps is too complex

其中 9 个卡在 BTreeMap 的搜索和下降,2 个卡在 &str 比较,2 个卡在 panic 路径的 core::fmt。

scx_simple.o 只有 2,052 条指令,验证器却在累计处理约 5.9 万到 22.1 万条指令后放弃。原因是现有验证器沿着每条路径逐个探索,同一条指令会在不同路径、不同循环轮次下被反复分析。挡住程序的不是代码太大,而是证明它安全的代价太高。

已在评审中的 SCEV(标量演化分析,Eduard Zingerman 的 36 个补丁)能把一些有界循环“一次算完”,例如在 7,152 个测试程序中让 243 个的验证量下降。但它要求循环边界在入口处是常量,而 BTreeMap 的边界是运行时从节点里读出来的长度,SCEV 帮不上忙。

4. 解法:验证器改走不动点分析,0/13 到 6/13

Alexei 的方案是重写主验证过程:不再一条条走路径,而是在路径汇合处合并状态,在循环头“拓宽”取值范围,直到状态不再变化,也就是达到不动点(fixpoint)。打个比方,原来要分别验证 i=0、i=1、i=2……,现在只需验证“i 在某个范围内、每轮加 1”。合并也有分寸,只有不影响后续安全检查时才合并。

这套方案是 44 个补丁的 RFC,演示时还没发到邮件列表,同时去掉了状态剪枝和 100 万条指令的处理上限。

验证器重构:从路径探索和剪枝,转向控制流图上的状态合并与不动点分析

图 3:新的不动点验证过程(RFC)(图源:Alexei Starovoitov《Rust-BPF updates》第 10 页,LPC 2026)

效果有两组数字:

测试 结果
veristat top 集合(3,200 个程序) 验证处理的指令量约减少 86%,耗时 3.58 秒降到 2.61 秒
libarena(136 个) 指令量约减少 21%,但耗时 2.58 秒升到 2.77 秒
13 个 Rust 调度器程序 可加载数从 0 个到 6 个,剩下 7 个仍卡在 core::fmt

数据来自演示稿第 12 页的 veristat 结果,百分比由原始数据计算,衡量的是加载时的验证工作,不是程序运行速度。代价是验证器代码变多了:verifier.c 从 22,755 行增长到 27,295 行。

还有一点要注意:验证时状态收敛,不代表运行时循环一定会结束。 方案会在这类循环头插入 may_goto,超时就沿循环出口退出。演示稿第 11 页给出的预算是 250 ms,但第 13 页也承认,预算按每次函数调用单独计算,N 次调用可以跑 N × 250 ms,所以它不是整个程序的总运行上限。

5. 还没打通:指针、panic 清理和工具链

指针。 Rust 容器会把地址存进内存再读出来,验证器读出来的只是一个普通数字,无法再当指针用;Rust 也写不了 C 里的 __arena 标注。方案是 BPF_F_ARENA_SCALAR 标志:带这个标志的程序,通过数字访问内存一律按 arena 访问处理,由 JIT 在运行时约束范围,安全性和现有 arena 指针相同。正式的内存分配器还没有。

arena 方案:为标量形式的地址提供受约束的内存访问,并调整全局数据放置

图 4:把内存访问统一到 arena(图源:Alexei Starovoitov《Rust-BPF updates》第 14 页,LPC 2026)

panic 清理。 unwrap()、索引、除法都可能 panic。现有的 bpf_throw() 直接丢掉调用帧,如果当时持有锁或处于 RCU 临界区,本该执行的 Drop 就被跳过了,所以验证器干脆拒绝。Yonghong Song 的补丁系列(v8,评审中)提出 bpf_unwind() 和 landing pads(清理入口),让每层调用帧在退出时执行自己的清理。目前只支持带 ORC 的 x86 和 arm64。

异常清理机制:通过 bpf_unwind 和 landing pads,让各调用帧执行自己的 Drop 清理

图 5:bpf_unwind 与 landing pads(图源:Alexei Starovoitov《Rust-BPF updates》第 17 页,LPC 2026)

工具链。 rustc、core、alloc 都用上游版本,但构建链里有三步是自定义的,其中一个 Python 脚本直接改中间代码,用来绕过 kfunc 原型、memcpy、panic 路径等六类问题。演示稿第 19 页也写明,两个调度器示例构建时仍把可能抛异常的 invoke 改成了普通 call,也就是说完整的异常清理在示例里还没真正跑通。

rust-bpf 构建链路:rustc、llvm-link、opt、llc 为上游工具,target、bpf-postproc、add_ksyms.py 为自定义环节

图 6:rust-bpf 构建链路,绿色为上游原版工具,橙色为项目自定义环节(据演示稿第 18 页绘制)

覆盖面在扩大:Mykola Lysenko 把内核 BPF selftests 移植成 Rust,577 个对象能构建,550 个在 QEMU 里通过测试,过程记录在 rust-bpf, baby steps。

和 Aya 的关系,演示稿说得很清楚:Aya 是现在用 Rust 写 BPF 的主流选择,用 Rust 写的加载器和 bpf-linker;rust-bpf 走 libbpf 和上游 LLVM,目标是与 C 对齐。两者是 Rust on BPF 的两条路线。

6. 总结:调用已通,验证器重构是关键一步

回到开头的问题,用标准 Rust 写 BPF 还差什么:

  • 已经有的:更多参数、16 字节值、16 层调用、2 KiB 栈和 callx 都已合入。
  • 最关键的:不动点验证器 RFC 让 13 个复杂程序中 6 个能加载,但还没发到邮件列表。
  • 还在路上的:arena 指针访问和 bpf_unwind() 在评审中,构建链仍靠脚本补洞。
  • 悬而未决的:分配器只是占位,栈上数组下标没有检查,core::fmt 的递归还不允许(演示稿第 22 页)。

Rust 带给 BPF 的不只是另一种语法。 常规 Rust 抽象成了真实的工作负载,逼着验证器回答过去靠限制写法就能回避的问题。下一个有说服力的节点,是去掉这些临时适配之后,复杂程序仍能通过验证。

延伸阅读

  1. 当 eBPF 遇到 Rust ?
  2. Rust Aya 开发 eBPF 程序
  3. 终于整齐活了!Rust + eBPF + UProbe 跟踪 Go 程序 gRPC 流