vtrnd_harvest runs unbounded virtqueue_poll() busy-wait inside the periodic softclock callout β a stalled host entropy backend permanently hangs the guest kernel
Summary
The harvest reuses the dump-path polling primitive (an infinite spin by design) in softclock context: a wedged host virtio-rng backend (stalling EGD chardev, vhost restart/migration, OOB reset) makes the per-CPU softclock thread spin forever - all callouts on that CPU starve, no panic, no dump, forced reset only; no shutdown disarm either. Host-side trigger (guest not attacker-reachable within the virtio trust model) - fault-tolerance defect. Bounded-10ms + graceful-disable fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5120 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw | |
| manifest.json | β | 1014 B | view raw |
DF-5120 β vtrnd_harvest: unbounded virtqueue_poll() busy-wait runs in the softclock callout thread
File: sys/dev/virtual/virtio/random/virtio_random.c
Class: robustness / denial of service (CWE-835 β loop with unreachable exit condition)
Severity: Low Β· Confidence: likely (mechanism is code-certain; trigger requires a host-side fault)
What the code does
vtrnd_attach()arms a periodic callout every 5 s:callout_reset(&sc->vtrnd_callout, 5 * hz, vtrnd_timer, sc)(virtio_random.c:142), re-armed unconditionally byvtrnd_timer()(virtio_random.c:229).vtrnd_harvest()enqueues one 4-byte writable descriptor pointing at a stack variable, notifies the device, then callsvirtqueue_poll(vq, NULL)(virtio_random.c:205-215).virtqueue_poll()is an unbounded busy-wait:
c
void *
virtqueue_poll(struct virtqueue *vq, uint32_t *len)
{
void *cookie;
/* We only poll the virtqueue when dumping to virtio-blk */
while ((cookie = virtqueue_dequeue(vq, len)) == NULL)
;
(sys/dev/virtual/virtio/virtio/virtqueue.c:572-580)
It was designed for the virtio-blk dump path (crash-dump I/O, where sleeping is
impossible and the device is assumed responsive). vtrnd reuses it from a periodic
callout that runs in the softclock thread.
Why that is a hazard
If the host entropy backend never advances the used ring, the softclock thread for the arming CPU spins forever with preemption effectively pinned:
- every future callout on that CPU starves (timers, watchdogs, protocol ticks);
- the guest becomes unusable β a hard hang with no panic, no watchdog, no recovery except a forced reset.
Realistic ways the used ring never advances while guest timers keep running:
- QEMU/KVM
virtio-rngbacked by a blocking EGD chardev on the host that stalls (this is a documented operational failure mode of virtio-rng backends). - Host-side device wedged/reset out-of-band while the guest keeps scheduling (vhost restart, live-migration stall of the rng backend, host suspend/resume race).
- Theoretical in-guest window:
vtpci_shutdown()(virtio_pci.c:347-349) forcibly stops the device while the vtrnd callout is still armed β vtrnd has no shutdown method to disarm it β but the shutdownβcpu_reset window is normally far shorter than 5 s, so this variant needs a stalled post-shutdown handler to matter.
The virtio threat model treats the host as trusted, so this is not exploitable by a guest-local unprivileged user; it is a fault-tolerance defect: one wedged host entropy backend turns into a full guest kernel hang instead of a degraded-but-alive system.
Why not reproduced here
The audit guest has no virtio-rng device at all (dfbsd-qemu/vm.sh attaches only
virtio-blk and virtio-net; guest dmesg shows no vtrnd), and reproducing the hang
requires host-side fault injection (a stalled rng backend), which is exactly the
trigger under test. Analysis is complete at the source level; see VERDICT.md.
Recommended fix (see fix.diff)
Bound the wait in vtrnd_harvest() (~10 ms cap using virtqueue_pending() +
DELAY()), and on timeout simply stop harvesting: the still-queued descriptor keeps
virtqueue_empty() false, which already gates every future vtrnd_harvest() call
(virtio_random.c:205-206), so the driver degrades to "no virtio entropy" instead of
hanging softclock.
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:
- The audit guest's QEMU command line (dfbsd-qemu/vm.sh:55-60) attaches only
virtio-blkandvirtio-net. There is novirtio-rng-pcidevice, sovtrnd_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: onlyvtnet0andvtblk0on virtio_pci0/1. - 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-pciwith 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), thenvirtqueue_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
idOOB (host-controlleduep->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 viavtpci_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 callsvtrnd_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 sovtrnd_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) indexesinject_counter[srcid & 255](sys/sys/csprng.h:38) andsrc_pool_idx[src_id & 0xff]incsprng_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 fullbytesper 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.
Fix verification
not_testableFix kernel not built/run: the finding is unverified Low severity, the guest has no virtio-rng device, and the fault under test is host-side; the diff is minimal and confined to virtio_random.c (uses DELAY already declared in sys/systm.h).
fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md (mechanism + trigger model); VERDICT.md (source trace virtio_random.c:142,205-215,229 and virtqueue.c:572-580; negative results incl. callout_terminate sync semantics kern_timeout.c:1041-1075 killing the detach-UAF hypothesis); fix.diff (bounded ~10ms wait, degrades to no-harvest via the existing virtqueue_empty() gate); guest dmesg shows no vtrnd (only vtnet0/vtblk0)
PoC changes
No runnable PoC authored: trigger is host-side (stalled entropy backend), not guest-local; the audit guest lacks a virtio-rng device entirely.
Verified recommended fix
Bound the poll in vtrnd_harvest() (~10ms via virtqueue_pending()+DELAY loop); on timeout return with the descriptor still queued so the existing virtqueue_empty() gate permanently disables harvest instead of hanging softclock
Verdict
Mechanism code-certain: vtrnd_harvest() calls virtqueue_poll() (virtio_random.c:215), which is an unbounded busy-wait (virtqueue.c:578-579), from the periodic 5s softclock callout (virtio_random.c:142,229). If the host entropy backend never advances the used ring (stalling EGD chardev, wedged vhost backend, out-of-band device reset), the softclock thread spins forever - permanent guest hang, no panic, no watchdog. Not reproduced: the audit guest has no virtio-rng device (vm.sh attaches only virtio-blk/virtio-net) and the trigger is host-side fault injection, outside the guest-attacker model; filed as Low hardening/robustness.
No comments yet.