# DF-2879 VERDICT — phantom swap tail from swaponvp() stripe round-up

## Status: REPRODUCED (impact: dos; system-wide pageout livelock), fix validated

## Root cause (path:line)

`sys/vm/vm_swap.c:swaponvp()`:

* :332-333 — device size is converted to PAGE_SIZE blocks (`nblks`, real size, floored to a page multiple).
* :365-366 — `aligned_nblks = (nblks + SWB_DMMASK) & ~SWB_DMMASK` **rounds UP** to a `SWB_DMMAX` (64-page = 256 KiB) stripe multiple.
* :367 — `sp->sw_nblks = aligned_nblks` (the per-device bound later used by `swapdev_strategy`, :128).
* :377-383 — the free-loop hands whole 64-page stripes `[SWB_DMMAX, aligned_nblks)` to the global `swapblist` as allocatable swap and adds them to `vm_swap_size`/`vm_swap_max`. Because `aligned_nblks` is a round-UP of the real size, the final stripe's tail (`aligned_nblks - nblks` ∈ [1..63] pages) **does not exist on the device**. The `blk = min(aligned_nblks - dvbase, SWB_DMMAX)` clip is dead code for an aligned value, so nothing clips the phantom tail. (FreeBSD's ancestor code clipped to the *real* `nblks`.)

## Consequence chain (verified on the guest)

1. Registration: a 266,240-byte vn (65 pages = 520 512-blocks) is registered as
   1024 512-blocks; `vm.swap_size` gains 64 free pages of which exactly **1**
   is real (device stripe 0 `[0,64)` is reserved by design; the only real page
   in the freed stripe `[64,128)` is page 64).
2. `swp_pager_getswapspace()` (sys/vm/swap_pager.c:532-568) allocates the
   phantom blocks from the blist like any other.
3. `swapdev_strategy()` (sys/vm/vm_swap.c:92-155) passes them: its only bound
   check is `nblkno + sz > sp->sw_nblks` (:128) against the inflated
   `sw_nblks`.
4. The disk layer rejects the beyond-device bio (`dscheck` → EINVAL;
   subr_disk.c:1240-1246 / subr_diskslice.c) — the bio completes with
   `B_ERROR`.
5. `swp_pager_async_iodone()` (sys/vm/swap_pager.c:1855-1865 prints
   "swap_pager: I/O error - pageout failed ... error 22"; :1965-1994) drops the
   swap assignment (SWM_FREE, block leaks out of the blist), re-dirties and
   re-activates the page.
6. The pageout daemon picks the still-dirty page again → new allocation →
   error → repeat. Observed **25,712 / 18,215** error iterations in two runs;
   the loop never terminates, the guest stops responding (`dmesg` fails with
   `Cannot allocate memory`, ssh hangs, `vm.sh status` → down). With the
   phantom device as the *only* swap, effectively 100 % of pageout targets are
   phantom, so the system cannot free any anonymous memory at all.

No memory-safety consequence: read errors mark the page `m->valid = 0`
(swap_pager.c:1947) → SIGBUS, and write errors re-dirty the page in place
(:1980-1993); the failure mode is availability, not corruption or disclosure.

## Reproduction (fresh `vm.sh reset with-src`, stock kernel)

See `README.md` / `trigger.sh` / `run.sh` / `run.log` / `wedgelog.txt`.
Both runs wedged the guest identically at interleaved offsets 1368064 and
1433600 (device pages 78 and 94 of a 65-page device — pure phantom region).

## Fix validation

`fix.diff` replaces the round-up with the real page-truncated count
(`aligned_nblks = nblks`), making the pre-existing `min()` clip in both the
swapon free-loop (vm_swap.c:378) and the swapoff fill-loop (vm_swap.c:540)
effective, exactly as in FreeBSD's ancestor code. After building
`make -j6 nativekernel KERNCONF=X86_64_GENERIC` in the guest and rebooting:

* `trigger.sh`: vn1 registers its real 520 blocks; `vm.swap_size` gains
  exactly 1 page. (see fix_run.log)
* `run.sh` (same 5.2 GB hog, primary swap removed): **zero**
  "swap_pager: I/O error - pageout failed" lines; the pager reports
  swap-full and the guest remains responsive; `vm.sh status` → up throughout.

## Threat model

`swapon` requires `SYSCAP_RESTRICTEDROOT` (vm_swap.c:202), so the trigger is a
root configuration action — but the consequence is a full-system availability
loss (unkillable pageout livelock) from a *plausible* configuration (any swap
device whose size is not a 256 KiB multiple — e.g. an oddly-sized NFS swap
file, a vn image, or an unaligned partition), not an obviously-invalid one.
There is no privilege-boundary crossing.
