* [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()
@ 2026-08-06 7:17 Krystian Slowik
2026-08-06 7:22 ` Peter Zijlstra
0 siblings, 1 reply; 4+ messages in thread
From: Krystian Slowik @ 2026-08-06 7:17 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, linux-kernel,
Krystian Slowik, stable
Since commit 650952d3fb38 ("sched: Make __do_set_cpus_allowed() use the
sched_change pattern"), do_set_cpus_allowed() dequeues and re-enqueues
the target task through the sched_change guard whenever it is queued.
The idle task counts as queued: init_idle() sets
idle->on_rq = TASK_ON_RQ_QUEUED. But the idle sched class implements no
real dequeue_task() (only the "bad: scheduling from the idle thread!"
debug stub, which even drops and re-takes the rq lock in the middle of
the guarded section) and no enqueue_task() at all, so running the guard
on the idle task jumps through a NULL pointer in sched_change_end():
bad: scheduling from the idle thread!
CPU: 3 UID: 0 PID: 0 Comm: swapper/3 Kdump: loaded Not tainted 7.0.0-28-generic #28-Ubuntu PREEMPT(lazy)
Call Trace:
dequeue_task_idle+0x29/0x50
dequeue_task+0xfb/0x300
sched_change_begin+0x1ff/0x240
migrate_disable_switch.isra.0+0xf8/0x190
__schedule+0xdd/0x650
schedule_idle+0x22/0x40
BUG: kernel NULL pointer dereference, address: 0000000000000000
#PF: supervisor instruction fetch in kernel mode
RIP: 0010:0x0
Call Trace:
enqueue_task+0x89/0x1d0
sched_change_end+0x18e/0x1d0
migrate_disable_switch.isra.0+0x11e/0x190
__schedule+0xdd/0x650
schedule_idle+0x22/0x40
do_idle+0xb6/0xf0
cpu_startup_entry+0x29/0x30
start_secondary+0x125/0x180
The path is reachable since commit 942b8db96500 ("sched: Fix
migrate_disable_switch() locking") moved migrate_disable_switch() to
the top of __schedule(), where it runs on every schedule out of the
idle loop rather than only on an actual context switch: any
migrate_disable() taken in the idle loop (e.g. from a tracing or BPF
callback) that is still held when the idle task schedules triggers the
pinning path.
Pinning the idle task is meaningless to begin with: it is a per-CPU
task that can never migrate. Skip it. This also keeps
___migrate_enable() unreachable for the idle task, since its cpus_ptr
is never repointed.
The check uses p == rq->idle rather than is_idle_task(), because the
latter also matches idle-injection threads (PF_IDLE), which are
ordinary queueable tasks.
Observed in production on two separate x86-64 machines running the
Ubuntu 7.0.0-28 kernel, both panicking from the idle loop with the
oops above.
Fixes: 650952d3fb38 ("sched: Make __do_set_cpus_allowed() use the sched_change pattern")
Cc: <stable@vger.kernel.org> # v7.0+
Signed-off-by: Krystian Slowik <me@krystianslowik.com>
---
kernel/sched/core.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 9622670..823af64 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -2461,6 +2461,10 @@ static void migrate_disable_switch(struct rq *rq, struct task_struct *p)
if (p->cpus_ptr != &p->cpus_mask)
return;
+ /* The per-CPU idle task never migrates, there is nothing to pin. */
+ if (p == rq->idle)
+ return;
+
scoped_guard (task_rq_lock, p)
do_set_cpus_allowed(p, &ac);
}
--
2.54.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()
2026-08-06 7:17 [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch() Krystian Slowik
@ 2026-08-06 7:22 ` Peter Zijlstra
2026-08-06 9:01 ` Krystian Slowik
0 siblings, 1 reply; 4+ messages in thread
From: Peter Zijlstra @ 2026-08-06 7:22 UTC (permalink / raw)
To: Krystian Slowik
Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, linux-kernel, stable
On Thu, Aug 06, 2026 at 09:17:40AM +0200, Krystian Slowik wrote:
> Since commit 650952d3fb38 ("sched: Make __do_set_cpus_allowed() use the
> sched_change pattern"), do_set_cpus_allowed() dequeues and re-enqueues
> the target task through the sched_change guard whenever it is queued.
>
> The idle task counts as queued: init_idle() sets
> idle->on_rq = TASK_ON_RQ_QUEUED. But the idle sched class implements no
> real dequeue_task() (only the "bad: scheduling from the idle thread!"
> debug stub, which even drops and re-takes the rq lock in the middle of
> the guarded section) and no enqueue_task() at all, so running the guard
> on the idle task jumps through a NULL pointer in sched_change_end():
>
> bad: scheduling from the idle thread!
> CPU: 3 UID: 0 PID: 0 Comm: swapper/3 Kdump: loaded Not tainted 7.0.0-28-generic #28-Ubuntu PREEMPT(lazy)
> Call Trace:
> dequeue_task_idle+0x29/0x50
> dequeue_task+0xfb/0x300
> sched_change_begin+0x1ff/0x240
> migrate_disable_switch.isra.0+0xf8/0x190
> __schedule+0xdd/0x650
> schedule_idle+0x22/0x40
> BUG: kernel NULL pointer dereference, address: 0000000000000000
> #PF: supervisor instruction fetch in kernel mode
> RIP: 0010:0x0
> Call Trace:
> enqueue_task+0x89/0x1d0
> sched_change_end+0x18e/0x1d0
> migrate_disable_switch.isra.0+0x11e/0x190
> __schedule+0xdd/0x650
> schedule_idle+0x22/0x40
> do_idle+0xb6/0xf0
> cpu_startup_entry+0x29/0x30
> start_secondary+0x125/0x180
>
> The path is reachable since commit 942b8db96500 ("sched: Fix
> migrate_disable_switch() locking") moved migrate_disable_switch() to
> the top of __schedule(), where it runs on every schedule out of the
> idle loop rather than only on an actual context switch: any
> migrate_disable() taken in the idle loop (e.g. from a tracing or BPF
> callback) that is still held when the idle task schedules triggers the
> pinning path.
>
> Pinning the idle task is meaningless to begin with: it is a per-CPU
> task that can never migrate. Skip it. This also keeps
> ___migrate_enable() unreachable for the idle task, since its cpus_ptr
> is never repointed.
>
> The check uses p == rq->idle rather than is_idle_task(), because the
> latter also matches idle-injection threads (PF_IDLE), which are
> ordinary queueable tasks.
>
> Observed in production on two separate x86-64 machines running the
> Ubuntu 7.0.0-28 kernel, both panicking from the idle loop with the
> oops above.
What is actually doing migrate_disable() here? Why would the idle thread
ever hit this...
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()
2026-08-06 7:22 ` Peter Zijlstra
@ 2026-08-06 9:01 ` Krystian Slowik
2026-08-06 14:13 ` Peter Zijlstra
0 siblings, 1 reply; 4+ messages in thread
From: Krystian Slowik @ 2026-08-06 9:01 UTC (permalink / raw)
To: Peter Zijlstra
Cc: Krystian Slowik, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, linux-kernel, stable
[Resend as plain text, the lists rejected my first attempt for HTML.]
> What is actually doing migrate_disable() here? Why would the idle thread
> ever hit this...
Nothing, as far as I can tell. I went back into the vmcore and the
counter didn't come from migrate_disable() at all:
swapper/3->migration_disabled == 8 /* 0b1000 */
cpu3 rq->nr_pinned == 0
the other 15 idle tasks: 0 and 0
That pair can't come from the API. Surrounding bytes are clean, the
whole anomaly is one set bit:
swapper/2 +2416: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
swapper/3 +2416: 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00
migration_pending ^^ migration_disabled
A second dump from the same box two days later (rcu_cblist_dequeue,
unrelated) has all 16 idle tasks at 0.
Correction to my patch description while I'm here: "two separate
machines" was wrong, both idle-loop oopses are the same box.
Fleet context, since it changes how much you should care: three
Ryzen 7 7840HS boxes, no ECC, DDR5 at JEDEC 5600, no EXPO. Same
board, BIOS and microcode, identical apart from DIMM vendor and SSD.
Taint 0 on all three, no out-of-tree modules ever, BPF is systemd
cgroup boilerplate only. Two of the three have produced corruption
oopses over five weeks (css_rstat_flush GPF twice at the same RIP
plus a slab freelist on one, the rcu_cblist fault plus this bit flip
on the other), and they were on different kernel builds at the time.
So I can't separate a scribbler from a platform fault yet. DDR5
on-die ECC makes marginal cells the less likely half, though it's
SEC per burst so not excluded. Memtest next, with low expectations
at 11 events in five weeks.
Which makes this not a fix but an assertion, and the stable Cc
should go. If you want it at all:
if (likely(!p->migration_disabled))
return;
if (WARN_ON_ONCE(p == rq->idle))
return;
behind the existing early return, so the hot path doesn't pay for it
and the state screams instead of oopsing under the rq lock.
Placement is a guess though. Anything reaching do_set_cpus_allowed()
with p == rq->idle hits the same missing enqueue_task(), so
sched_change_begin() might be the better home. And if you'd rather
not paper over corruption in the scheduler at all, that's a fine
answer too. The thread already answered the question I actually had.
Thanks,
Krystian
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()
2026-08-06 9:01 ` Krystian Slowik
@ 2026-08-06 14:13 ` Peter Zijlstra
0 siblings, 0 replies; 4+ messages in thread
From: Peter Zijlstra @ 2026-08-06 14:13 UTC (permalink / raw)
To: Krystian Slowik
Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, linux-kernel, stable
On Thu, Aug 06, 2026 at 11:01:32AM +0200, Krystian Slowik wrote:
> [Resend as plain text, the lists rejected my first attempt for HTML.]
>
> > What is actually doing migrate_disable() here? Why would the idle thread
> > ever hit this...
>
> Nothing, as far as I can tell. I went back into the vmcore and the
> counter didn't come from migrate_disable() at all:
>
> swapper/3->migration_disabled == 8 /* 0b1000 */
> cpu3 rq->nr_pinned == 0
> the other 15 idle tasks: 0 and 0
>
> That pair can't come from the API. Surrounding bytes are clean, the
> whole anomaly is one set bit:
>
> swapper/2 +2416: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
> swapper/3 +2416: 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00
> migration_pending ^^ migration_disabled
>
> A second dump from the same box two days later (rcu_cblist_dequeue,
> unrelated) has all 16 idle tasks at 0.
>
> Correction to my patch description while I'm here: "two separate
> machines" was wrong, both idle-loop oopses are the same box.
>
> Fleet context, since it changes how much you should care: three
> Ryzen 7 7840HS boxes, no ECC, DDR5 at JEDEC 5600, no EXPO. Same
> board, BIOS and microcode, identical apart from DIMM vendor and SSD.
> Taint 0 on all three, no out-of-tree modules ever, BPF is systemd
> cgroup boilerplate only. Two of the three have produced corruption
> oopses over five weeks (css_rstat_flush GPF twice at the same RIP
> plus a slab freelist on one, the rcu_cblist fault plus this bit flip
> on the other), and they were on different kernel builds at the time.
> So I can't separate a scribbler from a platform fault yet. DDR5
> on-die ECC makes marginal cells the less likely half, though it's
> SEC per burst so not excluded. Memtest next, with low expectations
> at 11 events in five weeks.
Oof, memory corruption is a pain.
> Which makes this not a fix but an assertion, and the stable Cc
> should go. If you want it at all:
>
> if (likely(!p->migration_disabled))
> return;
>
> if (WARN_ON_ONCE(p == rq->idle))
> return;
>
> behind the existing early return, so the hot path doesn't pay for it
> and the state screams instead of oopsing under the rq lock.
>
> Placement is a guess though. Anything reaching do_set_cpus_allowed()
> with p == rq->idle hits the same missing enqueue_task(), so
> sched_change_begin() might be the better home. And if you'd rather
> not paper over corruption in the scheduler at all, that's a fine
> answer too. The thread already answered the question I actually had.
Well, do_set_cpus_allowed() or any of the other sched_change users. I
think I'm leaning towards no change here, as you found we already get a
'nice' splat if this happens. Additionally, tripping a WARN inside
schedule() is commonly fatal all on its own (printk likes to do a
wakeup, which doesn't really work all that great from inside the
scheduler -- this is being worked on on the prink side).
Thanks!
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-06 14:14 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-06 7:17 [PATCH] sched/core: Don't pin the idle task in migrate_disable_switch() Krystian Slowik
2026-08-06 7:22 ` Peter Zijlstra
2026-08-06 9:01 ` Krystian Slowik
2026-08-06 14:13 ` Peter Zijlstra
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox