Summary
A guest that makes a malformed split- or packed-virtqueue chain available drives its virtio device worker into a tight, non-terminating loop that pins the worker thread and makes the device un-cancellable — it can no longer observe a stop signal or be reset/torn down, which also blocks orderly shutdown of that VM.
Root cause
Descriptor-chain validation is correct — after queue_size descriptors the parser returns QueueError::TooLong (this also catches cycles), and out-of-bounds descriptor/indirect reads return QueueError::Memory. The defect is error recovery:
QueueCore::try_next_work() advances the available-ring index only on the Ok(Some(_)) arm; on any error the index is left unchanged, so the same malformed head is re-parsed on the next poll — vm/devices/virtio/virtio/src/queue.rs:283-292.
- The queue stream surfaces the error immediately: in
poll_next_buffer, self.try_next()? returns Poll::Ready(Err(_)) before reaching poll_kick, so the stream never waits for the next kick — vm/devices/virtio/virtio/src/common.rs:414-423.
- The blk worker logs the error and re-enters its loop; with no in-flight I/O the inner
poll_fn returns Ready every iteration, so the .await never yields Pending and the task never returns to the executor — vm/devices/virtio/virtio_blk/src/lib.rs:171-213.
Because run never returns from a single poll(), stop.until_stopped(...) can't observe the stop and worker.stop().await hangs during reset/teardown.
Reproduction
Found by the virtio-villain conformance/fault-injection suite. Triggering tests (virtio-blk, PCI): T0001, T0002, T0003, T0054, B0125, T0082, P0003 (packed), plus T0025/T0084 which exercise out-of-bounds available-ring indices. Each makes a bad head available and the device stops responding (guest observes WEDGED / timeout).
Impact
The offending guest wedges its own device: a pinned worker thread and a stuck, unresettable device that blocks orderly VM teardown. Confined to the guest that submitted the malformed chain.
Suggested fix
On a parse error, advance past (or otherwise retire) the offending head and complete it with an error / stop the queue, rather than re-polling the same head. Ensure the worker awaits the next kick (returns Pending) instead of spinning when the queue yields an error, and that a queue error still lets stop be observed.
Summary
A guest that makes a malformed split- or packed-virtqueue chain available drives its virtio device worker into a tight, non-terminating loop that pins the worker thread and makes the device un-cancellable — it can no longer observe a stop signal or be reset/torn down, which also blocks orderly shutdown of that VM.
Root cause
Descriptor-chain validation is correct — after
queue_sizedescriptors the parser returnsQueueError::TooLong(this also catches cycles), and out-of-bounds descriptor/indirect reads returnQueueError::Memory. The defect is error recovery:QueueCore::try_next_work()advances the available-ring index only on theOk(Some(_))arm; on any error the index is left unchanged, so the same malformed head is re-parsed on the next poll —vm/devices/virtio/virtio/src/queue.rs:283-292.poll_next_buffer,self.try_next()?returnsPoll::Ready(Err(_))before reachingpoll_kick, so the stream never waits for the next kick —vm/devices/virtio/virtio/src/common.rs:414-423.poll_fnreturnsReadyevery iteration, so the.awaitnever yieldsPendingand the task never returns to the executor —vm/devices/virtio/virtio_blk/src/lib.rs:171-213.Because
runnever returns from a singlepoll(),stop.until_stopped(...)can't observe the stop andworker.stop().awaithangs during reset/teardown.Reproduction
Found by the virtio-villain conformance/fault-injection suite. Triggering tests (virtio-blk, PCI):
T0001,T0002,T0003,T0054,B0125,T0082,P0003(packed), plusT0025/T0084which exercise out-of-bounds available-ring indices. Each makes a bad head available and the device stops responding (guest observes WEDGED / timeout).Impact
The offending guest wedges its own device: a pinned worker thread and a stuck, unresettable device that blocks orderly VM teardown. Confined to the guest that submitted the malformed chain.
Suggested fix
On a parse error, advance past (or otherwise retire) the offending head and complete it with an error / stop the queue, rather than re-polling the same head. Ensure the worker awaits the next kick (returns
Pending) instead of spinning when the queue yields an error, and that a queue error still letsstopbe observed.