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)
PoC verification
Evidence pack
findings/poc/DF-3959 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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-Werrorrc=0 (fix_build.log); patched kernel boots; vtnet0 healthy.- The ctrl queue is exercised for real on the guest at attach
(
vtnet_set_promiscat 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.
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: replacevirtqueue_poll()with a boundedfor (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/srccleanly (fixapply2.log), kernel rebuilt-Werrorrc=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
- sys/dev/virtual/virtio/net/if_vtnet.c:2372-2409 (exec_ctrl_cmd)
- sys/dev/virtual/virtio/net/if_vtnet.c:2407 (virtqueue_poll call)
- sys/dev/virtual/virtio/net/if_vtnet.c:361 (attach-time reachability)
- sys/dev/virtual/virtio/virtio/virtqueue.c:572-582 (unbounded spin)
- sys/dev/virtual/virtio/virtio/virtqueue.c:550-554 (completion condition)
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
fixedfix.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
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.
No comments yet.