# DF-2685 — mlock()/mlockall() wiring faults zero-fill non-resident pages

## What

`mlock()` (and every user-wire / kernel-wire fault that goes through
`vm_fault_wire()`) on memory whose pages are **not currently resident**
(swapped-out anonymous memory, or file pages reclaimed from the vnode object)
**returns success but presents ZERO-FILLED pages** instead of paging the data
back in. Silent, unreported memory destruction in the faulting process.

Root cause: `TRYPAGER()` at `sys/vm/vm_fault.c:383-385` disables the pager for
**every** fault with a `VM_FAULT_WIRE_MASK` flag:

```c
#define TRYPAGER(fs)	\
		(fs->ba->object->type != OBJT_DEFAULT &&		\
		(((fs->fault_flags & VM_FAULT_WIRE_MASK) == 0)))
```

so `vm_fault_object()` (vm_fault.c:1845) never calls `vm_pager_get_page()`
for a wiring fault; the backing-chain walk terminates at the terminal object
and executes `vm_page_zero_fill(fs->mary[0])` at `sys/vm/vm_fault.c:2327`,
destroying the mapping's view of the data. `pmap_enter()` then installs the
zero page (wired), so subsequent accesses see zeros with no fault.

POSIX requires mlock to make the pages resident *with their contents*.

## Reproduce

```
./build.sh          # cc -O2 -o /tmp/mt/mlockswap mlockswap.c
./run.sh            # ~3-4 min (dirties 4.6GB to force ~1GB into swap)
```

Baseline (stock kernel) — `baseline_stock_run.log`:
```
/dev/vbd0s1b        4096M     1026M    3070M    25%    Interleaved
mismatch off=0 i=1 got=00000000 want=00000001
mlock-verify: bad=16253937 of 16777216 words
BUG REPRODUCED: mlock zero-filled swapped pages (silent data destruction)
rc=2
```

Patched kernel (fix.diff applied, rebuilt) — `patched_run.log`: `OK: data intact after mlock`, rc=0.

## Reachability / threat

* On x86-64 stock DFly, `mlock()`/`mlockall()` are gated by
  `caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)` (sys/vm/vm_mmap.c:1031,
  the `#else` branch — `pmap_wired_count` is not defined on pc64), so the
  direct caller must be root / hold that capability. Verified live: plain
  user `mlock` => EPERM.
* The victims are therefore **privileged processes that wire memory** —
  exactly the population that uses mlock (key material in sshd/gpg-agent/
  openvpn/tor style daemons, mlockall'd services). Under memory pressure
  (which any local user can create by allocating), their swapped pages are
  silently zeroed at the next mlock/mlockall of the range: authentication
  keys, database buffers, disk-encryption keys become zeros with no error.
* `mlockall(MCL_FUTURE)` + `brk()` extension wires new heap pages through the
  same path (`sys/vm/vm_unix.c:151`), and NVMM wires guest RAM through
  `vm_map_kernel_wiring()` (`sys/dev/virtual/nvmm/nvmm_dragonfly.c:193`) —
  the same TRYPAGER hole.
* On platforms/configurations where mlock is not root-gated (i386-style
  `pmap_wired_count` builds enforce only RLIMIT_MEMLOCK), any user limited
  by RLIMIT triggers it directly.

## Files

* `mlockswap.c`   — trigger source (self-verifying)
* `build.sh`/`run.sh` — exact commands
* `baseline_stock_run.log` — full stock output (16.25M/16.77M words zeroed)
* `patched_run.log` — output on fix-validated kernel
* `env.txt` — guest environment
* `fix.diff` — the fix (TRYPAGER allows VM_FAULT_USER_WIRE)
* `verdict.json`, `manifest.json`
