# DF-2865 — pbuf reservation counters: unsynchronized check-then-decrement race

`sys/vm/vm_pager.c` — `getpbuf()` / `getpbuf_kva()` / `getpbuf_mem()` /
`trypbuf()` / `trypbuf_kva()`.

## The bug

The per-subsystem pbuf reservation counters (`pfreecnt`: `nsw_rcount`,
`nsw_wcount_sync/async`, `vp->v_pbuf_count`, `mp->mnt_pbuf_count`,
`nfs/smbfs/vinum` counters) are meant to cap how many pbufs one subsystem
holds concurrently.  The allocator gates on the counter **without any
lock** (vm_pager.c:418/463/512):

```c
while (pfreecnt && *pfreecnt <= 0) {      /* unlocked read */
        tsleep_interlock(pfreecnt, 0); ...
}
... bucket scan ...
spin_lock(&bswspin_kva[iter]);            /* per-BUCKET spinlock */
TAILQ_REMOVE(...);
if (pfreecnt)
        atomic_add_int(pfreecnt, -1);     /* :489-490 */
```

Because the gate and the decrement are not atomic together — and the
spinlock is per-bucket (`iter = mycpuid & BSWHMASK`), so two CPUs are
typically under *different* locks — N CPUs can each observe
`*pfreecnt == 1`, each pass the gate, and each decrement, driving a
counter that should never go below 0 to `1-N` and putting N pbufs in
flight for a reservation of 1.

Consequences:
1. Subsystem pbuf caps are violable (over-consumption of the shared
   kva/raw pools by up to ncpu-1 concurrent allocators).
2. Negative excursions make `getpbuf*` sleepers block on
   `*pfreecnt <= 0` while pbufs are actually free — an additional
   recovery delay stacked on the (separately filed, DF-0953) wake
   threshold defect.

Unprivileged reachability: `trypbuf_kva(&mnt_pbuf_count)` /
`getpbuf_kva(&mnt_pbuf_count)` run for every clustered file
read/write (sys/kern/vfs_cluster.c:924-927, 1525-1527); `nsw_rcount`
runs for swap-in under memory pressure (sys/vm/swap_pager.c:1447).

Distinct from DF-0953 (known): DF-0953 is the *release-side* wake
threshold in `relpbuf()`; this is the *allocator-side* check/decrement
atomicity.  Different function, different lines, different fix.

## PoC

`pbufres.c` — KLD harness.  Six kernel threads (one per CPU) hammer
`getpbuf_kva()/relpbuf()` against a reservation counter with cap 1,
tracking the minimum observed counter value and the maximum number of
simultaneous pbuf holders.

```sh
./build.sh          # build pbufres.ko on the guest
./run.sh            # load, spin 10 s, read stats, rescue, unload
```

Success criterion (stock kernel): `vm.pbres_neg > 0`,
`vm.pbres_min < 0`, `vm.pbres_maxinflight > 1`.
On a kernel with the cmpxchg reservation fix: all three clean
(`neg=0`, `min >= 1`, `maxinflight <= 1`).

`vm.pbres_bump=100` is harness plumbing only: it wakes sleepers left
gated by the known DF-0953 wake-threshold defect so the module can be
unloaded cleanly (the *stall* it relieves is DF-0953's, not this
finding's).

## Artifacts

- `run.2.log` — decisive stock-kernel run: `pbres_neg: 2`,
  `pbres_min: -1`, `pbres_maxinflight: 2`, console
  `PBUFRES: counter NEGATIVE (-1), cap=1`.
- `run.log` — first stock run (same numbers; the unload step timed out
  because cap=1 also exercises DF-0953's missed wakeup — 5 threads
  asleep while `vm.pbuf_kva_count: 512`, i.e. the pool 100 % idle).
- `fix.diff` — cmpxchg reservation (all five allocators) **and** the
  DF-2866 `pagertab[]` OBJT_MARKER slot (combined kernel build).
- `run.patched.log` — same harness on the fixed kernel.
