# DF-5787 evidence pack — stale `b_bio[12].bio_flags` survive buffer-cache recycling

## Finding (short)

`sys/kern/vfs_bio.c` never clears a buffer header's embedded `b_bio1.bio_flags`
when the buffer is released and later reacquired:

- `getnewbuf()` resets `bp->b_flags = B_BNOCLIP` only (sys/kern/vfs_bio.c:2095)
  and calls `reinitbufbio()`, which resets `bio_done`/`bio_offset` but **not**
  `bio_flags` (sys/kern/vfs_bio.c:743-751).
- `getblk()`'s found-in-cache path performs no bio-flag hygiene either
  (sys/kern/vfs_bio.c:2746-2988).

The kernel itself documents that stale `BIO_DONE` is load-bearing:
`vn_strategy()` has `KKASSERT((bio->bio_flags & BIO_DONE) == 0)`
(sys/kern/vfs_bio.c:3385). Every in-tree completion path must clear the flags
by hand (`_biowait()` clears `BIO_DONE|BIO_SYNC` on success at :3284; the
breadcb/cluster_readcb callback contract puts clearing on the callback — see
hammer's `hammer_indirect_callback`, hammer_io.c:1640). Any forgetful or
abandoning completion path (callback bug, `biowait_timeout` abandonment)
permanently poisons the buffer header, and the *next* `bread()`/`bwrite()` on
that buffer:

1. panics at `vn_strategy` (:3385) on an always-KKASSERT kernel, or
2. on the `biowait` side, `_biowait()` observes the stale `BIO_DONE` and
   returns *before* the device I/O completes — the caller reads `b_data`
   while the device is still DMAing into it (torn data), then `brelse()`s the
   buffer with the I/O in flight, so the asynchronous `biodone()` later runs
   `bpdone()` on a requeued (possibly reused) buffer → queue corruption /
   double-accounting.

## Rig

KLD module `biopoison.c` (root-only, simulates the documented-but-undefended
caller-contract violation):

- Case 0 (control): `getblk` → clear `B_CACHE` → `bqrelse` → `bread` —
  must return 0 with no panic (proves rig mechanics).
- Case 1 (poison): `getblk` → clear `B_CACHE` **and leave `BIO_DONE` set**
  (exactly what `breadcb()`'s cached fast path does at vfs_bio.c:883, minus
  the callback's cleanup) → `bqrelse` → `bread`.

## Result (baseline, unpatched kernel)

```
biopoison: vp=0xfffff80116870500 v_type=1            (hammer2 file vp, /etc/fstab)
biopoison: === control run (no stale flag) ===
biopoison[0]: getblk -> bp=0xfffff8004f7442d0 b_flags=21000160 bio1.flags=00000000 B_CACHE=1
biopoison[0]: released (stale BIO_DONE=0), issuing bread()
biopoison[0]: bread returned 0 (no panic)
biopoison: === poisoned run (stale BIO_DONE across bqrelse -> getblk recycling) ===
biopoison[1]: getblk -> bp=0xfffff8004f7442d0 b_flags=21001164 bio1.flags=00000000 B_CACHE=1
biopoison[1]: released (stale BIO_DONE=1), issuing bread()
panic: assertion "(bio->bio_flags & BIO_DONE) == 0" failed in vn_strategy at /usr/src/sys/kern/vfs_bio.c:3385
Trace: vn_strategy <- breadnx <- bpio_case <- biopoison_handler <- module_register_init
```

Same buffer header (0xfffff8004f7442d0) in both cases — the flag crossed the
`bqrelse` → queue → `getblk` boundary. Verdict: **reproduced** (panic;
guest entered ddb, ssh session died).

## Build

```
# on the guest (kernel obj tree must exist):
mkdir -p /root/biopoison && cd /root/biopoison    # biopoison.c + Makefile here
make                                             # SYSDIR=/usr/src/sys in Makefile
# -> biopoison.ko
```

## Run

```
kldload /root/biopoison/biopoison.ko     # console/serial shows the output above
```

## Fix validation

`fix.diff`: assignment-style flag arming in `breadnx()`/`bwrite()`
(`bp->b_bio1.bio_flags = BIO_SYNC;` instead of `|=`), plus
`reinitbufbio()` clearing `bio_flags` for recycled headers. After building the
patched kernel in the guest (`make quickkernel && make installkernel`,
reboot), the identical module load runs both cases with **no panic** — see
VERDICT.md.
