OOB write into cpu_topology_nodes[MAXCPU] during boot topology construction on high-CPU-count systems
| Field | Value |
|---|---|
| ID | DF-0083 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/kern/subr_cpu_topology.c |
| Lines | 115-154 |
| Area | kern (CPU topology / boot) |
| Confidence | likely |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
build_topology_tree() unconditionally increments a global cpu_node_t
*last_free_node cursor through the fixed-size static array
cpu_topology_nodes[MAXCPU] (MAXCPU = 256 on amd64, param.h:71-72). The
cursor starts at root+1 (:185) and is bumped once per allocated node at
:140-141:
node->child_node[i] = *last_free_node;
(*last_free_node)++; /* sys/kern/subr_cpu_topology.c:140-141 */
There is no upper-bound check against &cpu_topology_nodes[MAXCPU] anywhere.
The tree needs 1 + chips + chipsΓcores + N nodes (root + package + core +
thread levels). For a single-socket SMT system, total = 2 + 3Γcores. For
N = 256 logical CPUs (128 cores Γ 2 threads) the tree needs 386 nodes,
exceeding the 256-slot array by 130 cpu_node_t entries (~270 KB of BSS). The
overflow threshold is ~170 logical CPUs (85 cores Γ 2 threads β 257 nodes).
Reachability: boot-time SYSINIT (SI_BOOT2_CPU_TOPOLOGY, :823-824).
The topology parameters come from CPUID on each CPU β firmware/hypervisor
controlled. A malicious hypervisor presenting β₯170 vCPUs in a single package
triggers it deterministically every boot; bare-metal modern many-core silicon
(AMD EPYC-class) with β₯256 threads likewise.
Impact: large kernel BSS corruption at boot β the node-struct writes at
:125-132,143,152 stomp adjacent BSS with topology pointers. Boot crash or
potentially exploitable corruption. Not runtime-exploitable by a local user.
Recommended fix
Bound the cursor:
--- a/sys/kern/subr_cpu_topology.c
+++ b/sys/kern/subr_cpu_topology.c
@@ for (i = 0; i < node->child_no; i++) {
+ if (*last_free_node >= &cpu_topology_nodes[MAXCPU])
+ panic("cpu_topology_nodes overflow");
node->child_node[i] = *last_free_node;
(*last_free_node)++;
Root cause: cpu_topology_nodes[] is sized for MAXCPU leaves but needs
interior nodes too. Either size the array to 2*MAXCPU + LEVEL_NO + 1 (covers
the worst-case single-socket C + CK β€ 2N), or fall back to a flat topology
when the projected node count would exceed the array.
Timeline
- 2026-06-30 Discovered during automated file-by-file audit of
sys/kern/subr_cpu_topology.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0083 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| topo_oob.c | trigger-source | faithful userspace replica of build_topology_tree cursor proving the unbounded OOB | 5.0 KB | view raw |
| build.sh | build-script | cc -O2 -o topo_oob topo_oob.c | 110 B | view raw |
| run.sh | run-script | ./topo_oob | 198 B | view raw |
| build.log | build-log | harness compile | 0 B | β download |
| run.log | run-log | overflow table: threshold N=170, 130 OOB writes for N=256 (~268KB BSS) | 1.2 KB | view raw |
| env.txt | environment | uname + guest topology (6 vCPU, dormant) | 250 B | view raw |
| VERDICT.md | verdict | full analysis: boot-time BSS corruption, firmware-controlled, not local-user exploitable | 4.1 KB | β raw |
| README.md | readme | reproduce instructions | 1.2 KB | β raw |
| fix.diff | suggested-fix | bound cursor in build_topology_tree; graceful truncation on overflow | 758 B | view raw |
| fix_build.log | build-log | fix compiles+links (part of a combined kernel build, rc=0); runtime not_testable (needs >=170 CPUs) | 5.6 MB | β download |
DF-0083 β OOB write into cpu_topology_nodes[MAXCPU] at boot
| Verdict | REPRODUCED (harness/code-level) β boot-time, high-CPU-only |
| Impact | boot-time BSS corruption on >=170-CPU systems (not runtime on this 6-vCPU guest) |
| File | sys/kern/subr_cpu_topology.c:140-141 (array :63, SYSINIT :823) |
Build
cc -O2 -o topo_oob topo_oob.c
Run
./topo_oob
Expected (bug present, proven by harness)
170 1pkg SMT2 257 OVERFLOW 1 256 1pkg SMT2 386 OVERFLOW 130 RESULT: OOB_CONFIRMED
The unbounded cursor at subr_cpu_topology.c:140-141 overruns
cpu_topology_nodes[MAXCPU=256] for topologies needing >256 nodes (threshold
N=170 logical CPUs). On N=256 that is 130 OOB writes (~268 KB of BSS).
Why not a live runtime test
The trigger is the boot-time CPU topology (CPUID, firmware/hypervisor
controlled). This guest has 6 vCPUs (needs ~14 nodes), so the bug is dormant
here. The harness faithfully reproduces the cursor arithmetic and proves the
OOB deterministically. Not a local-user syscall bug β no uid=0 chain applies.
Fix
fix.diff bounds the cursor with graceful topology truncation (instead of the
finding's panic). See VERDICT.md.
DF-0083 β OOB write into cpu_topology_nodes[MAXCPU] at boot
Verdict: REPRODUCED at the harness / code level (boot-time, high-CPU-only)
Mechanism
build_topology_tree() (sys/kern/subr_cpu_topology.c:115-154) bumps a global
cursor *last_free_node once per allocated child node with no upper-bound
check against the fixed static array cpu_topology_nodes[MAXCPU]
(:63, MAXCPU=256):
/* subr_cpu_topology.c:139-141 */
for (i = 0; i < node->child_no; i++) {
node->child_node[i] = *last_free_node;
(*last_free_node)++; /* <-- unbounded */
The cursor starts at root+1 (:185). A topology tree needs
1 + packages + packages*cores + N nodes (root + package + core + leaf
levels). For a single-package SMT2 system that is 2 + 3*cores = 2 + 3N/2,
exceeding 256 at N >= 170 logical CPUs (386 nodes for N=256). Each node is
2112 bytes (sizeof(cpu_node_t), incl. child_node[MAXCPU]), so N=256
overruns by 130 nodes (~268 KB of kernel BSS).
Reachability: boot-time SYSINIT(cpu_topology, SI_BOOT2_CPU_TOPOLOGY, ...)
(:823) β build_cpu_topology() (:819) β build_topology_tree(). The
parameters come from CPUID on each CPU β firmware/hypervisor controlled. A
malicious hypervisor presenting >=170 vCPUs in one package, or bare-metal
many-core silicon (>=256 threads), triggers it deterministically every boot.
Writes at :125-132,143,152 then stomp adjacent BSS with topology pointers.
Proof (harness)
This guest has only 6 vCPUs, so the bug cannot fire at runtime here (the
guest topology needs just 11-14 nodes). A faithful userspace replica of
build_topology_tree's cursor logic (same struct layout, same increment β see
topo_oob.c) deterministically reports the overflow:
N(cpu) topology nodes_needed vs MAXCPU(256) OOB_writes 6 1pkg SMT2 11 ok 0 169 1pkg SMT2 255 ok 0 170 1pkg SMT2 257 OVERFLOW 1 256 1pkg SMT2 386 OVERFLOW 130 512 1pkg SMT2 770 OVERFLOW 514 Threshold: nodes_needed > 256 first at N=170 (2 + 3*85 = 257). For N=256: 386 nodes -> 130 OOB writes (~268 KB BSS corruption at boot). This guest: hw.ncpu=6 -> tree needs 14 nodes (no OOB); dormant here. RESULT: OOB_CONFIRMED
This matches the finding's threshold (>=170) exactly.
Impact / realism & escalation assessment
- Not runtime-exploitable by an unprivileged local user. The trigger is
the boot-time CPU topology (CPUID, firmware/hypervisor controlled), not any
syscall surface. There is no privilege boundary for a local user to cross
here, so no
uid=0chain applies (this is a valid hard-blocker per the "root-only/firmware-only reachability" category β the attacker is the hypervisor/firmware, not a local process). - Realistic impact: on a >=170-CPU system the kernel corrupts its own BSS at
boot β boot crash or potentially exploitable corruption of adjacent static
data (the overrun is into whatever follows
cpu_topology_nodesin BSS). Medium severity because it needs unusual hardware/a malicious hypervisor. - On THIS guest (6 vCPUs) it is dormant β no runtime effect, hence
guest_dirty=false.
Fix
fix.diff: bound the cursor in build_topology_tree β when
*last_free_node >= &cpu_topology_nodes[MAXCPU], print a warning and truncate
the topology (node->child_no = i; return;) instead of overflowing. This is
safer than the finding's suggested panic() (a boot panic on a legitimate
high-CPU box would be worse than a partial topology) and more robust than
enlarging the array to a guessed worst-case (the true worst case on multi-level
topologies can exceed 2*MAXCPU, so a fixed bound check is the correct fix).
Supersedes the finding's proposal (graceful truncation instead of panic).
Reproduce
cc -O2 -o topo_oob topo_oob.c # build.sh ./topo_oob # run.sh β prints the overflow table
(The harness proves the OOB cursor primitive; runtime firing needs >=170 CPUs, absent on this 6-vCPU guest.)
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (harness). build_topology_tree cursor no bounds vs cpu_topology_nodes[256] -> BSS OOB at >=170 CPUs. Boot-time. 6 vCPUs on guest.
No comments yet.