0/8 已展开

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 的回复

补丁目的

本系列同时完成两件事:

  1. 修复 scheduler debugfs handlers 中多个 use-after-free / TOCTOU / RCU 误用问题(涉及 rq->rdrq->currp->mmleaf_cfs_rq_list);
  2. /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 概览

  1. Patch 1/6 sched: Annotate rq->rd with __rcu …(core.c, deadline.c, fair.c, sched.h;+30/-25):加注解并补齐 13 处 lockless reader。
  2. Patch 2/6 sched/debug: Protect lockless rq->rd access in print_dl_rq()(debug.c, topology.c;+3/-2):dl_bw UAF fix。
  3. Patch 3/6 sched/debug: Protect lockless rq->curr access in print_cpu()(debug.c;+4/-1):task_struct UAF fix。
  4. Patch 4/6 sched/debug: Protect p->mm access in sched_show_numa()(debug.c;+2/-0):mm_struct TOCTOU/UAF fix。
  5. Patch 5/6 sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()(debug.c, fair.c, sched.h;+83/-40):RCU 链表遍历 + 截断。
  6. 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:风险与注意点

  1. Patch 1 的回退风险__rcu 注解会强制 Sparse 把所有直接 rq->rd 解引用视为违规。如果 patch 漏改一处 reader,下游编译/测试可能触发警告甚至 -Werror。Reviewer 应检查所有 ->rd 解引用是否都已包进 rcu_dereference_*
  2. guard(rcu)() 嵌套print_cpu()print_dl_rq() 现在都 guard(rcu)(),需要确认 debugfs 的 seq_file 回调路径上没有其他 RCU 关键段嵌套导致 RCU 嵌套深度计数出错。
  3. 迭代上限的边界:4096 是经验值;如果某服务器 cgroup 层级特别深,截断提示会频繁出现。后续是否需要根据 nr_cgroups 动态调优,需要观察。
  4. per-CPU debugfs 与 CPU hotplug 的关系:CPU offline 时返回 -ENODEV,但 debugfs_cpu_init()sched_init_debug()(boot 期)一次性创建所有 possible CPU 节点,hotplug 后上线的 CPU 不会自动出现,需要确认社区接受这种语义。
  5. Patch 5 的 Fixes: tag:v4 起把 Fixes: 重新指向 039ae8bcf7a5(sched/fair 的 O(nr_cgroups) 修复),说明原作者对此问题溯源再次核对过。
  6. 作者最后说"please ignore, will rebase against tip/sched/core":意味着 v5 在 tip/sched/core 上可能存在冲突/树偏差,需要等待 v6,本系列当前处于"自撤"状态。

版本变化

  • v1 -> v2:把动机限定到"大 SMP 上的单核交互式调试";offline CPU 返回 -ENODEV(Peter Zijlstra、Zhan Xusheng)。
  • v2 -> v3print_cpu()rcu_read_lock() + READ_ONCE()rq_attach_root()rcu_assign_pointer();新增 Patch 3(sched_show_numa TOCTOU);新增 Patch 4 迭代上限。
  • v3 -> v4:Patch 1 改用 rcu_dereference(rq->curr) 保留 __rcuprint_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;strcpystrscpy;Patch 5 的 Fixes: 改回 039ae8bcf7a5
  • v4 -> v5:仅拆出 Patch 1 作为独立前置 patch,没有功能改动。
  • v5 -> 回复:作者自述要 rebase,预期会有 v6。

一句话总结

Aaron Tomlin 用 6 个 patch 把 scheduler debugfs 从"无锁裸奔"改成"全 RCU 化",修了 4 处内存安全隐患,并顺手引入 per-CPU debugfs 入口以支撑大 SMP 单核诊断。