0/18 已展开

LLM 分析

BPF Arena:让指针跨 kfunc / struct_ops 边界自动换址

系列概况

  • 标题:[PATCHSET SLOP RFC 0/6] bpf: make arena pointers first-class kfunc and struct_ops arguments
  • 作者:Tejun Heo <tj@kernel.org>
  • 版本:RFC v1;仅 3/6 局部重投 v2
  • 规模:6 个实际 patch
  • 修改文件:21 个,核心涉及 BPF verifier、struct_ops、sched_ext 与 selftests
  • 代码统计:+622/-72
  • Message-ID:20260713024414.3759854-1-tj@kernel.org
  • 完整性:18 封邮件已组全,6/6 patch 齐全;b4 以 v1 补齐局部 v2,无缺失和 tip-bot2 污染

一句话总结:用 __arena 标注让边界代码自动完成 arena 偏移与内核虚拟地址之间的双向转换,调用双方拿到可直接解引用的正确指针。

补丁目的

BPF arena 同一块共享内存有两套地址:BPF 侧使用低 32 位偏移,内核侧使用
kern_vm_start + offset。旧接口只能把 arena 指针伪装成 scalar,接收端再手工加减
基址和强制类型转换,重复且丢失类型语义。

该 RFC 给 kfunc 参数和 struct_ops stub 参数增加 __arena 后缀,把换址统一移到
verifier/JIT 边界。1–4/6 是 BPF 核心及测试;5–6/6 只是 sched_ext 演示,明确标为
NOT_SIGNED_OFF,作者要求不要应用。

BPF 可访问对象分类

分类例子特点
内核对象task_structcgroup内核创建,BPF 只能受限访问
BPF 栈对象函数局部变量临时存在,函数返回后消失
BPF 全局对象.bss/.data/.rodata加载时创建,大小固定,底层是全局数据 map
普通 Map 对象Hash、Array、Per-CPU Map通过 key 查找,由内核管理
Local StorageTask/Cgroup/Socket Storage附着在相应内核对象上并随其生命周期管理
BPF 动态对象bpf_obj_new()BPF 动态创建,受 verifier 引用与生命周期规则约束
Arena 对象bpf_arena_alloc_pages()共享大内存,可构造复杂数据结构,用户态与 BPF 访问同一批页面

对象属于哪一类取决于存储来源,不取决于它在 init_task()cgroup_init() 等哪个
回调中创建。本系列只解决 Arena 对象指针跨 kfunc/struct_ops 边界的问题;普通 map、
Local Storage 和内核对象不需要这套 Arena 基址换算。

旧流程的问题

旧的 kfunc 路径要求 BPF 主动“洗成”scalar,kfunc 再加 arena 内核基址;旧的
struct_ops 路径则由内核先手工减基址,再让 BPF cast 回 arena 指针。结果是每个接口
都复制换址代码,BTF/verifier 也看不见参数的真实指针类型。

新流程

BPF arena ptr                      kernel arena address
  低 32 位偏移                          base + offset
       │                                      │
       │ kfunc 调用:zext32;非 NULL 时 +base │
       ├─────────────────────────────────────>│ kfunc 直接解引用
       │                                      │
       │ struct_ops:非 NULL 时 -base;zext32 │
       │<─────────────────────────────────────┤ 内核传原生指针
       │ BPF callback 直接解引用               │

v2 防线:struct_ops ctx 被原地换址后禁止 tail call,避免下一程序再次减 base。

旧:接口各自传 scalar、换址、cast。新:参数名携带 __arena 语义,边界自动换址,
NULL 保持为 NULL;但最新审阅建议把非空与可空语义拆开,并改由架构 JIT 实现。

Patch 概览

Patch核心改动
1/6kfunc 识别 __arena,校验 arena/寄存器类型并在调用前把偏移 rebase 为内核地址
2/6覆盖 PTR_TO_ARENA、scalar、NULL、5 参数、未映射页恢复及拒绝路径
3/6struct_ops ctx 支持 PTR_TO_ARENA,入口 prologue 反向 rebase;v2 禁止 tail call
4/6增加 struct_ops 正常、无 arena attach 失败和 tail-call 拒绝测试
5/6演示 ops_cid.set_cmask() 直接接收内核 arena 指针,删除手工转换
6/6演示 scx_bpf_cid_override() 接收 arena 数组,scx_qmap 数据迁入 arena

关键实现

1. kfunc:验证后在调用点换址

