sched discussion
[PATCH v5 1/6] sched: Annotate rq->rd with __rcu and update lockless readers
LLM 分析
sched/debug:引入 per-CPU debugfs 文件与若干并发/UAF 修复
系列概况
- 标题: [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files
- 作者: Aaron Tomlin atomlin@atomlin.com
- 版本: v5(v1 -> v2 -> v3 -> v4 -> v5 完整演进)
- 规模: 6 patches
- 修改文件: kernel/sched/{core,deadline,debug,fair,topology}.c, kernel/sched/sched.h
- 代码统计: 封面信 +165/-68;各 patch 单独统计见 Patch 概览
- Message-ID: 20260825141413.868997-1-atomlin@atomlin.com
- 完整性: 1 封 cover letter + 6 个 patch + 1 封作者自述 rebase 的回复
补丁目的
本系列同时完成两件事:
- 修复 scheduler debugfs handlers 中多个 use-after-free / TOCTOU / RCU 误用问题(涉及
rq->rd、rq->curr、p->mm、leaf_cfs_rq_list); - 在
/sys/kernel/debug/sched/cpu/cpu<N>/debug下引入 per-CPU debugfs 文件,便于在大 SMP 拓扑上做单核诊断。
旧流程的问题
rq->rd通过rq_attach_root()写入,但缺__rcu注解,reader 直接->rd->dl_bw解引用 -> release store 缺失 + RCU grace period 期间 free 的双重 UAF 风险(CPU 热插拔、cpuset repartition 场景)。print_cpu()无锁读rq->curr-> 任务并发 exit 时put_task_struct() -> call_rcu(__put_task_struct_rcu_cb)触发 task_struct UAF。sched_show_numa()先读p->mm,再解引用mm->numa_scan_seq->exit_mm()并发清空p->mm+mmput()触发 TOCTOU + mm_struct UAF。print_cfs_stats()用list_for_each_entry_safe()(非 RCU 宏)在 RCU 链表上无锁遍历,writer 用list_add_rcu时 reader 缺READ_ONCE()-> 指针乱跳、回退、死循环,严重时 RCU stall。
新流程
- 注解
struct rq::rd为__rcu,writer 用rcu_assign_pointer(),reader 用rcu_dereference()/rcu_dereference_sched()(Patch 1)。 - 各 debugfs handler 用
guard(rcu)()或显式rcu_read_lock()包裹关键读取(Patches 2/3/5)。 sched_show_numa()用task_lock(p)锁住p->mm检查与解引用(Patch 4)。- 引入
for_each_leaf_cfs_rq_rcu()(list_for_each_entry_rcu())+ 迭代上限SCHED_DEBUG_MAX_ITER=4096+ 截断提示(Patch 5)。 - 新建
/sys/kernel/debug/sched/cpu/cpu<N>/debug,target CPU offline 时返回-ENODEV(Patch 6)。
Patch 概览
- Patch 1/6
sched: Annotate rq->rd with __rcu …(core.c, deadline.c, fair.c, sched.h;+30/-25):加注解并补齐 13 处 lockless reader。 - Patch 2/6
sched/debug: Protect lockless rq->rd access in print_dl_rq()(debug.c, topology.c;+3/-2):dl_bw UAF fix。 - Patch 3/6
sched/debug: Protect lockless rq->curr access in print_cpu()(debug.c;+4/-1):task_struct UAF fix。 - Patch 4/6
sched/debug: Protect p->mm access in sched_show_numa()(debug.c;+2/-0):mm_struct TOCTOU/UAF fix。 - Patch 5/6
sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()(debug.c, fair.c, sched.h;+83/-40):RCU 链表遍历 + 截断。 - Patch 6/6
sched/debug: Introduce per-CPU debugfs files(debug.c;+43/-0):新特性入口。
关键实现
Patch 1 — RCU 注解
/* kernel/sched/sched.h */
struct rq {
...
- struct root_domain *rd;
+ struct root_domain __rcu *rd;
};
/* writer: kernel/sched/topology.c::rq_attach_root */
- rq->rd = rd;
+ rcu_assign_pointer(rq->rd, rd);
/* reader: kernel/sched/fair.c */
- struct root_domain *rd = this_rq()->rd;
+ struct root_domain *rd = rcu_dereference(this_rq()->rd);
Patch 2 — print_dl_rq()
- dl_bw = &cpu_rq(cpu)->rd->dl_bw;
+ guard(rcu)();
+ dl_bw = &rcu_dereference(cpu_rq(cpu)->rd)->dl_bw;
guard(rcu)() 是 RCU 读侧的 scope-based 包装,作用域结束自动 rcu_read_unlock()。
Patch 3 — print_cpu()
+ rcu_read_lock();
SEQ_printf(m, " .%-30s: %ld\n", "curr->pid",
- (long)(task_pid_nr(rq->curr)));
+ (long)(task_pid_nr(rcu_dereference(rq->curr))));
+ rcu_read_unlock();
Patch 4 — sched_show_numa()
+ task_lock(p);
if (p->mm) P(mm->numa_scan_seq);
+ task_unlock(p);
exit_mm() 在 task_lock(p) 之内置 current->mm = NULL,所以持锁期间 p->mm 稳定。
Patch 5 — for_each_leaf_cfs_rq_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, "\n");
/* output "stats truncated at 4096 iterations" then break */
break;
}
}
宏在 CONFIG_FAIR_GROUP_SCHED 时展开为 list_for_each_entry_rcu(...),否则退化为单元素循环(保持原行为)。
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;
}
debugfs_cpu_init() 在 sched_init_debug() 中调用,为每个 possible CPU 创建 /sys/kernel/debug/sched/cpu/cpuN/debug。
RCU 读侧作用范围示意
+-------------------------------+ +--------------------------------+
| print_dl_rq() OLD | | print_dl_rq() NEW |
| rcu_read_lock NOT held | | guard(rcu)() == rcu_read_lock |
+-------------------------------+ +--------------------------------+
| |
v v
+-------------------------------+ +--------------------------------+
| cpu_rq(cpu)->rd->dl_bw | | rcu_dereference(rq->rd) |
| direct C pointer load | | READ_ONCE + data dependency |
+-------------------------------+ +--------------------------------+
| |
v v
+-------------------------------+ +--------------------------------+
| UAF risk: | | safe: |
| rq_attach_root -> | | call_rcu(free_rootdomain) |
| call_rcu(free_rootdomain) | | deferred until rcu_read_unlock |
| can kfree(old_rd) concurrent | | |
+-------------------------------+ +--------------------------------+
per-CPU debugfs 入口结构
/sys/kernel/debug/sched/
|-- debug (legacy: all online CPUs)
`-- cpu/
|-- cpu0/
| `-- debug (single-CPU view)
|-- cpu1/
| `-- debug
...
`-- cpuN-1/
`-- debug
类比
可以把 scheduler debugfs 想象成 医院放射科的影像中心:
- 旧的
/sys/kernel/debug/sched/debug像 一次拍全身 X 光片:所有病床的病人都被拍一遍,胶卷巨大、抓不到单点。 - 新增的
/sys/kernel/debug/sched/cpu/cpu<N>/debug像 针对某张病床的可移动 X 光机:医生(运维)可以对准 CPU<N> 这一个病人精准拍片。 - 而前面 5 个 RCU/UAF 修则像 放射技师终于戴上了铅手套:旧流程是徒手抓感光片(无锁访问),可能在放射科关灯(RCU grace period)时被烫伤(UAF);新流程要求戴手套(
guard(rcu))、拿授权(__rcu注解)后才能取片。 SCHED_DEBUG_MAX_ITER=4096的截断机制,则像 放射科规定一次检查最多拍 4096 张片,超过就贴"已截断"标签,避免占用整间机房(CPU 长时间持 RCU 读锁)导致"电路跳闸"(RCU stall)。
Highlight:风险与注意点
- Patch 1 的回退风险:
__rcu注解会强制 Sparse 把所有直接rq->rd解引用视为违规。如果 patch 漏改一处 reader,下游编译/测试可能触发警告甚至-Werror。Reviewer 应检查所有->rd解引用是否都已包进rcu_dereference_*。 guard(rcu)()嵌套:print_cpu()与print_dl_rq()现在都guard(rcu)(),需要确认 debugfs 的 seq_file 回调路径上没有其他 RCU 关键段嵌套导致 RCU 嵌套深度计数出错。- 迭代上限的边界:4096 是经验值;如果某服务器 cgroup 层级特别深,截断提示会频繁出现。后续是否需要根据
nr_cgroups动态调优,需要观察。 - per-CPU debugfs 与 CPU hotplug 的关系:CPU offline 时返回
-ENODEV,但debugfs_cpu_init()在sched_init_debug()(boot 期)一次性创建所有 possible CPU 节点,hotplug 后上线的 CPU 不会自动出现,需要确认社区接受这种语义。 - Patch 5 的
Fixes:tag:v4 起把Fixes:重新指向039ae8bcf7a5(sched/fair 的 O(nr_cgroups) 修复),说明原作者对此问题溯源再次核对过。 - 作者最后说"please ignore, will rebase against tip/sched/core":意味着 v5 在
tip/sched/core上可能存在冲突/树偏差,需要等待 v6,本系列当前处于"自撤"状态。
版本变化
- v1 -> v2:把动机限定到"大 SMP 上的单核交互式调试";offline CPU 返回
-ENODEV(Peter Zijlstra、Zhan Xusheng)。 - v2 -> v3:
print_cpu()加rcu_read_lock()+READ_ONCE();rq_attach_root()加rcu_assign_pointer();新增 Patch 3(sched_show_numaTOCTOU);新增 Patch 4 迭代上限。 - v3 -> v4:Patch 1 改用
rcu_dereference(rq->curr)保留__rcu;print_dl_rq()用guard(rcu)();sched_show_numa()用task_lock/task_unlock替代READ_ONCE(p->mm);SCHED_DEBUG_MAX_ITER从 1024 提到 4096 并加截断提示;SEQ_printf*移到 sched.h;strcpy换strscpy;Patch 5 的Fixes:改回039ae8bcf7a5。 - v4 -> v5:仅拆出 Patch 1 作为独立前置 patch,没有功能改动。
- v5 -> 回复:作者自述要 rebase,预期会有 v6。
一句话总结
Aaron Tomlin 用 6 个 patch 把 scheduler debugfs 从"无锁裸奔"改成"全 RCU 化",修了 4 处内存安全隐患,并顺手引入 per-CPU debugfs 入口以支撑大 SMP 单核诊断。