sched discussion
[syzbot] [kernel?] WARNING in __set_cpus_allowed_ptr_locked
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.c:migration_cpu_stop()中在__migrate_task失败时回填cpus_mask
- 代码统计:diffstat 显示一个 hunk 调整加锁粒度,另一个 hunk 在 scheduler 路径上做兜底
- 触发报告:syzbot bug
6a9919ac.94649fcc.25487e.0005.GAE@google.com,bug extid6835d5c11145e4f77057 - 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。
旧流程的问题
- IRQ core 在
IRQTF_AFFINITY置位后唤醒需要重新调亲和的 irq 线程。 - irq 线程在
irq_thread_check_affinity()路径上裸调set_cpus_allowed_ptr(current, mask),并未持有cpus_read_lock。 set_cpus_allowed_ptr()选一个当前 online 的 CPU,但搬移由migration_cpu_stop()完成,期间 task 暂停。- 同一窗口内,CPU hotplug 可让该 CPU 离开
cpu_online_mask,__migrate_task()因is_cpu_allowed()拒绝而 fallback 回rq(当前运行队列),不是 requested rq。 - 结果:
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 == NULL但cpus_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.