[tip: sched/urgent] sched/fair: Floor tg_cpus() at 1

tip-bot2 for Jake Steinman posted 1 patch 1 month, 1 week ago
kernel/sched/fair.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
[tip: sched/urgent] sched/fair: Floor tg_cpus() at 1
Posted by tip-bot2 for Jake Steinman 1 month, 1 week ago
The following commit has been merged into the sched/urgent branch of tip:

Commit-ID:     23906f3a1686c737bf356fdd21183b40722b2437
Gitweb:        https://git.kernel.org/tip/23906f3a1686c737bf356fdd21183b40722b2437
Author:        Jake Steinman <j@metarealtyinc.ca>
AuthorDate:    Wed, 19 Aug 2026 09:20:59 -04:00
Committer:     Peter Zijlstra <peterz@infradead.org>
CommitterDate: Thu, 20 Aug 2026 11:01:34 +02:00

sched/fair: Floor tg_cpus() at 1

tg_cpus() returns cpuset_num_cpus() unfloored, while its sibling
tg_tasks() already floors its result at 1. calc_concur_shares() feeds

	nr = min(tg_tasks(tg), tg_cpus(tg))

into __calc_smp_shares() as shares_max, so an nr of 0 makes shares_max 0.
__calc_smp_shares() ends with

	return clamp_t(long, shares, MIN_SHARES, shares_max);

and clamp() yields hi when hi < lo, so a zero shares_max silently defeats
the MIN_SHARES floor and returns 0 -- the exact case the comment above
that line says must return MIN_SHARES instead of 0.

That leaves a group sched_entity with load.weight == 0, and
__calc_prop_weight() then divides by cfs_rq->load.weight:

	weight *= se->load.weight;
	if (parent_entity(se))
		weight /= cfs_rq->load.weight;

which takes a #DE inside enqueue_task_fair():

  Oops: divide error: 0000 [#1] SMP NOPTI
  RIP: 0010:enqueue_task_fair+0x422/0x950
  Call Trace:
   <TASK>
   enqueue_task+0x8e/0x250
   wake_up_new_task+0x148/0x2e0
   kernel_clone+0x1c6/0x390
   __x64_sys_clone+0xcc/0x100
   do_syscall_64+0x147/0x3c0
   </TASK>

This is not survivable in practice: with panic_on_oops=0 the kernel took
the first #DE and continued for 476 ms, then faulted at the same RIP with
identical register state and an identical RSP, because the oops recovery
path (kill task -> schedule()) re-enters the same enqueue while the rq
lock is held mid-enqueue. The second fault escalates to a panic.

Flooring tg_cpus() at 1 makes it symmetric with tg_tasks() and keeps
shares_max >= tg_shares, so the MIN_SHARES floor in __calc_smp_shares()
can no longer be bypassed.

Note this only removes the division hazard. Whether cpuset_num_cpus() can
legitimately return 0 -- via the cpu hotplug/suspend path where a v2
cpuset may transiently become empty, or via an RCU race -- is a separate
question still open on the report thread.

Fixes: 90ac22ffef48 ("sched/fair: Add cgroup_mode: max")
Signed-off-by: Jake Steinman <j@metarealtyinc.ca>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://lore.kernel.org/all/20260818231333.1441757-1-j@metarealtyinc.ca/
Link: https://patch.msgid.link/20260819132104.2148918-1-j@metarealtyinc.ca
---
 kernel/sched/fair.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 0011401..6d881e5 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -4934,7 +4934,12 @@ static int tg_cpus(struct task_group *tg)
 			nr = cpuset_num_cpus(cgrp);
 	}
 
-	return nr;
+	/*
+	 * An empty cpuset would propagate a 0 shares_max into
+	 * __calc_smp_shares(), where clamp() yields hi when hi < lo and so
+	 * defeats the MIN_SHARES floor. Match tg_tasks(), which floors at 1.
+	 */
+	return max(nr, 1);
 }
 
 static inline int tg_tasks(struct task_group *tg)