0/6 已展开

LLM 分析

sched_ext: two more context-safety fixes found in the NMI kfunc audit

系列概况

字段内容
标题[PATCH 0/2] sched_ext: two more context-safety fixes found in the NMI kfunc audit
作者Wanwu Li liwanwu@kylinos.cn
版本v1(2 patches)
规模2 files changed, 21 insertions(+), 2 deletions(-)
修改文件kernel/sched/ext/idle.ckernel/sched/ext/internal.h
代码统计+21 / -2
Message-ID20260903032953.659847-1-liwanwu@kylinos.cn
完整性完整——Tejun Heo 在 message #6 回信 Applied 1-2 to sched_ext/for-7.4,已被合入 sched_ext/for-7.4 分支

补丁目的

sashiko bot 之前发起的 NMI-reject 审计发现 BPF tracing kfunc 在 NMI 上下文调用存在安全隐患,作者继续遍历了所有暴露给 BPF_PROG_TYPE_TRACING 的 kfunc(any / idle / cid 上下文过滤集合),又找出两处上下文安全问题:

  • Patch 1:三个 "any" 类 kfunc 走 scx_locked_rq() 的不安全快路径,把 NMI 中"误以为自己持锁"当成"真持锁"。
  • Patch 2:idle 路径每 CPU 的 per_cpu_unvisited 临时 nodemask 只受 preempt_disable() 保护,IRQ 嵌套会覆写它,导致节点遍历错乱。

整体目标:在源头统一拦截,把 NMI/IRQ 上下文安全基线从"逐个 kfunc 自求多福"收回到"调用接口处一次判断"。

旧流程的问题

问题一:scx_locked_rq() 在 NMI 中撒谎。 每 CPU 维护 scx_locked_rq_state,记录当前上下文持锁的 rq。Tracing BPF 程序可以从 NMI 调用 kfunc,但 NMI 不会替换被打断的上下文,只是插入一条中断流——它读到 scx_locked_rq_state 仍是被打断上下文留下的"我持锁"。结果三个 "any" kfunc 出错:

  • scx_bpf_task_set_slice() 直接写 p->scx.slice,与 update_curr_scx() 对同一字段的非原子 read-modify-write 竞争。
  • scx_bpf_dsq_nr_queued()SCX_DSQ_LOCAL 解析为被中断上下文的本地 DSQ,统计长度张冠李戴。
  • scx_bpf_locked_rq() 把被中断上下文的 rq 交给 BPF 程序,BPF 随后以"持锁者"身份操作它。

问题二:idle-search scratch 在 IRQ 嵌套下被覆写。 pick_idle_cpu_from_online_nodes() 用每 CPU 的 per_cpu_unvisited nodemask 作 scratch,靠 preempt_disable() 保护。但 preempt_disable() 不屏蔽 IRQ,IRQ handler 在同一 CPU 嵌套时会调用 nodes_copy() 覆写 mask,把外层正在遍历的节点集打乱,导致最终选错 idle CPU。

新流程

  • Patch 1scx_locked_rq() 内加 if (unlikely(in_nmi())) return NULL;,让三个调用方在 NMI 中拿到 NULL 并自动回落到 unlocked 路径。
  • Patch 2preempt_disable() / preempt_enable() 替换为 scoped guard guard(irqsave)(),使 IRQ 嵌套在同一 CPU 上无法进入。

NMI 嵌套在 Patch 2 中被明确故意不处理——理由是没有合法理由从 NMI 调用 pick_idle,且不会触发崩溃。

Patch 概览

Patch 1/2 — scx_locked_rq() 在 NMI 中返回 NULL

修改 kernel/sched/ext/internal.h

static inline struct rq *scx_locked_rq(void)
{
    if (unlikely(in_nmi()))
        return NULL;
    return __this_cpu_read(scx_locked_rq_state);
}

三个调用方在 NMI 中收到 NULL 后自动走 unlocked 分支:

调用方回落行为
scx_bpf_task_set_slice()把请求暂存到 atomic 字段 p->scx.slice_oob,待持锁路径应用
scx_bpf_dsq_nr_queued()回退到 this_rq() 解析 SCX_DSQ_LOCAL
scx_bpf_locked_rq()报错并通过 NMI-safe exit 终止调度器

安全性边界:scx_kf_allowed_ctx() 在更外层拦住真正持锁的 kfunc,struct_ops 回调从不进入 NMI,其他 caller 不受影响。

Patch 2/2 — 用 irqsave 保护 idle-search scratch

修改 kernel/sched/ext/idle.c

