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)
PoC verification
Evidence pack
findings/poc/DF-5289 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_count1 β 0 while still kmalloc-owned and kernel-mapped β a page-reclaim/UAF-class memory-corruption primitive. - 2nd cycle (deterministic):
vm_page_unwire()withwire_countalready 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.
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
- 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). mlock()βsys_mlock(priv-gatedSYSCAP_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_faulton the UKSMAP entry.vm_faultUKSMAP branch (vm_fault.c:562-580): builds a stackstruct vm_page fakemwithPG_FICTITIOUS;memuksmap()setsfakem.phys_addr= the real upmap page's frame (kern_memio.c:446-471 βpmap_kextract(p->p_upmap + offset));vm_map_lookupsetsFW_WIREDfromentry->wired_count(vm_map.c:4540) βpmap_enter(pmap, va, &fakem, prot, wired=TRUE, entry).- pmap.c:2429-2432 (
wiredblock):pm_stats.wired_count+++vm_page_wire(m)β the latter is a no-op for PG_FICTITIOUS (vm_page.cvm_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. munlock()βvm_map_user_wiring(wire=1)βvm_fault_unwire. vm_fault.c:2679-2681:fictitious = entry->ba.object && (DEVICEβ¦)β UKSMAP entries haveba.object == NULLβ fictitious = 0 (asymmetric with vm_fault_wire:2614 which handles UKSMAP).- 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. - vm_fault.c:2691-2696:
vm_page_busy_wait(m); vm_page_unwire(m, 1);- kernel slab pages carry exactly wire_count==1 fromkmem_slab_alloc'spmap_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_unwiresees(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, backtracevm_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.creferences__build_id_start/endsince 2026-06-22; the vkernelSYSTEM_LDlinks as a host userspace binary with no script PROVIDE). A 2-symbol shim was appended to the guest'ssys/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:vkd0by the driver. The prompt cannot be answered interactively on a vkernel becausevconscheckc(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 reportsid: 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
fixedfix.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)
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).
No comments yet.