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

* Re: [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series)
  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
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: K Prateek Nayak @ 2026-08-19  5:49 UTC (permalink / raw)
  To: Jake S, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
  Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
	Valentin Schneider, Waiman Long, Tejun Heo, linux-kernel, cgroups

Hello Jake,

Thank you for the report.

On 8/19/2026 4:43 AM, Jake S wrote:
> [You don't often get email from j@metarealtyinc.ca. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ]
> 
> 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.

Was your laptop suspended at that point?

> 
> 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?

Since you mentioned idle + lid closed, and
pm_sleep_disable_secondary_cpus() -> freeze_secondary_cpus() on the
suspend path, I'm wondering if that path can have any effect here but
afaict, sc->effective_cpus should be unaffected on that path and I'm
not sure if we can even get a fork() + wakeup before we thaw all the
process.

That said I do see a bunch of cpumask_empty(cs->effective_cpus) in
kernel/cgroup/cpuset.c so I'm not sure if that might be at play here.
I'll defer to folks who understand cpusets better.

> 
> === 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.

I think your current instrumentation from "Proposed guard" is good
enough to tell where the problem is if it reproduces. Are you running
with it on your setup currently?

> 
> 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

-- 
Thanks and Regards,
Prateek


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

* Re: [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series)
  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
  3 siblings, 1 reply; 6+ messages in thread
From: Peter Zijlstra @ 2026-08-19  7:36 UTC (permalink / raw)
  To: Jake S, Waiman Long
  Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
	Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
	K Prateek Nayak, Waiman Long, Tejun Heo, linux-kernel, cgroups

On Tue, Aug 18, 2026 at 07:13:31PM -0400, Jake S wrote:
> 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.

Durr, 0 weight not good, in any scheme. Much of the code strives to
never let it get below 2 or so.

> 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

Moo..

> 
> 	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?

tg_cpus() should probably floor at 1, just to be both symmetric and
avoid this. But I too was under the impression a cpuset would never have
an empty set.

However, now that I think about it, IIRC there is a cpu hotplug (and
suspect I suppose) case where cpuset-v2 allows a cpuset to become empty
like this, *however* it would then take the parent cgroup until it would
find one that is non empty.

Now, cpuset_num_cpus() uses RCU, so perhaps there is a race somewhere.
Waiman, you know this cpuset stuff beter than me, did I get it wrong?

Anyway, if you send a patch adding the floow to tg_cpus(), I'll apply
that just on symmetry grounds. If Waiman spots a fail with the cpuset
bits we can fix that too.

Thanks for the excellent report!

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

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



在 2026/8/19 15:36, Peter Zijlstra 写道:
> On Tue, Aug 18, 2026 at 07:13:31PM -0400, Jake S wrote:
>> 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.
> 
> Durr, 0 weight not good, in any scheme. Much of the code strives to
> never let it get below 2 or so.
> 
>> 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
> 
> Moo..
> 
>>
>> 	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?
> 
> tg_cpus() should probably floor at 1, just to be both symmetric and
> avoid this. But I too was under the impression a cpuset would never have
> an empty set.
> 
> However, now that I think about it, IIRC there is a cpu hotplug (and
> suspect I suppose) case where cpuset-v2 allows a cpuset to become empty
> like this, *however* it would then take the parent cgroup until it would
> find one that is non empty.
> 

This reminded me of a CPU hotplug / suspend issue I ran into recently.

I had a case where the last online HK_TYPE_DOMAIN CPU could be offlined
during regular hotplug because the check in _cpu_down() did not exclude
the outgoing CPU. That left scheduler-domain rebuilds with an empty span
and eventually caused a crash:

https://lore.kernel.org/all/20260811121307.168471-1-guopeng.zhang@linux.dev/

While testing this with domain isolation, I also saw suspend/freeze reach
a state with no active HK_TYPE_DOMAIN CPUs. I later sent an RFC for
handling that case in the scheduler/cpuset paths:

https://lore.kernel.org/all/20260722115238.351821-1-guopeng.zhang@linux.dev/

The reproducer used:

    isolcpus=domain,0,3-31

I have not checked whether this can make cpuset_num_cpus() return 0 in
the path Jake hit, so this may well be unrelated. I just thought it was
worth mentioning since you brought up the hotplug/suspend case.

Thanks,
Guopeng

> Now, cpuset_num_cpus() uses RCU, so perhaps there is a race somewhere.
> Waiman, you know this cpuset stuff beter than me, did I get it wrong?
> 
> Anyway, if you send a patch adding the floow to tg_cpus(), I'll apply
> that just on symmetry grounds. If Waiman spots a fail with the cpuset
> bits we can fix that too.
> 
> Thanks for the excellent report!
> 


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

* [PATCH] sched/fair: floor tg_cpus() at 1
  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 13:20 ` 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
  3 siblings, 0 replies; 6+ messages in thread
From: Jake Steinman @ 2026-08-19 13:20 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot
  Cc: Jake Steinman, Dietmar Eggemann, Steven Rostedt, Ben Segall,
	Mel Gorman, Valentin Schneider, K Prateek Nayak, Waiman Long,
	Tejun Heo, Guopeng Zhang, linux-kernel, cgroups

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.

Link: https://lore.kernel.org/all/20260818231333.1441757-1-j@metarealtyinc.ca/
Signed-off-by: Jake Steinman <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
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -4895,7 +4895,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)
--
2.55.0

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

* Re: [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series)
  2026-08-18 23:13 [BUG] sched/fair: divide error in __calc_prop_weight() from the enqueue path (flat-hierarchy series) Jake S
                   ` (2 preceding siblings ...)
  2026-08-19 13:20 ` [PATCH] sched/fair: floor tg_cpus() at 1 Jake Steinman
@ 2026-08-19 13:22 ` Jake Steinman
  3 siblings, 0 replies; 6+ messages in thread
From: Jake Steinman @ 2026-08-19 13:22 UTC (permalink / raw)
  To: K Prateek Nayak
  Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
	Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
	Valentin Schneider, Waiman Long, Tejun Heo, Guopeng Zhang,
	linux-kernel, cgroups

Hi Prateek,

> Was your laptop suspended at that point?

No -- awake, but it had been through a suspend/resume cycle earlier in
the same boot. From the journal of the boot that died:

  15:32:43  Lid closed
  15:32:44  PM: suspend entry (s2idle)
  16:25:55  Lid opened
  16:25:55  PM: suspend exit
  16:42:54  Lid closed
  20:17:49  Lid opened
  20:31:59  Lid closed
  21:10:16  <last log line -- crash>

So the s2idle cycle ended 4h44m before the fault. At 21:10 the lid had
been shut for ~38 minutes, but the machine was on AC with
LidAction=TurnOffScreen, so it was awake with the display off, not
suspended -- the wallpaper daemon was still rendering once a minute
right up to the final log line, and the fork that tripped the divide
happened then.

Also relevant to the freeze_secondary_cpus() angle: that boot logged
zero CPU hotplug events. No smpboot lines, no "Disabling non-boot CPUs".
Caveat: s2idle's freeze path may not log at my loglevel, so I would not
read that as proof it never happened -- only that nothing surfaced.

> I think your current instrumentation from "Proposed guard" is good
> enough to tell where the problem is if it reproduces. Are you running
> with it on your setup currently?

Yes. All three hunks have been in the running kernel since 2026-08-18,
now on 7.2.0. Current status:

  7.2.0-1, 17.3 h uptime with the guards in
  "sched: tg_cpus() == 0, empty cpuset"        -- 0 hits
  "sched: cfs_rq->load.weight == 0"            -- 0 hits

So nothing has reproduced yet. Given the original took 11.7 h of mostly
idle uptime to hit once and has not recurred in the days since, I read
17 h of silence as "not yet", not as evidence against the cpuset route.
I will report either way -- and if the tg_cpus() WARN is the one that
fires, that settles the mechanism on the spot.

I have sent the tg_cpus() floor as a separate patch per Peter's request.
I deliberately kept it to that one hunk: with tg_cpus() floored and
tg_tasks() already flooring at 1, nr >= 1, so shares_max >= tg_shares
and the inverted clamp in __calc_smp_shares() becomes unreachable. The
other two hunks are belt-and-braces and I did not want to bundle them
into something Peter offered to take on symmetry grounds.

@Guopeng: thanks, the isolcpus=domain reproducer is interesting. I am
not using isolcpus here and have no domain isolation configured, so my
case is not that exact path, but if you want a second machine to test
either of your series on I am happy to run them -- this box reproduces
suspend/resume cycles all day and has 16 CPUs with a P/E/LP-E split,
which may be a useful shape for hotplug edge cases.

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.