This document defines how protocol 1.0 request and response frames inhabit Virtio descriptor chains. It is normative together with specification.md and wire-abi.md.
The portable reference path targets a split virtqueue. It relies only on ordered readable and writable byte regions plus queue publication/completion operations, so future transport adapters do not need to expose guest addresses or transport-specific descriptor types to the command engine.
The Virtio 1.3 specification continues to supply the base rules, including descriptor-loop prohibition, negotiated indirect descriptors, queue publication barriers, notification suppression, and transport reset. This document adds device-specific framing requirements.
Protocol 1.0 defines command virtqueue index zero. It defines no device-specific multi-queue feature.
The reference device MUST NOT offer VIRTIO_F_RING_PACKED, VIRTIO_F_IN_ORDER,
VIRTIO_F_RING_RESET, VIRTIO_F_INDIRECT_DESC, or VIRTIO_F_EVENT_IDX. It uses direct split-ring
descriptors and the basic available/used notification-suppression flags. A future adapter may
support one of these base Virtio features only when it preserves the device-specific rules here and
adds corresponding conformance evidence. Whole-device reset remains mandatory.
For validation, a transport adapter presents one available descriptor head as:
- an ordered list of device-readable byte regions;
- followed by an ordered list of device-writable byte regions; and
- the original queue head token used for completion.
A future adapter may flatten a valid indirect descriptor table when VIRTIO_F_INDIRECT_DESC was
negotiated. The configured max_chain_descriptors then applies after flattening and includes both
readable and writable descriptors. The protocol 1.0 reference profile rejects indirect
descriptors because it does not negotiate that feature.
The command engine sees region lengths and byte access only. It MUST NOT receive guest physical addresses, descriptor indices, indirect-table addresses, ring pointers, or notification objects.
A protocol 1.0 command chain is valid only when all of the following hold:
- The chain is acyclic and structurally valid under the base Virtio specification.
- Its flattened descriptor count is 2 through configured
max_chain_descriptors. - Every descriptor length is nonzero.
- At least one device-readable descriptor precedes at least one device-writable descriptor.
- No device-readable descriptor follows a device-writable descriptor.
- The readable total is representable, no greater than configured
max_request_bytes, and exactly one complete request frame. - The writable total is representable and large enough for the command’s maximum possible response shape.
Cross-region reads and writes concatenate regions without inserting padding. Header or payload fields may cross descriptor boundaries at any byte.
The transport adapter MUST validate the descriptor topology and map every readable and required writable byte before the command engine performs semantic validation or invokes a backend.
Exactly one request frame occupies the complete readable portion of a chain. The first readable byte is request-header byte zero. The readable total MUST equal:
16 + request_header.payload_bytes
No bytes before the header, between fixed and variable payload portions, or after the declared payload are permitted.
The response begins at writable byte zero. Writable capacity beyond the actual response remains untouched and is not included in used length.
The device validates a chain in this order:
- descriptor topology, direction, count, nonzero lengths, addressability, and total-length arithmetic;
- availability of a complete 16-byte request header and 16-byte writable response header;
- configured request limit and exact
payload_bytesequality; - opcode and request-header flags;
- command-specific fixed length, variable counts, reserved fields, scalar namespaces, object relationships, and required writable capacity;
- quotas and semantic preconditions; and
- backend invocation.
No failure in steps 1 through 5 may mutate device semantic state or invoke the backend.
If descriptor topology is invalid, the request header is truncated, or writable capacity is less than 16 bytes, the device returns the descriptor head used with used length zero and writes no bytes.
If the request header is valid enough to recover request_id, and writable capacity is at least 16,
later validation failures produce a complete error response header. The used length is 16.
Required success capacity is checked before semantic mutation. A short success buffer therefore produces used length zero, no response bytes, and no semantic state change; it is not converted to a smaller protocol error because the driver failed the response-buffer contract.
The device MUST commit a response atomically from the protocol’s point of view:
- every byte counted as used is initialized;
- the response header’s
payload_bytesequals the bytes written after it; - no byte beyond
16 + payload_bytesis written; and - the split-ring used element length equals
16 + payload_bytes.
Used length counts device-written bytes only. It never includes readable request bytes.
Response bytes may span writable descriptors. A device MUST write them as if to one concatenated byte sequence.
An unexpected mapping or write failure after the preflight succeeds indicates broken transport or
memory state. If semantic mutation has not occurred, the chain may complete with used length zero.
If mutation or uncertain backend acceptance has occurred, the device MUST set
DEVICE_NEEDS_RESET, quarantine retained resources, and avoid a response that falsely claims
rejection.
The baseline command queue permits out-of-order command completion. The device MUST consume available heads in available-ring order, but may dispatch their semantic work concurrently and return them used in a different order as work completes. Drivers therefore track both descriptor heads and nonzero request IDs.
Request-ID uniqueness lasts until the corresponding chain is returned used. A driver MUST NOT infer ordering from numeric request IDs.
Command completion and accelerator execution completion are distinct:
SUBMITcommand completion reports admission and returns an event ID;- the accelerator operation may remain pending after the command chain is used; and
POLL_EVENTobserves execution completion independently of command ordering.
Implementations may serialize command execution, but they MUST NOT promise in-order completion as protocol behavior.
Queue capacity is transport state, not a protocol error response.
- A driver with no free descriptor head or insufficient writable buffers MUST retain the command locally and report backpressure to its caller.
- It MUST NOT publish a partial command or reuse descriptors still owned by the device.
- A device may defer consumption or completion without busy-looping.
- Resource limits discovered after a valid chain is consumed produce
RESOURCE_LIMIT; descriptor scarcity before publication does not.
The reference guest API should represent pre-publication backpressure separately from protocol statuses so callers can retry without constructing a new semantic command.
Available and used notifications follow the base Virtio split-ring suppression rules. Suppressing a notification changes neither command visibility nor completion semantics.
- The driver publishes descriptors and the available-ring entry before deciding whether to notify.
- The device publishes the used element and used index before deciding whether to notify.
- Both sides must recheck queue state when required by the base specification to avoid lost wakeups.
The device-specific protocol defines no polling interval, thread, executor, or interrupt affinity.
The following are isolated command-chain failures and do not by themselves require device reset:
- descriptor loops or an invalid indirect table caught before byte access;
- too many, zero-length, missing-readable, missing-writable, or interleaved-direction descriptors;
- truncated headers or payloads;
- oversized frames;
- exact-length or reserved-zero violations;
- unknown opcodes, flags, or scalar values; and
- insufficient writable capacity detected before semantic mutation.
The device completes a structurally recoverable malformed chain according to section 5 and continues with later available chains.
Repeated malformed input may be rate-limited by the transport or device integration, but rate limiting MUST NOT change object ownership or fabricate successful responses.
Protocol 1.0 uses whole-device reset. The reference device does not negotiate independent
VIRTIO_F_RING_RESET.
When reset begins, the device:
- stops fetching new available chains;
- stops publishing used entries and notifications;
- prevents new backend admission;
- resolves, cancels, or quarantines already-started operations according to known ownership;
- invalidates all object IDs and request tracking from the old reset epoch; and
- resets command-queue available/used state as required by the base transport.
After steps 1 and 2 quiesce queue access, the transport gives exclusive ownership to the portable command processor for one bounded teardown pass. A reusable result permits queue and object-table reinitialization with a fresh namespace. A discard-required result forbids further backend calls; the transport discards that processor/backend instance and creates a new one before exposing new queues.
Descriptor chains that were available or in progress when reset began are not completed after reset. Once the driver has observed reset completion, it may reclaim all queue memory and descriptors under the base Virtio reset rule. It MUST NOT expect response bytes or used entries for those chains.
After reinitialization, request IDs may be reused, all object IDs from the prior epoch are stale, and the queue starts from its base initial state. No late backend completion from the old epoch may write guest memory or a new used ring.
Ordinary hostile input does not set DEVICE_NEEDS_RESET. The device sets it when continued protocol
operation cannot preserve ownership or response truth, including:
- indeterminate provider release that invalidates a guest object but leaves backend ownership unknown;
- inability to report or retain an indeterminate accepted submission;
- response-write failure after semantic mutation;
- internal state corruption or accounting contradiction; or
- backend device loss that prevents bounded recovery.
After observing DEVICE_NEEDS_RESET, the driver SHOULD stop submitting commands and perform
whole-device reset. The device may finish responses whose ownership and output remain provably safe,
but it MUST NOT accept new semantic work.
The portable conformance suite uses these stable case identifiers:
| ID | Required assertion |
|---|---|
VQ-001 |
One readable and one writable descriptor carries a valid command |
VQ-002 |
Header and every fixed payload decode across every possible byte split |
VQ-003 |
Multiple readable and writable segments concatenate without padding |
VQ-004 |
Missing readable or writable region completes with used length zero |
VQ-005 |
Readable descriptor after a writable descriptor is rejected before backend invocation |
VQ-006 |
Zero-length, looping, excessive, or invalid-indirect chains are rejected |
VQ-007 |
Truncated request header writes nothing and reports used length zero |
VQ-008 |
Valid header plus malformed payload writes exactly a 16-byte error response |
VQ-009 |
Unknown opcode returns UNSUPPORTED without semantic mutation |
VQ-010 |
Nonzero request, command, or reserved flags are rejected |
VQ-011 |
Trailing readable bytes are rejected |
VQ-012 |
Short command-specific response capacity is detected before mutation and writes nothing |
VQ-013 |
Success and error used lengths equal exact initialized response bytes |
VQ-014 |
Distinct requests can complete out of publication order and retain correct request IDs |
VQ-015 |
SUBMIT command completion is independent of event completion |
VQ-016 |
Queue-full pre-publication behavior is retryable backpressure, not a protocol status |
VQ-017 |
Notification suppression does not lose available or used work |
VQ-018 |
Reset produces no late used entries or guest writes from the old epoch |
VQ-019 |
Reset invalidates every old object ID and permits request-ID reuse |
VQ-020 |
Post-mutation output failure or indeterminate release sets DEVICE_NEEDS_RESET |
The byte vectors under conformance/v1.0 cover protocol frames. Queue-model
implementations consume the case identifiers above so the same behavioral scenarios can be reused
by the in-memory split ring and future platform transports. The dependency-free
virtio-accel-transport crate provides the executable region, ownership, reset-epoch, backpressure,
and notification port contracts. virtio-accel-split-queue exercises the ring-level portions of
VQ-001, VQ-003 through VQ-006, VQ-013, VQ-014, and VQ-016 through VQ-018. The reference
guest and end-to-end scenarios exercise the complete VQ-001 through VQ-020 lifecycle.
This appendix is non-normative. DriverQueue::publish consumes a complete driver chain on success
and returns it unchanged through PublishError on pre-publication failure. pop_used or reset
returns every successfully published chain, preventing safe callers from reusing descriptors while
the device owns them.
DeviceQueue::pop_available returns a non-Copy DeviceChain. Completion consumes that value and
must compare its ChainId epoch with current QueueState before publishing bytes or ring state.
ChainIo contains only flattened ChainRegion values and generic readable/writable byte ports; it
cannot carry guest addresses or a concrete descriptor type.
All steady-state queue-port methods are specified as nonblocking and allocation-free in their Rust
documentation. Publication/completion establish release ordering, their peer-side pops establish
acquire ordering, and notification enablement returns WorkPending when its mandatory recheck finds
work that raced with suppression.
This appendix is non-normative. SplitQueue preallocates one descriptor-ownership table, chain
record table, available ring, and used ring from a validated power-of-two QueueSize. Publication,
available consumption, out-of-order completion, used consumption, and notification rechecks move
owned values or update fixed slots; none allocates or coalesces payload bytes.
DriverChain records each descriptor's logical byte span during its bounded construction pass.
Segmented byte access binary-searches the first intersecting span and visits only descriptors that
contain requested bytes, avoiding repeated prefix scans when a command decoder performs many small
reads. The span metadata remains caller-owned chain storage and is never allocated from descriptor
byte lengths.
DriverChain::direct constructs the baseline direct profile. DriverChain::raw retains malformed
topology for deterministic device tests, while SplitQueue::inject_available bypasses only normal
driver-side profile validation. Traversal remains bounded by the supplied descriptor table and
never allocates from a descriptor byte length, next value, or other guest-controlled scalar.
Unknown flags and VIRTQ_DESC_F_INDIRECT are rejected because protocol 1.0 negotiates neither.
The four public RingCounters use the split ring's wrapping u16 index arithmetic. Test hooks can
place an empty queue at a chosen index and choose the next descriptor allocation slot, making ring
wrap, descriptor wrap, exhaustion, and notification races reproducible without timing or threads.
Reset publishes a new atomic epoch before removing ring storage; every old ChainIo byte access and
completion checks that epoch before touching a payload or used entry.