# DF-2846 VERDICT — reproduced (runtime-proven on QEMU guest, source-traced)

**Status: reproduced. Impact: none (incorrect kernel-exported topology
information; no memory corruption, no privilege boundary). Confidence:
certain.**

## How it was verified

`check.sh` (see run.log) compares the eagerly-rendered topology sysctl
strings against `hw.ncpu` and the lazily-rendered tree on the stock
INVARIANTS guest (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC, 6 vCPUs,
QEMU):

* `hw.ncpu: 6`
* `hw.cpu_topology.members: cpus(0)` — root members; must be `cpus(0-5)`
* `hw.cpu_topology.cpuN.physical_siblings: cpus(0)` for every N — the whole
  guest is one chip, must be `cpus(0-5)`
* `hw.cpu_topology.cpuN.core_siblings: cpus()` (empty) for N=1..5 — each core
  is single-threaded, must be `cpus(N)`
* `sysctl -n hw.cpu_topology.tree` — rendered at READ time — correctly shows
  `PACKAGE MEMBERS: cpus(0-5)`, `CHIP ID 0: cpus(0-5)`, `CORE ID n: cpus(n)`.

The lazily-rendered tree being complete while every boot-time-rendered
string is frozen at "cpu 0 only" is exactly the source-predicted signature of
`ncpus == 1` during `SI_BOOT2_CPU_TOPOLOGY`.

## Why (path:line)

1. `sys/kern/subr_cpu_topology.c:823-824` — `SYSINIT(cpu_topology,
   SI_BOOT2_CPU_TOPOLOGY, SI_ORDER_FIRST, init_cpu_topology)`.
2. `sys/sys/kernel.h:156-158` — `SI_BOOT2_CPU_TOPOLOGY = 0x1a58000` sorts
   BEFORE `SI_BOOT2_START_APS = 0x1a60000`.
3. `sys/platform/pc64/x86_64/machdep.c:2687` — `ncpus = 1` at early boot; it
   is only set to `naps + 1` inside `start_all_aps()`
   (sys/platform/pc64/x86_64/mp_machdep.c:544), which runs from
   `mp_start_aps()` — `SYSINIT(startaps, SI_BOOT2_START_APS, ...)`
   (mp_machdep.c:384).
4. `init_pcpu_topology_sysctl()` (subr_cpu_topology.c:563-610) and the
   `members` rendering (:656-660) call `sbuf_print_cpuset()`, whose
   `CPUSET_FOREACH(i, *mask)` (subr_cpu_topology.c:727) expands to
   `for (i = 0; i < ncpus; i++) if (CPUMASK_TESTBIT(mask, i))`
   (sys/sys/cpu_topology.h:63-65). With `ncpus == 1`, only bit 0 is ever
   tested: cpu 0 prints when present, everything else silently disappears.

The masks themselves are complete — the tree, `get_cpumask_from_level()`,
and all in-kernel consumers are unaffected. Only the pre-rendered strings
lied.

## Second manifestation (source-proven, not guest-testable on Intel QEMU)

`fix_amd_topology()` is invoked from `build_cpu_topology()`
(subr_cpu_topology.c:285) at the same too-early sysinit. Its
`lwkt_cpusync_simple(CPUMASK_ASSALLONES, amd_get_compute_unit_id, NULL)`
(mp_machdep.c:1936-1937) is masked down to the BSP-only
`gd_other_cpus`/`smp_active_mask` (sys/kern/lwkt_ipiq.c:855-856), so
`compute_unit_id` (CPUID 0x8000001e EBX[7:0]) is only ever stored on the
BSP's node; every other node keeps 255 from subr_cpu_topology.c:128. The
merge at subr_cpu_topology.c:313-314 compares `compute_unit_id` for
equality, so on AMD TOPOEXT systems whose tree is 3-level (SMT off) all
non-BSP cores of a chip compare 255==255 and are collapsed into a single
bogus compute-unit node — a wrong scheduler topology that also perturbs
`get_cpumask_from_level()` masks for those CPUs. Could not be exercised on
this guest (Intel QEMU CPU: `fix_amd_topology()` returns -1 at the vendor
check, mp_machdep.c:1931-1932).

## Exploit chain

None. No memory-safety impact: the strings are fixed-size SBUF_FIXEDLEN
buffers (8*MAXCPU), and `sbuf` truncation is bounds-checked. The bug exports
wrong topology information to user space (CPU-placement hints, NUMA tools)
and degrades AMD scheduling quality. No route to uid=0 exists or is claimed.

## Fix

`fix.diff` bounds the iteration by the mask width (MAXCPU bits) rather than
`ncpus`, making the rendering independent of when it runs. It is a
two-statement semantic equivalent of the macro (`CPUMASK_TESTBIT` already
masks the bit index), so the string content for bit positions < ncpus is
unchanged at read time; only the missing bits are added.

Fix validation (patched-kernel rebuild + rerun) was **not** performed:
contract requires it only for reproduced memory-corruption findings; this is
a Low correctness bug. The guest was left untouched (guest_dirty=0).

The AMD `fix_amd_topology` manifestation is NOT fixed by this diff: it needs
the compute-unit pass deferred until after `SI_BOOT2_START_APS` (e.g. a
`SI_BOOT2_FINISH_SMP` sysinit that runs `fix_amd_topology()` before
`init_pcpu_topology_sysctl()` re-derives the per-cpu ids), which reorders
interactions with `SI_BOOT2_NUMA` consumers and belongs in an upstream
design decision, not a finding patch.
