# DF-5815 — vm.max_launder accepts negative values: laundering silently disabled, pagedaemon livelock (reproduced DoS)

**Finding:** `vm.max_launder` (SYSCTL_INT, CTLFLAG_RW, root) is used
without any range validation in `vm_pageout_scan_inactive()`
(sys/vm/vm_pageout.c:828-836):

```c
max_launder = (long)vm_max_launder / PQ_L2_SIZE;   /* negative for vm<0 */
if (pass) max_launder *= 2;
max_launder = (max_launder + MAXSCAN_DIVIDER - 1) / MAXSCAN_DIVIDER;
if (max_launder <= 1) max_launder = 1;
if (max_launder >= vm_max_launder / 16)            /* ALWAYS true when   */
        max_launder = vm_max_launder / 16 + 1;     /* vm_max_launder < 0 */
```

For `vm_max_launder <= -16` the final cap *overwrites the floor* and the
per-queue launder budget goes negative, so `vm_pageout_page()`'s branch

```c
} else if (*max_launderp > 0) {        /* sys/vm/vm_pageout.c:1171 */
```

is never taken: **no dirty page is ever paged out again**. The pagedaemon
can then never rebuild free+cache from dirty memory; it spins forever
(`avail_shortage` never retires, `pass` grows, "Continuous paging"
warning once per minute), every allocation that hits `vm_wait()` blocks
forever, and the machine wedges until rebooted (the sysctl is
boot-reset, but a wedged box needs a hard reset).

## Reproduced (2/2 deterministic), impact=dos

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), 4 GB RAM,
8 GB swap, HAMMER2 root. Hog = unprivileged (`maxx`, uid 1001).

### Control (stock `vm.max_launder: 4076`)
- `df5815_hog 3400 60` (3.4 GB anon dirty hog for 60 s):
  swap pager pageouts **39047**, hog **completed in 61 s** ("hog done"),
  ssh alive throughout, zero "Continuous paging" console lines,
  `dmesg | grep -c "Continuous paging"` → 0.

### Attack (`sysctl vm.max_launder=-32` as root)
- Run #1 (3400 MB hog): swap pager pageouts **frozen at 205** (nothing
  laundered), `pagedaemon: WARNING! Continuous paging for 1 minutes`
  printed **5×** (once per minute, indefinitely), ssh dead ("Connection
  timed out during banner exchange") for 6+ minutes, guest unrecoverable
  → hard reset. Console captured in `panic_wedge_console.txt`.
- Run #2 (deliberate, same params): sysctl set 03:38:48, hog started
  03:38:49; by ~03:41 ssh dead and 3× "Continuous paging" on the serial
  console (`attack2_console.txt`) → hard reset.

Intermediate observation (2900 MB hog, -32): system idles at 80 MB free
with daemon counters frozen (no wakeups, nothing laundered) — the wedge
requires free+cache exhaustion, then nothing recovers.

## Threat
Root must misconfigure one tunable (or a management agent feeding it a
bad value); afterwards *any unprivileged user's ordinary memory usage*
freezes the whole machine (denial of service against all tenants), and
remote administration is lost until power-cycle. Severity Low (requires
root for the write) but deterministic and total.

## Files
- `df5815_hog.c` — unprivileged anon hog (build.sh/run.sh)
- `observe.sh` — guest-side counter/vmstat snapshot helper
- `observe_baseline_stock.log` / `observe_attack.log` — captured snapshots
- `panic_wedge_console.txt` — run #1 serial console (5× warning, wedge)
- `attack2_console.txt` — run #2 serial console
- `control_vs_attack.txt` — side-by-side summary
- `fix.diff` — clamp `vm_max_launder` at the use site (applies clean)
- `verdict.json` / `manifest.json`
