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

vm_fault_unwire() UKSMAP fictitious asymmetry: munlock/exit-unwire of mlock()ed /dev/upmap or /dev/mem UKSMAP ranges runs vm_page_unwire() on never-wired pages and an unchecked PHYS_TO_VM_PAGE() array index

Summary

vm_fault_wire classifies UKSMAP entries as fictitious (the fault path pmap_enters a fictitious stack page, never vm_page_wires) but vm_fault_unwire computes fictitious ONLY from object type β€” UKSMAP entries have ba.object==NULL β€” so unwiring such a range runs pmap_unwire+vm_page_busy_wait+vm_page_unwire on a never-wired page, and pmap_unwire returns PHYS_TO_VM_PAGE(pa) unchecked. Guest-reproduced as root: mmap /dev/mem at pa 0xFF000000 (above Maxmem, ~4000 entries past vm_page_array end), mlock, munlock -> stock panic 'vm_page_unwire: invalid wire count: 0', guest down; /dev/upmap (0666) variant silently decrements the live kernel upmap page's wiring (wire_count 1->0 -> vm_page_activate of a kernel heap page) β€” delayed corruption. mlock is SYSCAP_RESTRICTEDROOT-gated on x86-64 so no unprivileged route (Low). Maptype-switch mirror fix validated by nativekernel rebuild: both variants survive, rc=0.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5802 Β· 13 files
FileTypeDescriptionSize
uksmap_unwire.c β€” 3.6 KB view raw
build.sh β€” 134 B view raw
run.sh β€” 169 B view raw
build.log β€” 28 B view raw
run.log β€” 567 B view raw
run.patched.log β€” 732 B view raw
fix_run.log β€” 732 B view raw
fix_build.log β€” 1.3 KB view raw
panic.txt β€” 2.1 KB view raw
env.txt β€” 376 B view raw
fix.diff β€” 1.1 KB view raw
VERDICT.md β€” 4.9 KB ↓ raw
verdict.json β€” 3.8 KB view raw
VERDICT.md
↓ download raw

DF-5802 VERDICT β€” REPRODUCED (panic, privileged trigger)

What the bug is

vm_fault_wire() and vm_fault_unwire() classify "fictitious" entries with two DIFFERENT rules:

  • sys/vm/vm_fault.c:2607-2620 (wire): a switch (entry->maptype) β€” VM_MAPTYPE_UKSMAP β‡’ fictitious = TRUE (case at :2614-2616). Correct: the UKSMAP fault path (vm_fault() :562-580) pmap_enter()s a stack fakem page and never calls vm_page_wire() on anything.
  • sys/vm/vm_fault.c:2679-2681 (unwire): fictitious computed ONLY from entry->ba.object->type (OBJT_DEVICE/OBJT_MGTDEVICE). UKSMAP entries have ba.object == NULL (set by vm_map_lookup, sys/vm/vm_map.c:4552-4555) β‡’ fictitious = FALSE.

Consequently, after mlock() of a UKSMAP range (which the wire path handles correctly), munlock() β€” or process-exit unwiring β€” executes vm_fault_unwire() :2691-2695:

m = pmap_unwire(pmap, &va);              /* pmap.c:5633: m = PHYS_TO_VM_PAGE(pa); */
vm_page_busy_wait(m, FALSE, "vmwrpg");
vm_page_unwire(m, 1);

on a page the wire path never wired:

  1. RAM-backed UKSMAP mappings (/dev/upmap, minor 5, sys/kern/kern_memio.c:726+ β†’ pmap_kextract of the live per-process p_upmap kernel page): PHYS_TO_VM_PAGE() yields the real vm_page of a kernel heap page whose wire_count belongs to kmem, not to this mapping. vm_page_unwire() either panics (vm_page.c:3302-3303, wire_count == 0) or silently steals one wiring from the kernel page (wire_count 1β†’0 β‡’ vm_page_activate() of a live kernel data page β€” delayed-corruption face). On the stock guest this variant survived visibly β€” i.e. it took the silent-corruption path.

  2. /dev/mem at pa above Maxmem (minor 0, fake->phys_addr = ptoa(pindex), sys/kern/kern_memio.c:448-452 β€” the mmap offset IS the physical address, user-chosen): PHYS_TO_VM_PAGE(pa) is an unchecked vm_page_array index (sys/vm/vm_page.h:454-455 &vm_page_array[atop(pa) - first_page]). For pa = 0xFF000000 on the reference guest (v_page_count 998273, Maxmem β‰ˆ 0xFDBC0000) the index runs ~4,000 entries past the end of the array; vm_page_busy_wait() / vm_page_unwire() then read/write kernel memory beyond vm_page_array. Observed: panic: vm_page_unwire: invalid wire count: 0 from garbage wire_count, depending on adjacent memory the outcome is a panic, a DDB wedge, or an unmonitored atomic write past the array.

