sched discussion
[PATCH v6 0/6] sched/debug: Introduce per-CPU debugfs files
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.c、kernel/sched/deadline.c、kernel/sched/debug.c、kernel/sched/fair.c、kernel/sched/sched.h、kernel/sched/topology.c - Message-ID:
20260825184637.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自动捕获。
补丁目的
本系列做两件事:
- 修复
kernel/sched/debug.c中几条 debugfs 路径上的 UAF / TOCTOU / RCU 遍历违规。 - 在
/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->bwUAF。 print_cpu()无锁解引用rq->curr,与正在 exit 的 task 经__put_task_struct_rcu_cb释放 task_struct 赛跑 → task_struct UAF。sched_show_numa()在无task_lock下读p->mm;exit_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_SCHEDstub。 - 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/6 | struct rq::rd 注解 __rcu;core/deadline/fair 三处 lockless reader 改 RCU 原语 |
| 2/6 | rq_attach_root 用 rcu_assign_pointer();print_dl_rq guard(rcu) + rcu_dereference |
| 3/6 | print_cpu 在 rcu_read_lock 内 rcu_dereference(rq->curr) |
| 4/6 | sched_show_numa 用 task_lock(p) 包住 p->mm 检查与解引用 |
| 5/6 | 新 for_each_leaf_cfs_rq_rcu;SCHED_DEBUG_MAX_ITER=4096 + 截断提示;SEQ宏提到 sched.h 并加 CGROUP stub |
| 6/6 | sched_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而非 RCU:exit_mm在task_lock下清 NULL 再mmput,所以保护类型与rq->curr的 RCU 路径不同,混用会 fix 不到位。 list_for_each_entry_safe的真正风险:除缺READ_ONCE,cfs_rq在rq->lock外重挂可造成遍历倒走;用list_for_each_entry_rcu+ 4096 上限双保险。- Sparse / weak-ordering 收益:patch 1不仅是修 bug,更让 Sparse 在所有架构上静态验证 RCU 用法,未来新增 lockless reader 不会再悄悄出错。
CONFIG_CGROUP_SCHEDstub:SEQ_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_cpu改rcu_read_lock + READ_ONCE;print_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_rq改guard(rcu)();sched_show_numa改task_lock;SCHED_DEBUG_MAX_ITER1024→4096 并加截断提示;SEQ_printf*提到sched.h;strcpy→strscpy;Fixestag 修正为039ae8bcf7a5。 - v5 → v6:仅 rebase 到
tip/sched/core sched-core-2026-08-17。
一句话总结
v6 系列在为单 CPU 调试提供 cpu<N>/debug 视图的同时,把 sched/debug 路径中四条无锁解引用(rq->rd、rq->curr、p->mm、leaf_cfs_rq_list)补上 rcu_dereference / task_lock / list_for_each_entry_rcu + 4096 截断,从根上消除 sashiko-bot 触发的 UAF / TOCTOU / RCU stall。