0/13 已展开

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.ckernel/sched/ext/internal.hkernel/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->lockkfunc 入口 early-return
bpf_iter_scx_dsq_new()后两函数 dsq->lock_new() 检查,留 kit->dsq=NULL 让后续 no-op
scx_bpf_dsq_reenq()rq->deferred_reenq_lockearly-return
scx_bpf_cpuperf_set() / cidperf_set()rq->lockearly-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:风险与注意点

  1. 覆盖审计的边界:Andrea 提议更彻底的做法是把"持锁 + 状态变更"的 kfunc 从 scx_kfunc_ids_any 拆出来,只给 BPF_PROG_TYPE_STRUCT_OPS 注册,tracing 仍保留只读子集。补丁选了"先加运行时检查",留下了向"per-kfunc 可见性"迁移的窗口。
  2. scx_bpf_kick_cpu() 仍有合法 tracing 用例scx_pair 就在 tracing 中 kick CPU;不能一刀切禁掉"any"集合下所有可变 kfunc。
  3. scx_locked_rq() 误报(sashiko 后续发现):三个 "any" 分类 kfunc 把它当"已持锁"判断,NMI 下可能误命中。Tejun 方向是给 scx_locked_rq()in_nmi() 让其返回 NULL 走 unlocked 分支;Wanwu 表示会发后续补丁。
  4. pick_idle_cpu_from_online_nodes()preempt_disable() 也不挡 NMIper_cpu_unvisited 只受 preempt_disable() 保护;Tejun 认为"在 NMI 里调它就是自找麻烦,只要不死机就行",但仍建议加 irqsave
  5. 错误码语义:v2 改用 -EDEADLK("正在避免死锁"),比 -EBUSY(暗示重试)更准确——这是 review 中被指出的命名点。
  6. 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