sched discussion
[PATCH v8 1/6] sched: Annotate rq->rd with __rcu and update lockless readers
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.c、kernel/sched/deadline.c、kernel/sched/debug.c、kernel/sched/fair.c、kernel/sched/rt.c、kernel/sched/sched.h、kernel/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 修订的回复)
补丁目的
本系列同时做两件事:
- 修掉 scheduler debugfs 里一组真实的内存安全问题:把
rq->rd、rq->curr、p->mm、rq->leaf_cfs_rq_list这些"看着是本 CPU 局部读、实际被远端并发改写"的字段,全部放到合适的同步原语(RCU 读锁 /task_lock)下,避免 use-after-free、TOCTOU 与 RCU CPU stall。 - 新增 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 2:
rq_attach_root()写rq->rd用裸=,free_rootdomain()通过call_rcu()释放;debugfs 无rcu_read_lock(),可能在kfree()之后才读到悬空指针。 - Patch 3:
task_pid_nr(rq->curr)无 RCU 保护,任务退出走call_rcu(__put_task_struct_rcu_cb),并发形成 UAF。 - Patch 4:
sched_show_numa()裸读p->mm,exit_mm()在task_lock(p)下清p->mm后立即mmput(),构成本窗口内 UAF。 - Patch 5:
for_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/6 | core.c / deadline.c / fair.c / rt.c / sched.h | rq->rd 加 __rcu,引入 rcu_dereference_root_domain(),全调度子系统替换 | 基础设施(前提) |
| 2/6 | debug.c / topology.c | print_dl_rq() 用 guard(rcu)+rcu_dereference;rq_attach_root() 改 rcu_assign_pointer | UAF 修复 |
| 3/6 | debug.c | print_cpu() 中 rq->curr 加 rcu_read_lock+rcu_dereference | UAF 修复 |
| 4/6 | debug.c | sched_show_numa() 用 task_lock(p)/task_unlock(p) 包裹 p->mm 读 | TOCTOU+UAF 修复 |
| 5/6 | debug.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/6 | debug.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:风险与注意点
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直接红。- 锁假设与未来读者:patch 1 的 helper 只承认
sched_domains_mutex与 RCU-sched 两种合法语境。任何后续在新语境里 derefrq->rd(例如某些 workqueue / cgroup v2 freezer 路径)都必须自证属于这两者。 SCHED_DEBUG_MAX_ITER=4096是拍脑袋定的:足够大到不截断普通负载,但极端机器或NR_CGROUPS巨大时可能真的截断;输出截断信息是"软告警",不能掩盖更严重的 list churn 根因,需要后续观察是否需要再调。SEQ_printf_task_group_path()移出 debug.c:sched_debug_lock/sched_debug_group_path现在是extern,未EXPORT_SYMBOL,预期只在 scheduler 内部使用——若其它子系统想用,要单独讨论导出策略。- 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_SCHED 下 SEQ_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_rq 用 guard(rcu)();sched_show_numa 改 task_lock;SCHED_DEBUG_MAX_ITER 1024→4096 + 截断提示;strcpy 改 strscpy;SEQ_printf_task_group_path 移到 sched.h;修 patch 5 的 Fixes tag 到 039ae8bcf7a5。
v3 → v4:rq->curr 改 rcu_dereference(保留 __rcu);patch 2 加 rcu_assign_pointer;新增 patch 3 修 TOCTOU;新增 SCHED_DEBUG_MAX_ITER 安全熔断。
v2 → v3:补 rcu_read_lock+READ_ONCE;rq->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 处理。