# DF-4423 VERDICT — REPRODUCED (silent success for unperformed I/O on vtblk detach; stale kernel memory returned as data)

## Root cause (line-accurate)

`sys/dev/virtual/virtio/block/virtio_blk.c:1202-1207`:

```c
static void
vtblk_finish_bio(struct bio *bio, int error)
{
	biodone(bio);          /* <-- `error` is dropped; B_ERROR/b_error never set */
}
```

Every caller passes a meaningful error that is silently discarded:

- `virtio_blk.c:1078` — `vtblk_drain_vq()`: in-flight/queued requests at
  detach are completed with ENXIO that never reaches the bio.
- `virtio_blk.c:1103` — `vtblk_drain()`: bioq leftovers, same.
- `virtio_blk.c:532,543` — `vtblk_strategy()` ENXIO path (DETACH set /
  sc==NULL): same.

In DragonFly, `biodone()` without `B_ERROR` in `bp->b_flags` is a
*successful* completion regardless of `b_error` (error is only honored
when B_ERROR is set). Thus any I/O that the device never performed is
reported to the buffer/page cache and to userspace as fully successful.

## Trigger surface

`vtblk_detach()` runs on: driver kldunload, device unplug/hot-remove,
and the attach-failure path (`vtblk_attach` fail: `vtblk_detach(dev)`).
Detaching while bios are in the bioq/virtqueue (or arrive during the
detach window) yields silent-success completions. Realistic events:
host-side `device_del`/disk removal, driver reload, kldunload on a busy
data disk, attach failure after partial setup.

## Reproduction (private QEMU, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC)

Environment: private QEMU (KVM) instance; system disk on IDE
(`hammer2:/dev/ad0s1d` root) so that `vtblk0` is a non-root 256MB
virtio-blk data disk; guest `DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu
Jul 2 06:02:54 UTC 2026 ... X86_64_GENERIC x86_64`; virtio_blk compiled
into the kernel; INVARIANTS on.

Because virtio_blk is built-in (not unloadable) and DragonFly has no
PCI/PCIe hotplug handler in `sys/bus/pci/pci.c` (no SLOTCTL/ACPI-PHP
handling; monitor `device_add` is silently ignored by the guest), the
detach was driven by a 30-line helper KLD (`dfdetach.ko`) that performs
`device_detach(devclass_get_device(devclass_find("vtblk"), 0))` — the
identical `vtblk_detach()` code path used by kldunload/unplug.

Decisive run (`killshot2.sh`, see README):
- 96 threads × one 2MB `pread` each = 201,326,592 bytes requested; every
  thread's read returned **exactly at the 5.0s detach instant**
  (`read_ms ≈ 4850-5050` for all 96 threads — they were mid-flight,
  the natural cold-cache duration of this volley is ~96s per the earlier
  control), **with `errors=0`** (run_ks2.out).
- QEMU `info blockstats` delta across the same window: **17,956,864
  bytes actually transferred by the device (8.9%)**.
- Therefore **185MB of the "successful" reads were fabricated**: their
  buffers were completed by the drain without any DMA into them. The
  bytes userspace received are recycled kernel buffer/page contents —
  leak samples show `0xAA` fill-data and `0x5A` marker-data patterns
  from *previous* boots' disk I/O resident in stale physical memory
  (leak_sample_0.bin / leak_sample_1.bin). This is simultaneously:
  1. **silent data corruption** (a filesystem mounted on the device
     receives blocks that were never read/written, reported valid), and
  2. **kernel memory disclosure** (stale kernel pages handed to
     userspace as file data — cross-boot/cross-tenant content in cloud
     reuse scenarios).
- Controls: identical volley with no detach = all chunks real, 95.81s
  (cold), and every earlier run where the detach missed the volley
  showed 100% real data — ruling out tester artifacts (a signedness bug
  in the classifier was found and fixed mid-investigation; the control
  proves the final classifier correct).

## Why the write path matters equally

Queued/in-flight WRITEs drained at detach are biodone'd as success
without the device having received the data — writes silently lost with
no error. (Read demonstration chosen for the pack because fabricated
data is directly observable.)

## Not a use-after-free

Post-detach reads fail cleanly (`dd: /dev/vbd0: Bad file descriptor`),
so the demonstration is not polluted by the separate, already-known
DF-1605 family issue.

## Recommended fix

Propagate the error (fix.diff):

```c
static void
vtblk_finish_bio(struct bio *bio, int error)
{
	struct buf *bp;

	if (unlikely(error != 0)) {
		bp = bio->bio_buf;
		bp->b_flags |= B_ERROR;
		if (bp->b_error == 0)
			bp->b_error = error;
	}
	biodone(bio);
}
```

Also set `bp->b_resid = bp->b_bcount` for drained bios so short/failed
transfers are not mistaken for complete ones.

fix validation: not built in this run (non-memory-corruption class;
kernel rebuild not exercised) — fix_status `not_testable`, baseline
documented above.

## Impact classification

status=reproduced, impact=leak (stale kernel memory returned to
userspace as I/O data; the integrity half is silent fs corruption).
Severity Medium: requires privileged/host-side trigger (kldunload,
unplug, attach-failure), but the consequence is fabricated I/O with
zero error signaling on default configs.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
