# DF-2686 VERDICT — pip double-wakeup + leak in vm_fault_object() error tail

## Status: REPRODUCED (stock) / FIXED (validated by kernel rebuild + rerun)

## How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), QEMU/KVM.
Loopback soft NFS inside the guest (rpcbind+mountd+nfsd serving /export to
localhost, `mount -t nfs -o soft,udp`), 16MB sparse file with real data at
its last page.

1. `victim.c`: open /mnt2/data.bin O_RDWR, mmap MAP_PRIVATE 16MB, close(fd)
   (closing the fd keeps the process off the forced-unmount kill list —
   `sys/kern/vfs_syscalls.c:929-940` only kills fd/cwd holders), write-fault
   page 0 (creates the COW shadow = fs->first_ba), print READY, sleep 20.
2. Operator kills nfsd/mountd/rpcbind during the window.
3. Victim reads the pager-backed page at 16MB-4K: the fault descends
   shadow → vnode object → `vm_pager_get_page()` → NFS RPC fails on the dead
   server → `nfs_softterm()` sets error = EINTR (`sys/vfs/nfs/nfs_socket.c:1994`)
   and **EINTR == 4 == VM_PAGER_ERROR** (`sys/vm/vm_pager.h:87`) → the
   buggy error tail runs.
4. Victim exits → shadow object teardown hangs.

Stock kernel observations (`baseline_stock.txt`, process pid 907):

```
nfs server localhost:/export: not responding
vnode_pager_getpage: I/O read error
vm_fault: pager read error, pid 907 (victim)      <- the buggy tail (vm_fault.c:2218)
nfs send error 61 for server localhost:/export
warning: refcount_wait objtrm1: long wait          <- every 60s, forever
warning: refcount_wait objtrm1: long wait
warning: refcount_wait objtrm1: long wait
$ ps -axo pid,stat,wchan,comm
907 D1E    objtrm1  victim                         <- still hung 10+ minutes later
```

The process is permanently wedged in uninterruptible D-state inside
`vm_object_terminate()` → `vm_object_pip_wait("objtrm1")` (`sys/vm/vm_object.c:762`);
`_refcount_wait` (`sys/kern/kern_refcount.c:59-77`) never gives up. kill -9
cannot remove it. The vnode object's count separately wrapped to 0xFFFFFFFF
(double decrement), wedging its later termination in the vnode-recycle path.

## Why (line-level)

Fault state: pip was added on `fs->first_ba->object` (the shadow) at entry
(`sys/vm/vm_fault.c:1859`) and on the backing vnode object at descent
(`vm_fault.c:2347`). The error tail then does:

* `vm_fault.c:2241` `vm_object_pip_wakeup(object)` — `object` is the
  **terminal/backing** object (`vm_fault.c:2124`), so the backing pip goes
  1→0, and
* `vm_fault.c:2242` `unlock_things(fs)` → `cleanup_fault()` at
  `vm_fault.c:277` executes `vm_object_pip_wakeup(fs->ba->object)` — the
  **same object again** → 0→0xFFFFFFFF (u_int wrap),
* the shadow's pip (+1 from line 1859) is **never** woken on this path.

Every other error/return path in `vm_fault_object()` wakes
`fs->first_ba->object` (lines 1888, 1909, 1931, 2000, 2007, 2016, 2045,
2107, 2119, 2183 — line 2241 is the sole wakeup that targets `object`).
The success protocol relies on `cleanup_fault()` releasing the terminal
object's pip — the tail must release the FIRST object's.

## Exploit chain

DoS only: one pager error on a backing object during a shadowed (COW)
fault permanently wedges the faulting process (unkillable D-state) and
corrupts the vnode object's pip (vnode-recycle wedge). No memory corruption
or info leak. Unprivileged when the backing store is NFS: any user process
mmap'ing a file on a soft NFS mount whose server hiccups (the classic EIO)
hits it; no special setup for the attacker beyond normal mmap use.

## Fix validation

`fix.diff`: line 2241 `vm_object_pip_wakeup(object)` →
`vm_object_pip_wakeup(fs->first_ba->object)`.

* baseline (stock): `vm_fault: pager read error` + repeated
  `refcount_wait objtrm1: long wait` + pid stuck `D1E objtrm1` forever.
* patched (same guest, `make nativekernel` rebuild with both DF-2685 and
  DF-2686 fixes, rebooted): same trigger sequence — fault still fails (the
  pager error is genuine), but the process **exits cleanly**; no objtrm1
  wchan in ps; no long-wait warnings in dmesg. See `patched_run.txt`.
