Chapter 11: virtio -- The Paravirtualized Device Model
Emulating a particular hardware device preserves compatibility with its existing driver, including every register and historical behavior that driver expects. Virtio defines a device family for virtual environments instead. The guest uses a virtio driver, places buffer descriptions in shared guest memory, and notifies the device backend after publishing one or more requests. The backend returns completed buffers through the same queue structure.
This is paravirtualization: both sides implement an interface designed for a virtual device. It reduces register traffic and makes batching explicit without removing the normal device concepts of feature negotiation, DMA-like buffer access, interrupts, and transport discovery.
virtio is that contract, standardized. The OASIS virtio Committee Specification v1.2 CS01 defines the shared-memory ring formats, feature negotiation, MMIO, PCI, and channel-I/O transports, and the protocol for each device class. It is the interface that Firecracker, crosvm, Cloud Hypervisor, and QEMU all implement -- which means a Linux guest compiled once can run on any of them without modification, because the driver it loads is the kernel's standard virtio driver, not a VMM-specific one.
The Virtqueue
Every virtio device exposes one or more virtqueues: shared-memory rings
through which the driver (the guest's kernel driver) submits work and the
device (the VMM backend) returns completions. The virtio v1.2 spec defines two
queue formats. The split virtqueue (spec section 2.7) uses three separate
memory regions. The packed virtqueue (spec section 2.8), introduced in
v1.1 and enabled by feature bit VIRTIO_F_RING_PACKED = 34, collapses those
three regions into one circular ring plus two small event-suppression
structures. Firecracker implements split virtqueues only; the packed format is
not supported.
Three Regions, Two Writers
A split virtqueue consists of three physically independent memory regions. The driver writes the descriptor table and available ring; the device writes the used ring:
Guest Memory
+------------------------------------------------------+
| Descriptor Table (16 bytes x Queue Size) | <- Driver writes
| Alignment: 16 bytes | Device reads
+------------------------------------------------------+
| Available Ring (4 + 2 x Queue Size, plus event) | <- Driver writes
| Alignment: 2 bytes | Device reads
+------------------------------------------------------+
| Used Ring (4 + 8 x Queue Size, plus event) | <- Device writes
| Alignment: 4 bytes | Driver reads
+------------------------------------------------------+
The alignment constants are defined in linux/include/uapi/linux/virtio_ring.h
as VRING_DESC_ALIGN_SIZE = 16, VRING_AVAIL_ALIGN_SIZE = 2, and
VRING_USED_ALIGN_SIZE = 4. Queue Size must be a power of two, at least 1,
and at most 32,768 (0x8000). Firecracker caps every queue at 256 entries
(FIRECRACKER_MAX_QUEUE_SIZE = 256).
The ownership rule prevents both sides from updating the same ring metadata, but it
does not eliminate concurrency. A driver must finish descriptor and available-ring
writes before publishing avail.idx; a device must finish buffer and used-ring
writes before publishing used.idx. The specification therefore requires memory
barriers around those index updates.
The Descriptor Table
Each entry in the descriptor table is a struct virtq_desc (16 bytes, all
fields in little-endian):
struct virtq_desc {
le64 addr; /* offset 0: guest-physical buffer address */
le32 len; /* offset 8: buffer length in bytes */
le16 flags; /* offset 12: control flags */
le16 next; /* offset 14: index of next descriptor (if chaining) */
};
Three flag bits control how the descriptor is used. VIRTQ_DESC_F_NEXT = 0x1
means the descriptor is not the last in a chain -- the next field holds the
index of the next descriptor. VIRTQ_DESC_F_WRITE = 0x2 marks the buffer as
device-writable; without it the buffer is device-readable. VIRTQ_DESC_F_INDIRECT = 0x4
signals that addr and len point not to data but to an in-memory table of
further virtq_desc entries, enabled by feature bit
VIRTIO_F_RING_INDIRECT_DESC = 28. Within an indirect table, only
VIRTQ_DESC_F_WRITE and VIRTQ_DESC_F_NEXT are valid; VIRTQ_DESC_F_INDIRECT
is forbidden in indirect entries, and the device must ignore
VIRTQ_DESC_F_WRITE on the outer descriptor that points to the table.
Descriptors chain together to describe a single I/O request. A virtio-blk read, for example, uses three descriptors in a chain: a device-readable header (16 bytes: request type, reserved padding, sector number), one or more device-writable data buffers, and a device-writable one-byte status field. All device-readable descriptors precede all device-writable ones in the chain -- this is a hard split-virtqueue rule, not a convention.
The driver builds these chains by filling descriptor table entries, then publishes the chain by placing the head descriptor's index into the available ring.
The Available Ring
The available ring is the driver's outbox. Its layout (from spec section 2.7.6):
struct virtq_avail {
le16 flags; /* VIRTQ_AVAIL_F_NO_INTERRUPT = 1 */
le16 idx; /* where driver will write next head index */
le16 ring[/* Queue Size */]; /* head indices of published chains */
le16 used_event; /* only if VIRTIO_F_EVENT_IDX negotiated */
};
The idx field wraps naturally at 2^16. The driver increments it by the
number of chains it publishes, stores the head indices in ring[idx % QueueSize]
through ring[(idx + n - 1) % QueueSize], then issues a write memory barrier
before notifying the device. The device reads ring[(last_seen_idx % QueueSize)]
through ring[(avail.idx - 1) % QueueSize] to collect new chains.
Notice that idx is never reset -- it grows monotonically, modulo 2^16. A
device that tracks the last idx it saw can detect new work without any
locking; the index is the only synchronization signal.
The Used Ring
The used ring is the device's completion outbox. Its layout (section 2.7.8):
Each virtq_used_elem is 8 bytes. When the device finishes a chain, it writes
the head index and byte count into the current used slot, increments idx,
and -- unless notification suppression says otherwise -- signals the guest
interrupt. The driver scans from its last-seen idx to used.idx - 1 to
harvest completions.
Notification Suppression
Left to themselves, driver and device fire an interrupt or a doorbell write after every descriptor batch. For high-throughput paths, that overhead adds up. virtio provides two suppression mechanisms.
The coarse mechanism uses the binary flags: the driver sets
avail.flags = VIRTQ_AVAIL_F_NO_INTERRUPT to suppress device-to-driver
interrupts; the device sets used.flags = VIRTQ_USED_F_NO_NOTIFY to suppress
driver-to-device kicks. Either side can assert its flag at any time. The
tradeoff is crude -- all notifications or none.
The fine-grained mechanism, enabled by VIRTIO_F_RING_EVENT_IDX = 29, uses
used_event and avail_event thresholds. The notification test compares the old
and new wrapping indices to determine whether publication crossed the peer's event
index; equality alone is not sufficient around 16-bit wrap. Firecracker implements
this calculation in prepare_kick() and checks it with Kani proofs.
Firecracker's Queue Implementation
Current Firecracker implements split queues in
src/vmm/src/devices/virtio/queue.rs; it does not depend on the rust-vmm
virtio-queue crate. Queue::initialize() validates readiness, power-of-two size,
the 256-entry device limit, guest-memory bounds, and the 16-, 2-, and 4-byte
alignments of the descriptor, available, and used regions.
Ring fields are accessed with volatile reads and writes. An acquire fence follows
observation of available work, and a release fence precedes publication of
used.idx. A descriptor chain carries a TTL initialized to the queue size, so a
cycle cannot make traversal unbounded. The backend can add several used elements,
publish the used index once, and call prepare_kick() once for the batch. Kani
harnesses cover index arithmetic, notification suppression, queue bounds, and chain
construction.
Feature Negotiation
The spec imposes a strict handshake before the device becomes usable. This is the mechanism by which a driver compiled three years ago negotiates with a device model compiled last week: each side publishes what it supports; the intersection is what they use. Neither side assumes the other is current.
The Nine-Step Sequence
Spec section 3.1.1 defines the mandatory initialization sequence. The driver must follow these steps in order:
Features are read and written in two 32-bit pages via DeviceFeaturesSel and
DriverFeaturesSel: page 0 covers bits 0-31, page 1 covers bits 32-63. This
matters in practice because VIRTIO_F_VERSION_1 = 32 sits at bit 0 of page 1.
A device presenting itself as modern must advertise this bit; a driver that
does not acknowledge it is treated as a legacy driver, and a v2 MMIO device
must reject initialization if the driver fails to acknowledge it.
The six device status register bits (from
linux/include/uapi/linux/virtio_config.h) are the handshake signals:
| Constant | Value | Meaning |
|---|---|---|
VIRTIO_CONFIG_S_ACKNOWLEDGE |
1 | Driver found the device |
VIRTIO_CONFIG_S_DRIVER |
2 | Driver knows how to drive it |
VIRTIO_CONFIG_S_FEATURES_OK |
8 | Feature negotiation complete |
VIRTIO_CONFIG_S_DRIVER_OK |
4 | Driver is live |
VIRTIO_CONFIG_S_NEEDS_RESET |
64 | Device needs reset (unrecoverable) |
VIRTIO_CONFIG_S_FAILED |
128 | Fatal error |
Status starts at 0. The driver must not clear individual bits; only writing 0 resets the register and the device.
Kernel Implementation
virtio_dev_probe() in drivers/virtio/virtio.c implements steps 2-7: it
sets DRIVER, calls virtio_get_features(), ANDs the device and driver
feature tables, calls dev->config->finalize_features(), sets FEATURES_OK,
and reads back status. If FEATURES_OK is absent, it returns -ENODEV.
virtio_device_ready() sets DRIVER_OK after queue setup completes.
virtio_features_ok() in drivers/virtio/virtio.c checks that
VIRTIO_F_VERSION_1 is in the negotiated set before writing DriverFeatures
to a modern device.
The Transport-Layer Feature Bits
Most of these bits live in the range VIRTIO_TRANSPORT_F_START = 28 through
VIRTIO_TRANSPORT_F_END = 42 and apply to every device type.
VIRTIO_F_ANY_LAYOUT is listed here for completeness -- it predates the formal
transport range and sits at bit 27, just outside it.
The Linux kernel uapi headers (virtio_ring.h) name the indirect-descriptor
and event-index bits VIRTIO_RING_F_INDIRECT_DESC and VIRTIO_RING_F_EVENT_IDX;
the OASIS spec uses VIRTIO_F_RING_INDIRECT_DESC and VIRTIO_F_RING_EVENT_IDX
for the same bits (28 and 29). This chapter follows the spec naming.
| Constant (OASIS spec) | Bit | Meaning |
|---|---|---|
VIRTIO_F_ANY_LAYOUT |
27 | Device handles any descriptor ordering (predates transport range) |
VIRTIO_F_RING_INDIRECT_DESC |
28 | Indirect descriptor tables |
VIRTIO_F_RING_EVENT_IDX |
29 | Descriptor-granularity notification suppression |
VIRTIO_F_VERSION_1 |
32 | Modern device (mandatory for modern devices) |
VIRTIO_F_ACCESS_PLATFORM |
33 | IOMMU DMA required |
VIRTIO_F_RING_PACKED |
34 | Packed virtqueue format |
VIRTIO_F_IN_ORDER |
35 | Buffers used in availability order |
VIRTIO_F_RING_RESET |
40 | Per-queue reset |
Firecracker advertises VIRTIO_F_VERSION_1 on its modern devices, but feature
sets differ by device. Net, virtio-blk, and vsock advertise
VIRTIO_F_RING_EVENT_IDX; rng, balloon, pmem, and virtio-mem currently do not.
No Firecracker device advertises VIRTIO_F_RING_PACKED.
Config Space Atomicity
Device-specific configuration fields (capacity, MAC address, queue pair count,
and so on) live in a config space region that can be updated at any time -- for
example, a network link-state change arriving mid-probe. Spec section 2.5
requires the driver to read config_generation, read the needed fields, then
read the generation again. If the values differ, the driver retries. The device
must return a changed value whenever there is a risk that the driver could observe
an inconsistent configuration. The PCI field is only 8 bits, so blindly incrementing
on every change is not by itself sufficient across wraparound.
The MMIO Transport
The MMIO transport (spec section 4.2) exposes the device as a flat register window mapped into the guest's physical address space. There is no bus, no enumeration protocol, no capability list -- just a base address and an IRQ number that the VMM communicates to the guest out-of-band.
Register Map
All registers are 4 bytes wide, 4-byte-aligned, at fixed offsets from the base
address (from linux/include/uapi/linux/virtio_mmio.h):
| Register | Offset | Dir | Purpose |
|---|---|---|---|
MagicValue |
0x000 |
RO | Must read 0x74726976 ("virt" in LE ASCII) |
Version |
0x004 |
RO | 2 = modern; 1 = legacy |
DeviceID |
0x008 |
RO | virtio device type |
VendorID |
0x00c |
RO | Vendor identifier |
DeviceFeatures |
0x010 |
RO | 32-bit feature page |
DeviceFeaturesSel |
0x014 |
WO | Feature page selector (0 or 1) |
DriverFeatures |
0x020 |
WO | Accepted feature bits |
DriverFeaturesSel |
0x024 |
WO | Driver feature page selector |
QueueSel |
0x030 |
WO | Select active queue (0-indexed) |
QueueNumMax |
0x034 |
RO | Maximum queue size |
QueueNum |
0x038 |
WO | Actual queue size (driver chooses) |
QueueReady |
0x044 |
RW | Write 1 to activate queue |
QueueNotify |
0x050 |
WO | Write queue index to kick device |
InterruptStatus |
0x060 |
RO | Bit 0 = used-buffer; bit 1 = config change |
InterruptACK |
0x064 |
WO | Acknowledge interrupt bits |
Status |
0x070 |
RW | Device status register |
QueueDescLow |
0x080 |
WO | Descriptor Table GPA bits 31:0 |
QueueDescHigh |
0x084 |
WO | Descriptor Table GPA bits 63:32 |
QueueAvailLow |
0x090 |
WO | Available Ring GPA bits 31:0 |
QueueAvailHigh |
0x094 |
WO | Available Ring GPA bits 63:32 |
QueueUsedLow |
0x0a0 |
WO | Used Ring GPA bits 31:0 |
QueueUsedHigh |
0x0a4 |
WO | Used Ring GPA bits 63:32 |
ConfigGeneration |
0x0fc |
RO | Config space atomicity counter |
Config |
0x100+ |
RW | Device-specific config (up to 0xfff) |
The legacy (Version 1) layout adds GuestPageSize at 0x028, QueueAlign at
0x03c, and QueuePFN at 0x040, and collapses the split 64-bit address
pairs into a single page-frame number. Modern drivers do not touch these.
Device Discovery
MMIO has no self-describing discovery mechanism (spec section 4.2.1). The guest
must learn each device's base address and IRQ from the VMM. Linux's
drivers/virtio/virtio_mmio.c driver supports three paths: a device tree node
with compatible = "virtio,mmio", a kernel command-line parameter
virtio_mmio.device=<size>@<baseaddr>:<irq>[:<id>] (requires
CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES), and static platform device registration
in board code.
Firecracker has used more than one discovery path. Older x86_64 guests learned
about virtio-MMIO devices through one virtio_mmio.device=... kernel-command-line
entry per device. Current x86_64 Firecracker uses minimal ACPI tables to
enumerate virtio-MMIO devices and their interrupts; aarch64 guests learn the
same information from the device tree. The transport is still virtio-MMIO in all
three cases. Only the discovery side channel changes.
Firecracker's MMIO Backend
Firecracker's MMIO transport is implemented in
src/vmm/src/devices/virtio/transport/mmio.rs as MmioTransport, which
implements the BusDevice trait. Configuration-register accesses are dispatched
through BusDevice::read and BusDevice::write. Queue notifications are registered
as ioeventfds, so an ordinary write to QueueNotify signals the device eventfd
without returning that write through the vCPU exit dispatcher.
MMIO_VERSION = 2 is hardcoded, so the device presents the modern register
layout. VENDOR_ID = 0 is a known mismatch with the specification's requirement
for a nonzero vendor value. Reading DeviceFeatures with DeviceFeaturesSel = 1 ORs
in 0x1 unconditionally, so VIRTIO_F_VERSION_1 (bit 32) is always visible
to the driver regardless of what the inner device model advertises.
set_device_status() enforces the spec state machine with a VALID_TRANSITIONS
table; any status write that is not a legal transition logs a warning.
Transition to DRIVER_OK calls locked_device().activate(), which hands
control to the device backend -- at that point, the virtqueues are live and
the device can begin processing descriptors.
The guest kernel requires CONFIG_VIRTIO_MMIO=y. On x86_64,
CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES is required for legacy command-line discovery
but may be disabled when ACPI supplies the device descriptions. Aarch64 uses the
device tree.
The PCI Transport
The PCI transport (spec section 4.1) is discoverable through the PCI bus. The guest scans functions, matches virtio IDs, and walks vendor-specific capabilities that locate the common configuration, notification, ISR, and device-specific regions. The transport also defines an optional PCI-configuration access capability; a device need not expose all five capability types.
Device IDs
PCI device IDs split into two ranges. Legacy (transitional) devices use IDs
0x1000-0x103F; modern devices use 0x1040 + the virtio device ID, so
virtio-net is 0x1041, virtio-blk is 0x1042, virtio-rng is 0x1044, and
virtio-vsock is 0x1053.
Core PCI Capabilities
Each capability uses cap_vndr = PCI_CAP_ID_VNDR and a cfg_type field. The
five original configuration types in linux/include/uapi/linux/virtio_pci.h are:
cfg_type |
Value | Purpose |
|---|---|---|
VIRTIO_PCI_CAP_COMMON_CFG |
1 | Common configuration struct (virtio_pci_common_cfg) |
VIRTIO_PCI_CAP_NOTIFY_CFG |
2 | Queue doorbell addresses |
VIRTIO_PCI_CAP_ISR_CFG |
3 | Interrupt status byte |
VIRTIO_PCI_CAP_DEVICE_CFG |
4 | Device-specific configuration |
VIRTIO_PCI_CAP_PCI_CFG |
5 | Alternative PCI config-space access window |
The v1.2 specification also defines capabilities for shared-memory regions and vendor data. Required capabilities depend on the device and negotiated facilities; the PCI configuration access window is optional.
struct virtio_pci_cap records which BAR holds the region (bar, 0-5), the
byte offset within that BAR (offset), and the region's length (length).
The notification capability also carries notify_off_multiplier; the doorbell
address for queue N is cap.offset + queue_notify_off x notify_off_multiplier.
struct virtio_pci_common_cfg exposes the feature selectors and data fields,
the queue count, the device status register, config_generation, the queue
selector and size, queue_enable, and the split 64-bit GPA fields
queue_desc_lo/hi, queue_avail_lo/hi, and queue_used_lo/hi -- a
superset of the MMIO register map, accessed through a memory-mapped struct
rather than individual register offsets.
Firecracker's Transport Choice
Firecracker began with virtio-MMIO because it required less machine-model code.
Current Firecracker optionally creates all virtio devices behind PCI when started
with --enable-pci; without the flag it uses MMIO. The current getting-started and
kernel-policy documents recommend PCI for higher throughput and lower latency.
MSI-X gives queues separate message-signaled interrupt vectors, while MMIO devices
share level-triggered interrupt status and acknowledgement registers. Workload and
guest configuration still determine the realized difference, so a development-branch
benchmark is not a portable transport guarantee.
Enabling PCI mode requires additional guest kernel configuration:
CONFIG_PCI, CONFIG_PCI_MMCONFIG, CONFIG_PCI_MSI, CONFIG_PCIEPORTBUS,
CONFIG_VIRTIO_PCI, CONFIG_BLK_MQ_PCI, CONFIG_PCI_HOST_COMMON, and
CONFIG_PCI_HOST_GENERIC. The guest must not pass pci=off on its command
line.
Selected Device Protocols
The following five devices show the recurring protocol shapes: packet queues, request/response storage, connection-oriented messages, memory reporting, and a device-writable byte stream. Current Firecracker also implements virtio-pmem and virtio-mem, plus a vhost-user backend option for virtio-blk; those additions do not change the queue and transport mechanics developed here.
virtio-net (Device ID 1)
The network device presents the guest with an Ethernet interface. At the virtio
layer, the useful facts are the descriptor shape and the virtio_net_hdr_v1
header that travels with each frame. The host-side TAP setup belongs to Chapter
21, because it is host networking rather than virtio protocol.
The virtio-net header. Every frame crossing the TAP/virtqueue boundary
carries a virtio_net_hdr_v1 (12 bytes) that describes the offload state of
the packet (defined in linux/include/uapi/linux/virtio_net.h):
| Offset | Field | Notes |
|---|---|---|
| 0 | flags |
VIRTIO_NET_HDR_F_NEEDS_CSUM = 1 |
| 1 | gso_type |
NONE=0, TCPV4=1, UDP=3, TCPV6=4, ECN flag=0x80 |
| 2-3 | hdr_len |
Total L2+L3+L4 header length |
| 4-5 | gso_size |
Desired MSS for segmentation |
| 6-7 | csum_start |
Byte offset where checksum computation begins |
| 8-9 | csum_offset |
Offset from csum_start to place the checksum |
| 10-11 | num_buffers |
Merged receive buffer count (if VIRTIO_NET_F_MRG_RXBUF) |
Queues. Firecracker implements exactly two virtqueues: RX_INDEX = 0 and
TX_INDEX = 1, each capped at NET_QUEUE_MAX_SIZE = 256 descriptors. The
spec allows a multi-queue extension (VIRTIO_NET_F_MQ = 22) with one transmit
and one receive queue per CPU, but Firecracker does not implement it; each
virtio-net device has a single queue pair. MAX_BUFFER_SIZE = 65562 bytes
(64 KiB plus the virtio-net header overhead) is the largest receive buffer
the device will accept.
Feature bits Firecracker advertises. From linux/include/uapi/linux/virtio_net.h,
Firecracker sets: VIRTIO_NET_F_CSUM (0), VIRTIO_NET_F_GUEST_CSUM (1),
VIRTIO_NET_F_GUEST_TSO4 (7), VIRTIO_NET_F_GUEST_TSO6 (8),
VIRTIO_NET_F_GUEST_UFO (10), VIRTIO_NET_F_HOST_TSO4 (11),
VIRTIO_NET_F_HOST_TSO6 (12), VIRTIO_NET_F_HOST_UFO (14),
VIRTIO_NET_F_MRG_RXBUF (15), plus VIRTIO_F_RING_EVENT_IDX (29) and
VIRTIO_F_VERSION_1 (32). VIRTIO_NET_F_MAC (5) is added when a MAC address
is configured; VIRTIO_NET_F_MTU (3) when an MTU override is set.
The full specification's virtio_net_config has later fields for status, queue
pairs, speed, and duplex. Firecracker exposes a 12-byte prefix: MAC, zero-filled
status and max_virtqueue_pairs slots, and MTU. It advertises only the MAC and MTU
fields when configured.
virtio-blk (Device ID 2)
The block device exposes a single virtqueue to the guest. Firecracker
implements BLOCK_NUM_QUEUES = 1, sized to 256 descriptors. IO_URING_NUM_ENTRIES = 128
(half the queue depth) because one block request typically spans two to three
descriptors; a full 256-entry submission ring would overflow an io_uring ring
of the same size.
The sector model. SECTOR_SIZE = 512 bytes (1 << 9). The capacity field
in the config struct is a u64 reporting the total sector count. Sector N
begins at byte offset N x 512 from the start of the backing file or device.
Request layout. Each I/O request is a three-descriptor chain:
- A 16-byte device-readable header:
type(u32),reserved(u32),sector(u64). - One or more data buffers -- device-readable for writes, device-writable for reads.
- A one-byte device-writable status field:
VIRTIO_BLK_S_OK = 0,VIRTIO_BLK_S_IOERR = 1, orVIRTIO_BLK_S_UNSUPP = 2.
The type field in the header selects the operation: VIRTIO_BLK_T_IN = 0
(read), VIRTIO_BLK_T_OUT = 1 (write), VIRTIO_BLK_T_FLUSH = 4 (cache
flush), VIRTIO_BLK_T_GET_ID = 8 (identify device: returns a 20-byte
ASCII string). All defined in linux/include/uapi/linux/virtio_blk.h.
Feature bits Firecracker advertises. VIRTIO_F_VERSION_1 (32) and
VIRTIO_F_RING_EVENT_IDX (29) always. VIRTIO_BLK_F_FLUSH (9) when the
backing disk is in writeback-cache mode. VIRTIO_BLK_F_RO (5) when the
disk is read-only.
virtio-vsock (Device ID 19)
vsock gives the guest and host a socket channel without a network interface.
The guest opens a socket with socket(AF_VSOCK, SOCK_STREAM, 0) and
addresses the host by its well-known CID. Firecracker translates the guest
virtio-vsock protocol to a host Unix-domain socket backend.
Each endpoint is addressed by a (CID, port) pair. Reserved CIDs:
VMADDR_CID_HYPERVISOR = 0,
VMADDR_CID_LOCAL = 1, VMADDR_CID_HOST = 2, VMADDR_CID_ANY = 0xFFFFFFFF.
Firecracker sets VSOCK_HOST_CID = 2 for the host-side endpoint.
Queues. Firecracker implements three queues (VSOCK_NUM_QUEUES = 3): RXQ
(index 0) for data from host to guest, TXQ (index 1) for data from guest to
host, and EVQ (index 2) for event messages. All three are sized to 256
descriptors. Each descriptor chain encodes exactly one vsock packet: a 44-byte
header followed by an optional payload up to MAX_PKT_BUF_SIZE = 65536 bytes.
The header. virtio_vsock_hdr is 44 bytes, packed:
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0-7 | src_cid |
le64 | Source context ID |
| 8-15 | dst_cid |
le64 | Destination context ID |
| 16-19 | src_port |
le32 | Source port |
| 20-23 | dst_port |
le32 | Destination port |
| 24-27 | len |
le32 | Payload byte count |
| 28-29 | type |
le16 | Socket type (1=STREAM, 2=SEQPACKET) |
| 30-31 | op |
le16 | Operation code |
| 32-35 | flags |
le32 | Operation-specific flags |
| 36-39 | buf_alloc |
le32 | Receiver buffer allocation (flow control) |
| 40-43 | fwd_cnt |
le32 | Bytes consumed by receiver (flow control) |
The op field drives the connection state machine. VIRTIO_VSOCK_OP_REQUEST = 1
initiates a connection; VIRTIO_VSOCK_OP_RESPONSE = 2 accepts it;
VIRTIO_VSOCK_OP_RST = 3 rejects or aborts; VIRTIO_VSOCK_OP_SHUTDOWN = 4
begins a graceful close; VIRTIO_VSOCK_OP_RW = 5 carries data;
VIRTIO_VSOCK_OP_CREDIT_UPDATE = 6 and VIRTIO_VSOCK_OP_CREDIT_REQUEST = 7
implement receive-window flow control through buf_alloc and fwd_cnt in
the header -- the receiver advertises available buffer space, and the sender
tracks how much of it it has consumed.
Feature bits. Firecracker advertises VIRTIO_F_VERSION_1 (32),
VIRTIO_F_IN_ORDER (35), and VIRTIO_F_RING_EVENT_IDX (29), combined as
AVAIL_FEATURES = (1 << 32) | (1 << 35) | (1 << 29). The device-specific
feature VIRTIO_VSOCK_F_SEQPACKET = 1 (SOCK_SEQPACKET support) is not
advertised.
virtio-balloon (Device ID 5)
The balloon device lets a cooperating guest return memory to the host without
stopping. Firecracker writes a target page count into num_pages; the guest driver
allocates pages into the balloon and reports their PFNs on the inflate queue.
Firecracker validates those ranges and discards their host mappings, which on its
anonymous private guest memory releases resident pages. A later access faults in a
zero-filled page. Deflation returns pages to the guest allocator; Firecracker only
acknowledges that queue because the earlier discard was a one-time operation.
This is cooperative reclamation, not an enforceable memory limit. A compromised or nonresponsive guest driver can ignore the target or report misleading statistics, so the host must remain able to accommodate the VM's configured memory.
Config struct. virtio_balloon_config (from
linux/include/uapi/linux/virtio_balloon.h):
| Field | Type | Meaning |
|---|---|---|
num_pages |
le32 | Host-requested balloon size in 4 KiB pages |
actual |
le32 | Current balloon size in 4 KiB pages |
free_page_hint_cmd_id |
le32 | Command ID for free page hinting |
All balloon accounting is in 4 KiB pages; 256 pages equals 1 MiB. The host
sets num_pages; the guest updates actual as it completes inflation or
deflation.
Queues. The inflate queue (index 0) and deflate queue (index 1) are always present, with Firecracker's current 256-entry queue size. Optional queues are appended in configuration order: statistics when polling is enabled, free-page hinting when requested, and free-page reporting when requested. Their numeric indices therefore depend on which earlier options are present.
Feature bits Firecracker advertises. VIRTIO_F_VERSION_1 (32) is always
present. VIRTIO_BALLOON_F_DEFLATE_ON_OOM (2), VIRTIO_BALLOON_F_STATS_VQ (1),
VIRTIO_BALLOON_F_FREE_PAGE_HINT (3), and VIRTIO_BALLOON_F_REPORTING (5) are
advertised only when their corresponding Firecracker options are enabled.
DEFLATE_ON_OOM lets the guest kernel reclaim balloon pages when a guest memory
allocation would otherwise enter its OOM path; it is unrelated to the host OOM
killer.
Statistics. The stats queue carries packed 10-byte entries with a u16 tag
and a u64 value. The defined tags include swap-in and swap-out counts
(VIRTIO_BALLOON_S_SWAP_IN = 0, VIRTIO_BALLOON_S_SWAP_OUT = 1), page fault
counts (VIRTIO_BALLOON_S_MAJFLT = 2, VIRTIO_BALLOON_S_MINFLT = 3), free
and total memory (VIRTIO_BALLOON_S_MEMFREE = 4, VIRTIO_BALLOON_S_MEMTOT = 5),
available memory (VIRTIO_BALLOON_S_AVAIL = 6), page cache size
(VIRTIO_BALLOON_S_CACHES = 7), huge-page allocation results (8 and 9), and,
with the guest support Firecracker documents for Linux 6.12 and later, OOM,
allocation-stall, scan, and reclaim counters (10 through 15). The polling interval is
configurable in Firecracker; setting it to 0 disables polling. The guest
kernel requires CONFIG_MEMORY_BALLOON=y and CONFIG_VIRTIO_BALLOON=y.
virtio-rng (Device ID 4)
The entropy device is the simplest virtio device in the spec: linux/include/uapi/linux/virtio_rng.h
contains no device-specific feature bits -- it includes only virtio_ids.h
and virtio_config.h. There is no device-specific config space. The entire
protocol fits in a paragraph.
Firecracker exposes one queue (RNG_NUM_QUEUES = 1) and advertises only
VIRTIO_F_VERSION_1 (32). The queue direction is device-writable only: the
guest posts write-only descriptors pointing to buffers it wants filled with
entropy, and the device fills them. The guest never sends data to the device.
MAX_ENTROPY_BYTES = 65536 (64 KiB) bounds the host allocation for one request,
including a malicious chain whose overlapping descriptors inflate its apparent
length. Firecracker draws
entropy from aws_lc_rs::rand (the AWS LibCrypto Rust bindings), not from
/dev/random or getrandom() directly. Rate limiting is available via the
Firecracker API, with independent controls for bytes-per-second and
operations-per-second.
The simplicity is the point. A random number device has no protocol state, no connection setup, no flow control, and no error conditions beyond buffer exhaustion. It is what virtio looks like when nothing is left to remove.
Wiring It Together
flowchart TB
gk["Guest Kernel Driver"]
vq["Virtqueue<br/>(desc / avail / used rings)"]
mmio["virtio-MMIO or PCI Transport"]
vmm["VMM Device Backend<br/>(Firecracker)"]
tap["TAP /dev/net/tun"]
blkfile["Block backing file"]
vsockunix["Unix socket<br/>(vsock muxer)"]
awslc["aws_lc_rs::rand"]
gk -->|"write head idx to avail.ring,\nkick QueueNotify"| vq
vq -->|"queue notification via ioeventfd"| mmio
mmio -->|"wake device event handler"| vmm
vmm -->|"net: read/write virtio_net_hdr + frame"| tap
vmm -->|"blk: read/write 512-byte sectors"| blkfile
vmm -->|"vsock: virtio_vsock_hdr + payload"| vsockunix
vmm -->|"rng: fill entropy bytes"| awslc
vmm -->|"write id+len to used.ring,\ninterrupt guest"| vq
vq -->|"driver polls used.idx"| gk
Sources And Further Reading
- OASIS virtio Committee Specification v1.2, CS01, 1 July 2022 (HTML): https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.html
- OASIS virtio v1.2 CS01 (PDF): https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.pdf
oasis-tcs/virtio-speccanonical C headers (virtio-queue.h): https://github.com/oasis-tcs/virtio-spec/blob/master/virtio-queue.h- Linux UAPI
linux/virtio_ring.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_ring.h - Linux UAPI
linux/virtio_config.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_config.h - Linux UAPI
linux/virtio_mmio.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_mmio.h - Linux UAPI
linux/virtio_pci.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_pci.h - Linux UAPI
linux/virtio_ids.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_ids.h - Linux UAPI
linux/virtio_net.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_net.h - Linux UAPI
linux/virtio_blk.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_blk.h - Linux UAPI
linux/virtio_vsock.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_vsock.h - Linux UAPI
linux/virtio_balloon.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_balloon.h - Linux UAPI
linux/virtio_rng.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_rng.h - Linux UAPI
linux/if_tun.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/if_tun.h - Linux
drivers/virtio/virtio.c(feature negotiation implementation): https://github.com/torvalds/linux/blob/master/drivers/virtio/virtio.c - Linux
drivers/virtio/virtio_mmio.c: https://github.com/torvalds/linux/blob/master/drivers/virtio/virtio_mmio.c - Linux kernel virtio driver API documentation: https://docs.kernel.org/driver-api/virtio/virtio.html
vsock(7)man page: https://man7.org/linux/man-pages/man7/vsock.7.html- Firecracker split-queue implementation and Kani harnesses: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/queue.rs
- Firecracker
src/vmm/src/devices/virtio/transport/mmio.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/transport/mmio.rs - Firecracker
src/vmm/src/devices/virtio/net/device.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/net/device.rs - Firecracker
src/vmm/src/devices/virtio/net/tap.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/net/tap.rs - Firecracker
src/vmm/src/devices/virtio/block/virtio/device.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/block/virtio/device.rs - Firecracker
src/vmm/src/devices/virtio/vsock/device.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/vsock/device.rs - Firecracker
src/vmm/src/devices/virtio/balloon/device.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/balloon/device.rs - Firecracker
src/vmm/src/devices/virtio/rng/device.rs: https://github.com/firecracker-microvm/firecracker/blob/main/src/vmm/src/devices/virtio/rng/device.rs - Firecracker ballooning documentation: https://github.com/firecracker-microvm/firecracker/blob/main/docs/ballooning.md
- Firecracker network setup documentation: https://github.com/firecracker-microvm/firecracker/blob/main/docs/network-setup.md
- Firecracker guest kernel transport requirements: https://github.com/firecracker-microvm/firecracker/blob/main/docs/kernel-policy.md
- Firecracker virtio-mem and virtio-pmem documentation: https://github.com/firecracker-microvm/firecracker/blob/main/docs/memory-hotplug.md, https://github.com/firecracker-microvm/firecracker/blob/main/docs/pmem.md
- Paired research note: virtio