Problem
Two independent descriptor-chain walkers read guest memory in this tree. SplitVirtqueue::pop_avail (src/devices/virtqueue.rs) serves the userspace vsock device. Virtio9pDevice::process_queue (src/devices/virtio_9p.rs:503) walks its own chain instead: it reads the available ring, follows next indices through the descriptor table, splits readable from device-writable descriptors, and writes the used ring by hand.
Both implement the same virtio split-queue contract over the same untrusted input — a guest chooses the queue size, the three ring base addresses, and every descriptor field the walk reads. Every bound one walker applies, the other needs too, and nothing enforces that. The two drifted, and each ended up with guards the other lacked:
| Guard |
SplitVirtqueue::pop_avail |
Virtio9pDevice::process_queue |
| Queue size of 0 refused |
missing — divided by zero on the first kick |
present |
next index at or past the queue size |
missing — read outside the descriptor table |
present |
| Chain length bounded by the queue size |
present |
missing — a self-referential next looped forever |
| Ring base plus offset arithmetic checked |
missing — panicked under overflow-checks |
missing — same, through unchecked_add |
Four of the seven defects #169 fixed are rows of that table. Both walkers now carry all four guards and share virtqueue::ring_addr for the arithmetic, so there is no live defect here. The duplication that let them diverge is still in place.
Proposed change
Give Virtio9pDevice a SplitVirtqueue and delete its private walk. The device keeps the 9P-specific work at the caller level: splitting chain.descriptors on VRING_DESC_F_WRITE, capping assembled request bytes at the negotiated msize, and writing the reply back into the writable descriptors.
What stands in the way
SplitVirtqueue::new takes a kick fd and a call fd, and signal_guest writes to them. The 9P device has no eventfds — it raises a polled interrupt_status bit instead. Either the fds become optional, or the device passes -1 the way the fuzz harness does; the first is the honest shape.
Queue state lives in different places. Virtio9pDevice holds QueueState plus its own avail_idx / used_idx fields, driven from mmio_write; SplitVirtqueue holds last_avail_idx / last_used_idx and takes its geometry at construction. The MMIO register handlers have to build or update the queue rather than write struct fields.
Error propagation differs. process_queue returns crate::Result and propagates a guest-memory read failure; pop_avail returns None and drops the reason. A caller that wants the errno keeps needing it.
Done when
src/devices/virtio_9p.rs contains no descriptor-table read of its own, and grep unchecked_add src/devices/ is empty.
- The existing 9P transport tests still pass:
process_queue_caps_descriptor_reads_at_msize and process_queue_stops_at_an_out_of_table_descriptor_index.
cargo +nightly fuzz run nine_p_transport runs clean, and e2e_mount passes on Linux (virtio-9p is the transport it exercises).
Follow-up from #169. Related to #155.
Problem
Two independent descriptor-chain walkers read guest memory in this tree.
SplitVirtqueue::pop_avail(src/devices/virtqueue.rs) serves the userspace vsock device.Virtio9pDevice::process_queue(src/devices/virtio_9p.rs:503) walks its own chain instead: it reads the available ring, followsnextindices through the descriptor table, splits readable from device-writable descriptors, and writes the used ring by hand.Both implement the same virtio split-queue contract over the same untrusted input — a guest chooses the queue size, the three ring base addresses, and every descriptor field the walk reads. Every bound one walker applies, the other needs too, and nothing enforces that. The two drifted, and each ended up with guards the other lacked:
SplitVirtqueue::pop_availVirtio9pDevice::process_queuenextindex at or past the queue sizenextlooped foreveroverflow-checksunchecked_addFour of the seven defects #169 fixed are rows of that table. Both walkers now carry all four guards and share
virtqueue::ring_addrfor the arithmetic, so there is no live defect here. The duplication that let them diverge is still in place.Proposed change
Give
Virtio9pDeviceaSplitVirtqueueand delete its private walk. The device keeps the 9P-specific work at the caller level: splittingchain.descriptorsonVRING_DESC_F_WRITE, capping assembled request bytes at the negotiatedmsize, and writing the reply back into the writable descriptors.What stands in the way
SplitVirtqueue::newtakes a kick fd and a call fd, andsignal_guestwrites to them. The 9P device has no eventfds — it raises a polledinterrupt_statusbit instead. Either the fds become optional, or the device passes-1the way the fuzz harness does; the first is the honest shape.Queue state lives in different places.
Virtio9pDeviceholdsQueueStateplus its ownavail_idx/used_idxfields, driven frommmio_write;SplitVirtqueueholdslast_avail_idx/last_used_idxand takes its geometry at construction. The MMIO register handlers have to build or update the queue rather than write struct fields.Error propagation differs.
process_queuereturnscrate::Resultand propagates a guest-memory read failure;pop_availreturnsNoneand drops the reason. A caller that wants the errno keeps needing it.Done when
src/devices/virtio_9p.rscontains no descriptor-table read of its own, andgrep unchecked_add src/devices/is empty.process_queue_caps_descriptor_reads_at_msizeandprocess_queue_stops_at_an_out_of_table_descriptor_index.cargo +nightly fuzz run nine_p_transportruns clean, ande2e_mountpasses on Linux (virtio-9p is the transport it exercises).Follow-up from #169. Related to #155.