# DF-5667 VERDICT — vkernel64 `is_globaldata_space()` blesses all 256 reserved per-CPU slots; only `ncpus` are ever mapped

**Status: reproduced (latent hazard, runtime-proven). Impact: none in-tree
today (Info). Fix validated.**

## 1. Root cause

`sys/platform/vkernel64/platform/globaldata.c:82-89`:

```c
int
is_globaldata_space(vm_offset_t saddr, vm_offset_t eaddr)
{
	if (saddr >= (vm_offset_t)&CPU_prvspace[0] &&
	    eaddr <= (vm_offset_t)&CPU_prvspace[MAXCPU]) {
		return (TRUE);
	}
	return (FALSE);
}
```

`MAXCPU == SMP_MAXCPU == 256` (`sys/cpu/x86_64/include/param.h:71-72`). The
KVA window `[CPU_prvspace, CPU_prvspace+256*S)` is only **reserved**
(`init.c:727-733` bumps `virtual_start` by `sizeof(struct privatespace) *
SMP_MAXCPU`); actual pages are mapped **only** for CPUs that come online —
cpu0 in `init_globaldata()` (init.c:735-748) and each AP in
`start_all_aps()` (mp.c:407-419). `optcpus` (`-n`, bounded 1..MAXCPU at
init.c:389-397) determines `ncpus`; on the default `-n 2` boot, 254 of the
256 slots (7.94 MB of the 8 MB window, S measured = 32 KB) are
reserved-but-unmapped KVA, and `is_globaldata_space()` blesses every byte
of them.

The function's only consumer is `kernacc()` (`sys/vm/vm_glue.c:119`), whose
comment explicitly documents the special case ("The globaldata space is not
part of the kernel_map proper"). On pc64 the same function is `#if 0`'d out
and replaced by a DMAP check (`sys/platform/pc64/x86_64/machdep.c:2964-2976`)
because there the globaldata space *is* in kernel_map — the live check is a
vkernel-specific contract, and it validates 128x more address space than
exists.

## 2. Runtime proof (guest, stock VKERNEL64 + DDB `call` probes)

Rig: nativekernel-built `kernel.debug`, `-m 512m -n 2`, console via mkfifo,
DDB entered with `sysctl -w debug.enter_debugger=ddb`, driven with raw
`nm`-derived addresses (the vkernel's DDB has no symbol table —
`kdb_init()` is `#if 0`'d in init.c; DDB args must be 0x-hex because ddb's
input radix is 16). Measured: `CPU_prvspace = 0x8000400000`, `ncpus = 2`,
`sizeof(struct privatespace) = 0x8000` (16 live pages), window = 8 MB.

Baseline (run.log, values in hex per ddb radix):

| probe | result | meaning |
|---|---|---|
| `is_globaldata_space(0x8000420123, 0x800042012b)` | **1** | dead slot (slot ≥2) blessed |
| `kernacc(0x8000420123, 8, VM_PROT_READ)` | **1** | MI kernacc claims kernel access to unmapped KVA |
| `pmap_kextract(0x8000420123)` | **0x123** | garbage: PDE==0 path returns `va & PAGE_MASK` as the "physical address" (pmap.c defect, see §5) |
| `kvm_access_check(0x8000420000, 0x8000421000, 1)` | **0xe (EFAULT)** | the composed /dev/kmem check still rejects the dead page — via its pmap_kextract page loop (vm_machdep.c:322-325), which runs **before** kernacc |
| `kernacc(0x8000400040, 8, 1)` | 1 | live slot — intended TRUE |
| `pmap_kextract(0x8000400040)` | 0x197040 | real guest-phys of cpu0's mdglobaldata |
| `/dev/kmem` page scan, 1700 pages | `PG 0 OK … PG 16 EF addr=0x8000500000`, uniform EFAULT thereafter | live slots readable, every dead page EFAULT, no porous pages, vkernel alive |

So: the hazard (`kernacc` TRUE on unmapped KVA) **reproduces**; the only
in-tree composed path (`/dev/kmem` → `kvm_access_check`) is protected
solely by the PTE-loop ordering — a `kernacc()`-believing consumer would
receive silent TRUE and panic the vkernel on touch. No such consumer exists
in-tree today ⇒ Info.

## 3. Fix validation (same guest, same driver, single-function patch)

`fix.diff` bounds the check by `ncpus` (slots 0..ncpus-1 are exactly the
mapped ones; DF vkernels have no CPU hotplug, `ncpus` is fixed at
`mp_start()` long before any `/dev/kmem` open).

| probe | baseline | patched |
|---|---|---|
| `is_globaldata_space(dead)` | 1 | **0** |
| `kernacc(live)` / `pmap_kextract(live)` / `igds(live)` | 1 / 0x197040 / 1 | unchanged |
| `kvm_access_check(dead page)` | EFAULT | EFAULT (unchanged) |
| page scan | PG0-15 OK, PG16 EF… | identical |

`kernacc(dead)` still returns 1 on the patched kernel — **not** via
`is_globaldata_space` (now 0) but via kernacc's `kernel_map` fallback: the
vkernel's `kernel_map` spans `[0x8000000000, 0x1000000000000)` (min/max
read via /dev/kmem at the static map struct, 0xf15680; nentries=0x37), and
`vm_map_check_protection()` blesses the unmapped KVA in between (probed:
kernacc TRUE at 0x8000300123 / 0x8000420123 / 0x8000c10123 — below, inside,
above the window). That residual bless is a vm-subsystem property
(vm_glue.c/vm_map.c/vm_machdep.c — different files), orthogonal to this
finding; the practical gate for /dev/kmem remains kvm_access_check's
pmap_kextract loop. Recorded here as a follow-up lead, not re-scoped.

