用 Rust 写 BPF 还差什么:LPC 2026 Alexei rust-bpf 进展解读
本文地址: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 路径。源码几行,验证起来却很重。
图 1:用 Rust 写的 scx_simple 调度器(图源:Alexei Starovoitov《Rust-BPF updates》第 3 页,LPC 2026)
另一个前提是使用未经修改的上游 core 和 alloc,不维护 BPF 专用的删减版标准库。
2. 已就绪:函数调用这一层基本放开了
rustc 生成的代码经常超出 BPF 原有的调用限制,2026 年合入的几项改动把这些限制放宽了:
图 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 指针相同。正式的内存分配器还没有。
图 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。
图 5:bpf_unwind 与 landing pads(图源:Alexei Starovoitov《Rust-BPF updates》第 17 页,LPC 2026)
工具链。 rustc、core、alloc 都用上游版本,但构建链里有三步是自定义的,其中一个 Python 脚本直接改中间代码,用来绕过 kfunc 原型、memcpy、panic 路径等六类问题。演示稿第 19 页也写明,两个调度器示例构建时仍把可能抛异常的 invoke 改成了普通 call,也就是说完整的异常清理在示例里还没真正跑通。
图 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 抽象成了真实的工作负载,逼着验证器回答过去靠限制写法就能回避的问题。下一个有说服力的节点,是去掉这些临时适配之后,复杂程序仍能通过验证。
延伸阅读
- 原文作者:DavidDi
- 原文链接:https://www.ebpf.top/post/rust-bpf-evolution/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 最后更新时间:2026-10-27 08:13:26.038875279 +0000 UTC