0/13 已展开

LLM 分析

BPF cpumask:阻止借用掩码被当作可写目标

系列概况

  • 标题:[PATCH bpf-next v3 0/2] bpf: require an owned cpumask for bpf_cpumask_populate()
  • 作者:Nicholas Dudar main.kalliope@gmail.com
  • 版本:v3(v1/v2 为发往 security@kernel.org 的私密报告)
  • 规模:2 个实际 patch
  • 修改文件:5 个(BPF cpumask kfunc、sched_ext 兼容声明及 BPF selftests)
  • 代码统计:+29/-10
  • Message-ID:20260709182800.2037938-1-main.kalliope@gmail.com
  • 完整性:完整;cover 的 0/2 与 2 个 patch 一致,原始线程共 13 封邮件,无缺失警告及 tip-bot2 污染

一句话总结:把 bpf_cpumask_populate() 的目标参数收紧为“拥有且可写”的 bpf_cpumask,让 verifier 在程序加载期拒绝修改借来的只读 cpumask。

补丁目的

bpf_cpumask_populate() 会用 bitmap_copy() 改写目标,但旧签名却是
struct cpumask *。该类型也用于只读、借用的掩码,例如
scx_bpf_get_online_cpumask() 的返回值,因此 verifier 无法从 kfunc
签名判断调用者是否拥有目标,错误地允许写入全局或其他对象中的掩码。

本系列把写操作统一到 struct bpf_cpumask *:查询 kfunc 仍接收
const struct cpumask *,只有修改操作要求 owned BPF cpumask。

旧流程的问题

旧签名只表达“这是 cpumask”,没有表达“这是调用者拥有的可写对象”。
BPF C 代码即使拿到借用指针,也能将其传给 populate;调用通过 verifier
后,bitmap_copy() 会真实覆盖底层位图,破坏只读契约及共享状态。

问题源自 950ad93df2fc ("bpf: add kfunc for populating cpumask bits")
它不是复制长度或对齐检查错误,而是目标指针的 BTF 类型过宽。

新流程

BPF 程序调用 bpf_cpumask_populate(dst, src, size)
                         |
                         v
             verifier 检查 dst 的 BTF 类型
                  /                     
     owned struct bpf_cpumask *      borrowed struct cpumask *
                |                            |
                v                            v
       允许加载;运行时复制          加载期拒绝:类型不匹配
                |
                v
      &dst->cpumask <- src bits

旧:任何满足 struct cpumask * 的借用指针都可能成为写目标。

新:只有 struct bpf_cpumask * 能进入写路径;只读查询接口不变。

Patch 概览

Patch核心改动
1/2收紧 kfunc 与 sched_ext 兼容声明,并从内嵌的 cpumask->cpumask 取得位图
2/2同步 selftest 声明、删除旧强转,并新增借用目标必须被 verifier 拒绝的失败测试

关键实现

  1. 类型即权限边界

    __bpf_kfunc int bpf_cpumask_populate(struct bpf_cpumask *cpumask,
                                         void *src, size_t src__sz)
    {
            /* 原有长度、对齐检查保持不变 */
            bitmap_copy(cpumask_bits(&cpumask->cpumask), src, nr_cpu_ids);
            return 0;
    }
    

    变化的核心不是复制算法,而是 kfunc 元数据向 verifier 暴露的参数类型。
    struct bpf_cpumask 封装真实 struct cpumask,因此运行时多取一层成员。

  2. 失败测试验证 BTF,而非 C 强转

    新测试把借用的 task->cpus_ptr 显式强转为
    struct bpf_cpumask *。C 强转只能让编译器接受代码,不能改变寄存器
    实际携带的 BTF 来源;verifier 应报告 R1 是 STRUCT cpumask,而期望
    STRUCT bpf_cpumask。这证明调用者不能靠强转绕过所有权边界。

  3. 成功与错误路径保持原语义

    现有小缓冲区 -EACCES、不支持架构上的非对齐源 -EINVAL、正常复制
    等测试只删除多余的 (struct cpumask *) 强转。没有新增锁、RCU 临界区
    或运行时分配,热路径成本基本不变。

  4. sched_ext 兼容声明同步

    tools/sched_ext/include/scx/compat.bpf.h 的弱符号签名同时更新,避免
    兼容宏继续向 BPF 编译器描述旧类型;注释说明该 compat 宏计划在
    v6.19 删除。

类比

旧接口像一台只检查“是不是房卡”的写卡机:访客卡虽然只该查看房间,
也能被拿去修改门锁权限。新接口要求“管理员卡”;C 强转只是给卡贴上
管理员标签,门禁 verifier 仍会读取芯片里的真实卡种并拒绝访客卡。

Highlight:风险与注意点

  • 这是有意的 BPF kfunc 源码接口收紧。树外程序若把普通或借用
    cpumask * 传给 populate,将在编译或 verifier 加载阶段失败;真正拥有
    bpf_cpumask 的调用只需移除旧强转。
  • selftest 使用 task->cpus_ptr 代表借用对象,没有直接调用最初场景中的
    sched_ext online/idle cpumask kfunc。Emil 指出这一覆盖差异,但认为缺少
    sched_ext selftest 基础设施时,当前 tracepoint 测试足以验证禁止
    cpumask -> bpf_cpumask 转换的核心规则。
  • Sashiko 对 1/2 单独扫描时提示 selftests 未同步;2/2 已完整处理,因此
    这是按单 patch 观察产生的提醒,不是系列遗留缺陷。
  • Emil 对两个 patch 均给出 Reviewed-by;Kumar 认为不会破坏预期用法;
    Tejun 给出 Acked-by。但 Tejun 的邮件晚于 patchwork 的合入通知,不能
    将该 Ack 描述成合入前已具备的条件。
  • patchwork-bot 通知系列已由 Kumar 合入 bpf/bpf-next.git,对应
    63a704c8b3695aaad4d9fe9f。当前本地仓库不含这两个对象,且
    git.kernel.org 的补丁页受反爬挑战阻挡,因此这里只确认了原始 v3 diff、
    邮件评审和合入通知,未对最终对象做逐字节复核。

版本变化

公开线程只有 v3。cover 说明 v1/v2 是私密安全报告;v3 将修复与测试拆成
两个 patch,并把目标树从 bpf 改为 bpf-next。不可见的私密版本内容
无法独立比较,除 cover 明示的两点外不应推断更多差异。

一句话总结

该系列用精确的 BTF 参数类型把“可读 cpumask”和“拥有且可写的
bpf_cpumask”分开,在 verifier 加载期堵住借用掩码被覆盖的路径,并用
正反 selftest 固化这一所有权规则,运行时复制逻辑及同步语义不变。