0/14 已展开

LLM 分析

scheduler debugfs:Introduce per-CPU debugfs files(v7)

系列概况

  • 标题[PATCH v7 0/6] Introduce per-CPU debugfs files
  • 作者:Aaron Tomlin <atomlin@atomlin.com>
  • 版本:v7(前序 v1–v6,跨度2026-07-28 至 2026-08-26,共 7 版)
  • 规模:6 个 patch,7 个文件,合计 +248/-127 行(cover letter 统计)
  • 修改文件kernel/sched/core.ckernel/sched/deadline.ckernel/sched/debug.ckernel/sched/fair.ckernel/sched/rt.ckernel/sched/sched.hkernel/sched/topology.c
  • 代码统计:core.c(±)、debug.c 大幅改写、fair.c 新增 RCU 宏、rt.c(±)、sched.h 新增调试头文件宏- Message-ID(首封)20260826224238.936456-1-atomlin@atomlin.com
  • 完整性:6 个子 patch 完整;后续 v7 回复涉及 Vincent Guittot、Peter Zijlstra、Aaron Tomlin 自身;附 base-commit: 68e37487810a3da43c48340fab7a55b3b6efdae3-- 2.55.0

补丁目的

本系列同时解决三件事:

  1. struct rq 上的 rd 指针打 __rcu 注释,让 Sparse / CONFIG_PROVE_RCU 把整个调度子系统纳入正确的 RCU 检查。
  2. 修补 /sys/kernel/debug/sched/debug 打印路径里的 4 类 RCU 违例:UAF(dl_bwtask_structmm_struct)、TOCTOU(p->mm)、以及 leaf_cfs_rq_list 的无锁遍历在 churn 时反向回跳触发 RCU stall。
  3. 新增 /sys/kernel/debug/sched/cpu/cpu<N>/debug 的 per-CPU 调试入口,离线时返回 -ENODEV

旧流程的问题

/sys/kernel/debug/sched/debug 走 seq_file,不持任何锁读取 rq->rdrq->currp->mmleaf_cfs_rq_list

  • rq->rdrq_attach_root() 用普通 C 赋值发布,旧 root_domain 经 call_rcu(&old_rd->rcu, free_rootdomain) 回收;写端缺 release barrier,读端缺 RCU 临界区。
  • rq->currtask_pid_nr() 时若进程正在 exitput_task_struct()call_rcu 释放 task_struct
  • sched_show_numa()p->mm 的检查与 mm->numa_scan_seq 引用不在同一原子窗口。
  • for_each_leaf_cfs_rq_safe() 展开为 list_for_each_entry_safe,无 READ_ONCE;cgroup throttle/wakeup 频繁 re-link 时会出现「读到 next 指针但内部字段未初始化」、或反向遍历触发 RCU stall。

读端打印数据「best-effort」没错,但 UAF/Oops 是真错,不是「数据陈旧」。

新流程

按 6 个 patch 顺次:

  • Patch 1rq->rd__rcu;core/deadline/fair/rt/topology 的无锁读者改 rcu_dereference_sched()(读路径)或 rcu_dereference_protected()(持锁路径);写端 rq_attach_root()rcu_assign_pointer();init 用 RCU_INIT_POINTER()
  • Patch 2print_dl_rq()guard(rcu)()dl_bwrcu_dereference()rq_attach_root()rcu_assign_pointer()
  • Patch 3print_cpu()rq->currrcu_read_lock()/rcu_read_unlock() 临界区内用 rcu_dereference() 取,再做 task_pid_nr()
  • Patch 4sched_show_numa()task_lock(p) / task_unlock(p) 包裹 p->mm 检查与 P(mm->numa_scan_seq),与 exit_mm() 在同一锁下完成 NULL 与释放。
  • Patch 5:新增 for_each_leaf_cfs_rq_rcu()list_for_each_entry_rcu),打印时包 guard(rcu)(),并加 SCHED_DEBUG_MAX_ITER=4096 的 per-CPU circuit-breaker,超出时显式打 SCHED_DEBUG_TRUNCATED_MSG
  • Patch 6debugfs_create_dir("cpu") 下为每个 possible CPU 建 cpu<N> 子目录,挂 debug 文件,复用 print_cpu();用 cpu_online() 做离线检查。

Patch 概览

+------------------------+----------------------------+---------------------------+
| Patch                  | Key path                    | Fix type                  |
+------------------------+----------------------------+---------------------------+
| 1/6 rq->rd __rcu       | core/deadline/fair/rt/... | RCU annotation + readers |
| 2/6 print_dl_rq UAF    | debug.c + topology.c        | rq->rd writer+reader      |
| 3/6 print_cpu  UAF     | debug.c                     | rq->curr UAF              |
| 4/6 sched_show_numa    | debug.c                     | p->mm TOCTOU + UAF        |
| 5/6 print_cfs_stats    | debug.c + fair.c + sched.h  | leaf list RCU + ceiling   |
| 6/6 per-CPU debugfs    | debug.c                     | new debugfs entry         |
+------------------------+----------------------------+---------------------------+

关键实现

/* Patch 1 -- let rq->rd become a real RCU pointer */
struct root_domain *rd = rcu_dereference_protected(rq->rd,
                       lockdep_is_held(&rq->__lock));