guard(irqsave)();   /* 替代 preempt_disable() */
/* ... 遍历 per_cpu_unvisited nodemask ... */

关键实现

统一拦截 vs 逐个守卫:Patch 1 选择在 scx_locked_rq() 源头加 in_nmi() 判断,而不是给三个 kfunc 各加一遍守卫——既减少重复代码,也避免未来再加 scx_locked_rq() 调用方时再次踩同一个坑。前提是 scx_locked_rq() 在 NMI 中返回 NULL 对所有调用方都是安全的;commit message 已逐个分析并写明回落路径。

scoped guard 的安全性guard(irqsave)() 由内核 scoped-guard 框架提供,作用域结束自动恢复 flags。即便中途有提前 return 或异常路径也不会泄漏 IRQ 屏蔽,比手工 local_irq_save() + local_irq_restore() 配对更稳健。

NMI 故意不防的依据:作者明确说"从 NMI 调用 pick_idle 没有合法理由且不会崩溃"——属于"假定善用"的设计哲学,避免为不可能存在的调用者增加无谓的 NMI 屏蔽开销。

类比

Patch 1:钥匙位置牌。 scx_locked_rq_state 像贴在车上的"现在钥匙在车里"小牌子,平时只有驾驶员会取这辆车。NMI 来的时候像不速之客——看到牌子写"钥匙在车里",会误以为那是别人家的车钥匙,自己能开走。在牌子上加规则"NMI 来时把牌子翻成空白"(in_nmi() ? NULL : read),强迫不速之客走备用方案(步行 / 公交),这就是 scx_locked_rq() 源头拦截。

Patch 2:共用草稿本。 per_cpu_unvisited 像两张共用草稿纸。两位办事员在同一工位(CPU)轮流办公,外层办事员还在抄第3项时,内层 IRQ 来的人冲进来把整张纸重新誊抄了一遍。irqsave 等于给抽屉加把锁——一次只允许一个人打开抽屉用纸,覆写不会发生。

+--------------------+
| Tracing BPF kfunc|
     |  (any/idle/cid) |
     +--------+-------+
              |
              v
+----------------------+ +---------------------------+
| scx_locked_rq()   |<------>| per-CPU scx_locked_rq_state|
| (after Patch 1)    |        | (set by interrupted ctx)  |
+----------+-----------+        +---------------------------+
           |
   in_nmi()?
 +-- yes --> return NULL  -->  callers take unlocked path
           | - slice_oob stash
           v - fall back to this_rq()
 return __this_cpu_read(...)               - error + NMI-safe exit

----- Patch 2 flow -----

pick_idle_cpu_from_online_nodes()
 |
          v
   guard(irqsave)()   <-- replaces preempt_disable()
          |
          v
   use per_cpu_unvisited nodemask
          |
          v
   guard auto-restores IRQ flags on scope exit

Highlight:风险与注意点

  1. sashiko 新增告警scx_bpf_cpuperf_set()scx_bpf_cidperf_set() 在 NMI 中当 scx_locked_rq() 返回 NULL 后是否仍能死锁,sashiko bot 标记为 High,需要后续单独补丁确认。
  2. 预先存在的体系问题:Tracing BPF 在 NMI 调用任一 "any" 类别 kfunc 都可能因要拿 raw spinlock 而死锁——本系列只补了 scx_locked_rq() 路径,剩余 kfunc 仍属"假定不在 NMI 调用"。
  3. Patch 2 故意不覆盖 NMI 嵌套:如果未来出现合法 NMI 调用 pick_idle 的场景,需要叠加 NMI 屏蔽或重新设计临时变量生命周期。
  4. 被取代的 per-kfunc guard:本 patch 用源头拦截取代对每个 kfunc 加 NMI 守卫,需要保证"所有调用方都接受 NULL 返回"——commit message 已逐个列出回落路径,但仍要警惕未来新加的 scx_locked_rq() 调用方。
  5. 与 sashiko 系列联动:本次补丁源自 sashiko bot 之前发起的 NMI-reject 审计,属于"接着审计再挖两个坑"性质的工作。
  6. Tejun 快速接受:作者得到 maintainer 在同一天的应用动作(Applied 1-2 to sched_ext/for-7.4),说明问题描述与补丁切点足够清晰,争议较小。

版本变化

仅 v1,没有迭代历史可对比。

一句话总结

两个 patch 把 sched_ext 在 NMI/IRQ 上下文的两处安全隐患统一在调用接口处一次性拦截,已被 Tejun Heo 当日合入 sched_ext/for-7.4 分支。