sched-ext discussion
[PATCH 0/2] sched_ext: two more context-safety fixes found in the NMI kfunc audit
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.c、kernel/sched/ext/internal.h |
| 代码统计 | +21 / -2 |
| Message-ID | 20260903032953.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 1 在
scx_locked_rq()内加if (unlikely(in_nmi())) return NULL;,让三个调用方在 NMI 中拿到 NULL 并自动回落到 unlocked 路径。 - Patch 2 把
preempt_disable()/preempt_enable()替换为 scoped guardguard(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:风险与注意点
- sashiko 新增告警:
scx_bpf_cpuperf_set()和scx_bpf_cidperf_set()在 NMI 中当scx_locked_rq()返回 NULL 后是否仍能死锁,sashiko bot 标记为 High,需要后续单独补丁确认。 - 预先存在的体系问题:Tracing BPF 在 NMI 调用任一
"any"类别 kfunc 都可能因要拿 raw spinlock 而死锁——本系列只补了scx_locked_rq()路径,剩余 kfunc 仍属"假定不在 NMI 调用"。 - Patch 2 故意不覆盖 NMI 嵌套:如果未来出现合法 NMI 调用
pick_idle的场景,需要叠加 NMI 屏蔽或重新设计临时变量生命周期。 - 被取代的 per-kfunc guard:本 patch 用源头拦截取代对每个 kfunc 加 NMI 守卫,需要保证"所有调用方都接受 NULL 返回"——commit message 已逐个列出回落路径,但仍要警惕未来新加的
scx_locked_rq()调用方。 - 与 sashiko 系列联动:本次补丁源自 sashiko bot 之前发起的 NMI-reject 审计,属于"接着审计再挖两个坑"性质的工作。
- Tejun 快速接受:作者得到 maintainer 在同一天的应用动作(
Applied 1-2 to sched_ext/for-7.4),说明问题描述与补丁切点足够清晰,争议较小。
版本变化
仅 v1,没有迭代历史可对比。
一句话总结
两个 patch 把 sched_ext 在 NMI/IRQ 上下文的两处安全隐患统一在调用接口处一次性拦截,已被 Tejun Heo 当日合入 sched_ext/for-7.4 分支。