0/8 已展开

LLM 分析

scheduler debugfs 内存安全与 per-CPU 调试:v8 系列分析

系列概况

  • 标题:[PATCH v8 0/6] Introduce per-CPU debugfs files
  • 作者:Aaron Tomlin atomlin@atomlin.com
  • 版本:v8(自 v1 起累计迭代 8 次;base-commit: 68e37487810a3da43c48340fab7a55b3b6efdae3)
  • 规模:6 个 patch;7 个文件修改,255 行新增、130 行删除
  • 修改文件kernel/sched/core.ckernel/sched/deadline.ckernel/sched/debug.ckernel/sched/fair.ckernel/sched/rt.ckernel/sched/sched.hkernel/sched/topology.c
  • 代码统计:7 files changed, 255 insertions(+), 130 deletions(-)
  • Message-ID:cover 20260827194014.977758-1-atomlin@atomlin.com;分片 -2-7;作者回复 zekeanf2poef2qh2dkinxxwhme5utfibmz6bmvqrz2dzd3o6ub@gfokk2j2xroc
  • 完整性:完整(cover letter + 6 个分片 + 1 封作者主动预告 v9 修订的回复)

补丁目的

本系列同时做两件事:

  1. 修掉 scheduler debugfs 里一组真实的内存安全问题:把 rq->rdrq->currp->mmrq->leaf_cfs_rq_list 这些"看着是本 CPU 局部读、实际被远端并发改写"的字段,全部放到合适的同步原语(RCU 读锁 / task_lock)下,避免 use-after-free、TOCTOU 与 RCU CPU stall。
  2. 新增 per-CPU debugfs:在 /sys/kernel/debug/sched/cpu/cpu<N>/debug 提供单 CPU 视角,运维不再需要每次 cat /sys/kernel/debug/sched/debug 把全机所有 CPU 的内容一起倒出来;offline CPU 读返回 -ENODEV

问题来源:Reported-by: sashiko-bot,属于"真实存在但概率低、难以复现"那一类隐患。

旧流程的问题

[user cat /sched/debug]
 |
        v
[debugfs open / seq_file read]
        |
        v
[print_* callback - no lock held]
        |
        +--> print_dl_rq:     rq->rd->dl_bw            (no RCU -> UAF)
        +--> print_cpu:       rq->curr->pid            (no RCU -> UAF)
        +--> sched_show_numa: p->mm->numa_scan_seq     (no task_lock -> TOCTOU + UAF)
        +--> print_cfs_stats: list_for_each_entry_safe (non-RCU macro -> missing READ_ONCE -> RCU stall)

四个具体隐患:

  • Patch 2rq_attach_root()rq->rd 用裸 =free_rootdomain() 通过 call_rcu() 释放;debugfs 无 rcu_read_lock(),可能在 kfree() 之后才读到悬空指针。
  • Patch 3task_pid_nr(rq->curr) 无 RCU 保护,任务退出走 call_rcu(__put_task_struct_rcu_cb),并发形成 UAF。
  • Patch 4sched_show_numa() 裸读 p->mmexit_mm()task_lock(p) 下清 p->mm 后立即 mmput(),构成本窗口内 UAF。
  • Patch 5for_each_leaf_cfs_rq_safe() 展开为 list_for_each_entry_safe(),不是 RCU 宏,缺 READ_ONCE() 与内存屏障;cfs_rq 在 enqueue/dequeue 时还会原地改 next,锁外读者可能回退甚至死循环,触发 RCU CPU stall。

新流程

[user cat /sched/debug or /sched/cpu/cpuN/debug]
        |
        v
[debugfs open / sched_debug_cpu_show with cpu_online check -> -ENODEV if offline]
        |
        v
[print_* callback under guard(rcu) / task_lock]
        |
        +--> print_dl_rq:     rcu_dereference_root_domain(rq->rd)->dl_bw
        | (writer: rcu_assign_pointer in rq_attach_root)
        +--> print_cpu:       rcu_dereference(rq->curr)->pid
        |                       (rcu_read_lock / rcu_read_unlock bracket)
        +--> sched_show_numa: task_lock(p); ... p->mm->...; task_unlock(p)
        +--> print_cfs_stats: guard(rcu)(); for_each_leaf_cfs_rq_rcu(); max_iter ceiling + truncation msg