check_kfunc_args() 通过 BTF 参数名后缀识别 __arena:参数必须是指针,程序必须
关联 arena,实参只能是 PTR_TO_ARENA 或 scalar。随后记录 R1–R5 位图,fixup 对每个
参数执行近似逻辑:

arg = (u32)arg;
if (arg)
	arg += kern_vm_start;

常量盲化开启时,补丁改用 R0 保存基址、AX 保存零值;关闭时用 AX 和立即数比较。

2. struct_ops:入口原地转换 ctx slot

prepare_arg_info()__arena stub 参数登记为 PTR_TO_ARENA;入口 prologue 从
扁平化 ctx[] 取内核地址,非 NULL 时减 kern_vm_start,截为低 32 位后写回原 slot。
btf_ctx_access() 因而能把后续 ctx load 恢复成 arena 指针类型。

原地写回使 tail call 复用同一 ctx 时会二次减基址。Sashiko 指出这一高风险问题后,
3/6 v2 在 check_struct_ops_btf_id() 中拒绝“arena 参数 + tail call”。

3. 地址安全契约

补丁不做逐参数长度检查,而依赖 4 GiB arena 映射尾部约 32 KiB guard 和 arena fault
恢复。未填充页会落到 scratch page。作者认为当前 kfunc 只访问小型固定对象;若未来
对象超过 guard,应扩大 guard,而非给每个 kfunc 添加 bounds check。

4. sched_ext 只是效果演示

5/6 让 set_cmask() 由 struct_ops 自动换址;6/6 把 cpu_to_cid 数组放入 arena,
把“字节长度”参数改成“元素数”。当前工作树仍保留旧的 buffer+size 实现,印证这两项
演示尚未合入,正式版本要等 bpf-next 与 sched_ext 重整后重做。

类比

arena 偏移像酒店房号“301”,内核地址像“酒店完整地址 + 301”。旧接口要求每位客人
自己补全或删掉酒店地址;__arena 像前台盖章,进内核时自动补全,回 BPF 时自动只留
房号。最新审阅则建议再区分“必有房号”和“允许空房号”,避免每次都做 NULL 分支。

Highlight:风险与注意点

  • 最新人类审阅认为总体方向合理,但建议 __arena 表示非 NULL,另增
    __arena_nullable;这也解决 offset 0 与 NULL 被当前方案合并的问题。
  • 同一审阅建议不要注入通用 BPF fixup/prologue:kfunc 方向应由 x86/arm64 JIT 利用
    已有 arena 基址寄存器转换;struct_ops 应在单节点 trampoline 的 save_args()
    直接生成转换后的 ctx,避免额外 load/modify/store。其他架构或通用多程序
    trampoline 应返回不支持。因此 v2 仍需要较大重构,不能视为已认可。
  • v2 已封堵 struct_ops tail call 的二次 rebase,并增加拒绝测试;这是明确修复项。
  • 32 KiB guard 是接口契约也是约束:callee 若按可控大长度越界访问,guard 可能不够;
    文档应明确最大安全对象大小,未来大对象需要扩大 guard 或重新设计校验。
  • 6/6 从共享 arena 读取 CID 未用 READ_ONCE()。作者同意正式 sched_ext 重做时加固,
    但认为当前局部变量通常留在寄存器中;演示补丁本身未修。
  • 作者声明系列大量借助 AI 且自己不熟悉 verifier;现有结果仅报告自测在
    bpf_jit_harden=0/1/2 通过,不替代维护者审阅。
  • 线程没有 Reviewed-by/Acked-by;Sashiko 是自动审阅,人类回复提出后续重构,不能称为
    已获认可或已准备合入。

版本变化

v1 发布完整 6 patch。Sashiko 发现 struct_ops ctx 原地改写后 tail call 会重复 rebase;
作者只重投 3/6 v2,增加 tail-call 禁止逻辑。其余 patch 由 b4 从 v1 补齐,所以 b4 输出
中的“v2 1/6、2/6、4/6–6/6”是重建标签,不是独立 v2 邮件。

随后 Kumar Kartikeya Dwivedi 建议拆分 nullable 语义,并把双向换址分别下沉到架构 JIT
和 struct_ops trampoline;这代表下一版的主要方向,当前线程尚无完成该重构的版本。

一句话总结

该 RFC 证明了 arena 指针可以通过 BTF 后缀自动跨越 kfunc/struct_ops 边界,并已修复
tail-call 二次换址;但 NULL 语义、架构支持和 trampoline 落点仍待按维护者意见重做。