0/7 已展开

LLM 分析

Linux scheduler:scheduler debugfs 内存安全与 per-CPU 调试文件

系列概况

  • 标题:[PATCH v9 0/6] Introduce per-CPU debugfs files
  • 作者:Aaron Tomlin atomlin@atomlin.com
  • 版本:v9(共 9 个迭代版本;本帖是 v9)
  • 规模:6 个补丁- 修改文件kernel/sched/core.ckernel/sched/deadline.ckernel/sched/debug.ckernel/sched/fair.ckernel/sched/rt.ckernel/sched/sched.hkernel/sched/topology.c
  • 代码统计:cover letter 末尾给出 +/−数字片段,按子系列大致几十到几百行;具体 diff stat 在 patch 3给出 1 file,4 insertions(+), 1 deletion(-),patch 2 给出 2 files, 3 insertions(+), 2 deletions(-),patch 4 给出 1 file, 2 insertions(+),patch 5 给出 3 files, 83 insertions(+), 40 deletions(-),patch 6 给出 1 file, 43 insertions(+)
  • Message-ID20260827221809.988394-1-atomlin@atomlin.com(6 个 patch 的子 ID 在同 thread_key 下递增 2..7)
  • 完整性:thread 只包含 cover letter + 6 个 patch,缺独立回复;本帖仅展示该系列 v9 的代码改动,无 maintainer 收尾讨论

补丁目的

/sys/kernel/debug/sched/debug 在打印每个 CPU runqueue 状态时,存在多处不加锁的指针解引用,可能在并发 hot-unplug、cgroup cpuset repartition、任务退出、cfs_rq 重新挂链等场景下触发 use-after-free、TOCTOU 或者 RCU 遍历违例。本系列同时修这些隐患并新增针对单个 CPU 的 debugfs 入口 /sys/kernel/debug/sched/cpu/cpu<N>/debug,离线时返回 -ENODEV,便于排查特定 CPU 的调度延迟异常。

旧流程的问题

        +-------------------+ lockless read
        | debugfs reader |      rcu not held
        +---------+---------+
                  |
                  v +-------------------+      race window
        | print_dl_rq()/... |----> use-after-free / TOCTOU
        +-------------------+      (rd / curr->pid / p->mm)
 |
                  v
        +-------------------+
        | leaf_cfs_rq_list  |----> backward jump, RCU stall
        +-------------------+

新流程

+-----------------------+      RCU read lock (guard(rcu))
| debugfs reader |      + rcu_dereference_root_domain()
+----------+------------+ + rcu_dereference(rq->curr)
           |                   + task_lock(p)
 v
+-----------------------+      task_group / cfs_rq safe via
| print_*() helpers     |----> call_rcu() and READ_ONCE via
+----------+------------+      list_for_each_entry_rcu()
           |
           v
+-----------------------+
| circuit breaker4096  |----> truncation notice
+-----------------------+
/* Patch 2 节选(print_dl_rq) */
guard(rcu)();
dl_bw = &rcu_dereference_root_domain(cpu_rq(cpu)->rd)->dl_bw;

/* Patch 3 节选(print_cpu) */
rcu_read_lock();
SEQ_printf(m, " .%-30s: %ld\n", "curr->pid",
            (long)(task_pid_nr(rcu_dereference(rq->curr))));
rcu_read_unlock();

/* Patch 5 节选(print_cfs_stats) */
#define for_each_leaf_cfs_rq_rcu(rq, cfs_rq) \
    list_for_each_entry_rcu(cfs_rq, &(rq)->leaf_cfs_rq_list, \
                            leaf_cfs_rq_list)

Patch 概览

  • 1/6sched: Annotate rq->rd with __rcu,新增 rcu_dereference_root_domain() 宏,更新 core/deadline/fair/topology 中的 rq->rd 解引用,sparse 与 lockdep-RCU 干净- 2/6sched/debug: Protect lockless rq->rd access in print_dl_rq(),在 writer 侧 rcu_assign_pointer(rq->rd, rd),reader 侧 guard(rcu)() + rcu_dereference()
  • 3/6sched/debug: Protect lockless rq->curr access in print_cpu(),把 task_pid_nr(rq->curr) 包在 RCU 临界区
  • 4/6sched/debug: Protect p->mm access in sched_show_numa(),用 task_lock(p)/task_unlock(p) 避免 exit_mm() 并发导致的 TOCTOU
  • 5/6sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats(),引入 for_each_leaf_cfs_rq_rcu()guard(rcu)()SCHED_DEBUG_MAX_ITER=4096 熔断 + 截断提示
  • 6/6sched/debug: Introduce per-CPU debugfs files,新增 /sys/kernel/debug/sched/cpu/cpu<N>/debug,离线 CPU 返回 -ENODEV

