β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5667

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5667 Β· 16 files
FileTypeDescriptionSize
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 raw nm-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_map spans [0x8000000000, 0x1000000000000) β€” see VERDICT.md for the implications.
VERDICT.md
↓ download raw

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 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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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
↓ fix.diffper-fix-DF-5667

Confirmed kernel references

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.