DF-5289 / verdict.json
{ "finding_id": "DF-5289", "status": "reproduced", "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 kmallocd 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_unwires 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).", "reproduced": true, "impact": "panic", "confidence": "certain", "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 kmallocd upmap page -> vm_page_unwire drops the slab pages 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": "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)", "kernel_refs": [ "sys/platform/vkernel64/platform/pmap.c:2429", "sys/platform/vkernel64/platform/pmap.c:2441", "sys/platform/vkernel64/platform/pmap.c:2652", "sys/vm/vm_fault.c:2679", "sys/vm/vm_fault.c:2614", "sys/vm/vm_fault.c:562", "sys/vm/vm_fault.c:2691", "sys/kern/kern_slaballoc.c:1830", "sys/vm/vm_mmap.c:1030", "sys/kern/kern_memio.c:95" ], "poc_changes": "PoC authored fresh (reserved IDs 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 commits 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 drivers cncheckc returns EOF when no input is buffered (the mountroot prompt cannot wait).", "attempts": 5, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (vkernel inside: DragonFly 6.5-DEVELOPMENT #1: Fri Oct 2 05:54:28 UTC 2026, VKERNEL64)", "runtime_sec": 1650, "guest_dirty": 1, "build_cmd": "guest: cd /usr/src && make -j6 nativekernel KERNCONF=VKERNEL64 ; cc -static -O -o /mnt/root/poc df5289.c ; cc -static -O -o /mnt/bin/vkinit vkinit.c", "run_cmd": "guest (root): sh run.sh # boots /usr/obj/usr/src/sys/VKERNEL64/kernel.debug -m 512m -r /vkroot.img -e init_path=/bin/vkinit -s, feeds console via fifo, runs /root/poc", "code_hash": "66653717b0793845b64b5020962b3135ac111f8ca5c95f94af92228e03a62115", "notes": "Same trigger chain exists on pc64 (shared vm_fault.c misclassification + pc64 pmap_unwires identical unmanaged PHYS_TO_VM_PAGE at pc64/pmap.c:5598-5603/5627-5633); filed against vkernel64/platform/pmap.c per assignment. The cycle-0 silent half (slab page wire 1->0 on a live kmalloc page) is the memory-corruption primitive; the cycle-1 panic is its deterministic witness. vkernel64 target has an unrelated build regression at this commit (kern_mib.c __build_id_* vs script-less SYSTEM_LD) - recorded, shimmed around, worth a separate maintenance note upstream.", "recommended_fix": "vm_fault_unwire: mirror vm_fault_wires 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)", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": true, "fix_verdict": "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.", "fix_evidence": "run.fixed.log (clean cycles + SURVIVED marker); fix-build.log (patched rebuild); fix.diff (the applied diff)" } |