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

Krystian Slowik <[email protected]>
Newsgroups org.kernel.vger.stable,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
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: <[email protected]> # v7.0+
Signed-off-by: Krystian Slowik <[email protected]>
---
 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.