Reproduction (stock kernel, guest 6.5-DEVELOPMENT #0 X86_64_GENERIC)

  • Built in-guest with base cc 8.3 (build.log).
  • run.log: variant upmap β€” survived munlock (silent wiring-accounting corruption face); variant mem β€” output truncated at munlock(), guest DOWN immediately after.
  • panic.txt (serial console): exact causal chain sys_munlock β†’ vm_map_user_wiring β†’ vm_fault_unwire β†’ vm_page_unwire β†’ panic "vm_page_unwire: invalid wire count: 0".

Fix validation

  • fix.diff mirrors vm_fault_wire()'s maptype switch in vm_fault_unwire() (VM_MAPTYPE_UKSMAP β‡’ fictitious = TRUE).
  • Applied to the guest's /usr/src (Hunk #1 succeeded at 2676), make nativekernel KERNCONF=X86_64_GENERIC (buildkernel refused: no buildworld tools), make installkernel, reboot into #1 kernel (fix_build.log).
  • Re-ran the identical run.sh: both variants print SURVIVED munlock, rc=0, guest stays up (run.patched.log, fix_run.log). The panic is gone; the upmap variant's silent unwire is gone with it.
  • Guest reset to the clean with-src snapshot afterwards.

Impact ceiling β€” honest

Privileged trigger: mlock(2) is SYSCAP_RESTRICTEDROOT-gated on x86-64 (pc64 does not define pmap_wired_count, so sys_mlock compiles the privilege branch, sys/vm/vm_mmap.c:1028-1034), and the decisive mem variant additionally needs /dev/mem (root:kmem 0640). /dev/upmap is 0666, but the mlock gate still binds for it. So: root user → kernel panic / kernel-memory write past vm_page_array / corruption of kmem wiring — robustness defect, not a privilege boundary crossing. No user→root route (class doesn't permit one from an unprivileged seat). Severity Low, confidence certain.

Why this is not DF-0944/2685/2686

Those are: MGTDEVICE pager-FAIL NULL page (readrest path), mlock wiring zero-filling swapped pages (TRYPAGER wire mask), and the pager-error PIP wakeup. This finding is the unwire classification asymmetry β€” different function (vm_fault_unwire), different mechanism (fictitious misclassification + unchecked PHYS_TO_VM_PAGE), different fix locus.

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

fix.diff applied to guest /usr/src (hunk at 2676), nativekernel rebuild + installkernel, reboot into 'DragonFly 6.5-DEVELOPMENT #1 Mon Oct 12 23:04:55 UTC 2026'; identical run.sh then printed SURVIVED munlock for both variants with rc=0 and the guest stayed up - the stock panic and the silent upmap unwire are both gone.

fix_build.log (patch + kernel build/install), fix_run.log == run.patched.log (both variants survive), VERDICT.md fix-validation section
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Oct 12 23:04:55 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (full serial-console backtrace), run.log (stock decisive run: upmap survived, mem killed guest at munlock), run.patched.log/fix_run.log (both variants SURVIVED on fix kernel), fix_build.log (Hunk #1 succeeded at 2676; kernel #1 Mon Oct 12 23:04:55 UTC 2026)

PoC changes

no seed existed; trigger written fresh for this pass. Two variants: 'upmap' (RAM-backed kernel page, silent wiring corruption) and 'mem' (pa above Maxmem, OOB vm_page_array index -> panic). fix.diff first drafted by hand was malformed; regenerated mechanically with diff -u against the tree.

Verified recommended fix

In vm_fault_unwire(), mirror vm_fault_wire()'s maptype switch: treat VM_MAPTYPE_UKSMAP entries as fictitious so pmap_unwire()/vm_page_unwire() never run on their never-wired device ptes.

Verdict

vm_fault_unwire() (sys/vm/vm_fault.c:2679-2681) derives 'fictitious' only from entry->ba.object->type, while vm_fault_wire() (:2614-2616) additionally treats VM_MAPTYPE_UKSMAP as fictitious; UKSMAP entries have ba.object==NULL, so munlock()/exit-unwire of a mlock()ed /dev/upmap or /dev/mem UKSMAP range runs pmap_unwire()->PHYS_TO_VM_PAGE(pa) (unchecked vm_page_array index, vm_page.h:454) and vm_page_unwire() on a page the wire path never wired. Reproduced on the stock guest: panic 'vm_page_unwire: invalid wire count: 0' via sys_munlock->vm_map_user_wiring->vm_fault_unwire->vm_page_unwire for /dev/mem pa 0xFF000000 (~4000 entries past vm_page_array, v_page_count 998273); the RAM-backed /dev/upmap variant silently steals a wiring from the live kernel upmap page. Single-fix nativekernel rebuild with fix.diff (mirror vm_fault_wire()'s maptype switch): both variants survive, rc=0, guest healthy. Trigger is privileged (mlock is SYSCAP_RESTRICTEDROOT-gated on pc64, sys/vm/vm_mmap.c:1030; /dev/mem is 0640) -> Low severity, no unprivileged user->root route.