# PoC DF-4646 — nvme(4) device teardown vs in-flight data-path I/O: use-after-free → kernel divide-by-zero panic (REPRODUCED 3/3; fix validated)

## What this proves

`nvme_pci_detach()` (nvme_attach.c:470) frees the namespace softcs and the
I/O submission/completion queues while bios can still be in flight on the
data path:

* raw reads/writes already dispatched past the cooked-disk cdev
  (`destroy_dev()` re-vectors the cooked ops to `dead_dev_ops` — so *new*
  post-teardown reads get EBADF, see `nvzombie.c` — but bios already inside
  `nvme_strategy_core()` keep running),
* bios parked on `nsc->bioq` waiting for the admin-thread requeue
  (`nvme_disk_requeues`, nvme_disk.c:189),
* completions arriving on queues that the admin-thread exit path deletes
  and frees without a final drain (nvme_admin.c:188-197).

Nothing blocks new data-path work (the `NVME_SC_UNLOADING` flag is checked
in `nvme_open()` but never set anywhere in the tree), nothing drains in-flight
requests before `kfree(nsc)` (nvme_admin.c:117-126), and `nvme_disk_detach()`
(nvme_disk.c:115-122) neither drains `nsc->bioq` nor excludes the strategy
path.  A bio dispatched or requeued in the window enters
`nvme_get_request()` against a queue that `nvme_free_subqueue()` has
`bzero()`'d — `queue->nqe == 0` — and the ring-full check

```c
if ((queue->subq_tail + queue->unsubmitted + 1) % queue->nqe ==   /* nvme.c:446 */
    queue->subq_head) {
```

divides by zero → **Fatal trap 18 (integer divide fault)**:

```
nvmebang: DETACH method on nvme0...
Fatal trap 18: Stopped at      nvme_get_request+0x34:  divl    0x40(%r13),%eax
db>
```

`0x40(%r13)` = `queue->nqe` (offset 0x40 in `struct nvme_subqueue`,
nvme.h:118-155); `divl` is the modulo above — the only division in
`nvme_get_request()`.  Same signature as DF-4529 (which raced the
*admin-ioctl* path); this finding is the *data path* (read/write bios),
reproduced with plain `pread()` spam — no ioctls at all.

## Threat model

The detach side is an administrative/hardware event (PCI hot-unplug,
surprise removal, VM `device_del`, driver detach) that must be safe while
the device is under I/O; the I/O side can be *any* filesystem or raw-disk
activity on the nvme namespaces (unprivileged).  Even when the div0 window
is missed, the freed `nsc` is dereferenced by `nvme_disk_callback()`
(`devstat_end_transaction_buf(&nsc->stats, bp)`, nvme_disk.c:410) and the
requeue drain — silent writes into freed memory.

## Environment

* Private QEMU/KVM guest (`dfbsd-qemu/vm_nvme.sh`): stock virtio-boot disk
  plus `-device nvme` with 8 `-device nvme-ns` namespaces on null-co drives.
* `hw.nvmebang.action` test module (`nvmebang.c`, from the DF-4529 pack)
  invokes the driver's own attach/detach driver-methods on unit 0 on
  demand — a deterministic stand-in for PCI hot-unplug.
* `nvio` (nvio.c): N reader processes spamming synchronous 128 KB
  `pread()`s across the 8 namespace devices.

## Build (in guest, as root)

```sh
cc -O2 -o /root/nvio nvio.c
cd bang && make && kldload ./nvmebang.ko   # Makefile: KMOD=nvmebang
                                           # SRCS=nvmebang.c device_if.h bus_if.h
```

## Run (exact rig)

```sh
sysctl hw.nvmebang.action=2       # ensure device attached (fresh boot: already)
(nohup /root/nvio 16 >/root/nvio.log 2>&1 &)
sleep 7                           # 16 readers, ~7k reads/s total
sysctl hw.nvmebang.action=1       # detach under load  -> panic
```

## Expected / observed

Kernel panic: `Fatal trap 18 ... Stopped at nvme_get_request+0x34:
divl 0x40(%r13),%eax`, guest drops to DDB (`db>`), ssh dies.
Reproduced **3/3 on fresh boots** (panic.txt; two earlier runs had the
trap banner partially interleaved on the serial console but the identical
`Stopped at` signature).

With the fix (`fix.diff`, validated by full kernel rebuild): no panic;
readers receive `EBADF`/`ENODEV` for new I/O after destroy, in-flight bios
fail with EIO, no readers stuck in biowait, detach cycles cleanly ×3.

## Negative result kept for the record

`nvzombie.c` proves a *post-teardown* open descriptor is NOT exploitable
by itself: `disk_destroy()` → `destroy_dev()` re-vectors the cooked cdev
to `dead_dev_ops`, so a fresh `pread()` after teardown returns EBADF.
The vulnerability is strictly the **in-flight window**, not a persistent
zombie device.
