[PATCH] sched/core: Don't pin the idle task in migrate_disable_switch()

From: Krystian Slowik

Date: Thu Aug 06 2026 - 04:23:23 EST


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@xxxxxxxxxxxxxxx> # v7.0+
Signed-off-by: Krystian Slowik <me@xxxxxxxxxxxxxxxxxx>
---
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