All of lore.kernel.org
 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
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ 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] 6+ messages in thread

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

Thread overview: 6+ 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
2026-08-19 13:20 ` [PATCH] sched/fair: floor tg_cpus() at 1 Jake Steinman
2026-08-19 13:22 ` [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series) Jake Steinman

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.