Linux cgroups development
 help / color / mirror / Atom feed
* [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series)
@ 2026-08-18 23:13 Jake S
  2026-08-19  5:49 ` K Prateek Nayak
  2026-08-19  7:36 ` Peter Zijlstra
  0 siblings, 2 replies; 4+ messages in thread
From: Jake S @ 2026-08-18 23:13 UTC (permalink / raw)
  To: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
  Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Waiman Long, Tejun Heo,
	linux-kernel, cgroups

Hi,

I hit a divide-by-zero panic in __calc_prop_weight(), reached from
enqueue_hierarchy() inside enqueue_task_fair(). This is the *enqueue*
path, not the task_tick_fair() variant reported in May and addressed by
the se->on_rq guard folded into 85570f10a4c6 -- enqueue_hierarchy() and
dequeue_hierarchy() carry no equivalent check.

The code is from the tip sched/core flat-hierarchy rework; it is not in
Linus' tree. I am running it via a distro kernel (CachyOS) that carries
the series, on 7.2-rc7 and 7.2.0.

I have separated what I verified from what I am guessing. The last link
in the causal chain is unexplained and I am asking about it rather than
asserting it.

=== The oops ===

  Oops: divide error: 0000 [#1] SMP NOPTI
  CPU: 12 UID: 1000 PID: 312907 Comm: bash
  Tainted: G     U   C OE       7.2.0-rc7-2-cachyos-rc #1 PREEMPT(full)
  Hardware name: Dell Inc. XPS 16 DA16260/0RMV2Y, BIOS 1.5.1 04/01/2026
  RIP: 0010:enqueue_task_fair.llvm.6536700009857788019+0x422/0x950
  Code: 0f 84 74 01 00 00 83 bd 68 01 00 00 00 45 0f 4f f4 48 8b 4d 00
        4c 89 e8 48 09 c8 48 c1 e8 20 0f 85 53 fd ff ff 44 89 e8 31 d2
        <f7> f1 41 89 c5 e9 4f fd ff ff 0f 0b e9 1d fe ff ff 4c 89 e6
  RAX: 0000000000000000 RBX: 0000000000000001 RCX: 0000000000000000
  RDX: 0000000000000000 RSI: fffff46fbf98e680 RDI: fffff46fbf98ffc0
  RBP: fffff46fbf98ffc0 R08: ffff8ee25f9b2a80 R09: 0000000000000000
  R10: 0000000000000000 R11: 0000000000000110 R12: 0000000000000001
  R13: 0000000000000000 R14: 0000000000000001 R15: fffff46fbf9901c0
  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
   asm_fred_entrypoint_user+0x41/0x41
   </TASK>

Machine was idle, lid closed, 11.66 h into the boot. bash forked, the
new task was enqueued, div trapped.

It is not survivable in practice. panic_on_oops was 0, so the kernel
took the first #DE, printed the oops and continued for 476 ms. It then
faulted at the same RIP with byte-identical registers and an identical
RSP (ffffd46fff53bbb0):

  Kernel panic - not syncing: Fatal exception
  Shutting down cpus with NMI

i.e. the oops-recovery path (kill task -> schedule()) re-entered the
same enqueue with the rq lock already held mid-enqueue.

=== Where it divides (confirmed) ===

kernel/sched/fair.c, __calc_prop_weight(), inlined into
enqueue_hierarchy() -> enqueue_task_fair():

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

RCX = cfs_rq->load.weight = 0. R13 = 0 means se->load.weight was 0 as
well, i.e. a group sched_entity carrying zero weight.

Not a miscompile: this is clang 22.1.8 + ThinLTO, hence the .llvm.<hash>
suffix. The 32-bit "div %ecx" against 64-bit C operands is clang's
BypassSlowDivision -- the preceding "or %rcx,%rax; shr $32,%rax; jne"
is its guard. The 64-bit slow path is present in the same function.

=== How the weight can reach zero (mechanism, partly inferred) ===

__calc_smp_shares() ends:

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

clamp() yields hi when hi < lo, so shares_max == 0 silently defeats the
MIN_SHARES floor and returns 0 -- exactly the case the comment directly
above it says must yield MIN_SHARES instead of 0. Note __clamp_once()
already carries

	BUILD_BUG_ON_MSG(statically_true(ulo > uhi), ...)

so lo > hi is considered a bug upstream; it just cannot fire on a
runtime-computed shares_max.

shares_max arrives from calc_concur_shares() as nr * tg_shares, where
nr = min(tg_tasks(tg), tg_cpus(tg)). tg_cpus() returns
cpuset_num_cpus(cgrp) unfloored, while its sibling tg_tasks() already
floors at 1. That asymmetry is the hole.

concur is the live mode here:

  $ cat /sys/kernel/debug/sched/cgroup_mode
  up smp (concur) max tasks

What I could NOT establish: that tg_cpus() actually returned 0, or what
would produce an empty effective cpuset. update_cpumasks_hier()
substitutes the parent's effective_cpus before storing; on this machine
no cgroup has an empty cpuset.cpus.effective and every
cpuset.cpus.partition reads "member". Twelve cgroups here have an empty
cpuset.cpus and all report effective = 0-15. I suspected a power daemon
that rewrites AllowedCPUs on the top-level systemd slices using an
empty-then-set idiom, but I could not make that yield an empty effective
mask, so I am not claiming it.

The missing floor looks like a hole regardless of what trips it, and I
would rather ask than guess: is there a path where cpuset_num_cpus() can
legitimately return 0, or should tg_cpus() simply floor at 1 the way
tg_tasks() does?

=== Proposed guard ===

Running locally on 7.2.0 for the past day. The WARN_ONCE in tg_cpus() is
deliberately diagnostic -- it confirms or refutes the cpuset route the
moment anyone reproduces this.

--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ __calc_prop_weight
+	unsigned long div;
+
 	weight *= se->load.weight;
-	if (parent_entity(se))
-		weight /= cfs_rq->load.weight;
-	else
+	if (parent_entity(se)) {
+		div = cfs_rq->load.weight;
+		if (unlikely(!div)) {
+			WARN_ONCE(1, "sched: cfs_rq->load.weight == 0 (se->load.weight=%lu)\n",
+				  se->load.weight);
+			return MIN_SHARES;
+		}
+		weight /= div;
+	} else {
 		weight /= NICE_0_LOAD;
+	}

 	return max(weight, MIN_SHARES);

@@ __calc_smp_shares
-	return clamp_t(long, shares, MIN_SHARES, shares_max);
+	/* clamp() yields hi when hi < lo, defeating the MIN_SHARES floor. */
+	return clamp_t(long, shares, MIN_SHARES,
+		       max_t(long, shares_max, MIN_SHARES));

@@ tg_cpus
+	if (WARN_ONCE(nr < 1, "sched: tg_cpus() == 0, empty cpuset\n"))
+		nr = 1;
 	return nr;

=== Reproducer / caveats ===

Not reliably reproducible: one occurrence in ~11.7 h of idle uptime, and
none since. I have no better trigger than "leave it running".

The kernel is tainted G U C OE -- out-of-tree camera drivers are loaded
on this machine. I cannot categorically exclude memory corruption from
those. Against that: no prior WARNs, no slab or list corruption, no DMAR
faults, no EDAC events, and the two oopses 476 ms apart had byte-identical
register state, which a wild write would not reproduce exactly. I mention
it so nobody wastes time on a report I cannot fully vouch for.

Happy to test patches or run instrumented builds on the affected machine.

Config: CONFIG_FAIR_GROUP_SCHED=y, CONFIG_SCHED_AUTOGROUP=y,
CONFIG_SCHED_CLASS_EXT=y (sched_ext disabled, not in use),
CONFIG_X86_NATIVE_CPU=y, no CONFIG_SCHED_BORE, no SCHED_ALT.
Hardware: Intel Core Ultra X7 358H (Panther Lake), 16 CPUs.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-08-19  9:50 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18 23:13 [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series) Jake S
2026-08-19  5:49 ` K Prateek Nayak
2026-08-19  7:36 ` Peter Zijlstra
2026-08-19  9:49   ` Guopeng Zhang

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox