# DF-5120 — VERDICT

**Status: untested** (not reproducible on the audit guest as configured; mechanism
proven from source, line by line, below).

## Reproduced?

No runtime reproduction was attempted:

1. The audit guest's QEMU command line (dfbsd-qemu/vm.sh:55-60) attaches only
   `virtio-blk` and `virtio-net`. There is no `virtio-rng-pci` device, so
   `vtrnd_probe()` never matches (`virtio_get_device_type(dev) != VIRTIO_ID_ENTROPY`,
   virtio_random.c:114) and the driver never attaches. Guest dmesg (2026-09-30)
   confirms: only `vtnet0` and `vtblk0` on virtio_pci0/1.
2. Even with the device present, the trigger under test is a *host-side* stall of the
   entropy backend (blocking EGD chardev, wedged vhost backend, out-of-band device
   reset). Demonstrating it requires host QEMU reconfiguration (add
   `-device virtio-rng-pci` with a blockable backend) plus mid-flight fault injection.
   In the virtio threat model the host is trusted, so this is not a guest-attacker
   path — the finding is filed as a fault-tolerance/hardening defect, severity Low.

## Why the mechanism is certain (source trace)

- `vtrnd_attach()` → `callout_reset(&sc->vtrnd_callout, 5*hz, vtrnd_timer, sc)`
  (virtio_random.c:142); `vtrnd_timer()` → `vtrnd_harvest(sc)` then unconditional
  re-arm (virtio_random.c:228-229). Callout is MP-safe (`callout_init_mp`,
  virtio_random.c:131) and executes in the per-CPU softclock thread
  (sys/kern/kern_timeout.c).
- `vtrnd_harvest()`: `virtqueue_empty()` gate (virtio_random.c:205), single writable
  4-byte stack descriptor (virtio_random.c:200-207), `virtqueue_notify(vq, NULL)`
  (virtio_random.c:214), then `virtqueue_poll(vq, NULL)` (virtio_random.c:215).
- `virtqueue_poll()` = `while ((cookie = virtqueue_dequeue(vq, len)) == NULL);`
  (virtqueue.c:578-579) — a spin with **no exit unless the host posts a used
  descriptor**. Its own comment states it exists for the virtio-blk dump path
  (virtqueue.c:577). If the host never completes, the softclock thread spins
  indefinitely → timer starvation on that CPU, total guest hang (no panic, no dump).

## Negative results (classes hunted and killed in this file)

- **OOB in harvest**: sglist is 1 seg × 4 bytes into a 1-seg sglist
  (virtio_random.c:192-203, bounded); `virtqueue_enqueue(vq, &value, &sg, 0, 1)`
  passes needed==sg_nseg==1 (virtqueue.c:501-506 assert + runtime checks); the
  host-provided used-ring length is passed over (`virtqueue_poll(vq, NULL)` —
  NULL len, virtio_random.c:215), so no length trust anywhere.
- **Used-ring `id` OOB** (host-controlled `uep->id` → `vq_ring_free_chain`,
  virtqueue.c:559-567): malicious-host only; host is trusted; virtio core file was
  audited clean this pass — not a virtio_random.c defect.
- **Queue-size `& (nentries-1)` masking**: `virtqueue_alloc()` rejects non-power-of-2
  host sizes (virtqueue.c, `powerof2(size)` check).
- **Detach/UAF (hunt item c)**: `vtrnd_detach()` → `callout_terminate()`
  (virtio_random.c:158) is a *synchronous* cancel: `_callout_cancel_or_stop(...,
  CALLOUT_CANCEL, sync=1)` sleeps until an executing callback completes and its
  CALLOUT_PREVENTED semantics block re-arm from inside the callback
  (sys/kern/kern_timeout.c:1041-1075). The vq is freed only afterwards via
  `vtpci_child_detached()` → `vtpci_release_child_resources()` → 
  `vtpci_free_virtqueues()` (virtio_pci.c:361-366, 1096-1117), i.e. strictly after
  child detach returns. No window.
- **Attach unwind (hunt item b)**: on `vtrnd_alloc_virtqueue()` failure the unwind
  calls `vtrnd_detach()` (virtio_random.c:136-146); the callout was never armed
  (`toc == NULL` → immediate return, kern_timeout.c:_callout_cancel_or_stop line
  ~1032); softc is zeroed by newbus so `vtrnd_vq == NULL`; partially-allocated vqs
  are cleaned by the PCI layer's child-detached path. No leak, no UAF.
- **Entropy accounting (hunt item d)**: `add_buffer_randomness_src(...,
  RAND_SRC_VIRTIO=0xa)` (virtio_random.c:217-218) indexes
  `inject_counter[srcid & 255]` (sys/sys/csprng.h:38) and `src_pool_idx[src_id &
  0xff]` in `csprng_add_entropy()` (sys/kern/subr_csprng.c) — all masked, arrays are
  256/32 entries. No OOB. (Side note for a future kern_nrandom.c audit:
  `add_buffer_randomness_src()` mis-splits buffers >256 bytes — passes full `bytes`
  per 256-byte advance, kern_nrandom.c:655-667 — unreachable from vtrnd which always
  passes exactly 4.)
- **Module state (hunt item e)**: modevent is a no-op returning 0 for
  LOAD/UNLOAD/SHUTDOWN (virtio_random.c:92-108); kldunload routes through
  device_detach → the synchronous callout_terminate above. Clean.

## Fix validation

Not performed: finding is Low/unverified, guest lacks the device, and building a
single-fix kernel cannot exercise a host-side stall in this harness.
`fix_status: not_testable`. The proposed `fix.diff` is minimal and confined to
virtio_random.c; on timeout it stops harvesting (the still-queued descriptor keeps
`virtqueue_empty()` false, which already gates every future harvest at
virtio_random.c:205-206), converting a permanent softclock wedge into a benign
"no virtio entropy harvested" degradation.

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