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

pmap_enter/pmap_unwire UKSMAP wire-accounting corruption: mlock() of /dev/upmap wires a transient stack-fictitious page with no vm_page wire, pmap_unwire() then returns the real kmalloc'd slab page which vm_fault_unwire unwires β€” slab page loses its only wire (silent corruption) and panics deterministically on the second cycle

Summary

vm_fault's UKSMAP branch builds a STACK fictitious vm_page and pmap_enter(wired=TRUE) sets VPTE_WIRED with zero page-wire backing; vm_fault_unwire misclassifies UKSMAP as non-fictitious (unlike vm_fault_wire's correct switch) so pmap_unwire fabricates m=PHYS_TO_VM_PAGE(frame) for the UNMANAGED wired PTE - the real kmalloc'd upmap page's only wire (from kmem_slab_alloc) is dropped 1->0 (pageout-reclaimable while still kernel-mapped: heap UAF-class primitive) and the second munlock panics deterministically. REPRODUCED end-to-end on a vkernel booted inside the QEMU guest (baseline: cycle-1 panic('vm_page_unwire: invalid wire count') with backtrace; fixed kernel: 4/4 cycles SURVIVED). Root-gated (mlock SYSCAP_RESTRICTEDROOT); pc64 shares the vm_fault.c half + identical pmap_unwire shape. maptype-switch + VPTE_MANAGED-gate fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5289 Β· 12 files
FileTypeDescriptionSize
df5289.c β€” 2.2 KB view raw
run.sh β€” 226 B view raw
run.baseline.log β€” 2.7 KB view raw
run.fixed.log β€” 2.6 KB view raw
panic.txt β€” 560 B view raw
build.log β€” 3.2 KB view raw
fix-build.log β€” 10.9 KB view raw
fix.diff β€” 1.9 KB view raw
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 6.4 KB ↓ raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 5.5 KB view raw

DF-5289 β€” vkernel pmap wire-accounting corruption via mlock() of UKSMAP mappings

What (the bug)

pmap_enter() (sys/platform/vkernel64/platform/pmap.c:2429-2442) marks a PTE VPTE_WIRED and bumps pm_stats.wired_count whenever wired=TRUE, but the vm_page_wire(m) it performs is a no-op for fictitious pages (vm_page_wire() skips PG_FICTITIOUS β€” sys/vm/vm_page.c). For VM_MAPTYPE_UKSMAP mappings (/dev/upmap, /dev/kpmap, /dev/lpmap), vm_fault() (sys/vm/vm_fault.c:562-580) fabricates a stack-allocated fictitious vm_page (fakem) and passes it to pmap_enter() β€” so a wired UKSMAP PTE is installed with no wire count taken anywhere.

On unwire, pmap_unwire() (pmap.c:2652-2657) does

pa = *pte & VPTE_FRAME;
m = PHYS_TO_VM_PAGE(pa);    /* held by wired count */

for ANY wired PTE β€” including the unmanaged UKSMAP PTE β€” and hands the real vm_page of the kmalloc'd kernel page (the upmap is kmalloc(M_WAITOK) β€” kern_proc.c:1272) back to vm_fault_unwire() (sys/vm/vm_fault.c:2690-2697), which then does vm_page_busy_wait(m); vm_page_unwire(m, 1); on a page that was never wired through the pmap (kernel slab pages carry exactly one wire from kmem_slab_alloc()'s pmap_enter(kernel_pmap, ..., wired=1) β€” sys/kern/kern_slaballoc.c:1830).

The trigger gate that lets this happen: vm_fault_unwire()'s fictitious classification (vm_fault.c:2679-2681) checks only OBJT_DEVICE/OBJT_MGTDEVICE β€” VM_MAPTYPE_UKSMAP (whose ba.object is NULL) is misclassified as non-fictitious, unlike vm_fault_wire() (vm_fault.c:2607-2620) which classifies UKSMAP correctly.

Impact

  • 1st mlock/munlock cycle (silent corruption): the live kernel slab page backing the upmap drops wire_count 1 β†’ 0 while still kmalloc-owned and kernel-mapped β€” a page-reclaim/UAF-class memory-corruption primitive.
  • 2nd cycle (deterministic): vm_page_unwire() with wire_count already 0 β†’ panic("vm_page_unwire: invalid wire count").

