# DF-0945 — swapoff_one blist race (missing vm_token)

## Summary

`swapoff_one()` (`sys/vm/vm_swap.c:441`) mutates the global `swapblist` radix
tree via `blist_fill`/`blist_destroy`/`blist_resize` while holding only
`swap_mtx`. The swap pager (`swp_pager_getswapspace`/`swp_pager_freeswapspace`)
mutates the SAME tree via `blist_allocat`/`blist_free` while holding only
`vm_token`. Neither lock nests the other → concurrent radix-tree mutation →
tree corruption. `swaponvp()` correctly acquires `vm_token` before `swap_mtx`,
but `sys_swapoff`/`swapoff_one` do not acquire `vm_token` at all.

## Impact

- **DoS (panic/OOM)**: corrupted blist → false "swap full" → OOM killer, or
  `panic("freeing free block")` on double-free. Confirmed live.
- **Potential cross-process info leak/corruption**: double-allocation of swap
  blocks could map two processes' pages to the same block. Not demonstrated
  in this run (requires longer race or INVARIANTS-OFF kernel).

## Threat model

Root initiates `swapoff` while an unprivileged user drives pager pressure.
The direct trigger is root-gated, but the corruption can cross privilege
boundaries if swap blocks are double-allocated across processes.

## How to reproduce

### Build (as maxx)

```sh
cc -O2 -o stress stress.c
```

### Run (as root)

```sh
# Orchestrated race: creates vn1 (8MB secondary swap), launches stressor,
# waits for swap activity, then toggles vn1 swapoff/swapon 200 times.
sh run_poc.sh /dev/vbd0s1b 8 1 3250 120

# Or the focused experiment with per-iteration tracking:
sh exp3.sh 8 3250 200
```

### Expected output (bug present, unpatched #0 kernel)

The serial console (`dfbsd-qemu/boot.log`) shows:
```
swap_pager_getswapspace: swap full allocating 16 pages
```
despite `vm.swap_free` showing ~2 GB free. This is the blist corruption
signature — `blist_allocat` returned `SWAPBLK_NONE` because the tree was
corrupted by concurrent `blist_fill`/`blist_resize` from `swapoff_one`.

### Expected output (FIXED, patched #1 kernel)

Zero "swap full" messages during 200 toggle iterations. `vm.swap_anon_use`
remains stable. System stable throughout.

## Files

| File | Description |
|------|-------------|
| `stress.c` | Unprivileged pager stressor (slow-dirty + random-access thrash) |
| `swap_toggle.sh` | Root-side swapoff/swapon loop |
| `run_poc.sh` | Full orchestrator (two-device race with cyclic pressure) |
| `exp3.sh` | Focused experiment with per-iteration swapoff rc tracking |
| `build.sh` | Build script |
| `run.sh` | Run script (root entry point) |
| `fix.diff` | Standalone git-apply-able fix |
| `VERDICT.md` | Full analysis and verdict |
| `run_baseline.log` | Unpatched #0 kernel race output (200 iters, 6 "swap full") |
| `fix_run.log` | Patched #1 kernel race output (200 iters, 0 "swap full") |
| `boot_run1.log` | Serial console from the baseline race |
| `panic.txt` | Swap-full messages extracted from boot.log |
| `fix_build.log` | Full single-fix kernel build output |
| `env.txt` | Guest environment |
