# VERDICT — DF-4623

**Status: reproduced (kernel panic, deterministic)** — and the one-line fix is
validated (panic gone, corruption rejected, normal operation intact).

## Root cause (code trace)

`virtqueue_dequeue()` reads the completion id straight from device-writable
shared memory and hands it to `vq_ring_free_chain()` with **no runtime
ownership validation** — every check on that path is a `VQASSERT` (KASSERT),
which is a no-op on production kernels:

- `sys/dev/virtual/virtio/virtio/virtqueue.c:559` — `desc_idx = (uint16_t)uep->id;`
- `:563` — `vq_ring_free_chain(vq, desc_idx);` runs *before* the cookie fetch
- `:779` — `VQ_RING_ASSERT_VALID_IDX` (bounds; debug-only) — this is the check
  DF-1767 is about
- `:786-787` — `vq->vq_free_cnt += dxp->ndescs; dxp->ndescs--;` — for an
  already-freed head `ndescs == 0`, so the decrement **underflows to 65535**
- `:790-794` — the `F_NEXT` walk decrements further (65535 → 65533 for a
  3-desc chain) with **no termination bound**
- `:796` — `VQASSERT(dxp->ndescs == 0, "failed to free entire desc chain")` —
  **live panic on INVARIANTS kernels; silent no-op in production**
- `:803-804` — `dp->next = vq->vq_desc_head_idx; vq->vq_desc_head_idx = desc_idx;`
  — the tail of the stale chain is relinked onto the free-list head, producing
  a **cycle** (`desc[tail].next == head`), and `vq_free_cnt` has already been
  inflated by the stale `ndescs`.

A duplicate id is *not* an out-of-bounds index — DF-1767's bounds problem is
orthogonal and not re-reported here. This is the missing **ownership** check:
the id refers to a descriptor the guest no longer has in flight.

## Post-corruption state (production kernels, from the on-guest projection)

After one duplicate id (chain head 0, 3 descriptors, 64-entry vq):

```
dxp->ndescs underflows to 65533 after 2 NEXT hops
desc[2].next = vq_desc_head_idx(0) -> CYCLE, head=0 while chain is idle
next virtqueue_enqueue() terminates at desc[2].next == 0 => head NEVER advances
=> every new request is handed desc 0..2 again, vq_descx[0].cookie overwritten
   while the previous request is IN FLIGHT (completion mis-attribution)
with F_NEXT forged on desc[2], vq_ring_free_chain(0) walks >256/64 entries
   without terminating => permanent kernel spin with serializer held (hang)
```

Driver-level consequences of mis-attributed completions:
- `virtio_blk.c:914-948` `vtblk_vq_intr()` immediately dereferences the
  returned cookie (`req->vbr_bio`) and finishes + recycles that request; a
  stale/aliased cookie finishes the **wrong** bio and re-queues a request
  object that is still referenced elsewhere (bio double-completion /
  use-after-free at the block layer), or leaks it forever (channel wedge).
- `if_vtnet.c:1389/1649/1702` — same shape for RX/TX mbufs (leak or
  stack re-injection of a freed mbuf).

## On-guest reproduction (baseline)

Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (INVARIANTS), boots on vtblk0.
The PoC kld (`df4623.c`) allocates a *private* virtqueue through the kernel's
own `virtqueue_alloc()` on the real vtblk0 child device (queue index 63 is
never registered with the host), runs a normal enqueue/complete/dequeue cycle
(healthy: cookie returned, free count restored), then writes a **duplicate**
used-ring entry — byte-for-byte what a malicious host DMAs — and calls the
real `virtqueue_dequeue()`:

```
[df4623] FIRE: forging DUPLICATE used id=0 and calling the real virtqueue_dequeue()...
panic: vq_ring_free_chain: df4623-probe - failed to free entire desc chain
vq_ring_free_chain() at vq_ring_free_chain+0xed
virtqueue_dequeue() at virtqueue_dequeue+0x53
```

Guest wedged in ddb; ssh died. Full serial capture: `panic.txt`.

## Fix validation

`fix.diff` adds (a) a runtime ownership gate in `virtqueue_dequeue()` —
reject (warn + drop) any used id that is out of bounds **or** whose
`vq_descx[id].cookie == NULL || ndescs == 0` (i.e. not in flight); and
(b) a defensive bound on the `F_NEXT` walk in `vq_ring_free_chain()` so a
host-forged cyclic chain cannot spin forever.

Applied to the guest's `/usr/src`, `make nativekernel && make installkernel`,
reboot (`uname -v` → `#1: Tue Sep 22 07:43:42 UTC 2026`), identical PoC fire:

```
[df4623] FIRE: forging DUPLICATE used id=0 and calling the real virtqueue_dequeue()...
vtblk0: df4623-probe: dropping bogus used entry id=0
[df4623] dup dequeue RETURNED 0 ..., free=64 nentries=64 head=0     <- state CONSISTENT
[df4623] ... vq_descx[0].cookie now 0xbbbb0002 ...                  <- B's cookie kept,
                                                                      no aliasing with C
```

No panic, ring state consistent, legit completion path still returns the
correct cookie (`0xaaaa0001`), module unloads cleanly. The baseline panic is
gone under the exact same trigger.

## Impact ceiling

Triggering requires the host (VMM) to emit a bogus used-ring entry — the same
trust boundary as DF-1767 (malicious host / buggy device model / compromised
VMM; also reachable via any host-side ring corruption). Severity Medium,
bucket memcorrupt: guest-kernel DoS is trivial (deterministic panic with
INVARIANTS, silent ring corruption or hard hang in production), and the
production-mode descriptor aliasing hands drivers wrong completion objects
(bio/mbuf lifetime confusion), though full guest code execution from this
primitive alone was not pursued given the host already owns guest memory in
this threat model.

## Artifacts

- `df4623.c` — PoC kld (safe/fire modes)
- `build.sh`, `Makefile` — build
- `run.log` — baseline run transcript (safe + fire + panic)
- `panic.txt` — serial console capture of the panic
- `run.2.log` / `fix_run_full.log` — fixed-kernel fire run (rejected, no panic)
- `fix_build.log` — fixed-kernel build log (nativekernel)
- `kmod_build.log` — kld build log
- `fix.diff` — the validated fix
- `env.txt` — guest/kernel/compiler

## 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.