## 4. Negative results (what was hunted and killed in this file)

- `globaldata_find()` (globaldata.c:58-63): KKASSERT bound is live
  (`options INVARIANTS` in VKERNEL64, config:60); every MI caller loops
  `< ncpus`. No out-of-range index reaches `CPU_prvspace[]`.
- `cpu_gdinit()` (globaldata.c:66-79): byte-identical to pc64's
  (machdep.c:2944-2959); the `td_sp -= 8; *(void**)td_sp = cpu_idle_restore`
  push lands at `idlestack_top − sizeof(struct pcb) − 24` — in bounds
  (vkernel64 `pmap_init_thread`, platform/pmap.c:1003-1009, sets
  `td_sp = td_pcb − 16`); `gd_prvspace` is set before both call sites
  (init.c:795, mp.c:423); `cpu_idle_restore`/`cpu_lwkt_switch` exist
  (swtch.s:598/678). A 128-byte /dev/kmem dump of slot0's mdglobaldata
  cross-validated the layout (`gd_prvspace` self-pointer at +0,
  `gd_idlethread` at +0x30).
- Vanished-export fields (DF-5658: gd_savefpu/gd_fpu_lock/gd_currentldt/
  gd_ss_eflags/gd_common_tssd/gd_tss_gdt): zero-initialized via
  `bzero(gd, sizeof(*gd))` (mp.c:422, init.c:787) — semantically correct
  defaults; no C runtime consumers in vkernel64 (only global.s aliases =
  DF-5658's filed territory). Not re-reported.
- `PRIVATESPACE_SEGPAD` (include/globaldata.h:102): dead macro in
  vkernel64 (no consumers; its PAGE_SIZE*4 term counts pc64's CPAGE/PPAGE
  pages that the vkernel struct lacks) — header-side stale lineage
  adjacent to DF-5659; noted, not a .c finding.
- Sizing chain closed: `-n` parse bounds optcpus 1..MAXCPU (init.c:389-397,
  usage_err otherwise); `naps = ncpus−1`; `start_all_aps` touches only
  slots 1..naps ⊂ 256-slot reservation. No config-supplied size can
  overflow the window.

## 5. Follow-up leads surfaced (other files, out of scope here)

1. **pmap_kextract() returns garbage for PDE==0** (vkernel64
   platform/pmap.c:731-748): `pa = (*pmap_pde_to_pte(&pde=0, va) &
   VPTE_FRAME) | (va & PAGE_MASK)` → runtime-proven `0x123` for an
   unmapped KVA with nonzero page offset — a bogus nonzero "physical
   address" instead of 0. (The DMAP+0 read itself is safe: guest-phys page
   0 is the bootstrap PML4 with only two nonzero entries —
   `create_pagetables()`, pmap.c — and those indices read as frames here.)
   Affects any unaligned-VA pmap_kextract consumer; page-aligned loops are
   unaffected. Deserves its own pmap.c pass-2 entry.
2. **kernacc()/kernel_map bless all unmapped vkernel KVA** (vm_glue.c +
   vkernel kernel_map span [0x8000000000, 0x1000000000000)): kernacc TRUE
   at three probed unmapped addresses; kvm_access_check's PTE loop is the
   only real gate. vm-subsystem pass-2 material.
3. **VKERNEL64 does not link on current HEAD**: `sysctl_build_id`
   (kern/kern_mib.c:103-134) references `__build_id_start/__build_id_end`
   unconditionally, but the vkernel64 links like a userspace program
   (platform/vkernel64/conf/Makefile:23, no `-T ldscript.x86_64`), so the
   ldscript's PROVIDEs never materialize. Build regression, not security;
   worked around here with `--defsym` during relink.

## 6. Environment

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, built Jul 2 2026),
KVM; vkernel = nativekernel VKERNEL64 + defsym relink, 2 vCPUs, 512 MB;
`vm.vkernel_enable=1`. Full logs: run.log (baseline), run.fixed.log
(patched), build.log (nativekernel + manual relink), driver*.log.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
