0/8 已展开

LLM 分析

sched/debug:每 CPU debugfs 文件与 scheduler debugfs 内存安全修复

系列概况

  • 标题[PATCH v6 0/6] sched/debug: Introduce per-CPU debugfs files
  • 作者:Aaron Tomlin <atomlin@atomlin.com>
  • 版本:v6(base-commit 68e37487810a3da43c48340fab7a55b3b6efdae3,rebase 到 tip/sched/core sched-core-2026-08-17
  • 规模:6 patches / 6 files changed / +165 / -68
  • 修改文件kernel/sched/core.ckernel/sched/deadline.ckernel/sched/debug.ckernel/sched/fair.ckernel/sched/sched.hkernel/sched/topology.c
  • Message-ID20260825184637.888364-1-atomlin@atomlin.com(cover letter)
  • 完整性:thread 含 8 封邮件(1 cover letter + 6 patches + 1 跟进 reply),patch 1/6 是2–6/6 的先决条件;bug 由 sashiko-bot 自动捕获。

补丁目的

本系列做两件事:

  1. 修复 kernel/sched/debug.c 中几条 debugfs 路径上的 UAF / TOCTOU / RCU 遍历违规。
  2. /sys/kernel/debug/sched/cpu/cpu<N>/debug 暴露每 CPU runqueue 视图,避免 cat 整个聚合文件来定位单个 CPU 的调度异常。

旧流程的问题

  • rq->rd 没有 __rcu 注解,rq_attach_root() 用裸赋值,写端无 release barrier。
  • debugfs 读路径无 rcu_read_lock,可与 CPU hot-unplug / cpuset repartition 触发的 free_rootdomain()(通过 call_rcu)赛跑,访问已被 kfree 的 root_domain → dl_bw->bw UAF。
  • print_cpu() 无锁解引用 rq->curr,与正在 exit 的 task 经 __put_task_struct_rcu_cb 释放 task_struct 赛跑 → task_struct UAF。
  • sched_show_numa() 在无 task_lock 下读 p->mmexit_mm()task_lock 下置 NULL 后 mmput(),读端面对 TOCTOU + UAF。
  • print_cfs_stats()list_for_each_entry_safe() 遍历 RCU 链表,缺 READ_ONCE()rq->lock 外被重新挂入可能让遍历倒走甚至触发 RCU stall。
  • /sys/kernel/debug/sched/debug 是聚合视图,单 CPU 调查必须读全部内容。

新流程

  • Patch 1:struct rq::rd__rcu,所有 lockless reader 改 rcu_dereference() / rcu_dereference_sched()
  • Patch 2:rq_attach_root()rcu_assign_pointer()print_dl_rq()guard(rcu)() + rcu_dereference()
  • Patch 3:print_cpu()rcu_read_lock() 访问 rcu_dereference(rq->curr)
  • Patch 4:sched_show_numa()task_lock(p)/task_unlock(p) 包住 p->mm
  • Patch 5:引入 for_each_leaf_cfs_rq_rcu() + SCHED_DEBUG_MAX_ITER=4096 上限与截断提示;SEQ_printf/SEQ_printf_task_group_path 提到 sched.h 并加 CONFIG_CGROUP_SCHED stub。
  • Patch 6:在 debugfs_sched/cpu/cpu<N>/debug 创建每 CPU 文件,cpu_online() 失败返回 -ENODEV

Patch 概览

v6 series structure (topological)

[1/6  sched: rq->rd __rcu + lockless reader update]
                       |
        +--------------+--------------+
        |              |              |
[2/6  dl_bw UAF]  [3/6 curr UAF]  [4/6 mm TOCTOU]
 [5/6 leaf_cfs_rq RCU]
                                          |
                                  [6/6 per-CPU debugfs]
#关键改动
1/6struct rq::rd 注解 __rcu;core/deadline/fair 三处 lockless reader 改 RCU 原语
2/6rq_attach_rootrcu_assign_pointer()print_dl_rq guard(rcu) + rcu_dereference
3/6print_cpurcu_read_lockrcu_dereference(rq->curr)
4/6sched_show_numatask_lock(p) 包住 p->mm 检查与解引用
5/6for_each_leaf_cfs_rq_rcuSCHED_DEBUG_MAX_ITER=4096 + 截断提示;SEQ宏提到 sched.h 并加 CGROUP stub
6/6sched_debug_cpu_fops + debugfs_cpu_init(),在 possible_cpu 上预创建每 CPU 目录

关键实现

/* Patch 1: __rcu 注解 */
struct root_domain __rcu *rd;

/* Patch 2: 写端发布屏障 */
void rq_attach_root(struct rq *rq, struct root_domain *rd)
{
    ...
    rcu_assign_pointer(rq->rd, rd);   /* 替代裸赋值 */
    ...
}

/* Patch 2: 读端 RCU critical section */
void print_dl_rq(struct seq_file *m, int cpu, struct dl_rq *dl_rq)
{
    guard(rcu)();
    dl_bw = &rcu_dereference(cpu_rq(cpu)->rd)->dl_bw;
    ...
}

/* Patch 3: rq->curr UAF 修复 */
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 TOCTOU 修复 */
task_lock(p);
if (p->mm)
    P(mm->numa_scan_seq);
task_unlock(p);

/* Patch 5: RCU 安全的链表遍历 */
#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)

#define SCHED_DEBUG_MAX_ITER 4096
if (--max_iter < 0) {
    SEQ_printf(m, SCHED_DEBUG_TRUNCATED_MSG);
    break;
}

/* 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;
}

类比

root_domain 想象成图书馆的「阅览室」,每个 runqueue 是一个「读者」。当图书馆重新分区(cpuset repartition)时,旧阅览室被标记为「待拆除」,并通过 call_rcu 安排几个宽限期后真正拆除。

  • 旧流程:debugfs 这位临时参观者没拿参观证(rcu_read_lock),用裸眼看门牌(裸解引用)。他看完门牌转身去找房间时房间已经被推倒(kfree)—— 被埋在瓦砾下就是 UAF。
  • 新流程
    • 写端挂新牌匾时喊了一嗓子「叮」(rcu_assign_pointer 的 release barrier),保证所有读者要么看到旧牌匾要么看到完整新牌匾。
    • debugfs 进门先领参观证(guard(rcu)()),持证期间任何房间拆除都被推迟,再不会踩到瓦砾。
  • patch 5 像把「顺着墙走一圈找阅览室」换成「按导览图走」,且最多走 4096 步就停下喊「清单截断」,避免在不断翻修的走廊里迷路。
  • patch 6 像在每个阅览室门口装独立查询机:以前只能在大屏看所有阅览室状态,现在想看 3 号就直接去 3 号机器;关门时(CPU offline)屏幕就显示 -ENODEV

Highlight:风险与注意点

  • task_struct UAF 的窗口仍存在rcu_read_lock 只保证读到的 task_struct 内存有效,不保证打印出的 PID 仍是此刻正在运行的任务——属于 best-effort,commit message 已说明。
  • mm 字段竞争用 task_lock 而非 RCUexit_mmtask_lock 下清 NULL 再 mmput,所以保护类型与 rq->curr 的 RCU 路径不同,混用会 fix 不到位。
  • list_for_each_entry_safe 的真正风险:除缺 READ_ONCEcfs_rqrq->lock 外重挂可造成遍历倒走;用 list_for_each_entry_rcu + 4096 上限双保险。
  • Sparse / weak-ordering 收益:patch 1不仅是修 bug,更让 Sparse 在所有架构上静态验证 RCU 用法,未来新增 lockless reader 不会再悄悄出错。
  • CONFIG_CGROUP_SCHED stubSEQ_printf_task_group_path 在非 CGROUP 下用 __printf 标注的 inline stub,调用点要记得 tg 可能为 NULL。
  • 每 CPU debugfs 持久存在:按 possible_cpu 创建,不是 online CPU;hotplug 频繁时文件不清理,目录数会一直增长。
  • Reported-by: sashiko-bot:CI/syzkaller 自动化触发,覆盖面广;后续在 tip/sched/core 持续验证。

版本变化

  • v1 → v2:根据 Peter Zijlstra 和 Zhan Xusheng 反馈,motivation 改为「大型 SMP 上做针对性交互式调试」。
  • v2 → v3:print_cpurcu_read_lock + READ_ONCEprint_dl_rq 加 RCU 保护;引入 for_each_leaf_cfs_rq_rcu
  • v3 → v4:patch 1 改用 rcu_dereference(rq->curr) 保留 __rcu;patch 2 加 rcu_assign_pointer;patch 3 用 READ_ONCE(p->mm);patch 4 加 4096 迭代上限。
  • v4 → v5:新增 patch 1(rq->rd 注解 + lockless reader 更新);print_dl_rqguard(rcu)()sched_show_numatask_lockSCHED_DEBUG_MAX_ITER 1024→4096 并加截断提示;SEQ_printf* 提到 sched.hstrcpy→strscpyFixes tag 修正为 039ae8bcf7a5
  • v5 → v6:仅 rebase 到 tip/sched/core sched-core-2026-08-17

一句话总结

v6 系列在为单 CPU 调试提供 cpu<N>/debug 视图的同时,把 sched/debug 路径中四条无锁解引用(rq->rdrq->currp->mmleaf_cfs_rq_list)补上 rcu_dereference / task_lock / list_for_each_entry_rcu + 4096 截断,从根上消除 sashiko-bot 触发的 UAF / TOCTOU / RCU stall。