sched-ext discussion
[PATCHSET SLOP RFC 0/6] bpf: make arena pointers first-class kfunc and struct_ops arguments
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_struct、cgroup | 内核创建,BPF 只能受限访问 |
| BPF 栈对象 | 函数局部变量 | 临时存在,函数返回后消失 |
| BPF 全局对象 | .bss/.data/.rodata | 加载时创建,大小固定,底层是全局数据 map |
| 普通 Map 对象 | Hash、Array、Per-CPU Map | 通过 key 查找,由内核管理 |
| Local Storage | Task/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/6 | kfunc 识别 __arena,校验 arena/寄存器类型并在调用前把偏移 rebase 为内核地址 |
| 2/6 | 覆盖 PTR_TO_ARENA、scalar、NULL、5 参数、未映射页恢复及拒绝路径 |
| 3/6 | struct_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 落点仍待按维护者意见重做。