Kernel divide-by-zero panic in vm_get_pg_color via writable CPU topology sysctls (CTLFLAG_RW)
| Field | Value |
|---|---|
| ID | DF-0941 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-369 Divide By Zero |
| File | sys/vm/vm_page.c |
| Lines | 1225-1232 |
| Area | vm |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
vm_get_pg_color() computes cpuscale = PQ_L2_SIZE / phys_ids /
core_ids / ht_ids and then takes (pindex + object_pg_color) %
cpuscale. The CPU topology variables cpu_topology_phys_ids,
cpu_topology_core_ids, and cpu_topology_ht_ids are all exposed as
CTLFLAG_RW sysctls (hw.cpu_topology_phys_ids etc.), so a privileged
user can set them to values that make cpuscale (or an intermediate
divisor) zero, causing an immediate divide-by-zero panic on the next
page allocation. vm_get_pg_color is called from every
vm_page_alloc(), so the panic is near-instantaneous after the sysctl
write.
Root cause
In vm_get_pg_color() (vm_page.c:1195-1232), the HT-aware branch
computes:
physcale = PQ_L2_SIZE / cpu_topology_phys_ids; /* :1225 */
grpscale = physcale / cpu_topology_core_ids; /* :1226 */
cpuscale = grpscale / cpu_topology_ht_ids; /* :1227 */
...
pg_color += (pindex + object_pg_color) % cpuscale; /* :1232 */
All three topology variables are SYSCTL_INT with CTLFLAG_RW in
sys/kern/subr_cpu_topology.c:81-86, writable by root with no
validation. Setting cpu_topology_phys_ids=0 causes division by zero
at :1225. Setting cpu_topology_core_ids to any value >
PQ_L2_SIZE / phys_ids (e.g. 2000 on a typical single-socket system
where physcale=1024) makes grpscale=0, cpuscale=0, and the modulo
at :1232 panics. The guard at vm_numa_organize (vm_page.c:534,
if (cpu_topology_phys_ids <= 1) return) does not protect
vm_get_pg_color, which only guards on
cpu_topology_ht_ids != 0.
Threat model & preconditions
- Attacker position: Root (or a process delegated
PRIV_SYSCTL_WRITE). - Privileges gained or impact: Local denial of service β unconditional kernel panic with no recovery path. The sysctl node presents as informational ("# of physical packages") but is writable with no bounds checking, making this a non-obvious DoS vector for a compromised privileged service.
- Required config or capabilities:
PRIV_SYSCTL_WRITE. - Reachability:
sysctl hw.cpu_topology_phys_ids=0β the very next page allocation (within milliseconds on any active system) callsvm_page_alloc β vm_get_pg_colorand triggers a divide-by-zero trap.
Proof of concept
PoC source: findings/poc/DF-0941/poc.sh
Build & run
# As root on a DragonFlyBSD system with hyperthreading: sysctl hw.cpu_topology_phys_ids=0 # Or on a 1-socket system: sysctl hw.cpu_topology_core_ids=2000
Either command returns immediately, but the very next page allocation triggers a divide-by-zero trap.
Expected output
Fatal trap 0: divide error while in kernel mode ... vm_get_pg_color(...) at vm_get_pg_color+0x... vm_page_alloc(...) at vm_page_alloc+0x... ...
Impact
Kernel panic (system-wide DoS) from a single sysctl write by root. No privilege escalation or info leak.
Recommended fix
Two-part fix: (1) make the topology sysctls read-only to prevent
runtime modification, and (2) add defensive guards in
vm_get_pg_color to prevent division by zero even if the values are
somehow wrong.
--- a/sys/kern/subr_cpu_topology.c
+++ b/sys/kern/subr_cpu_topology.c
@@ -78,11 +78,11 @@
SYSCTL_INT(_hw, OID_AUTO, cpu_topology_ht_ids, CTLFLAG_RD,
&cpu_topology_ht_ids, 0, "# of logical cores per real core");
SYSCTL_INT(_hw, OID_AUTO, cpu_topology_core_ids, CTLFLAG_RD,
&cpu_topology_core_ids, 0, "# of real cores per package");
SYSCTL_INT(_hw, OID_AUTO, cpu_topology_phys_ids, CTLFLAG_RD,
&cpu_topology_phys_ids, 0, "# of physical packages");
--- a/sys/vm/vm_page.c
+++ b/sys/vm/vm_page.c
@@ -1192,6 +1192,14 @@
object_pg_color = object ? object->pg_color : 0;
+ /*
+ * Guard against zero or pathological topology values that would
+ * cause a divide-by-zero. Fall back to the simple distribution.
+ */
+ if (cpu_topology_phys_ids <= 0 || cpu_topology_core_ids <= 0 ||
+ cpu_topology_ht_ids <= 0)
+ goto simple;
+
if (cpu_topology_ht_ids) {
int phys_id;
int core_id;
@@ -1225,6 +1233,13 @@
physcale = PQ_L2_SIZE / cpu_topology_phys_ids;
grpscale = physcale / cpu_topology_core_ids;
cpuscale = grpscale / cpu_topology_ht_ids;
+
+ /*
+ * If any scale collapsed to zero the topology is too wide
+ * for PQ_L2_SIZE; fall back to simple distribution.
+ */
+ if (cpuscale == 0)
+ goto simple;
References
sys/kern/subr_cpu_topology.c:81-86βCTLFLAG_RWsysctl declarations.sys/vm/vm_page.c:534βvm_numa_organizeguard (does not protectvm_get_pg_color).
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0941 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.sh | trigger-source | minimal trigger: sysctl hw.cpu_topology_phys_ids=0 as root | 622 B | view raw |
| run.sh | repro-script | run wrapper for the trigger | 641 B | view raw |
| build.sh | build-script | no-op build (PoC is pure shell) | 307 B | view raw |
| panic.txt | panic-signature | vm_get_pg_color+0x76 idivl divide-by-zero on baseline #0 | 443 B | view raw |
| env.txt | environment | uname, cc version, topology sysctl values | 278 B | view raw |
| fix.diff | suggested-fix | two-part fix: CTLFLAG_RD sysctls + vm_get_pg_color zero guards | 1.7 KB | view raw |
| fix_build.log | build-log | full nativekernel build output of the single-fix kernel (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel PoC re-run: sysctl read-only, no panic | 881 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, privilege analysis, fix, validation | 5.8 KB | β raw |
| README.md | readme | human-facing build/run/expected | 2.8 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0941 β Kernel divide-by-zero panic in vm_get_pg_color via writable CPU-topology sysctls
Summary
vm_get_pg_color() divides PQ_L2_SIZE (1024) by the CPU-topology counts
cpu_topology_phys_ids, cpu_topology_core_ids, and cpu_topology_ht_ids
(sys/vm/vm_page.c:1225-1227). Those three variables are exposed as
CTLFLAG_RW sysctls (sys/kern/subr_cpu_topology.c:81-86) writable by root
with no validation. Setting any to zero β or inflating one enough to
collapse the derived cpuscale to zero β triggers an unconditional
divide-by-zero panic on the next vm_page_alloc(). Because the write requires
PRIV_SYSCTL_WRITE, this is a rootβkernel DoS (Low severity); an
unprivileged user gets Operation not permitted.
Build
The PoC is a pure shell script β no compilation required:
./build.sh # no-op (just confirms poc.sh is present)
Run
Run as root on the guest (the sysctl requires PRIV_SYSCTL_WRITE):
./run.sh # equivalent to: sysctl hw.cpu_topology_phys_ids=0
Expected
On the UNPATCHED kernel (#0)
The sysctl write returns, then the next page allocation traps:
Stopped at vm_get_pg_color+0x76: idivl 0x61ef6c(%rip),%eax db>
The guest is down (DDB debugger). This is the divide at
sys/vm/vm_page.c:1225 (PQ_L2_SIZE / cpu_topology_phys_ids with
phys_ids=0).
On the FIXED kernel (#1)
The sysctl is now read-only:
sysctl: oid 'hw.cpu_topology_phys_ids' is read only
No panic; the guest stays up.
Variants
sysctl hw.cpu_topology_phys_ids=0β div0 atvm_page.c:1225sysctl hw.cpu_topology_core_ids=0β div0 atvm_page.c:1226sysctl hw.cpu_topology_core_ids=2000(1-socket guest) βgrpscale=0,cpuscale=0β div0 at the modulovm_page.c:1232
Files
| file | purpose |
|---|---|
poc.sh |
minimal trigger (root sysctl write) |
run.sh |
run wrapper |
build.sh |
no-op build wrapper |
panic.txt |
divide-by-zero signature from the baseline panic |
env.txt |
guest uname / cc / topology values |
fix.diff |
two-part fix: CTLFLAG_RD sysctls + vm_get_pg_color guards |
fix_build.log |
full single-fix kernel build output |
fix_run.log |
patched-kernel PoC re-run (read-only, no panic) |
VERDICT.md |
full analysis: mechanism, privilege, fix, validation |
manifest.json |
machine-readable catalog |
DF-0941 β Verdict
Verdict: REPRODUCED β FIX VALIDATED
Finding: Kernel divide-by-zero panic in vm_get_pg_color() via writable CPU-topology sysctls.
Severity: Low (root-only DoS; no privilege boundary crossed).
Status after fix: FIXED β panic eliminated, sysctl made read-only, defensive guard added.
Mechanism (confirmed by source trace + live panic)
vm_get_pg_color() (sys/vm/vm_page.c:1195-1263) is called from every
vm_page_alloc(). Its HT-aware branch computes page-coloring scales by
dividing the topology counts into PQ_L2_SIZE (1024):
physcale = PQ_L2_SIZE / cpu_topology_phys_ids; /* vm_page.c:1225 */
grpscale = physcale / cpu_topology_core_ids; /* vm_page.c:1226 */
cpuscale = grpscale / cpu_topology_ht_ids; /* vm_page.c:1227 */
...
pg_color += (pindex + object_pg_color) % cpuscale;/* vm_page.c:1232 */
The three topology variables are exposed as SYSCTL_INT(..., CTLFLAG_RW, ...)
in sys/kern/subr_cpu_topology.c:81-86 β writable by root with no
validation. The branch guard at :1195 is only if (cpu_topology_ht_ids),
which does NOT protect against phys_ids==0 or core_ids==0 (those divide-by-
zeros happen inside the already-entered branch). The vm_numa_organize()
guard at vm_page.c:534 (if (cpu_topology_phys_ids <= 1 || core_ids == 0)
return;) does not cover vm_get_pg_color, which only checks ht_ids.
Trigger: sysctl hw.cpu_topology_phys_ids=0 (as root). The sysctl write
itself returns cleanly, but the very next page allocation (milliseconds later
on any active system) executes idivl with a zero divisor and traps.
Live reproduction (unpatched #0 baseline)
On DragonFly 6.5-DEVELOPMENT #0 (build Thu Jul 2 06:02:54 UTC 2026):
# as root: sysctl hw.cpu_topology_phys_ids=0
Result: kernel drops into DDB with a divide-error trap (captured in
dfbsd-qemu/boot.log):
Stopped at vm_get_pg_color+0x76: idivl 0x61ef6c(%rip),%eax db>
The idivl at vm_get_pg_color+0x76 is exactly the integer divide at
vm_page.c:1225 (PQ_L2_SIZE / cpu_topology_phys_ids with phys_ids=0).
Guest is down after the trap. See panic.txt.
Privilege analysis (no escalation)
The sysctl requires PRIV_SYSCTL_WRITE, i.e. root. Confirmed as
unprivileged user maxx (uid 1001, not in wheel):
$ sysctl hw.cpu_topology_phys_ids=0 sysctl: hw.cpu_topology_phys_ids=0: Operation not permitted
Per the audit's bright-line rule, a write reachable only from an already-root
context has no privilege boundary to cross (rootβkernel is game-over by
definition). There is therefore no escalation chain β this is a
rootβkernel hardening gap (a compromised privileged service, a buggy
monitoring script, or a malicious admin can panic the kernel via an
unvalidated sysctl that presents as informational topology data). Severity Low
(CVSS AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H) is correct. No memory corruption
is involved (pure integer divide-by-zero trap), so Phase 6 escalation does not
apply.
The other two topology counts are equally unguarded:
- sysctl hw.cpu_topology_core_ids=0 β div0 at :1226.
- sysctl hw.cpu_topology_core_ids=2000 (on this 1-socket/6-core guest,
physcale=1024, so grpscale=1024/2000=0, cpuscale=0) β div0 at the
modulo :1232.
Fix (authored in fix.diff, matches finding proposal, hardened)
Two-part defense-in-depth fix:
-
sys/kern/subr_cpu_topology.c:81-86β change all three topology sysctls fromCTLFLAG_RWtoCTLFLAG_RD. These variables are only ever assigned once, during boot topology detection (subr_cpu_topology.c:605-618); there is no legitimate runtime writer. Making them read-only closes the user-facing attack surface completely. (Verified:rgfinds no runtime assignment outside boot detection.) -
sys/vm/vm_page.c:1195β extend the branch guard fromif (cpu_topology_ht_ids)to also requirecore_ids > 0andphys_ids > 0, so any zero/negative topology falls back to the simple even-distribution branch.sys/vm/vm_page.c:1228β addif (cpuscale == 0) goto simple;after the scale computation so a "too-wide" topology (wherePQ_L2_SIZE / phys / core / htcollapses to 0) also falls back instead of dividing by zero in the modulo. Asimple:label is added at the top of theelseblock (with a null;statement to satisfy C's label-must-precede-a-statement rule).
This supersedes the finding markdown's recommended fix by also adding the
cpuscale == 0 collapse guard (the finding's proposal only guarded the
zero-operand case, not the modulo-after-collapse case where e.g.
core_ids=2000 makes cpuscale zero without any single operand being zero).
Fix validation (Phase 8 β built + booted single-fix kernel)
- Baseline (#0, unpatched):
sysctl hw.cpu_topology_phys_ids=0succeeds β divide-by-zero panicvm_get_pg_color+0x76: idivlβ guest down. (before) - Patched (#1, build
Tue Jul 14 14:18:23 UTC 2026, sha2561dbfa610...): appliedfix.diffto clean/usr/src, built withmake -j6 nativekernel KERNCONF=X86_64_GENERIC(rc=0), overwrote/boot/kernel/kernelwithkernel.stripped, rebooted into #1. Re-ran the PoC:sysctl: oid 'hw.cpu_topology_phys_ids' is read only(rc=1), no panic, guest stays up. (after)
Before/after contrast saved in fix_run.log; full kernel build log in
fix_build.log.
fix_status: fixed β bad behavior (panic) is gone on the patched kernel AND present on the unpatched baseline.
PoC changes
The provided poc.sh was correct as written (no compile, root sysctl write).
No source changes were needed for reproduction. Added build.sh/run.sh
repro wrappers and this VERDICT.md; authored fix.diff (corrected from the
finding's proposal to also cover the modulo-collapse case and to add the
required null statement after the C label so the kernel compiles).
Fix verification
fixedVALIDATED: baseline panics idivl at vm_get_pg_color; patched sysctl read-only, no panic.
BEFORE: idivl guest DOWN. AFTER: read-only rc=1, guest UP.
Confirmed kernel references
Detail
Exploit chain
none -- pure divide-by-zero, root-only sysctl write. No privilege boundary to cross.
Evidence (decisive lines)
BEFORE: sysctl phys_ids=0 succeeds -> Stopped at vm_get_pg_color+0x76 idivl, guest DOWN. AFTER: 'is read only' rc=1, no panic.
PoC changes
Authored: poc.sh, fix.diff (CTLFLAG_RW->RD + vm_get_pg_color guards incl cpuscale-collapse), VERDICT.md, manifest.json.
Verified recommended fix
(1) Change 3 topology sysctls to CTLFLAG_RD at subr_cpu_topology.c:81-86; (2) guard ht_ids>0&&core_ids>0&&phys_ids>0 + cpuscale==0 goto simple at vm_page.c:1195/1228. Supersedes finding proposal. Full diff in findings/poc/DF-0941/fix.diff.
Verdict
REPRODUCED. sysctl hw.cpu_topology_phys_ids=0 (root, SYSCTL_INT CTLFLAG_RW no validation) -> vm_get_pg_color idivl by phys_ids -> #DE trap. vm_page.c:1225 PQ_L2_SIZE/phys_ids. maxx gets EPERM (PRIV_SYSCTL_WRITE).
No comments yet.