β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5120

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5120 Β· 5 files
FileTypeDescriptionSize
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 by vtrnd_timer() (virtio_random.c:229).
  • vtrnd_harvest() enqueues one 4-byte writable descriptor pointing at a stack variable, notifies the device, then calls virtqueue_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:

  1. QEMU/KVM virtio-rng backed by a blocking EGD chardev on the host that stalls (this is a documented operational failure mode of virtio-rng backends).
  2. 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).
  3. 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.

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.

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-5120

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.