The virtio Vsock Device
Vsock is a socket family — AF_VSOCK — that lets a process on the host and a process inside the
guest talk over a stream connection without any network stack, IP address, or NIC. There are no
Ethernet frames, no routing, no firewall: just an endpoint identified by a pair, (CID, port).
CID is a Context ID, a small integer naming a VM (or the host). The host is always CID 2
(VMADDR_CID_HOST); the guest's CID is whatever you configured (guest_cid). Inside the guest,
vsock looks like an ordinary socket family. On the host side, though, Firecracker is the vsock
device — it does not open a kernel AF_VSOCK socket. Instead it bridges the guest's vsock
traffic to host Unix domain sockets through an internal multiplexer (the "muxer").
This chapter dissects that device: the three virtqueues (RX, TX, EVENT) that carry vsock packets,
the (CID, port) addressing and the muxer that maps connections to host Unix sockets, the
per-connection state machine driven by the op field in each packet header, and the credit-based
flow control that keeps a fast sender from drowning a slow receiver. After this chapter you will be
able to: configure a vsock device; open a connection in both directions (guest→host and
host→guest) from the shell; trace a byte from a guest write() through a TX descriptor chain to a
host Unix-socket write(); and explain why the agent control channel in firecracker-containerd and
Kata rides on vsock.
Note: Keep three layers separate in your head. The transport (virtio-MMIO) tells the guest the device exists and wires up kicks and interrupts. The queues (virtqueues) are the split-ring data structures whose descriptor chains carry the bytes. This chapter is the device-type layer on top: what a vsock packet means and what the muxer does with it. Vsock is virtio device type ID 19.
Where the device lives
# The vsock device module — confirm the directory exists on your branch.
rg -n "TYPE_VSOCK|VIRTIO_VSOCK|device type|= 19" src/vmm/src/devices/virtio/vsock/
find src/vmm/src/devices/virtio/vsock -name '*.rs' | sort
# The top-level device struct and its VirtioDevice impl.
rg -n "struct Vsock|impl.*VirtioDevice for Vsock|fn device_type" src/vmm/src/devices/virtio/vsock/
The vsock device is one virtio device per microVM — you cannot configure two. Its top-level type
(call it Vsock<B>, generic over a backend B; verify the name on your branch) implements the
same VirtioDevice trait as block and net: device_type() returns 19, queues() returns three,
activate() wires it live. The interesting machinery — the muxer, the per-connection state, the
host Unix-socket backend — lives behind the backend type.
# The host-Unix-socket backend and the multiplexer.
rg -n "VsockUnixBackend|VsockMuxer|struct .*Muxer|uds_path|fn new" src/vmm/src/devices/virtio/vsock/
The three virtqueues
A vsock device exposes three virtqueues, not the two you might expect. Find the index constants:
rg -n "RXQ|TXQ|EVQ|RX_INDEX|TX_INDEX|EVENT|NUM_QUEUES|QUEUE_SIZES" src/vmm/src/devices/virtio/vsock/
| Queue | Index (verify) | Direction | Carries |
|---|---|---|---|
| RX | 0 | device → driver | Packets into the guest: bytes from the host, plus RESPONSE/CREDIT/RST ops the device originates. The guest pre-posts empty buffers; the device fills them. |
| TX | 1 | driver → device | Packets out of the guest: bytes the guest sends, plus REQUEST/SHUTDOWN/credit ops. The device drains and routes them. |
| EVENT | 2 | device → driver | Transport-level events — chiefly a transport reset notification (e.g. after snapshot restore) so the guest tears down stale connections. Low traffic. |
The direction convention is from the driver's point of view, the standard virtio convention:
"RX" is what the guest receives. So the device's job on the RX queue is to produce packets, and
on the TX queue to consume them. Each descriptor chain in TX or RX carries exactly one vsock
packet: a fixed header followed by an optional data payload. The mechanics of how a descriptor
chain is walked — DescriptorChain, reading the header out of guest memory,
the available/used rings — are entirely in virtqueues; the vsock code reuses that
machinery and only adds meaning to the bytes.
guest Firecracker (VMM thread) host
┌────────┐ TX queue (driver→device) ┌───────────────────────────┐
│ guest │ ───[hdr|data]──► descriptor ──► │ parse hdr → muxer route │ ──► ┌──────────────┐
│ process│ chain │ (dst_port → conn) │ │ Unix socket │
│ write │ │ write payload to host fd │ │ *_<port> │
└────────┘ │ │ └──────────────┘
▲ RX queue (device→driver) │ read host fd → build hdr │ │
└──────[hdr|data]◄── descriptor ◄───── │ fill guest RX buffer │ ◄──────────┘
chain └───────────────────────────┘
EVENT queue: reset notifications
# How the device picks apart a TX chain and assembles an RX chain.
rg -n "fn process_rx|fn process_tx|RxOps|TxBuf|VsockPacket|fn read_from|fn write_to" \
src/vmm/src/devices/virtio/vsock/
The vsock packet header
Every packet — TX or RX — begins with a fixed virtio_vsock_hdr carried in the first part of the
descriptor chain. This is the contract; learn its fields:
rg -n "virtio_vsock_hdr|struct VsockPacket|src_cid|dst_cid|src_port|dst_port|buf_alloc|fwd_cnt|fn op|fn type_" \
src/vmm/src/devices/virtio/vsock/
| Field | Meaning |
|---|---|
src_cid / dst_cid | Source / destination Context ID. Guest→host: dst_cid = 2. |
src_port / dst_port | Source / destination ports — the other half of the address. |
len | Length of the data payload following the header. |
type | Connection type; stream (VIRTIO_VSOCK_TYPE_STREAM) is the one you'll see. |
op | The opcode that drives the state machine (see below). |
flags | Per-op flags, e.g. shutdown direction (read/write half). |
buf_alloc | Sender's total receive-buffer size — input to flow control. |
fwd_cnt | Count of bytes the sender has consumed/forwarded — input to flow control. |
The (src_cid, src_port, dst_cid, dst_port) four-tuple uniquely identifies a connection. The muxer
keys its connection table on the (local_port, peer_port) pair (the CIDs are fixed for a given
device, so they fall out).
The op field and the connection state machine
The op field is the verb. The muxer reads it off each TX packet and drives a per-connection state
machine; it writes op values into RX packets it originates.
rg -n "VSOCK_OP_REQUEST|VSOCK_OP_RESPONSE|VSOCK_OP_RW|VSOCK_OP_CREDIT_UPDATE|VSOCK_OP_CREDIT_REQUEST|VSOCK_OP_SHUTDOWN|VSOCK_OP_RST|ConnState|enum .*State" \
src/vmm/src/devices/virtio/vsock/
op | Sent by | Meaning |
|---|---|---|
VSOCK_OP_REQUEST | initiator | "Open a connection to dst_port." Guest→host arrives on TX; host→guest is emitted by the device on RX. |
VSOCK_OP_RESPONSE | accepter | "Connection accepted." Completes the handshake. |
VSOCK_OP_RW | either | Carries len bytes of stream data. The workhorse op. |
VSOCK_OP_CREDIT_UPDATE | either | "Here is my current buf_alloc/fwd_cnt" — refreshes the peer's send window. |
VSOCK_OP_CREDIT_REQUEST | either | "Tell me your credit." Peer must answer with CREDIT_UPDATE. |
VSOCK_OP_SHUTDOWN | either | Half-close (flags say which direction). |
VSOCK_OP_RST | either | Hard reset / connection refused. The catch-all for "this connection is dead." |
stateDiagram-v2
[*] --> Closed
Closed --> LocalInit: guest TX VSOCK_OP_REQUEST (guest→host)
Closed --> PeerInit: host connect → device RX VSOCK_OP_REQUEST (host→guest)
LocalInit --> Established: RX VSOCK_OP_RESPONSE from host backend
PeerInit --> Established: guest TX VSOCK_OP_RESPONSE
Established --> Established: VSOCK_OP_RW (data) / CREDIT_UPDATE
Established --> Closing: VSOCK_OP_SHUTDOWN (one half)
Closing --> Closed: both halves shut, or VSOCK_OP_RST
Established --> Closed: VSOCK_OP_RST
LocalInit --> Closed: VSOCK_OP_RST (refused)
Note: A
RSTcan arrive in almost any state — it is how a refused or broken connection is reported. When you find a guest that connects and is immediately reset, you are looking for where the muxer emitsRST: usually "no host process is listening on the expected Unix socket."
# Where the muxer emits RST and how it maps a packet to a connection.
rg -n "VSOCK_OP_RST|send_bytes|recv_pkt|send_pkt|conn_map|insert|remove|fn process" \
src/vmm/src/devices/virtio/vsock/
The muxer: bridging to host Unix sockets
This is the part with no analogue in block or net. The muxer (VsockMuxer) holds a table mapping
each live connection (local_port, peer_port) to a host Unix-domain-socket file descriptor, and
shuttles bytes between virtqueue packets and those fds. It is registered in the
EventManager's epoll set so that both a guest kick (queue eventfd) and
host socket readiness (a Unix-socket fd becoming readable) wake the device.
rg -n "VsockMuxer|HashMap|epoll|EventSet|register|fn handle_event|LocalListener|fn notify" \
src/vmm/src/devices/virtio/vsock/
The two connection directions use the host Unix socket differently. This is the single most important operational fact about Firecracker vsock — internalize it:
Guest → host. A guest process connects to (CID 2, port P). The device receives a
VSOCK_OP_REQUEST on TX. The muxer then tries to connect, on the host, to a Unix socket whose path
is the configured uds_path with _<P> appended — "<uds_path>_<P>". Some host process must
already be listening there. If the connect succeeds, the muxer sends VSOCK_OP_RESPONSE back on
RX; if nothing is listening, it sends VSOCK_OP_RST.
Host → guest. A host process connects to the bare uds_path Unix socket, then writes a text
command "CONNECT <port>\n". The muxer parses it, opens a vsock connection to the guest by
emitting a VSOCK_OP_REQUEST on the RX queue toward (guest_cid, port), and on success replies on
the Unix socket with "OK <assigned_port>\n". After that line, the Unix socket is a transparent
byte pipe to the guest process.
rg -n "CONNECT|fn parse|OK |b\"OK|host_sock|uds_path|UnixListener|UnixStream" \
src/vmm/src/devices/virtio/vsock/
sequenceDiagram
participant H as host process
participant U as Unix socket(s)
participant M as VsockMuxer
participant Q as RX/TX queues
participant G as guest process
Note over H,G: Guest → host
G->>Q: TX VSOCK_OP_REQUEST (dst_cid=2, dst_port=P)
M->>U: connect to "<uds_path>_P"
U-->>M: connected (a process was listening)
M->>Q: RX VSOCK_OP_RESPONSE
G->>Q: TX VSOCK_OP_RW (data)
M->>U: write(data) to host fd
Note over H,G: Host → guest
H->>U: connect "<uds_path>", send "CONNECT P\n"
M->>Q: RX VSOCK_OP_REQUEST (dst_cid=guest_cid, dst_port=P)
G->>Q: TX VSOCK_OP_RESPONSE
M->>U: reply "OK <port>\n"
H->>U: write(data); M->>Q: RX VSOCK_OP_RW (data)
Configuration and activation
The vsock device is created via the API before boot — it is pre-boot only, and there is exactly one per microVM.
# The API handler and config struct.
rg -n "VsockDeviceConfig|guest_cid|uds_path|vsock_id|PUT.*vsock|fn set_vsock" \
src/vmm/src/ src/firecracker/src/
# Create the device: guest CID 3, bridged to /tmp/v.sock.
curl --unix-socket /tmp/fc.sock -i -X PUT 'http://localhost/vsock' \
-H 'Content-Type: application/json' \
-d '{ "vsock_id": "vsock0", "guest_cid": 3, "uds_path": "/tmp/v.sock" }'
guest_cid must be ≥ 3 (0, 1, 2 are reserved). When the guest driver finishes the virtio handshake
and writes DRIVER_OK, the transport calls the device's activate:
rg -n "fn activate|register.*queue|EventManager|epoll|ioeventfd|self\.queues" \
src/vmm/src/devices/virtio/vsock/
At activate, the device registers with the EventManager: the RX, TX, and
EVENT queue eventfds (each fired by a guest QueueNotify via KVM_IOEVENTFD — see
virtio-MMIO) and the muxer's epoll fd, which itself watches the host
uds_path listener plus every per-connection Unix-socket fd. From that point a single VMM-thread
epoll wakeup can mean "guest kicked a queue" or "a host socket has bytes."
Real-world use: the agent control channel
Vsock exists in Firecracker for one dominant reason: a control channel between an in-guest agent
and a host-side shim, without a network. In firecracker-containerd and Kata Containers, the
host runtime shim and the in-VM agent speak a gRPC/ttrpc protocol over vsock — the shim connects to
the guest agent's well-known port to start containers, stream stdio, and report exits. No tap
device, no bridge, no IP to assign or firewall. See
firecracker-containerd integration for the
real consumer.
Tip: When debugging a firecracker-containerd VM that "boots but the shim can't reach the agent," the vsock muxer is the prime suspect. Check the
uds_path, confirm the agent is listening on its port inside the guest, and confirm the host side is connecting to<uds_path>(host→guest) or listening on<uds_path>_<port>(guest→host) with the right port suffix.
The device also implements Persist so its connection-bearing state participates in
snapshot/restore; after restore the guest is told via the EVENT queue that the transport reset.
rg -n "impl Persist|fn save|fn restore|VsockState|TYPE_VSOCK|Persist for Vsock" \
src/vmm/src/devices/virtio/vsock/
Reading exercise
# 1. Confirm the device type ID and the three queues.
rg -n "= 19|TYPE_VSOCK|NUM_QUEUES|RXQ|TXQ|EVQ|QUEUE_SIZES" src/vmm/src/devices/virtio/vsock/
# 2. The packet header and its fields.
rg -n "virtio_vsock_hdr|struct VsockPacket|buf_alloc|fwd_cnt|fn op|fn len_" src/vmm/src/devices/virtio/vsock/
# 3. Every opcode and the state machine that consumes it.
rg -n "VSOCK_OP_|ConnState|enum .*State|fn state" src/vmm/src/devices/virtio/vsock/
# 4. The muxer, the connection table, and the CONNECT text protocol.
rg -n "VsockMuxer|conn_map|HashMap|CONNECT|uds_path|UnixListener|UnixStream" src/vmm/src/devices/virtio/vsock/
# 5. Credit-based flow control.
rg -n "buf_alloc|fwd_cnt|credit|peer_avail|tx_cnt|rx_cnt" src/vmm/src/devices/virtio/vsock/
# 6. activate(): queue eventfds + muxer epoll fd into the EventManager.
rg -n "fn activate|register|EventManager|epoll" src/vmm/src/devices/virtio/vsock/
# 7. On a running microVM (guest CID 3, uds_path /tmp/v.sock):
# GUEST→HOST: on the host, listen on the suffixed socket:
# socat - UNIX-LISTEN:/tmp/v.sock_5234
# then in the guest connect to (CID 2, port 5234) and type.
# HOST→GUEST: in the guest, listen on port 5234; on the host:
# socat - UNIX-CONNECT:/tmp/v.sock then send: CONNECT 5234<newline>
Answer:
- Why three virtqueues and not two? What does the EVENT queue carry, and from whose point of view are "RX" and "TX" named?
- Walk a guest→host connection from the guest's
connect((2, 5234))to host bytes arriving: which queue carries theREQUEST, what Unix-socket path does the muxer dial, and what op confirms it? - Do the same for host→guest: what does the host process write to
<uds_path>, and what does the muxer emit on which queue? - Pick three
VSOCK_OP_*values and place them on the state machine. Which op can appear in (almost) any state, and what real failure does it usually signal? - What do
buf_allocandfwd_cntmean, and which op refreshes them? - What gets registered with the EventManager at
activate, and why must host socket fds be in the same epoll set as the guest queue eventfds?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
Guest connect() to host port instantly returns "connection reset" | No host process listening on <uds_path>_<port>; muxer sends VSOCK_OP_RST | muxer guest→host connect path; rg "VSOCK_OP_RST" |
Host→guest hangs after CONNECT N | No guest process listening on port N, or the CONNECT/OK text handshake mis-parsed | rg "CONNECT"/parse; guest-side listener |
| Connection stalls mid-stream, sender blocked | Credit exhausted — CREDIT_UPDATE lost or fwd_cnt/buf_alloc miscomputed | credit accounting; rg "fwd_cnt|buf_alloc|credit" |
PUT /vsock rejected | Tried after boot, or a second vsock device, or guest_cid < 3 | pre-boot config validation; rg "guest_cid|VsockDeviceConfig" |
| Device never processes anything | activate not reached (DRIVER_OK missed) or eventfds not registered | fn activate; EventManager registration |
| After snapshot restore, guest uses dead connections | EVENT-queue reset notification not delivered/consumed | EVENT queue path; Persist/restore |
| Wrong process receives bytes | Connection keyed on wrong (local_port, peer_port) pair | muxer connection table insert/lookup |
Validation: prove you understand this
- Draw the vsock data path end to end for guest→host: guest
write()→ which queue → which descriptor-chain contents → which muxer step → which host syscall. Name the op on each leg. - Explain
(CID, port)addressing. What is the host CID, what constrainsguest_cid, and why does Firecracker not open a hostAF_VSOCKsocket? - List the three virtqueues, their directions (from the driver's view), and what each carries. Why is the EVENT queue necessary?
- Take
REQUEST,RESPONSE,RW,SHUTDOWN, andRSTand walk the connection state machine, saying which side emits each and on which queue it lands. - Explain credit-based flow control using
buf_allocandfwd_cnt: what does the sender compute, and which op closes the loop when the window opens? - A firecracker-containerd VM boots but the shim cannot reach the in-VM agent. Give the ordered list of vsock-specific checks — config, direction, socket path/suffix, port, listener — you would run to localize the break.
Next: The virtio Balloon Device — reclaiming guest memory back to the host on demand: the inflate/deflate queues, the page-hint protocol, and why a balloon is a cooperative, not coercive, memory control.