vkernel64 is_globaldata_space() blesses all SMP_MAXCPU(256) reserved per-CPU slots while only ncpus are ever mapped β kernacc() returns TRUE for ~7.9MB of unmapped KVA (latent panic landmine for any direct kernacc consumer; /dev/kmem saved only by kvm_access_check's PTE-loop ordering)
Summary
The range check bounds by MAXCPU (the 256-slot 8MB reservation) instead of ncpus (the mapped slots): on a default -n 2 boot, 254 slots are reserved-but-unmapped and kernacc returns TRUE for all of them. Runtime-proven via DDB call probes: is_globaldata_space(dead)=1, kernacc(dead)=1, while the sole in-tree consumer chain (/dev/kmem) EFAULTs dead pages ONLY because its pmap_kextract loop runs before kernacc (1700-page scan: live slots OK, uniform EFAULT from PG16, no panic). Any future direct kernacc consumer inherits a silent TRUE on unmapped KVA -> whole-vkernel panic. pc64 disabled its twin check entirely - the live check is a vkernel-specific contract validating 128x more space than exists. bound-by-ncpus fix validated on-guest (dead 1->0, live probes byte-identical).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5667 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 8.2 KB | β raw | |
| vk_image.sh | β | 1.5 KB | view raw | |
| vk_probe4.sh | β | 4.6 KB | view raw | |
| vk_probe5.sh | β | 2.9 KB | view raw | |
| vk_probe6.sh | β | 1.1 KB | view raw | |
| fix.diff | β | 776 B | view raw | |
| build.log | β | 638.4 KB | β download | |
| run.log | β | 3.2 KB | view raw | |
| run.fixed.log | β | 3.1 KB | view raw | |
| driver4.log | β | 1.5 KB | view raw | |
| driver4fixed2.log | β | 410 B | view raw | |
| ddb_artifact_panic.txt | β | 848 B | view raw | |
| env.txt | β | 1.3 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-5667 β vkernel64 is_globaldata_space() blesses all SMP_MAXCPU(256) reserved per-CPU slots while only ncpus are ever mapped
File: sys/platform/vkernel64/platform/globaldata.c (pass 2)
What this is
An Info-grade latent API hazard, runtime-proven: on a stock VKERNEL64,
is_globaldata_space() (and therefore the MI kernacc()) returns TRUE for
~7.9 MB of reserved but unmapped kernel virtual address space (the per-CPU
slots of CPUs that do not exist). Today no in-tree caller can be harmed by
this (the sole kernacc() consumer chain on the vkernel, /dev/kmem β
kvm_access_check(), page-table-validates every page before consulting
kernacc()), but the lie is load-bearing API surface: any current or future
direct consumer of the MI kernacc() on a vkernel would take the TRUE at
face value, touch the page, and panic the vkernel.
Reproduce (on the audit guest, as root)
# 1. build the vkernel (current HEAD needs the two defsyms: the vkernel64
# links like a userspace binary β conf/Makefile:23 β and does not get the
# __build_id_start/__build_id_end PROVIDEs its ldscript would provide)
cd /usr/src && make -j6 nativekernel KERNCONF=VKERNEL64 || true
cd /usr/obj/usr/src/sys/VKERNEL64
make -n kernel.debug 2>/dev/null | grep '^cc .*kernel.debug' > /tmp/linkcmd.txt
sh -c "$(cat /tmp/linkcmd.txt) -Wl,--defsym,__build_id_start=0 -Wl,--defsym,__build_id_end=0"
sh vk_image.sh # builds /vkroot.img (minimal root + devfs)
sysctl vm.vkernel_enable=1
mkdir -p /var/vkernel
# 2. baseline run: boots a 2-vCPU vkernel, enters DDB via
# sysctl debug.enter_debugger=ddb (DDB args MUST be 0x-hex; ddb input
# radix is 16), drives call-probes at raw nm-derived addresses, then
# scans every page of the window through /dev/kmem.
sh vk_probe4.sh # decisive baseline: see run.log
# expected decisive lines (run.log):
# db> call <is_globaldata_space>(0x8000420123, 0x800042012b) -> 1 (dead slot blessed)
# db> call <kernacc>(0x8000420123, 8, 1) -> 1 (MI kernacc lies)
# db> call <pmap_kextract>(0x8000420123) -> 123 (garbage PA, pmap.c defect)
# db> call <kvm_access_check>(0x8000420000, 0x8000421000, 1) -> e (EFAULT β PTE loop saves /dev/kmem)
# db> call <kernacc>(0x8000400040, 8, 1) -> 1 (live slot, intended)
# db> call <pmap_kextract>(0x8000400040) -> 197040 (real guest-phys)
# PG 0 OK addr=549760008192 (slots 0+1 readable: 16 pages = 2*32KB)
# PG 16 EF addr=549760073728 (first dead slot page -> EFAULT, uniformly, no porous pages)
# 3. fix validation: apply fix.diff to the guest's /usr/src copy, rebuild
# globaldata.o, relink as above, rerun the identical driver:
# is_globaldata_space(dead) 1 -> 0, all live probes unchanged, scan
# unchanged. See run.fixed.log.
Rig notes
- vkernel:
/usr/obj/usr/src/sys/VKERNEL64/kernel.debug -m 512m -r /vkroot.img -n 2 -e init_path=/bin/vkinit -s(console via mkfifo; adapted from the DF-5655 rig). - The vkernel's DDB has no symbol table (
kdb_init()is#if 0'd in init.c) β all probes use rawnm-derived addresses. - Measured layout (this build):
CPU_prvspace = 0x8000400000,sizeof(struct privatespace) = 0x8000(32 KB), window = 256*32 KB = 8 MB reserved, slots 0..1 mapped (ncpus=2).kernel_mapspans [0x8000000000, 0x1000000000000) β see VERDICT.md for the implications.
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:
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 INVARIANTSin VKERNEL64, config:60); every MI caller loops< ncpus. No out-of-range index reachesCPU_prvspace[].cpu_gdinit()(globaldata.c:66-79): byte-identical to pc64's (machdep.c:2944-2959); thetd_sp -= 8; *(void**)td_sp = cpu_idle_restorepush lands atidlestack_top β sizeof(struct pcb) β 24β in bounds (vkernel64pmap_init_thread, platform/pmap.c:1003-1009, setstd_sp = td_pcb β 16);gd_prvspaceis set before both call sites (init.c:795, mp.c:423);cpu_idle_restore/cpu_lwkt_switchexist (swtch.s:598/678). A 128-byte /dev/kmem dump of slot0's mdglobaldata cross-validated the layout (gd_prvspaceself-pointer at +0,gd_idlethreadat +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:
-nparse bounds optcpus 1..MAXCPU (init.c:389-397, usage_err otherwise);naps = ncpusβ1;start_all_apstouches 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)
- 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-proven0x123for 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. - 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.
- VKERNEL64 does not link on current HEAD:
sysctl_build_id(kern/kern_mib.c:103-134) references__build_id_start/__build_id_endunconditionally, 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--defsymduring 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.
Fix verification
fixedSingle-line fix applied to the guest's /usr/src copy, globaldata.o rebuilt, kernel relinked, identical driver re-run: is_globaldata_space(dead) 1->0 while all live-slot probes (kernacc=1, pmap_kextract=0x197040, igds=1) and the /dev/kmem page scan (PG0-15 OK, PG16+ EFAULT) are byte-identical. Residual kernacc TRUE on dead slots via the kernel_map fallback is a separate vm-subsystem issue (VERDICT.md section 3).
run.fixed.log DDB battery (igds(dead)->0) and scan section; VERDICT.md section 3 table
Confirmed kernel references
- sys/platform/vkernel64/platform/globaldata.c:82
- sys/platform/vkernel64/platform/globaldata.c:85
- sys/platform/vkernel64/platform/init.c:727
- sys/platform/vkernel64/platform/init.c:733
- sys/platform/vkernel64/platform/init.c:389
- sys/platform/vkernel64/x86_64/mp.c:407
- sys/vm/vm_glue.c:119
- sys/platform/vkernel64/x86_64/vm_machdep.c:322
- sys/cpu/x86_64/include/param.h:71
- sys/platform/pc64/x86_64/machdep.c:2964
Detail
Evidence (decisive lines)
run.log (baseline DDB battery + scan; decisive lines: 'call 0x...720f10(0x8000420123, 0x800042012b)' -> 1; 'kernacc(dead)' -> 1; 'kvm_access_check(0x8000420000,...)' -> e; 'PG 0 OK'/'PG 16 EF'); run.fixed.log (patched: igds(dead) -> 0, all else unchanged); VERDICT.md sections 2-3; env.txt measured layout
PoC changes
Rebuilt the whole rig (DF-5655's artifacts were gone): nativekernel VKERNEL64 needed a -Wl,--defsym relink workaround (kern_mib.c __build_id_* vs conf/Makefile:23 userspace link); image needs /usr/bin tools (grep/head/tail/od) and devfs mount in vkinit; vkernel DDB has no symbols (kdb_init #if 0) so all probes use raw nm addresses; ddb args must be 0x-hex (input radix 16 β decimal args caused probe v3's artifact panic, kept as ddb_artifact_panic.txt); base pointer parsed from an in-vkernel /dev/kmem od read (DDB x/x truncates to 32 bits).
Verified recommended fix
Bound is_globaldata_space() by the number of online CPUs: eaddr <= (vm_offset_t)&CPU_prvspace[ncpus] (see fix.diff)
Verdict
Runtime-proven latent API hazard, Info grade: on a stock VKERNEL64, is_globaldata_space() (globaldata.c:82-89) blesses the entire 256-slot (8MB, S=32KB measured) SMP_MAXCPU per-CPU KVA window while only ncpus (2) slots are ever mapped, so the MI kernacc() returns TRUE for ~7.9MB of unmapped KVA (DDB call probes: igds(dead)=1, kernacc(dead)=1). No in-tree impact today: the only kernacc consumer chain (/dev/kmem -> kvm_access_check) page-table-validates via pmap_kextract before kernacc and uniformly EFAULTs dead pages (1700-page scan: PG0-15 OK, PG16+ EFAULT, no panic, vkernel alive). Any future/direct kernacc consumer inherits a silent TRUE and panics the vkernel on touch. Fix (bound by ncpus) validated on-guest: igds(dead) 1->0, live probes and scan unchanged.
No comments yet.