sched discussion
[PATCH v4 0/5] sched/debug: Introduce per-CPU debugfs files
LLM 分析
sched/debug: per-CPU debugfs 文件 + 5 处 RCU/UAF 修复 (v4)
系列概况
- 标题:[PATCH v4 0/5] sched/debug: Introduce per-CPU debugfs files
- 作者:Aaron Tomlin atomlin@atomlin.com
- 版本:v4
- 规模:5 patch + 1 cover letter,10 封邮件
- 修改文件:kernel/sched/debug.c、kernel/sched/fair.c、kernel/sched/topology.c
- 代码统计:+76 / -9
- Message-ID:20260810015812.428999-1-atomlin@atomlin.com
- 完整性:完整(cover + 5 patch + 4 reply)
补丁目的
本系列一次性修复 sched debugfs 中 5 处 RCU/内存安全问题,并新增 per-CPU debugfs 接口 /sys/kernel/debug/sched/cpu/cpu<N>/debug,便于大 SMP 拓扑上只读目标 CPU 的 runqueue:
print_dl_rq()无锁访问rq->rd→ UAF。print_cpu()无锁访问rq->curr→ 任务退出时 UAF。sched_show_numa()p->mm双重求值 → TOCTOU。print_cfs_stats()用非 RCU 宏遍历 RCU 链表 → RCU stall。- 新增按 CPU debugfs 视图(离线 CPU 读返回
-ENODEV)。
旧流程的问题
/sys/kernel/debug/sched/debug 是全量 dump,读一次输出所有 online CPU 的 runqueue 状态。在大核数服务器上 dump 体量巨大,且 reader 与 hot-unplug、cpuset repartition、task exit 等事件完全无同步保护。写入侧用普通赋值 rq->rd = rd,既无 rcu_assign_pointer() 的 release barrier,reader 解引用时也没 RCU critical section:
// print_dl_rq() - 旧实现
dl_bw = &cpu_rq(cpu)->rd->dl_bw;
SEQ_printf(m, " .%-30s: %lld\n", "dl_bw->bw", dl_bw->bw);
SEQ_printf(m, " .%-30s: %lld\n", "dl_bw->total_bw", dl_bw->total_bw);
reader 读 dl_bw->bw 期间,老 root_domain 走 call_rcu(&old_rd->rcu, free_rootdomain) 后被 kfree → UAF;reader 拿到的也可能是部分初始化的字段。
类似问题:print_cpu() 的 task_pid_nr(rq->curr) 无锁;sched_show_numa() 的 if (p->mm) P(mm->numa_scan_seq); 中宏对 p->mm 二次求值;print_cfs_stats() 用 list_for_each_entry_safe() 遍历 RCU 链表,缺 READ_ONCE(),且 print_cfs_rq() 在 seq_file I/O 期间会 drop rq->lock,可能触发 RCU stall。
新流程
写入侧把普通指针升级为 RCU 指针,reader 侧加 RCU 临界区;新增 per-CPU debugfs 目录并以 cpu_online() 拦截:
writer reader
rq_attach_root() print_dl_rq / print_cpu
| |
rcu_assign_pointer(rq->rd, rd) rcu_read_lock()
[release barrier] READ_ONCE / rcu_dereference
list_add_rcu(leaf_cfs_rq) list_for_each_entry_rcu + cap
| |
(RCU grace period) read dl_bw / curr->pid / stats
| |
kfree(old_rd) rcu_read_unlock()
|
return 0 | -ENODEV (cpu offline)
per-CPU 路径:
/sys/kernel/debug/sched/cpu/
+-- cpu0/debug
+-- cpu1/debug
...
+-- cpuN/debug (read -> sched_debug_cpu_show -> print_cpu())
Patch 概览
- Patch 1/5
print_dl_rq():写入侧改用rcu_assign_pointer();reader 加rcu_read_lock()+READ_ONCE()+ NULL 检查。 - Patch 2/5
print_cpu():用rcu_dereference(rq->curr)替代READ_ONCE(),保留__rcu注解(v3→v4 关键调整)。 - Patch 3/5
sched_show_numa():READ_ONCE(p->mm)一次取值复用于局部指针,消除宏内二次求值。 - Patch 4/5
print_cfs_stats():新增for_each_leaf_cfs_rq_rcu()宏(基于list_for_each_entry_rcu()),加SCHED_DEBUG_MAX_ITER=1024硬上限。 - Patch 5/5 新增
debugfs_cpu_init(),在sched_init_debug()末尾调用;新增sched_debug_cpu_show/open/fops。
关键实现
rq_attach_root() 写入侧:
// topology.c
- rq->rd = rd;
+ rcu_assign_pointer(rq->rd, rd);
print_dl_rq() 读取侧:
// debug.c
+ struct root_domain *rd;
...
- dl_bw = &cpu_rq(cpu)->rd->dl_bw;
+ rcu_read_lock();
+ rd = READ_ONCE(cpu_rq(cpu)->rd);
+ if (rd) {
+ dl_bw = &rd->dl_bw;
SEQ_printf(m, " .%-30s: %lld\n", "dl_bw->bw", dl_bw->bw);
SEQ_printf(m, " .%-30s: %lld\n", "dl_bw->total_bw", dl_bw->total_bw);
+ }
+ rcu_read_unlock();
print_cpu():
- SEQ_printf(m, " .%-30s: %ld\n", "curr->pid", (long)(task_pid_nr(rq->curr)));
+ rcu_read_lock();
+ SEQ_printf(m, " .%-30s: %ld\n", "curr->pid",
+ (long)(task_pid_nr(rcu_dereference(rq->curr))));
+ rcu_read_unlock();
sched_show_numa():
- if (p->mm)
- P(mm->numa_scan_seq);
+ struct mm_struct *mm = READ_ONCE(p->mm);
+ if (mm)
+ __PS("mm->numa_scan_seq", mm->numa_scan_seq);
print_cfs_stats():
+ #define SCHED_DEBUG_MAX_ITER 1024
- struct cfs_rq *cfs_rq, *pos;
+ struct cfs_rq *cfs_rq;
+ int max_iter = SCHED_DEBUG_MAX_ITER;
- for_each_leaf_cfs_rq_safe(cpu_rq(cpu), cfs_rq, pos)
+ for_each_leaf_cfs_rq_rcu(cpu_rq(cpu), cfs_rq) {
+ if (--max_iter < 0)
+ break;
print_cfs_rq(m, cpu, cfs_rq);
+ }
新增 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;
}
static __init void debugfs_cpu_init(void)
{
struct dentry *d_cpu_dir = debugfs_create_dir("cpu", debugfs_sched);
for_each_possible_cpu(cpu) {
char buf[16];
struct dentry *d_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 想成 医院大厅的"今日挂号总表":每次查询都要把所有诊室翻一遍。新增的 /sys/kernel/debug/sched/cpu/cpu<N>/debug 相当于 每个诊室门口的自助叫号屏:只看诊室 3,不被 1~127 干扰。
RCU 修复:rcu_assign_pointer() 像"图书管理员上架新书时拉一根临时警戒绳,所有读者必须等他拉好绳才被允许拿书";READ_ONCE() 则是"读者隔着警戒绳看一眼目录,确保拿到的是完整上架的新书,而不是半本";SCHED_DEBUG_MAX_ITER=1024 像"自助叫号屏最多翻 1024 行就强制回到首页",避免被恶意循环拖死。
Highlight:风险与注意点
rq->rd缺__rcu注解:Aaron 在回复 Prateek 时承认struct rq::rd没有__rcu,直接rcu_dereference()会触发 sparse 警告;既然写入侧已用rcu_assign_pointer(),需要补__rcu,v5 应顺手补上。rcu_dereference_all()风格统一:Prateek 建议改用rcu_dereference_all()与写入侧对称;Aaron 同意。SCHED_DEBUG_MAX_ITER=1024合理性:硬上限是 magic number,需要在 commit message 解释为何选 1024;截断后输出统计会"少部分",是否在 seq_file 末尾打印"truncated"警告值得评估。- offline CPU 检查与 race:
cpu_online(cpu)与后续print_cpu()之间没有持锁,理论上仍可能 CPU 此时 down,是否需要 RCU + cpuhp 引用计数保护待 maintainer 拍板。 - per-CPU debugfs 与 cpuset hotplug:新增目录基于 possible CPU 全集,hotplug 出去的 CPU 目录会残留,长期看是否要在 CPU 真正消失时清理 dentry。
- sashiko-bot 自动报告:4 个 bugfix 都带
Reported-by: sashiko-bot,说明自动化静态分析器在持续扫这些 lockless 模式,后续值得观察是否还命中其他 sched 路径。
版本变化
- v1 → v2:受 Peter Zijlstra 与 Zhan Xusheng 启发,将动机重新框定为"大 SMP 上的 targeted interactive debugging";为
sched_debug_cpu_show()加cpu_online()检查并返回-ENODEV。 - v2 → v3:在
print_cpu()加rcu_read_lock()+READ_ONCE();保护rq->rd应对 hot-unplug / cpuset repartition;引入for_each_leaf_cfs_rq_rcu()基于list_for_each_entry_rcu()。 - v3 → v4:Patch 1 从
READ_ONCE()升级到rcu_dereference(rq->curr)保留__rcu;Patch 2 在写入侧补齐rcu_assign_pointer();新增 Patch 3(p->mmTOCTOU);Patch 4 新增SCHED_DEBUG_MAX_ITER=1024上限。
一句话总结
v4 把 sched debugfs 的 5 处无锁/RCU 隐患一次性清理(rq->rd、rq->curr、p->mm、leaf_cfs_rq_list),并落地 per-CPU debugfs 接口 cpu<N>/debug;struct rq::rd 缺 __rcu 注解、rcu_dereference_all() 风格统一、max_iter=1024 合理性等细节仍待 v5 跟进。