rq->rd 写入端改用 rcu_assign_pointer(rq->rd, rd) 提供 release barrier;读取端统一走新增的辅助宏 rcu_dereference_root_domain()(Patch 1),它要么在持 sched_domains_mutex 时退化为 rcu_dereference_protected(),要么在 RCU-sched 读侧退化为 rcu_dereference_sched(),让 Sparse 与 lockdep 都满意。

Patch 概览

Patch文件主题类型
1/6core.c / deadline.c / fair.c / rt.c / sched.hrq->rd__rcu,引入 rcu_dereference_root_domain(),全调度子系统替换基础设施(前提)
2/6debug.c / topology.cprint_dl_rq()guard(rcu)+rcu_dereferencerq_attach_root()rcu_assign_pointerUAF 修复
3/6debug.cprint_cpu()rq->currrcu_read_lock+rcu_dereferenceUAF 修复
4/6debug.csched_show_numa()task_lock(p)/task_unlock(p) 包裹 p->mmTOCTOU+UAF 修复
5/6debug.c / fair.c / sched.h新增 for_each_leaf_cfs_rq_rcu()cfs_rq_tg();把 SEQ_printf_task_group_path() 提到 sched.h;SCHED_DEBUG_MAX_ITER=4096 + 截断提示RCU 遍历修复 + 重构
6/6debug.c新增 /sys/kernel/debug/sched/cpu/cpu<N>/debug,offline 返回 -ENODEV新功能

关键实现

/* Patch 1: 新增辅助宏,lockdep 友好 */
#define rcu_dereference_root_domain(p)                                   \
        rcu_dereference_check((p),                                       \
                              lockdep_is_held(&sched_domains_mutex) ||   \
                              rcu_read_lock_sched_held())
/* Patch 2: 写端补 release barrier */
void rq_attach_root(struct rq *rq, struct root_domain *rd)
{
        ...
        rcu_assign_pointer(rq->rd, rd);   /* 原: rq->rd = rd; */
        ...
}
/* Patch 3: rq->curr 必须 RCU 读侧 */
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 必须 task_lock */
task_lock(p);
if (p->mm)
        P(mm->numa_scan_seq);
task_unlock(p);
/* Patch 5: RCU 列表遍历 + 熔断 */
#define SCHED_DEBUG_MAX_ITER 4096
guard(rcu)();
for_each_leaf_cfs_rq_rcu(cpu_rq(cpu), cfs_rq) {
        if (--max_iter < 0) {
                SEQ_printf(m, "\ncfs_rq[%d]: ... stats truncated at 4096 iterations\n", cpu);
                break;
        }
        /* 真正的 dump 逻辑 */
}
/* Patch 6: per-CPU 调试入口 */
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;
}

类比

把这套改动想成图书馆的总服务台改造

  • Patch 1 像是给"这本书当前所属分馆"(rq->rd)贴一张"必须登记后才能读"的标签,再统一发一张规范的登记表(rcu_dereference_root_domain()),否则门卫(lockdep/Sparse)要拦你。
  • Patch 2/3 像是给总台小窗口加闸门:查"这本书在哪个分馆"或"现在值班的是哪位馆员",必须先刷读者卡(rcu_read_lock()),否则分馆可能在你看的时候被拆掉(kfree)或下班(__put_task_struct_rcu_cb),你就拿到一张"幽灵分馆/幽灵馆员"小票。
  • Patch 4 像是查"这位读者常去哪个阅览室"(p->mm)必须把借阅证扣下(task_lock(p)),因为读者可能在你翻牌瞬间退卡走人。
  • Patch 5 像是有个馆员按一长串书架顺序贴标签,本来用普通记号笔,读到一半被人换走会读错位;改成 RCU"有色笔" + 巡检不超过 4096 个书架(熔断),并明确告诉你"我只巡到这里,再多我下班"。
  • Patch 6 像是除了"总台综合大屏",还在每个分馆装一台小屏幕(/sys/kernel/debug/sched/cpu/cpu<N>/debug),只看本馆;分馆今天关门(offline)就告诉你"本馆休息(-ENODEV)"。

