Linux 7.3 BPF新特性:binfmt_misc 可编程、arena 指针直通 kfunc、验证器"说人话"
本文地址:https://www.ebpf.top/post/linux-7-3-bpf-features
本文基于 Linux 7.3-rc7(2026-10-12)整理,正式版如有出入将另行更正。
1. 引子:373 个提交里的 BPF 全景
8 月 20 日,Linus 合并了 Daniel Borkmann 提交的 bpf-next-7.3 主 pull,一共 373 个提交。pull 说明里列出的五条主线是:
- 验证器报错重设计;
- kfunc / struct_ops 支持 arena 参数;
- 签名 loader 重做;
- ksock kfunc;
- helper 与 kfunc 参数校验统一。
除此之外,经 VFS 树合入的还有两块和 BPF 强相关的内容:binfmt_misc 的 BPF handler,以及 bpf_sock_read_xattr()。据 LWN 合并窗口统计(第一周、第二周),本周期 kfunc 删除 3 个、新增 32 个。
先用一张表看全貌:
| 方向 | 7.3 新增 | 主要受益者 |
|---|---|---|
| exec 路径 | binfmt_misc BPF handler | Nix 等可重定位二进制、qemu-user 场景 |
| socket | bpf_sock_read_xattr()、ksock kfunc |
BPF LSM 策略、内核内直接上报遥测 |
| kfunc 机制 | KF_SPINLOCK_SAFE、__arena、参数校验统一 |
kfunc 与 struct_ops 作者、sched_ext |
| 全局数据 | SEC(".percpu") |
需要每 CPU 计数的程序 |
| 验证器 | 结构化诊断报告 | 所有写 BPF 的人和 AI 智能体 |
| 签名 | 内核校验元数据 | 需要可信分发的发行版与平台 |
下面按"BPF 伸进了哪些新地方、机制如何演进、开发体验和安全"的顺序展开。
2. exec 路径可编程:binfmt_misc 的 BPF handler
2.1 痛点
Nix 风格的可重定位、hermetic 二进制,动态加载器的位置只能相对于二进制自身来确定。可 ELF 的 PT_INTERP 和 binfmt_misc 的解释器都是固定字符串,表达不了"相对路径",过去只能靠 patchelf 改写或者套 wrapper 脚本。
2.2 被否决的方案
最直观的做法是在 PT_INTERP 里展开 $ORIGIN,但它牵扯 AT_SECURE、LSM 和加载器注入风险,而且 bprm->file 可能根本没有可用路径,Christian Brauner 否决了这个方案;做一个可插拔的 ELF 解释器模块注册表的思路也没被接受。最终的答案是:把决策交给 BPF(合入提交,LWN 报道)。
2.3 struct_ops:match() 与 load()
|
|
- 两个回调都可睡眠,
bprm以可信 BTF 指针传入; - match 可以用
bpf_dynptr_from_file()读文件任意位置,不再局限于预取的 256 字节bprm->buf; - 一旦 match 命中,执行就归这个 handler:load 失败则 exec 失败,不会回落到后面的条目;返回
-ENOEXEC时交给其余 binfmt。
2.4 新 kfunc
只允许在 load 中调用(验证器按程序挂载的 struct_ops 成员区分):
bpf_binprm_set_interp(bprm, path, path__sz)bpf_binprm_set_interp_arg(...)bpf_binprm_set_flags(bprm, flags)bpf_binprm_select_interp()(后续补丁加入)
配合 bpf_path_d_path(&bprm->file->f_path) 就能拿到二进制自身的位置,进而拼出相对的加载器路径。
2.5 两种调度模式
相关提交:
- ‘T’ 透明调度:二进制通过 AT_EXECFD 交给解释器,argv 保持原样,
mm->exe_file和 comm 仍是二进制本身,并置AT_FLAGS_TRANSPARENT_INTERP;BPF 侧对应BPF_BINPRM_TRANSPARENT。 - ‘L’ 加载器替换:原生执行二进制,只替换 PT_INTERP,适用于本架构、带 PT_INTERP 的 ELF。
2.6 注册、预绑定解释器与 ‘D’
struct_ops attach 后,handler 按注册任务所在的 user namespace 登记,再用新的 ‘B’ 类型激活:
|
|
为了防止有人通过挂载命名空间把解释器路径换掉,可以先用 ‘D’(registered disabled)以禁用状态注册条目、预绑定解释器,再写入 1 激活;首次激活后不可再添加解释器绑定(相关提交):
|
|
预打开的数量按 ucounts 计入 /proc/sys/user/max_binfmt_misc_interpreters。
2.7 安全模型,以及一个越界读修复
计算出的解释器用 open_exec() 以调用者凭据打开,并作为下一级 binprm 走完整的 LSM 检查,也就是说程序"只能把调用者重定向到它本来就能 exec 的东西"。
不过 kfunc 本身也会出错:bpf_binprm_select_interp() 先 strnlen 再 strcmp,两步之间存在 TOCTOU,最多越界读 31 字节,Chris Mason 于 9 月 18 日修复。对写 kfunc 的人来说,这是个很好的提醒:来自 BPF 的缓冲区,长度检查和使用之间不能留窗口。
2.8 一个 handler 的工作流程
以 Nix 场景为例:match 回调读取 ELF 头或自定义 note,判断是否为目标二进制;load 回调用 bpf_path_d_path() 取得二进制自身路径,拼出 <dir>/../lib/ld-linux.so,再调用 bpf_binprm_set_interp() 设置解释器,并用 bpf_binprm_set_flags() 选择 ‘L’ 模式。用户态 attach struct_ops 后写入 ‘B’ 条目即可生效。完整可运行的写法可参考内核源码 tools/testing/selftests/exec/ 下的 binfmt_misc BPF 测试。
整条路径可以用下图概括:
图 1:binfmt_misc BPF handler 的 exec 流程(示意图,作者据合入提交自绘)
3. socket 新能力:xattr 标签与 ksock 发包
3.1 bpf_sock_read_xattr():给 socket 打标签
从 7.1 开始 socket 支持扩展属性,systemd 会给 socket 打 user.* xattr,用于限速、varlink registry 等。7.3 新增的 bpf_sock_read_xattr() 让 BPF LSM 能把这些标签读回来:
- 输入
struct socket,从 sockfs inode 读出user.*xattr 写入 dynptr,接口仿照bpf_cgroup_read_xattr(); - 注册为 KF_RCU,只开放给 BPF LSM 程序;
- 底层走
simple_xattr_get()的 RCU rhashtable 查找,不睡眠也不加锁,可睡眠和不可睡眠的 LSM 钩子都能用。
典型用法:用户态对 socket 执行 fsetxattr(fd, "user.tag", ...),BPF LSM 挂在 bind/connect 钩子上按标签放行或拒绝,相当于给 socket 加了一套由用户态定义的"标签策略"。
3.2 ksock kfunc:BPF 自己发 UDP
这组 kfunc 的目标是让 BPF 程序直接把遥测发到网络上,不经过用户态 agent。7.3 落地了 5 个 kfunc:bpf_ksock_create/connect/send/acquire/release。
- 目前只支持 UDP;
- create 和 connect 只能在 SYSCALL 程序中调用,send/acquire/release 在 SYSCALL 和 LSM 程序中都可用;
- 对象引用计数后存入 kptr map,释放走
queue_rcu_work; - socket 操作照常经过 LSM 钩子,挂在
security_socket_sendmsg上的程序禁止调用这组 kfunc,以免递归;create 会拷贝参数防 TOCTOU。
使用模式:先用 BPF_PROG_RUN 跑一个 SYSCALL 程序创建并连接 socket,存进 kptr map;之后 LSM 程序在事件发生时直接 send。
4. kfunc 机制演进:从隐式白名单到显式声明
4.1 KF_SPINLOCK_SAFE
持有 bpf_spin_lock 时能调哪些 kfunc,以前写死在验证器的 kfunc_spin_allowed() 白名单里,模块里的 kfunc 根本加不进去。7.3 改成声明式:
|
|
graph、数值迭代器、res spin lock、arena、stream 等现有 kfunc 都补上了标记,白名单随之删除。这组补丁的作者是 Kaitao Cheng。
4.2 __arena / __arena__nullable:arena 指针直通
以前 arena 指针只能当裸标量传给 kfunc,每个 kfunc 自己"加基址再强转";struct_ops 回调收到的也是 u64。7.3 之后(kfunc 侧、struct_ops 侧):
|
|
- JIT 在调用点重定基:
rN = kern_vm_start + (u32)rN。不需要边界检查,因为偏移落在带保护填充的映射里,访问未填充页由 arena 的 scratch page 兜底; __arena无条件重定基(NULL 也会变成基址),需要保留 NULL 用__arena__nullable;- JIT 需声明
bpf_jit_supports_arena_args(),否则返回-ENOTSUPP; - struct_ops 方向,trampoline 保存参数时做
ctx[slot] = (u32)(kaddr - kern_vm_start),程序拿到的是 PTR_TO_ARENA(仅单程序的间接 trampoline)。
sched_ext-for-7.3 已经用上了这套机制,arena 对象以带类型的指针跨越内核/BPF 边界。
4.3 helper 与 kfunc 参数校验统一
Amery Hung 的系列让 kfunc 参数完全按 BTF 分类,在 add-call 时生成一次 bpf_func_proto,helper 和 kfunc 从此走同一套校验逻辑。
5. 全局 per-CPU 数据:SEC(".percpu")
内核里有 DEFINE_PER_CPU(),BPF C 里一直没有,只能用 1 个条目的 percpu_array 凑合。7.3 配合 libbpf 1.8 之后,只要把全局变量放进 SEC(".percpu") 段,它就成了每个 CPU 一份的变量,程序里像普通全局变量一样读写,用户态通过骨架读出各 CPU 的值。
- 底层仍是 percpu_array map,但 per-CPU 访问被内联成指令;
- libbpf 会探测内核支持,bpftool 骨架支持该段;
- 同一系列修复了拷贝 per-CPU map 值时泄露未初始化内核堆内存的问题;
- 已知问题:Clang 17 上 1 字节 percpu 变量会崩,请用新版 Clang。
作者 Leon Hwang,合并时已是 v12(合入提交)。
6. 验证器报错重设计:让验证器“说人话”
写过 BPF 的人都见过这种报错:
R1 type=scalar expected=fp
然后只能在几千行逐指令日志里找原因。7.3 的这次重设计,说白了就是让验证器“说人话”:除了逐指令日志,还直接告诉你错在哪、为什么错、可以怎么改。在 AI 智能体写代码的场景下,这条反馈回路更成了瓶颈(呼应本号《智能体时代下的 BPF:自我变革迫在眉睫》)。
7.3 新增了 kernel/bpf/diagnostics.c,在原有 verbose 日志之后追加一份结构化报告:
Verification failed: <category>: <problem>
Reason: ...
At: 源码行 + 指令
Causal path: 压缩后的分支与状态事件
Suggestion: ...
- 覆盖类别:Register Type Safety、Memory Safety、Resource Lifetime Safety、Call Type Safety、Execution Context Safety、Program Structure and Policy,目前只覆盖部分报错点,会逐步迁移;
- 诊断历史放在上限 64MiB 的轮转缓冲区,每条因果路径保留最早和最新各 32 个事件;
- merge 时为 v5、14 个补丁,作者 Kumar Kartikeya Dwivedi(合入提交)。
merge 说明里的真实例子:cpumask/test_populate_invalid_destination,旧报错就是上面那行 R1 type=scalar expected=fp;新报告会直接告诉你 bpf_cpumask_populate 的第一个参数需要 24 字节的 struct bpf_cpumask 内存,并定位到 cpumask_failure.c:234:8。
新旧对比一目了然:
| 对比项 | 内容 |
|---|---|
| 旧报错 | R1 type=scalar expected=fp |
| 新报告 | 指出 bpf_cpumask_populate 的第一个参数需要 24 字节的 struct bpf_cpumask 内存,并定位到 cpumask_failure.c:234:8 |
想自己看完整输出,可以在 7.3 的 selftests 里运行 ./test_progs -j1 -a cpumask/test_populate_invalid_destination -vv。
7. sched_ext:子调度器"功能完整"
根调度器可以把一个 cgroup 子树连同可撤销的 CPU 授权交给嵌套的子调度器。另外还有 rescue execution(任务饿死时用小额带宽兜底)、abort 路径 NMI 安全、移除 scx_bpf_cpu_rq(),scx_qmap 示例演示了分层调度(sched_ext 合入)。
8. 签名 BPF 程序:内核终于校验元数据
本号写过《内核 6.18 证书签名机制实测》。6.18 的方案只签 loader 指令,元数据 map 的哈希由 loader 在 BPF 里自己比对,内核从不校验元数据,LSM/完整性社区一直有意见。
7.3 改为:BPF_PROG_LOAD 时,在 LSM 准入钩子和验证器之前,内核把 fd_array 中独占、已冻结 map 的内容拼到指令之后一起做 PKCS#7 校验,即 insns || metadata_0 || ...。没有新增 UAPI,结果只有 BPF_SIG_UNSIGNED / BPF_SIG_VERIFIED 两种,gen_loader 也不再携带校验逻辑。文档见 Documentation/bpf/signing.rst(合入提交)。
两代方案的区别在于“谁来校验元数据、在什么时候校验”:
图 2:6.18 与 7.3 签名校验位置对比(示意图,作者据合入提交自绘)
9. 循环、mmap 与网络 helper:更多实用改进
bpf_for()更快了:数值型 open-coded 迭代器被内联,每次迭代不再有 kfunc 调用开销(Puranjay Mohan),呼应本号《5种循环方式》。- mmap 型 array map 的 mmap 变为 O(1):内存改为惰性填充,mmap() 从与 map 大小成正比变为 O(1)(Song Liu)。
- 网络 helper 增强:
bpf_fib_lookup支持双向 VLAN;bpf_redirect_peer()支持BPF_F_EGRESS;bpf_skb_adjust_room()新增BPF_F_ADJ_ROOM_DECAP_*。 - 构建更省事:resolve_btfids 自动发现 kfunc 并生成 decl tag,减少对 pahole 的依赖。
此外还有各架构 JIT 增强(x86、s390、RISC-V)、timed may_goto、arena bitmap、jit_required 标志、tracing_multi link 信息进入 UAPI,以及 ringbuf 回绕、bpf_find_vma UAF 等一批修复。
10. 总结与展望:两条主线
7.3 的 BPF 有两条清晰的线:一是伸进更多内核路径(exec、socket 标签、直接发包),二是把隐式规则变成显式声明(KF_SPINLOCK_SAFE、__arena、统一参数校验、结构化报错)。后者对人和 AI 写 BPF 都是实打实的体验提升。
还在路上的:BPF 栈从 512 字节提到 2KiB、bpf_unwind 异常清理。用户态配套请升级 libbpf 1.8(USDT 优化要求 7.3+)。
延伸阅读
若还想顺着读,可看这几篇:
- 原文作者:DavidDi
- 原文链接:https://www.ebpf.top/post/linux-7-3-bpf-features/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 最后更新时间:2026-10-27 08:33:37.76037205 +0000 UTC