sched-ext discussion
[PATCH] sched_ext: Reject NMI calls to lock-taking kfuncs
LLM 分析
sched_ext:拒绝 NMI 上下文对持锁 kfunc 的调用
系列概况
- 标题:[PATCH] sched_ext: Reject NMI calls to lock-taking kfuncs
- 作者:Wanwu Li liwanwu@kylinos.cn
- 版本:v1 → v2 → v3,单补丁三轮修订
- 规模:3 文件,v3 最终 40 增 / 9 删
- 修改文件:
kernel/sched/ext/ext.c、kernel/sched/ext/internal.h、kernel/sched/ext/sub.c - 代码统计:新增
__scx_kf_allowed_ctx()内联 +scx_kf_allowed_ctx()宏;在 7 个 kfunc 入口加in_nmi()拒绝 - Message-ID:首封
20260901095652.1009104-1-liwanwu@kylinos.cn;尾封a66501d3-7bbe-40d4-b178-caae58ecc5dd@kylinos.cn - 完整性:v3 已被 Tejun Heo 标记 Applied to
sched_ext/for-7.4,附两处微调(恢复 inline 包装、修正 commit message 描述)
补丁目的
scx_kfunc_context_filter() 把 "any/idle/cid" 集合下的 kfunc 全部暴露给 BPF_PROG_TYPE_TRACING。tracing 程序可挂到运行在 NMI 上下文里的函数,因此一个 "any" 分类 kfunc 若在成功路径上取 rq/dsq/pshard 的 raw spinlock,就会和被打断的同 CPU 上下文死锁成 hard-lockup。
此前 e06ece82d7b0("Report NMI kicks with scx_error()")只在 scx_bpf_kick_cpu() 里加 in_nmi() 检查,并保证 scx_error() 走无锁 abort。本系列把同一道闸门推到所有"NMI 可达、又会持锁"的 kfunc:发现 NMI 就 scx_error() abort scheduler 并立即返回,不去碰锁。
旧流程的问题
tracing prog (in NMI) ---> scx_bpf_*_kfunc()
|
v (in_nmi() not checked)
|
v
raw_spin_lock_irqsave(rq/dsq/pshard)
|
v interrupted context on same CPU already holds the lock
|
v
spin forever ---> hard-lockup
错误路径已被 e06ece82d7b0 改成无锁 abort,但成功路径上的 raw_spin_lock_irqsave() 在 NMI 中仍致命旋转。
新流程
tracing prog (in NMI) ---> scx_bpf_*_kfunc()
|
v
scx_kf_allowed_ctx(sch)
|
+---+--------------------------+
| unlikely(in_nmi()) |
| +-- true -> scx_error(.. abort) |
| | return / -EDEADLK |
| +-- false -> return true |
+---+--------------------------+
|
v
business path continues (take lock, change DSQ/perf/sub-caps...)
scx_error() 走的是 f883dbb64ca5 修过的无锁 abort 通道,因此拒绝动作本身不会再抢任何调度器锁,避免自指。
Patch 概览
| kfunc | 锁 / 对象 | 拒绝方式 |
|---|---|---|
scx_kick_cpu()(含 kick_cid) | per-CPU kick list(仅 local_irq_save) | 内部 scx_kf_allowed_ctx() |
scx_bpf_destroy_dsq() | dsq->lock | kfunc 入口 early-return |
bpf_iter_scx_dsq_new() | 后两函数 dsq->lock | 仅 _new() 检查,留 kit->dsq=NULL 让后续 no-op |
scx_bpf_dsq_reenq() | rq->deferred_reenq_lock | early-return |
scx_bpf_cpuperf_set() / cidperf_set() | rq->lock | early-return / -EDEADLK |
sub_cap_preamble()(覆盖 sub_grant/revoke) | pshard 锁 | -EDEADLK |
只读类(dsq_peek/dsq_nr_queued/cpuperf_cur/cap/sub_caps/idle cpumask/cid lookups)经审计不持锁,未加守卫。select_cpu 路径取 pi_lock,但 BPF_PROG_TYPE_TRACING 拿不到这组入口,故无需守卫。
关键实现
/* kernel/sched/ext/internal.h */
static __always_inline bool __scx_kf_allowed_ctx(struct scx_sched *sch,
const char *who)
{
if (unlikely(in_nmi())) {
scx_error(sch, "%s called from NMI", who);
return false;
}
return true;
}
#define scx_kf_allowed_ctx(sch) __scx_kf_allowed_ctx((sch), __func__)
调用点示例:
__bpf_kfunc void scx_bpf_destroy_dsq(u64 dsq_id, const struct bpf_prog_aux *aux)
{
...
if (unlikely(!sch))
return;
if (!scx_kf_allowed_ctx(sch))
return;
destroy_dsq(sch, dsq_id);
}
v3 把守卫从 bpf_iter_scx_dsq_next/destroy 抽回到 _new():三者同处一个上下文,_new() 拒绝后让 kit->dsq 留 NULL,next/destroy 自动 no-op。
类比
把图书馆想象成只有一扇旋转门:
- 持锁 kfunc = 馆内唯一一台打印机,所有读者必须独占。
BPF_PROG_TYPE_TRACING= 装在门禁上的"自动人偶",火灾警报(NMI)响起时它也会冲进来。- 旧流程:人偶闯进来,撞上正在用打印机的读者,谁也不让谁,整栋楼僵死。
- 新流程:人偶进门时听到警报,直接走出大楼去喊"全员撤离"(
scx_error()走无锁 abort 通道),不再冲向打印机。 - 错误路径已经修好的事,相当于之前给"全员撤离"广播装了独立电池,不再依赖那台打印机(无锁 claim)才能发声。
Highlight:风险与注意点
- 覆盖审计的边界:Andrea 提议更彻底的做法是把"持锁 + 状态变更"的 kfunc 从
scx_kfunc_ids_any拆出来,只给BPF_PROG_TYPE_STRUCT_OPS注册,tracing 仍保留只读子集。补丁选了"先加运行时检查",留下了向"per-kfunc 可见性"迁移的窗口。 scx_bpf_kick_cpu()仍有合法 tracing 用例:scx_pair就在 tracing 中 kick CPU;不能一刀切禁掉"any"集合下所有可变 kfunc。scx_locked_rq()误报(sashiko 后续发现):三个 "any" 分类 kfunc 把它当"已持锁"判断,NMI 下可能误命中。Tejun 方向是给scx_locked_rq()加in_nmi()让其返回 NULL 走 unlocked 分支;Wanwu 表示会发后续补丁。pick_idle_cpu_from_online_nodes()的preempt_disable()也不挡 NMI:per_cpu_unvisited只受preempt_disable()保护;Tejun 认为"在 NMI 里调它就是自找麻烦,只要不死机就行",但仍建议加irqsave。- 错误码语义:v2 改用
-EDEADLK("正在避免死锁"),比-EBUSY(暗示重试)更准确——这是 review 中被指出的命名点。 - commit message 措辞:Tejun 在应用时把"struct_ops run in task context"更正为"struct_ops never run in NMI(
ops.tick()在 tick 中断里跑,但不在 NMI 里)"。
版本变化
- v1 → v2:
- 新增
__scx_kf_allowed_ctx()inline +scx_kf_allowed_ctx()宏,调用方不再手写函数名字符串。 - 错误返回从
-EBUSY改为-EDEADLK。 - 把
destroy_dsq()的守卫挪进scx_bpf_destroy_dsq(),让内部destroy_dsq()调用方不受影响;scx_kick_cpu()现在报自己名字(与kick_cid共享)。 - 代码注释精简,详细理由移到 commit message。
- 新增
- v2 → v3:
- 把 inline + 宏合并为单条 statement-expression 宏;Tejun 应用时又恢复为 inline + 宏形式("No need to wrap" 指函数签名换行)。
- DSQ 迭代三连只在
bpf_iter_scx_dsq_new()检查一次,next/destroy删除冗余守卫。 scx_bpf_destroy_dsq()用unlikely(!sch)与其它 kfunc 对齐。
- 应用时(Tejun):commit message 末段改写,更正 struct_ops 上下文描述。
一句话总结
通过把 "in_nmi() → 无锁 abort" 集中到 scx_kf_allowed_ctx(),把所有 NMI 可达、又会取调度器锁的 kfunc 拦住,避免 tracing BPF 程序把 "any" 分类 kfunc 变成整机 hard-lockup 的入口;v3 已被 Tejun Heo 拣入 sched_ext/for-7.4。