Privilege gate: sys_mlock requires SYSCAP_RESTRICTEDROOT (sys/vm/vm_mmap.c:1024-1033; pmap_wired_count is not defined anywhere, so the #else priv check always applies) β€” root inside the vkernel is needed. The same shared-vm_fault.c chain exists on pc64 (same gating), so this is not vkernel-only code rot; the pmap.c half under audit is the vkernel64 one.

Verified on the guest with a vkernel-in-QEMU-guest run: baseline vkernel panics at exactly the predicted point; kernel built with fix.diff runs the identical PoC cleanly.

Files

  • df5289.c PoC source (mmap /dev/upmap + 4x mlock/munlock)
  • run.sh guest-side driver (boots vkernel, feeds console)
  • run.baseline.log vulnerable vkernel: full console incl. panic + backtrace
  • run.fixed.log fixed vkernel: 4 clean cycles, shell alive
  • panic.txt the decisive panic excerpt
  • build.log vkernel build tail (nativekernel VKERNEL64)
  • fix-build.log patched-kernel rebuild log
  • fix.diff two-layer fix (vm_fault.c classification mirror + pmap_unwire() managed-only vm_page)
  • README.md / VERDICT.md / manifest.json / verdict.json

Reproduce

In the guest (root), with /usr/src present:

# one-time env:
sysctl vm.vkernel_enable=1
mkdir -p /var/vkernel
# vkernel64 link fix (unrelated build rot at this commit: kern_mib.c
# references __build_id_start/end which the userspace-style SYSTEM_LD
# never defines):
printf '\nchar __build_id_start[1] = { 0 };\nchar __build_id_end[1] = { 0 };\n' \
    >> /usr/src/sys/platform/vkernel64/platform/init.c
cd /usr/src && make -j6 nativekernel KERNCONF=VKERNEL64

# root image: whole-disk UFS (vkd0 has no partitions; answer the
# mountroot prompt with "ufs:vkd0" β€” the driver pre-feeds it):
vnconfig -c /dev/vn0 /vkroot.img
newfs /dev/vn0; mount /dev/vn0 /mnt
# populate: /bin/{sh,cat,ls,...}, /lib, /libexec/ld-elf.so.2, /root/poc
cc -static -O -o /mnt/root/poc df5289.c
cc -static -O -o /mnt/bin/vkinit vkinit.c   # console opener -> exec sh
umount /mnt; vnconfig -u vn0

sh run.sh            # baseline: panic at cycle 1 munlock
patch -p1 < fix.diff # in /usr/src
(cd /usr/obj/usr/src/sys/VKERNEL64 && make KERNEL=kernel all)
sh run.sh            # fixed: "SURVIVED - no panic"

Expected baseline:

DF5289: cycle 0 munlock = 0 errno=0 (Undefined error: 0)
DF5289: cycle 1 mlock   = 0 errno=0 (Undefined error: 0)
DF5289: cycle 1 munlock ...
panic: vm_page_unwire: invalid wire count: 0
vm_page_unwire() at ...
vm_fault_unwire() at ...
vm_map_user_wiring() at ...
sys_munlock() at ...

Expected fixed: all four cycles return 0, DF5289: SURVIVED - no panic.

VERDICT.md
↓ download raw

DF-5289 β€” VERDICT

Status: reproduced (vkernel-in-guest, baseline panic; fixed kernel clean). Impact: panic (deterministic whole-vkernel kernel panic on the 2nd mlock/munlock cycle) + silent kernel-slab wire-count corruption on the 1st cycle (live kmalloc'd kernel page loses its only wire count β†’ reclaim/UAF-class primitive). Confidence: certain.

The chain, proven end-to-end

  1. Guest process (root) mmaps /dev/upmap (mode 0666, sys/kern/kern_memio.c:845). The char device has .d_uksmap = memuksmap (kern_memio.c:95), so vm_mmap creates a VM_MAPTYPE_UKSMAP map entry (sys/vm/vm_mmap.c:1356-1371, 1456-1460).
  2. mlock() β†’ sys_mlock (priv-gated SYSCAP_RESTRICTEDROOT, vm_mmap.c:1030 β€” root passes) β†’ vm_map_user_wiring(wire=0) (sys/vm/vm_map.c:2631-2654; no maptype exclusion) β†’ vm_fault_wire (vm_fault.c:2590-2616; UKSMAP correctly treated as fictitious for its own unwind bookkeeping) β†’ vm_fault on the UKSMAP entry.
  3. vm_fault UKSMAP branch (vm_fault.c:562-580): builds a stack struct vm_page fakem with PG_FICTITIOUS; memuksmap() sets fakem.phys_addr = the real upmap page's frame (kern_memio.c:446-471 β†’ pmap_kextract(p->p_upmap + offset)); vm_map_lookup sets FW_WIRED from entry->wired_count (vm_map.c:4540) β†’ pmap_enter(pmap, va, &fakem, prot, wired=TRUE, entry).
  4. pmap.c:2429-2432 (wired block): pm_stats.wired_count++ + vm_page_wire(m) β€” the latter is a no-op for PG_FICTITIOUS (vm_page.c vm_page_wire: if ((m->flags & PG_FICTITIOUS) == 0)). PTE installed: pa | prot | V | U | WIRED, unmanaged (pmap.c:2404-2417 skips the pv/managed path for fictitious m). β†’ a wired PTE exists with zero page-wire backing.
  5. munlock() β†’ vm_map_user_wiring(wire=1) β†’ vm_fault_unwire. vm_fault.c:2679-2681: fictitious = entry->ba.object && (DEVICE…) β€” UKSMAP entries have ba.object == NULL β†’ fictitious = 0 (asymmetric with vm_fault_wire:2614 which handles UKSMAP).
  6. pmap.c:2652-2657: PTE is wired β†’ clear WIRED, then pa = *pte & VPTE_FRAME; m = PHYS_TO_VM_PAGE(pa); β€” fabricates the real vm_page of the kmalloc'd upmap page for an unmanaged PTE.
  7. vm_fault.c:2691-2696: vm_page_busy_wait(m); vm_page_unwire(m, 1); - kernel slab pages carry exactly wire_count==1 from kmem_slab_alloc's pmap_enter(kernel_pmap, ..., 1, ...) (sys/kern/kern_slaballoc.c:1830). - cycle 0: wire 1 β†’ 0 on a live kernel heap page (silent corruption: the page is now unwired/requeueable while kmalloc still owns it and the kernel pmap still maps it). - cycle 1: vm_page_unwire sees (int)m->wire_count <= 0 β†’ panic("vm_page_unwire: invalid wire count") β€” deterministic.

Guest evidence

  • Baseline (stock vkernel64 build): run.baseline.log β€” panic: vm_page_unwire: invalid wire count: 0, backtrace vm_page_unwire ← vm_fault_unwire ← vm_map_user_wiring ← sys_munlock, cycle-0 munlock returned 0 (the silent corruption) before the panic.
  • Fixed kernel (fix.diff: vm_fault.c classification mirror + pmap.c managed-only return in pmap_unwire): run.fixed.log β€” all 4 cycles return 0, DF5289: SURVIVED - no panic, shell prompt returns, vkernel process alive. Identical PoC, identical run driver.

Why it is in this file's scope

The finding is filed against sys/platform/vkernel64/platform/pmap.c because the two pmap-side semantic defects live here: - pmap_enter (2429-2442) takes a "wire" on a transient caller stack page and marks the PTE wired regardless, creating wired PTEs with no backing wire; - pmap_unwire (2652-2657) returns a vm_page for unmanaged wired PTEs ("held by wired count" is false for them), handing callers a page to unwire that the pmap never wired.

The trigger-side classification omission is in shared code (sys/vm/vm_fault.c:2679), which is why the same chain exists on pc64 (pc64 pmap_unwire has the identical unmanaged shape, pc64/pmap.c:5598-5603, 5627-5633). Fixing only vm_fault.c stops this trigger; the pmap.c hunks close the class for any future caller.

Privilege / threat model

mlock is SYSCAP_RESTRICTEDROOT-gated (root inside the vkernel / root on pc64). Within the audit's vkernel threat model (processes inside the vkernel attacking the vkernel kernel): a root process in the vkernel corrupts/panics the vkernel kernel. Severity Medium (gated privilege, but a genuine kernel memory-accounting corruption primitive, not a mere assertion: cycle 0's wire drop on a live slab page is reclaimable-UAF class). CVSS vector reflects local, privileged, availability+integrity impact.

Deviations from stock environment (recorded honestly)

  • The vkernel64 target at this commit does not link out of the box (kern_mib.c references __build_id_start/end since 2026-06-22; the vkernel SYSTEM_LD links as a host userspace binary with no script PROVIDE). A 2-symbol shim was appended to the guest's sys/platform/vkernel64/platform/init.c (build plumbing only; the code under audit untouched). Alternative (equivalent): DF-3218's pack used -Wl,--defsym,__build_id_start=0 -Wl,--defsym,__build_id_end=0.
  • Root image is a whole-disk UFS (vkd0 has no partitions); the vkernel mountroot prompt is answered ufs:vkd0 by the driver. The prompt cannot be answered interactively on a vkernel because vconscheckc (console.c) returns -1 ("EOF") when no input is buffered β€” the answer is pre-fed into the console fifo before boot reaches the prompt.
  • No id(1) on the minimal image (the shell reports id: not found); PID1 vkinitβ†’sh runs as root (uid 0), which the mlock privilege check requires and the successful mlock return (0) demonstrates.

Bottom line

Real, deterministically reproducible kernel memory-accounting corruption in the audited file's enter/unwire pair, reachable by a root process inside the vkernel with three syscalls (open+mmap+mlock), panic-proven and fix-proven on the live vkernel; the same trigger also exists on pc64 via shared vm_fault.c.

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 reproduced

fix.diff applied to guest /usr/src (patch -p1, both hunks), vkernel relinked (vm_fault.o + pmap.o rebuilt), identical PoC re-run: 4/4 mlock+munlock cycles return 0, no panic, vkernel alive at shell prompt; baseline (pre-fix) same binary panicked at cycle 1 munlock. Bad behavior eliminated.

run.fixed.log (clean cycles + SURVIVED marker); fix-build.log (patched rebuild); fix.diff (the applied diff)
↓ fix.diffper-fix-DF-5289

Confirmed kernel references

Detail

Exploit chain

open("/dev/upmap", O_RDWR) -> mmap(MAP_SHARED) creates a VM_MAPTYPE_UKSMAP entry -> mlock: vm_fault fabricates a STACK fictitious vm_page and calls pmap_enter(wired=TRUE) which sets VPTE_WIRED but vm_page_wire() is a no-op on fictitious pages -> munlock: vm_fault_unwire misclassifies UKSMAP as non-fictitious, pmap_unwire returns the REAL vm_page of the kmalloc'd upmap page -> vm_page_unwire drops the slab page's only wire (cycle 0, silent kernel heap corruption) and panics on cycle 1 ('vm_page_unwire: invalid wire count'). Push-to-root beyond panic was not pursued: the primitive corrupts wire accounting of an arbitrary-wire page only for UKSMAP-backed frames (upmap/kpmap/lpmap), and the trigger is already root-gated inside the vkernel, so escalation adds nothing in this threat model.

Evidence (decisive lines)

run.baseline.log (full vulnerable-vkernel console: cycle-0 munlock returns 0 then panic 'vm_page_unwire: invalid wire count: 0' with backtrace vm_page_unwire <- vm_fault_unwire <- vm_map_user_wiring <- sys_munlock); run.fixed.log (identical PoC on fix.diff kernel: 4 clean cycles, 'DF5289: SURVIVED - no panic'); panic.txt (decisive excerpt); df5289.c (PoC); run.sh (driver); build.log/fix-build.log (kernel builds); fix.diff (vm_fault.c + pmap.c two-layer fix, applied and validated in guest)

PoC changes

PoC authored fresh (reserved ID's class had no seed): open+mmap /dev/upmap + 4x mlock/munlock with per-call errno printing. Environment had to be built from scratch: VKERNEL64 nativekernel build (after working around this commit's broken vkernel64 link via a 2-symbol __build_id shim in init.c), a 1200MB whole-disk UFS vkroot.img (bin/lib/libexec + static PoC + vkinit console wrapper), and a fifo-console driver that PRE-FEEDS the mountroot answer 'ufs:vkd0' because the vkernel console driver's cncheckc returns EOF when no input is buffered (the mountroot prompt cannot wait).

Verified recommended fix

vm_fault_unwire: mirror vm_fault_wire's maptype switch (UKSMAP => fictitious=TRUE); pmap_unwire: return a vm_page only for VPTE_MANAGED wired PTEs (fix.diff, both hunks applied and validated on the guest)

Verdict

Reproduced on a vkernel booted inside the QEMU guest: two mlock/munlock cycles on a /dev/upmap (VM_MAPTYPE_UKSMAP) mapping make the stock vkernel panic with 'vm_page_unwire: invalid wire count: 0' via sys_munlock -> vm_map_user_wiring -> vm_fault_unwire -> vm_page_unwire; the first cycle already silently drops the only wire_count of the live kmalloc'd upmap kernel slab page (reclaim/UAF-class corruption). Root cause is split between this file (pmap_enter wires a transient fictitious stack page and marks the PTE VPTE_WIRED with no vm_page wire; pmap_unwire fabricates and returns the real vm_page for an unmanaged wired PTE) and the trigger-side misclassification in shared sys/vm/vm_fault.c:2679 (vm_fault_unwire's fictitious flag misses VM_MAPTYPE_UKSMAP, unlike vm_fault_wire:2614). A kernel built with fix.diff (vm_fault.c mirror + pmap_unwire managed-only) runs the identical PoC cleanly: all 4 cycles return 0, 'DF5289: SURVIVED - no panic', shell alive. mlock is SYSCAP_RESTRICTEDROOT-gated, so the trigger needs root inside the vkernel (same gating on pc64, which shares the vm_fault.c half).