cpumask_set_cpu(rq->cpu, rd->online);

/* Patch 2 -- writer uses rcu_assign_pointer */
void rq_attach_root(struct rq *rq, struct root_domain *rd)
{
    ...
    rcu_assign_pointer(rq->rd, rd);
}

/* Patch 3 -- rq->curr must hold RCU read lock */
rcu_read_lock();
SEQ_printf(m, " .%-30s: %ld\n", "curr->pid",
           (long)(task_pid_nr(rcu_dereference(rq->curr))));
rcu_read_unlock();

/* Patch 4 -- p->mm and exit_mm share task_lock */
task_lock(p);
if (p->mm)
    P(mm->numa_scan_seq);
task_unlock(p);

/* Patch 5 -- RCU-safe traversal with a safety ceiling */
#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)

guard(rcu)();
for_each_leaf_cfs_rq_rcu(cpu_rq(cpu), cfs_rq) {
    if (--max_iter < 0) { /* print truncation notice and break */ }
}

/* Patch 6 -- per-CPU debugfs */
d_cpu_dir = debugfs_create_dir("cpu", debugfs_sched);
for_each_possible_cpu(cpu) {
    snprintf(buf, sizeof(buf), "cpu%lu", cpu);
    d_cpu = debugfs_create_dir(buf, d_cpu_dir);
    debugfs_create_file("debug", 0444, d_cpu,
                        (void *)cpu, &sched_debug_cpu_fops);
}

类比

/sys/kernel/debug/sched/debug 想成一份全校体检报告:4000 份体检表一页纸打出来,想盯单个人都没法单独取;而且每份表都是「边印边改」的,没有任何保护,医生(reader)可能拿到半张表就被护士(writer)擦掉重写。

Patch 1–5 是在报告打印处加复印 + 装订:把每份表(rq->rdrq->currp->mmleaf_cfs_rq_list)放进 RCU 临界区复印一份,确保复印期间原件不会被销毁,同时给每页盖个「最多 4000 页」的章防止死循环。

Patch 6 是新增一条分诊通道 cpu/cpu<N>/debug:想盯某个人直接走分诊台,他下班了(offline)柜台直接亮「未到岗」。

Highlight:风险与注意点

  • 必须全系列合入:Patch 2 写端 rcu_assign_pointer() 单独合入没意义——读者还在无锁读会留下 release barrier 对不齐的窗口。
  • task_lock(p) 与 RCU 不要混:Patch 4 选用 task_lock() 而非 rcu_read_lock() 是因为 exit_mm() 自身就在 task_lock(p) 内做 mmput(),混用 RCU 无法阻止 mmput 立即释放。
  • SCHED_DEBUG_MAX_ITER=4096 是兜底:cgroup throttle 风暴下读文件仍可能截断,但比 RCU stall 报警 + watchdog 软死好得多;截断提示对现场解释非常关键。
  • cpu_online(cpu) 必须放在 single_open 之后读 m->private,否则 hot-unplug 边沿仍可能 race;当前实现把它放在 sched_debug_cpu_show 内是稳妥的。
  • helper 命名争论:Peter 倾向复用现有 rcu_dereference_sched_domain(),Aaron 倾向新增 rcu_dereference_root_domain() 维持 rd/sd 命名对称;最终 Peter 表示「随便,要么 alias 一下」。
  • 遗留风险print_cpu() 输出仍包含大量 rq->lock 之外读取的字段(rq->clockrq->nr_running 等),本系列只修了已发现的高危点,未做全字段审计。

版本变化

  • v6 → v7:所有 rq->rd 读取按 lock-held/lockless 拆分语义;移除 !CONFIG_CGROUP_SCHED stub 上的 __printf(3,4) 消除 -Wformat 警告;扩展 Patch 5 解释 SCHED_DEBUG_MAX_ITER per-CPU 含义与 cfs_rq 回收安全保证。
  • v5 → v6:rebase 到 tip/sched/core (sched-core-2026-08-17)
  • v4 → v5:新增 Patch 1(rq->rd __rcu);print_dl_rq()guard(rcu)() + rcu_dereference()sched_show_numa()task_lock()SCHED_DEBUG_MAX_ITER 由 1024 升 4096;SEQ_printf_task_group_path 移到 sched.hstrcpystrscpy;Patch 5 的 Fixes 修正为 039ae8bcf7a5
  • v3 → v4:Patch 1 用 rcu_dereference(rq->curr) 保留 __rcu;Patch 2 增 rcu_assign_pointer();新增 Patch 3(TOCTOU via READ_ONCE(p->mm));Patch 4 增迭代上限。
  • v2 → v3print_cpu()rcu_read_lock() + READ_ONCE()print_dl_rq() 防 hot-unplug/cpuset race;新增 for_each_leaf_cfs_rq_rcu()
  • v1 → v2:commit message 围绕「大 SMP 上针对性调试」重新组织;sched_debug_cpu_show()cpu_online() 返回 -ENODEV(Zhan Xusheng 提议)。

一句话总结

本系列把 /sys/kernel/debug/sched/debug 里 4 处 RCU 违规(rq->rd 缺注解、rq->curr UAF、p->mm TOCTOU、leaf list 反向遍历)一次性清理,并新增 cpu/cpu<N>/debug 让大机器上能按 CPU 抓现场。