关键实现

/* Patch 6 per-CPU debugfs 创建 */
static int sched_debug_cpu_show(struct seq_file *m, void *v)
{
    unsigned long cpu = (unsigned long)m->private;
    if (!cpu_online(cpu))
        return -ENODEV;
    print_cpu(m, cpu);
    return 0;
}

static __init void debugfs_cpu_init(void)
{
    struct dentry *d_cpu_dir = debugfs_create_dir("cpu", debugfs_sched);
    for_each_possible_cpu(cpu) {
        char buf[16];
        snprintf(buf, sizeof(buf), "cpu%lu", cpu);
        debugfs_create_file("debug", 0444,
                            debugfs_create_dir(buf, d_cpu_dir),
                            (void *)cpu, &sched_debug_cpu_fops);
    }
}

rcu_dereference_root_domain()sched_domains_mutexrcu_read_lock_sched() 两个语境统一校验,绕开之前 lockdep_is_held(&rq->__lock) 的误用(Peter指出)。SCHED_DEBUG_MAX_ITER 是“per-CPU 软上限”,意图是即使 cfs_rq churn 也能让 RCU 临界区终结。

类比

/sys/kernel/debug/sched/debug 想象成医院的总检大厅:所有病人都在同一张长椅上等候,每叫一个号就要顺手翻他的病历(rq->rdrq->currp->mm),但病历随时可能被转科、被归档。本系列相当于给总检大厅装上叫号锁(RCU 读锁)和病历夹保险扣task_lock/list_for_each_entry_rcu),让翻阅动作要么原子、要么熔断退出;最后的 per-CPU 文件则是新增挂号窗口,你说“只看7 号诊室”,就只把 7 号 CPU 的 runqueue 端上来,7 号不在岗就直接告诉你 ENODEV

Highlight:风险与注意点

  • rcu_dereference_root_domain()CONFIG_PROVE_RCU 下若被 cpuset 路径(如 dl_task_needs_bw_move)以非 RCU 上下文调用会误报,本系列在 v9 加了 guard(rcu)() 才消掉
  • SCHED_DEBUG_MAX_ITER=4096 是“兜底”,正常负载不应被截断;如果真实环境频繁看到 “stats truncated at 4096 iterations”,说明 leaf_cfs_rq churn 异常,应回头查 cgroup 配置
  • per-CPU 文件在 offline CPU 时返回 -ENODEV,不要在自动化脚本里把它当成成功读到空内容
  • sashiko-bot 报的若干项是同一族漏洞的分支,全部都已 Fixes: 链回 02968ccf7b80b32e86b4301e,回头 cherry-pick 到 stable 时要按 patch 顺序合入

版本变化

  • v1→v2:cover 围绕“大型 SMP 上定点调试”重写;cpu_online() 网关加入- v2→v3:print_cpurcu_read_lock+READ_ONCE,引入 for_each_leaf_cfs_rq_rcu()
  • v3→v4:新增 patch 1 标 __rcuprint_dl_rq 改用 guard(rcu)+rcu_dereference()sched_show_numa 改用 task_lock/task_unlock
  • v4→v5:把 SEQ_printf* 移到 sched.hstrcpy→strscpySCHED_DEBUG_MAX_ITER 1024→4096
  • v5→v6:rebase 到 tip/sched/core (sched-core-2026-08-17)
  • v6→v7:删除误用 rcu_dereference_protected(... &rq->__lock),引入新宏;恢复 check_update_overutilized_status() 注释
  • v7→v8:把剩余 rq->rd 解引用(add_nr_running__sched_setschedulerdl_task_check_affinity)转用新宏
  • v8→v9:给 dl_task_needs_bw_move()guard(rcu)(),消除 CONFIG_PROVE_RCU 误报

一句话总结

本系列把 /sys/kernel/debug/sched/* 中裸读 rq->rdrq->currp->mmleaf_cfs_rq_list 的几个 race 全部用 RCU/task_lock/list_for_each_entry_rcu() 包起来,并新增 per-CPU debugfs 入口做定点诊断。