Highlight:风险与注意点

  1. dl_task_needs_bw_move() 还遗留一个未修的 lockdep 警告:第 8 封(作者自己回复)与 cover letter 都承认,patch 1 转换后,sashiko 指出 dl_task_needs_bw_move()cpuset_mutex 持锁、但既无 sched_domains_mutex 也无 RCU-sched 读侧时被调用;现在用 rcu_dereference_root_domain() 会直接被 lockdep 报 suspicious RCU usage,作者已在 virtme-ng 复现并预告在 v9 处理——合入前必须先解决,否则 CONFIG_PROVE_RCU=y 直接红。
  2. 锁假设与未来读者:patch 1 的 helper 只承认 sched_domains_mutex 与 RCU-sched 两种合法语境。任何后续在新语境里 deref rq->rd(例如某些 workqueue / cgroup v2 freezer 路径)都必须自证属于这两者。
  3. SCHED_DEBUG_MAX_ITER=4096 是拍脑袋定的:足够大到不截断普通负载,但极端机器或 NR_CGROUPS 巨大时可能真的截断;输出截断信息是"软告警",不能掩盖更严重的 list churn 根因,需要后续观察是否需要再调。
  4. SEQ_printf_task_group_path() 移出 debug.csched_debug_lock / sched_debug_group_path 现在是 extern,未 EXPORT_SYMBOL,预期只在 scheduler 内部使用——若其它子系统想用,要单独讨论导出策略。
  5. per-CPU debugfs 是 best-effort:注释明确说 rq->curr 在读取过程中会动态变化,所以 curr->pid 等输出只是"快照",不要拿去做断言。

版本变化

v7 → v8:用新加的 rcu_dereference_root_domain() 替换了 v7 中错误的 rcu_dereference_protected(... lockdep_is_held(&rq->__lock)),把 lockdep 校验改成 sched_domains_mutex / RCU-sched 二选一(Peter Zijlstra);恢复 check_update_overutilized_status() 中被误删的注释(Vincent Guittot)。

v6 → v7:rq->rd 读端统一为 rcu_dereference_sched()(lockless)/ rcu_dereference_protected()(持锁);修掉 !CONFIG_CGROUP_SCHEDSEQ_printf_task_group_path() 桩带 __printf(3,4) 导致的 -Wformat;把 SCHED_DEBUG_MAX_ITER 与 cfs_rq 节点回收语义写进 changelog。

v5 → v6:rebase 到 tip/sched/core (sched-core-2026-08-17)。

v4 → v5:新增 patch 1 作为前提;print_dl_rqguard(rcu)()sched_show_numatask_lockSCHED_DEBUG_MAX_ITER 1024→4096 + 截断提示;strcpystrscpySEQ_printf_task_group_path 移到 sched.h;修 patch 5 的 Fixes tag 到 039ae8bcf7a5

v3 → v4:rq->currrcu_dereference(保留 __rcu);patch 2 加 rcu_assign_pointer;新增 patch 3 修 TOCTOU;新增 SCHED_DEBUG_MAX_ITER 安全熔断。

v2 → v3:补 rcu_read_lock+READ_ONCErq->rd 锁外访问加保护;新增 for_each_leaf_cfs_rq_rcu()

v1 → v2:动机改为面向大 SMP 拓扑的交互式调试(Peter Zijlstra/Zhan Xusheng);offline CPU 返回 -ENODEV(Zhan Xusheng)。

一句话总结

v8 系列用 __rcu 标注 + rcu_dereference_root_domain() 把 scheduler debugfs 一类散落的锁外裸读全部规范化,修掉了 rq->rd / rq->curr / p->mm / leaf_cfs_rq_list 上的 UAF / TOCTOU / RCU stall,并新增 /sys/kernel/debug/sched/cpu/cpu<N>/debug 让运维可以定点观测单个 CPU 的 runqueue;唯一遗留已知问题是 dl_task_needs_bw_move() 的 lockdep 警告,作者承诺在 v9 处理。