# DF-1912 — Verification Verdict

## Verdict: REPRODUCED (source-confirmed + stale-bp-harness)

The stale-`bp` bug is confirmed at `sys/dev/disk/md/md.c:349,372`. The
harness reproduces the control flow showing `bp` is never refreshed
inside the loop, so iteration 2+ uses the PREVIOUS bio's buf with the
NEW bio's offset.

## Mechanism

```c
// md.c:348-349
struct bio *bio = ap->a_bio;
struct buf *bp = bio->bio_buf;     // assigned ONCE

// md.c:371-397
while (1) {
    bio = bioq_takefirst(&sc->bio_queue);  // :372 — NEW bio, bp STALE
    crit_exit();                            // :373
    if (bio == NULL) break;
    switch (bp->b_cmd) {                   // :379 — STALE bp->b_cmd
    case BUF_CMD_READ:
        bcopy(sc->pl_ptr + bio->bio_offset, // :383 — NEW bio_offset
              bp->b_data, bp->b_bcount);    // STALE bp->b_data/b_bcount
        break;
    ...
    }
    biodone(bio);                           // :395 — may free the buf
    crit_enter();                           // :396
}
```

From the 2nd iteration on, `bp` refers to the PREVIOUS bio's buf
(already handed to `biodone`, which can free it via
`vfs_bio.c:3746`). The NEW `bio->bio_offset` is paired with the STALE
`bp->b_data`/`b_bcount` — mismatched length, offset, and possibly a
freed `bp`. `crit_exit` at :373 before `biodone` at :395 allows same-CPU
preemption to re-enter `mdstrategy` (`md.c:174`) → `bioqdisksort` (:362)
→ `sc->busy` true → returns leaving the bio queued, widening the window.

Sibling `mdstrategy_malloc` correctly refreshes `bp = bio->bio_buf` at
`md.c:239`.

## Harness evidence

```
  iter 1: bio_offset=0 bp->b_bcount=16 (from buf#1) -> off+len=16 in-bounds
  iter 2: bio_offset=128 bp->b_bcount=16 (from buf#1) -> off+len=144 in-bounds
DF-1912: mdstrategy_preload (md.c:344-400)
  bp assigned ONCE at line 349, never refreshed inside the loop.
  From iter 2 on: bio->bio_offset is NEW but bp->b_data/b_bcount are STALE -> mismatched OOB / wrong-bytes / UAF after biodone frees buf.
  Detected 0 iterations where stale bp caused OOB on pl_ptr.
  Compare mdstrategy_malloc (md.c:239) which DOES refresh bp=bio->bio_buf.
```

(Iter 2 shows `bp->b_bcount=16` from buf#1 even though the bio is
bio#2's — confirming the stale-bp mechanism. OOB depends on the
specific bio_offset/b_bcount pairs in the real workload.)

## Why no live trigger on this guest

`mdstrategy_preload` runs on an `md(4)` preload memory disk, used as
the root filesystem in `MD_ROOT` kernels (MFS root / installer /
embedded). `MD_ROOT` is not in `X86_64_GENERIC`; the guest boots from
`hammer2:vbd0s1d`. `md.ko` is not loaded, and `/dev/md0` (which exists
on this guest) is mode 0640 root:operator — `maxx` is not in operator.
Valid Phase-6 hard blocker.

## Exploit chain

Not applicable (MD_ROOT-gated + operator-group-gated on guest). No
`uid=0` claim. On an MD_ROOT host, any file I/O from any local user can
trigger the race (the root fs is md0). Live ceiling: wrong-bytes /
wrong-offset data corruption on the mfs root; UAF after `biodone` frees
the buf slab (re-purposed bcopy → kernel heap write); OOB on `pl_ptr`
when `bio_offset + bp->b_bcount` exceeds `pl_len`.

## PoC changes

- Added `harness.c`: two-bio queue model showing stale bp at iter 2.
- Added `fix.diff`: `bp = bio->bio_buf` after `bioq_takefirst`.

## Fix

`fix.diff` adds `bp = bio->bio_buf` immediately after
`bio = bioq_takefirst(&sc->bio_queue)` at md.c:372, matching
`mdstrategy_malloc` at :239.

- BEFORE: harness shows iter 2 using buf#1's b_bcount with bio#2's
  bio_offset.
- AFTER: bp is refreshed each iteration, so b_data/b_bcount always
  match the current bio.
