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

vtnet_exec_ctrl_cmd waits for control-queue completion with an unbounded spin loop (virtqueue_poll) under held serializers

Summary

The ctrl-queue poll is 'while (dequeue == NULL);' - no yield, no timeout; returns non-NULL only when the device advances used->idx. Runs under ASSERT_IFNET_SERIALIZED_ALL, so a wedged/malicious device that never completes a command permanently spins the calling kernel thread holding the driver serializers. Reachable with NO privileges at attach (set_promisc -> ctrl cmd) and via root ioctls. VERIFIED: loop-level replica against a never-advancing used ring - 200M spins at harness cutoff, kernel copy has none; FIXED arm gives up in ~1s. Same family as DF-2003/2022/3596. Live wedge intentionally not performed (the console rides this driver's queues).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3959 Β· 11 files
FileTypeDescriptionSize
trigger.c trigger-source loop-level replica of virtqueue_poll against a never-advancing used ring, plus the fixed bounded arm 3.7 KB view raw
build.sh build-script cc -O2 -Wall -o trigger trigger.c 65 B view raw
run.sh run-script ./trigger 89 B view raw
build.log build-log guest compile output 9 B view raw
run.log run-log baseline decisive run 647 B view raw
run.2.log run-log patched-kernel re-run 647 B view raw
fix.diff suggested-fix bounded ctrl-queue poll (git-apply-able) 1.0 KB view raw
fix_build.log build-log nativekernel with fix, -Werror, rc=0 5.3 KB view raw
env.txt environment guest uname, cc version 213 B view raw
VERDICT.md verdict full narrative 3.6 KB ↓ raw
README.md readme reproduce instructions 1.2 KB ↓ raw
README.md readme reproduce instructions
↓ download raw

DF-3959 β€” vtnet control-virtqueue command polls forever

vtnet_exec_ctrl_cmd() waits for control-queue completion with virtqueue_poll(), which is an unbounded spin loop with the ifnet serializers held. A wedged/malicious virtio device that never advances used->idx permanently hangs the calling kernel thread.

Reproduce (on the DragonFly guest)

./build.sh
./run.sh    # runs the exact loop condition against a never-advancing
            # used ring; reports 200M spins at the harness cutoff and
            # that the kernel loop has no cutoff; then runs the FIXED
            # bounded-poll arm

Verified

  • Loop logic replicated and run on the audit guest (baseline and patched kernels).
  • fix.diff (bounded 10000 x DELAY(100) poll, timeout leaves ack at VIRTIO_NET_ERR so callers return EIO) applies cleanly; kernel builds -Werror rc=0 (fix_build.log); patched kernel boots; vtnet0 healthy.
  • The ctrl queue is exercised for real on the guest at attach (vtnet_set_promisc at if_vtnet.c:361) β€” a benign device completes instantly, which is why the wedge itself is demonstrated at the loop level rather than by hanging the console box.
VERDICT.md verdict full narrative
↓ download raw

DF-3959 β€” VERDICT

REPRODUCED at the loop level (impact: dos), confidence: certain.

Root cause

sys/dev/virtual/virtio/net/if_vtnet.c:2407:

c = virtqueue_poll(vq, NULL);

sys/dev/virtual/virtio/virtio/virtqueue.c:572-582:

void *
virtqueue_poll(struct virtqueue *vq, uint32_t *len)
{
        void *cookie;

        while ((cookie = virtqueue_dequeue(vq, len)) == NULL)
                ;                      /* spin β€” no yield, no timeout */
        return (cookie);
}

virtqueue_dequeue() returns non-NULL only when the device advances used->idx (virtqueue.c:550-554). The caller, vtnet_exec_ctrl_cmd() (if_vtnet.c:2372-2409), executes under ASSERT_IFNET_SERIALIZED_ALL (if_vtnet.c:2380) β€” i.e. with the driver's serializers held β€” and there is no bound, no tsleep, and no way for a kernel spin loop to be preempted away by signals. A device that acknowledges VIRTIO_NET_F_CTRL_VQ but never completes a control command wedges the calling kernel thread permanently (CPU burn + serializer held => soft-lockup style hang of everything that later needs those serializers, e.g. the ifnet ioctl path).

Reachability

  • No privileges required at attach: vtnet_attach() β†’ vtnet_set_promisc() (if_vtnet.c:361) β†’ vtnet_ctrl_rx_cmd() β†’ vtnet_exec_ctrl_cmd() β†’ virtqueue_poll(). A malicious PCI device (or passed-through virtio function) wedges the boot/kernel thread simply by sitting silent after feature negotiation.
  • Runtime, root ioctls: SIOCSIFFLAGS β†’ vtnet_rx_filter() (if_vtnet.c:1018), SIOCADDMULTI β†’ vtnet_rx_filter_mac() (if_vtnet.c:1034), MTU/STOP-less reinit paths.
  • The virtio core comment admits the design: "Poll until the command is complete" (if_vtnet.c:2397-2406); virtqueue_poll's own comment says it is only intended for virtio-blk dumping (virtqueue.c:577).

Same family as DF-2003 (SMIC), DF-2022 (amdgpu mailbox), DF-3596 (bwn): unbounded device-wait polling wedging an unkillable kernel context.

Impact

Permanent kernel-thread wedge / CPU denial of service from a malicious/dead virtio host device; not triggerable by guest users on a benign host (the guest's QEMU device always answers instantly β€” the ctrl queue really runs at attach and on ifconfig operations on the audit guest).

Fix validation

  • fix.diff: replace virtqueue_poll() with a bounded for (i = 0; i < 10000; i++) { virtqueue_dequeue(); DELAY(100); } and return on timeout, leaving the caller's ack at VIRTIO_NET_ERR so every caller reports EIO (all callers already handle non-OK acks).
  • Applied to /usr/src cleanly (fixapply2.log), kernel rebuilt -Werror rc=0 (fix_build.log), installed, booted (kernel #1 Sep 17 05:23); vtnet0 attached and completed its attach-time ctrl command (vtnet_set_promisc) β€” the guest booted to ssh over vtnet0, proving the bounded poll works against a real (fast) device.
  • Harness FIXED arm: timeout after 10000 iterations, caller gets EIO, thread survives.

Kernel references

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applies cleanly; kernel rebuilt -Werror rc=0; booted into patched kernel #1; vtnet0 attach-time ctrl command (vtnet_set_promisc) completes against the real fast device (guest booted to ssh over vtnet0); harness FIXED arm shows clean timeout instead of infinite spin.

fixapply2.log; fix_build.log (nativekernel rc=0, -Werror); patched_boot.txt (kernel #1, vtnet0 RUNNING); run.2.log FIXED arm
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 17 05:23:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

malicious/dead virtio device negotiates VIRTIO_NET_F_CTRL_VQ -> guest driver sends ctrl command at attach (vtnet_set_promisc) or via root ifconfig -> device never writes used->idx -> virtqueue_poll spins forever holding ifnet serializers -> unkillable kernel-thread wedge / CPU burn / serializer deadlock for all later ifnet users

Evidence (decisive lines)

run.log 'kernel loop spun 200000000 times and never completed ... this loop has NO cutoff'; run.2.log FIXED arm 'gives up after 10000 iterations ... thread survives'; fixapply2.log; fix_build.log rc=0; VERDICT.md trace

PoC changes

n/a (first-generation pack)

Verified recommended fix

Bound the ctrl-queue wait: replace virtqueue_poll() with a 10000 x DELAY(100) dequeue loop and return on timeout leaving ack=VIRTIO_NET_ERR (callers already map non-OK ack to EIO).

Verdict

REPRODUCED at the loop level on the audit guest: vtnet_exec_ctrl_cmd() (if_vtnet.c:2407) waits for control-virtqueue completion via virtqueue_poll(), whose only body is while (virtqueue_dequeue(...) == NULL); (virtqueue.c:578) β€” an unbounded spin with the ifnet serializers held (if_vtnet.c:2380). A wedged or malicious virtio device that never advances used->idx permanently hangs the calling kernel thread: reachable with no privileges at attach (vtnet_attach -> vtnet_set_promisc, if_vtnet.c:361) and via root ioctls at runtime. The exact loop condition was run against a never-advancing used ring on the guest (200M spins at harness cutoff; the kernel copy has no cutoff). Live wedge not performed because the console rides this driver's sibling queues and the guest's QEMU device answers instantly; loop-level proof plus code trace is conclusive (the loop has no exit but device progress). Fix (bounded 10000 x DELAY(100) poll, ack stays VIRTIO_NET_ERR -> EIO) applied, kernel rebuilt -Werror rc=0, booted, attach-time ctrl command completes against the real device.