0/2 已展开

LLM 分析

sched:irq:CPU hotplug 与 set_cpus_allowed_ptr 之间的竞态

系列概况

  • 标题[RFC PATCH] sched: irq: cpu-hotplug race vs set_cpus_allowed_ptr()
  • 作者:Sebastian Andrzej Siewior <bigeasy@linutronix.de>
  • 版本:RFC,单 patch(diffstat 2 files, 6 insertions(+), 2 deletions(-)
  • 规模:2 个文件改动,仅 6 行新增 / 2 行删除
  • 修改文件
    • kernel/irq/manage.c:包裹 set_cpus_allowed_ptr() 进入 cpus_read_lock 临界区
    • kernel/sched/core.cmigration_cpu_stop() 中在 __migrate_task 失败时回填 cpus_mask
  • 代码统计:diffstat 显示一个 hunk 调整加锁粒度,另一个 hunk 在 scheduler 路径上做兜底
  • 触发报告:syzbot bug 6a9919ac.94649fcc.25487e.0005.GAE@google.com,bug extid 6835d5c11145e4f77057
  • Message-ID
    • 报告:6a9919ac.94649fcc.25487e.0005.GAE@google.com
    • RFC 补丁:20260907085825.f-CZ1Q5y@linutronix.de
  • 完整性:两封邮件构成完整 thread;上游基线 08dbfad3f504(merge tag 'for-linus'),可回溯到 v6.8(作者声明能在 v6.8 复现)

补丁目的

修复 CPU 热插拔与 IRQ 线程亲和性设置之间的 race。IRQ 线程响应 IRQTF_AFFINITY 时调用 set_cpus_allowed_ptr() 调整 affinity mask。选定一个 online CPU 之后到 affine_move_task() 真正搬任务这段窗口里,该 CPU 可能被 hotplug 下线,导致 __migrate_task()is_cpu_allowed() 检查中被拒绝,从而把任务搬到一个不在其 cpus_mask的 CPU 上运行。后续 migrate_enable() 会发起 push,但 migration_pending 已是 NULL,触发 kernel/sched/core.c:3057 的 warning。

旧流程的问题

  1. IRQ core 在 IRQTF_AFFINITY 置位后唤醒需要重新调亲和的 irq 线程。
  2. irq 线程在 irq_thread_check_affinity() 路径上裸调 set_cpus_allowed_ptr(current, mask),并未持有 cpus_read_lock
  3. set_cpus_allowed_ptr() 选一个当前 online 的 CPU,但搬移由 migration_cpu_stop() 完成,期间 task 暂停。
  4. 同一窗口内,CPU hotplug 可让该 CPU 离开 cpu_online_mask__migrate_task()is_cpu_allowed() 拒绝而 fallback 回 rq(当前运行队列),不是 requested rq。
  5. 结果:task->cpus_mask 与实际运行的 CPU 错位;后续 migrate_disable()->schedule()->migrate_enable() 看到 cpus_ptr 已变,会试图再迁移一次,但 migration_pending == NULL,于是 affine_move_task() 路径上 kernel/sched/core.c:3057 触发 WARN。

新流程

补丁给出两种相互独立的兜底,任选其一即可,patch 中两个都贴出来供评审挑:

  • 方案 A(IRQ 侧加锁):在 set_cpus_allowed_ptr() 调用外包一层 scoped_guard(cpus_read_lock),确保热插拔拿不到 cpus_write_lock,目标 CPU 不会在迁移完成前掉线。顺带建议在调度器入口检测所有 set_cpus_allowed_ptr() 调用者是否都持有 cpus_read_lock
  • 方案 B(scheduler 侧兜底)migration_cpu_stop() 里若 rq != cpu_rq(arg->dest_cpu),把 rq->cpu 强行写进 p->cpus_mask,让运行 CPU 仍然属于 mask,避免后续 migrate_enable() 误推。但作者提醒 sched_class::set_cpus_allowed 之前没见过这个 mask,且语义类似 select_fallback_rq() 的兜底但没 printk。

Patch 概览

// kernel/irq/manage.c
@@ -8,6 +8,7 @@
+#include <linux/cpuhplock.h>
@@ -1044,8 +1045,9 @@ static void irq_thread_check_affinity(...)
-       set_cpus_allowed_ptr(current, mask);
+       scoped_guard(cpus_read_lock) {
+               set_cpus_allowed_ptr(current, mask);
+       }

// kernel/sched/core.c
@@ -2674,6 +2674,8 @@ static int migration_cpu_stop(void *data)
        rq = __migrate_task(rq, &rf, p, arg->dest_cpu);
+       if (rq != cpu_rq(arg->dest_cpu))
+               cpumask_set_cpu(rq->cpu, &p->cpus_mask);

关键实现

  • scoped_guard(cpus_read_lock):利用 C11/__cleanup 风格的 scope guard 在 IRQ 线程检查亲和性时持有 cpus_read_lock。这把锁与 CPU hotplug 的 cpus_write_lock 互斥,hotplug 在写锁竞争时只能等待,从而让目标 CPU 不会在迁移过程中离线。需要 <linux/cpuhplock.h> 提供该接口。
  • cpumask_set_cpu(rq->cpu, &p->cpus_mask):在 migration_cpu_stop() 已经返回 fallback rq 时,把 rq 真实所在 CPU 写回 mask,保证 p->cpus_mask 与运行 CPU 一致。逻辑上是个“事后修正”兜底,避开 migrate_enable() 的二次 push。
  • affine_move_task 警告点(core.c:3057)migration_pending == NULLcpus_mask 与当前 CPU 不一致时会打印 warning;补丁消除触发条件。

类比

想象酒店前台给一位客人安排房间:前台先查“哪些楼层可用”,再把客人从大厅送去电梯,电梯运行期间楼层可能因为检修被关闭。方案 A 相当于前台“拿着楼层表不放”,检修队必须等电梯送完客;方案 B 相当于电梯到了,发现原楼层关了,临时把客人安排到“还开门”的同区楼层并更新房卡——保证客人实际所在楼层与房卡记录一致。下次客人出门找前台,误会就被化解了。

Highlight:风险与注意点

  • 方案 B 修改 cpus_mask 可能与 sched_class::set_cpus_allowed 的合约冲突:用户态可见的 mask 被悄悄扩大,未来要观察是否影响 cgroup、sched_setattr 等接口的语义。
  • 方案 A 的调用点检查:作者提议在 set_cpus_allowed_ptr() 入口加 WARN 提示必须持 cpus_read_lock,但本 patch 还没做,可能漏掉其它调用者。
  • scoped_guard 的死锁面:IRQ 线程路径若已间接持有 cpus_read_lock,再包一层会形成嵌套读写持锁,需要 review 整条 IRQ affinity 路径。
  • 回溯能力:作者称可复现到 v6.8,意味着这是 migrate-disable 引入以来的长尾 race,需要确认所有受影响 stable 树打 backport。
  • 是否需要 printk / 计数器:方案 B 默默改 mask,缺少 select_fallback_rq() 那样的可观测信号,建议在合并前加上 tracepoint 或统计。
  • CPU hotplug 同步原语升级:未来如果 hotplug 路径切到 cpus_rwsem 之外的同步机制(如 SRCU),scoped_guard(cpus_read_lock) 仍需重新评估语义。

版本变化

仅一个 RFC 版本,无 v1/v2 演进。作者明确两个修复方案只取其一,未来合并时可能收敛为单方案并加调用点检查。

一句话总结

通过在 IRQ 线程设置 affinity 时持有 cpus_read_lock,或在 migration_cpu_stop() 中回填 cpus_mask,消除 CPU hotplug 与 set_cpus_allowed_ptr() 之间的 race,避免 affine_move_task() 触发 WARN。

+----------------+        set_cpus_allowed_ptr        +---------------------+
|  irq thread    |  ----------------------------->    | affine_move_task    |
|  (no lock)     |                                    | pick CPU X (online) |
+----------------+                                    +---------------------+
        |                                                       |
        |  hotplug offline CPU X                                | migration_cpu_stop
        v                                                       v
+----------------+                                    +---------------------+
| WARN @ core.c  |  <----- rq != cpu_rq(dest) ------  | __migrate_task      |
| :3057          |                                    | fallback to running  |
+----------------+                                    +---------------------+

Fix A: scoped_guard(cpus_read_lock) -> hotplug must wait, race gone.
Fix B: when fallback happens, add rq->cpu to p->cpus_mask -> mask aligned.