The Linux Kernel Mailing List
 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox