Skip to content

virtio: five descriptor walk sites across four devices, each with a different subset of the bounds #170

Description

@cspinetta

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:networkSLIRP, NAT, 9p transport, proxy/egressenhancementNew feature or requestsecurityPublic hardening work; undisclosed vulns via private reporting

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions