# DF-5244 VERDICT — REPRODUCED (panic on instrumented kernel; race certain by inspection)

## Bottom line

`pmap_unwire()`'s user branch clears PG_W and re-reads the PTE frame as two
non-atomic steps, and nothing excludes `pmap_remove_all()`'s cmpset-based PTE
teardown. A cross-process `msync(MS_INVALIDATE)` on a shared MAP_SHARED vnode
mapping drives that teardown without any lock the unwire path holds. The race
was **reproduced as a kernel panic** on an instrumented kernel (the few-ns
window widened to ~10 µs), and the proposed atomic cmpset fix **was validated**
on the same instrumented kernel: identical stress, no panic.

## Root cause chain (path:line)

1. `sys_munlock` → `vm_map_user_wiring` (unwire) → `vm_fault_unwire`
   (sys/vm/vm_fault.c:2686-2697) → `pmap_unwire`
   (sys/platform/pc64/x86_64/pmap.c:5610-5637).
   `pmap_unwire` holds only the victim pmap's **pt_pv** (pv_get at pmap.c:5610):
   ```
   5627  if (pmap_pte_w(pmap, ptep)) {                 // test PG_W
   5628      atomic_add_long(&...wired_count, -1);
   5630      atomic_clear_long(ptep, pmap->pmap_bits[PG_W_IDX]);   // clear W
   5632      pa = *ptep & PG_FRAME;                    // RE-READ, racy
   5633      m = PHYS_TO_VM_PAGE(pa);                  // pa==0 → array[-1]
   ```
   then the caller does `vm_page_busy_wait(m); vm_page_unwire(m, 1);`
   (vm_fault.c:2692-2694). `vm_page_unwire` panics on wire_count ≤ 0
   (sys/vm/vm_page.c:3304).

2. Attacker-side teardown: `msync(MS_INVALIDATE)` → `vm_map_madvise`
   (sys/vm/vm_map.c:3164-3169) → `vm_object_page_remove` on the **shared**
   vnode object → wired-page branch (sys/vm/vm_object.c:1543-1551) →
   `vm_page_protect(p, VM_PROT_NONE)` → `pmap_remove_all`
   (pmap.c:4777) → `pmap_inval_smp_cmpset(ipmap, iva, iptep, ipte, 0)`
   (pmap.c:4816) → `pmap_removed_pte` → `vm_page_unwire(m, -1)` (pmap.c:439-440).
   The backing scan (PMAP_PAGE_BACKING_SCAN, pmap.c:176-222) holds only
   `iobj->backing_lk`; it takes neither the victim's vm_map lock, nor pt_pv,
   nor the PTE placemarker — so pt_pv (the only lock pmap_unwire holds)
   does not exclude it. ftruncate does NOT work as the B-side on this
   system: neither tmpfs (tmpfs_subr.c:1051-1063 — page removal skipped for
   the v_object bookkeeping case) nor hammer2 invalidate already-mapped pages
   in practice (verified with kernel counters: pmap_remove_all's cmpset never
   fired under 500k truncates).

3. Interleavings:
   - B's cmpset between A's clear and A's re-read → `pa = 0` →
     `PHYS_TO_VM_PAGE(0)` = `&vm_page_array[atop(0) - first_page]`
     (sys/vm/vm_page.h:454-455; first_page = phys_avail[0]/PAGE_SIZE,
     sys/vm/vm_page.c:410 — typically > 0, so index −1: memory **before**
     the vm_page array) → `vm_page_busy_wait`/`vm_page_unwire` on a forged
     vm_page (32-bit decrement at a fixed offset before the array).
   - B's cmpset after A's re-read (both observed PG_W) → both sides unwire
     the single wire → **panic "vm_page_unwire: invalid wire count: 0"**.

## Reproduction history (attempts)

1. tmpfs + mlock/munlock + ftruncate (user `maxx`): mlock → EPERM — on DF,
   mlock is `SYSCAP_RESTRICTEDROOT`-gated (vm_mmap.c:1030) because pc64 lacks
   `pmap_wired_count`. Switched to root.
2. Stock kernel, tmpfs + paced ftruncate, 8 lock threads (5 min): 176k
   lock/unlock cycles, no panic (tmpfs never tears the PTEs down).
3. Stock kernel, hammer2 (/var) + ftruncate, 10 processes (12 min): 190k
   cycles × 256 pages, 1.8M truncates, no panic.
4. Counter-instrumented kernel: `pmap_unwire` W-hit counter rose to millions;
   `pmap_remove_all` entry counter did NOT move under ftruncate (confirmed the
   B-side was inert).
5. B-side switched to cross-process `msync(MS_INVALIDATE)` (vm_map.c:3164
   path) on a hammer2 MAP_SHARED mapping; instrumented kernel (10 µs window
   inside pmap_unwire) → **panic in <40 s** (panic.txt): exactly the predicted
   double-unwire (`sys_munlock → vm_map_user_wiring → vm_fault_unwire →
   vm_page_unwire`).
6. Fixed kernel (fix.diff: atomic cmpset clear-and-capture) **with the same
   10 µs delay inside the cmpset loop**, 12 processes, 8+ minutes: no panic,
   no hang, all children completed. Fix validated.

## Why the stock kernel was not panicked directly

The exploitable window on stock code is the gap between `atomic_clear_long`
and `*ptep & PG_FRAME` (a few instructions, ~1-2 ns). With ~2.5M pmap_unwire
events and ~90k remove_all cmpsets per 40 s (counter-measured), the expected
hit rate is on the order of one per many minutes to hours; the audit's 20 min
of stock racing did not win the lottery. The instrumented run proves the
interleaving is reachable and fatal when the window opens; the fix removes
the window entirely (clear and capture are one atomic cmpset, and losing the
cmpset correctly drops out of the unwire).

## Exploitability / impact ceiling

- Proven: local DoS (panic) — **root-triggered on stock DF** (mlock is
  privileged here); unprivileged on any system where mlock is allowed
  (nonstandard for DF, standard elsewhere).
- Present but not separately triggered: the `PHYS_TO_VM_PAGE(0)` OOB path
  yields a forged `vm_page_t` (memory preceding the vm_page array) that the
  kernel busy-waits on and atomically decrements — a weak but real kernel
  heap corruption primitive at a fixed location, plus possible indefinite
  thread hangs (tsleep on a garbage busy channel).
- The kernel-pmap branch (pmap.c:5598-5603) has the identical non-atomic
  pattern with no locking at all (comment: "Assume elements in the kernel
  pmap are stable"); it is believed unreachable racing today because kernel
  wired-map teardown is serialized by kernel_map, but it is fixed by the
  same patch defensively.

## Fix

`fix.diff` in this directory (also summarized in the finding markdown):
replace the clear-then-reread with an atomic cmpset loop that clears PG_W and
captures the frame from the PTE it won, in both branches of pmap_unwire.
Losing the cmpset means a concurrent teardown handled the wire; the code then
correctly declines to unwire. Validated as described above.

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