vm-superio
Not every device a guest needs is a virtio device. A Linux kernel booting on
Firecracker expects a few legacy devices to exist at fixed, architecturally
mandated locations: a 16550 UART at the serial port (so it has a console
before any virtio driver is up), and on x86 a partial i8042 keyboard
controller (so a guest writing the magic reset byte can reboot). On aarch64 it
expects a PL031 real-time clock. These are emulated in rust-vmm's
vm-superio crate — and vm-superio is one of the three crates Firecracker
donated upstream: it was extracted from Firecracker's own device models so the
ecosystem could share the same audited UART. This is the rust-vmm crate behind the
serial console and legacy devices deep dive.
After this chapter you can: name what vm-superio provides (Serial,
I8042Device, Rtc) and the Trigger trait; explain how the serial console
turns guest writes into bytes on your terminal and host input into guest
interrupts; describe why these device models are generic enough to share; and find
where Firecracker constructs and drives a Serial for the console.
Note:
vm-superiois the donor relationship made concrete. The serial code you'll read here is (a generalized version of) Firecracker's old serial code. When you fix a UART bug, ask whether it belongs invm-superioupstream — Cloud Hypervisor uses the sameSerial.
# Confirm the dependency and the pinned version (verify on your branch):
rg -n "vm-superio" Cargo.toml src/vmm/Cargo.toml Cargo.lock
cargo doc -p vm-superio --no-deps --open
docs.rs: docs.rs/vm-superio.
What vm-superio provides
| Type | Emulates | Where it lives in the guest | Arch |
|---|---|---|---|
Serial<T, W> | the 16550A UART (the serial port) | PIO 0x3f8 (COM1) on x86 / an MMIO UART on aarch64 | both |
I8042Device<T> | the i8042 PS/2 controller (reset path only) | PIO 0x60/0x64 on x86 | x86 |
Rtc<T> | the PL031 ARM real-time clock | an MMIO register block on aarch64 | aarch64 |
Each is a small, self-contained state machine: it has registers, it reacts to
guest reads and writes of those registers, and it can raise an interrupt to the
guest. They are deliberately minimal — vm-superio (and Firecracker) implement
only what a booting Linux guest actually uses, which is the same minimal-device
philosophy that governs the whole VMM. The i8042, famously, implements barely more
than "the guest wrote the CPU-reset command byte, so reboot."
Why are these legacy devices needed at all when Firecracker prefers virtio?
Because the guest needs them before virtio exists. A kernel emits its very
first log lines — the decompression banner, the early boot messages — long before
it has probed and brought up a virtio console driver. The 16550 UART is the
device the kernel can talk to from the earliest moment, with a driver compiled in,
at an address it knows by architectural convention. So the serial console is not a
nicety; it is the only output channel during early boot, which is exactly why a
broken Serial shows up as "the microVM boots but I see nothing." The i8042 is
there for a symmetric reason on the input/control side: a Linux guest issues its
reboot by writing the i8042 reset line, so without a (partial) i8042 the guest
cannot cleanly reboot or be told to shut down via Ctrl+Alt+Del. Both are the
minimum legacy surface that makes a stock Linux kernel boot and shut down
correctly — and not one register more.
The Trigger trait: how a device raises an interrupt
A device model must be able to interrupt the guest — the UART raises an IRQ
when a byte arrives for the guest to read. But vm-superio is a pure-logic crate;
it must not depend on KVM or on how your particular VMM injects interrupts. So it
abstracts interrupt injection behind a tiny trait, Trigger:
find ~/.cargo/registry/src -maxdepth 2 -type d -name 'vm-superio-*' \
-exec rg -n "trait Trigger|fn trigger|struct Serial|impl.*Serial|fn enqueue_raw_bytes|fn write\b|fn read\b" {} +
#![allow(unused)] fn main() { // vm-superio's interrupt abstraction (shape): pub trait Trigger { type E; fn trigger(&self) -> Result<(), Self::E>; } }
You implement Trigger for your interrupt mechanism. Firecracker implements it for
an EventFd (vmm-sys-util.md) wired to KVM's irqfd
(KVM_IRQFD): when the Serial calls trigger(), it writes the eventfd, and KVM
injects the configured IRQ into the guest. That single seam is how a generic UART
model drives a real KVM interrupt without vm-superio knowing anything about KVM.
flowchart LR
G["guest reads/writes UART register"] --> EX["VM exit (PIO/MMIO)"]
EX --> S["Serial::read / Serial::write\n(vm-superio state machine)"]
S -->|"output byte"| OUT["W: io::Write → stdout / a pty / a file"]
HOSTIN["host input byte"] --> ENQ["Serial::enqueue_raw_bytes"]
ENQ --> T["Trigger::trigger()"]
T --> EFD["EventFd → KVM_IRQFD → guest IRQ"]
Serial is generic over two parameters: T: Trigger (how to interrupt) and
W: io::Write (where output bytes go). That second parameter is why the same
Serial can write the console to your terminal, to a file, or to a pty — you pick
W. Firecracker writes it to stdout (or wherever the serial output is configured).
How Firecracker uses Serial for the console
When you boot a microVM and see the kernel log and a login prompt, you are watching
a vm-superio Serial. The path:
# Where FC constructs the Serial and wires it onto the device bus:
rg -n "vm_superio|Serial::|Serial<|I8042|enqueue_raw_bytes|SerialDevice|SerialWrapper" src/vmm/src/devices/
rg -n "Trigger|trigger|irqfd|register_irqfd|EventFd" src/vmm/src/devices/legacy/ 2>/dev/null
# On x86 the serial sits on the PIO bus; find the bus registration:
rg -n "0x3f8|COM1|PortIODeviceManager|stdin|serial" src/vmm/src/
| Direction | What happens | Pieces |
|---|---|---|
| Guest → host (output) | guest writes the UART TX register → PIO exit → Serial::write → bytes go to W (stdout) | VcpuExit::IoOut → Serial → io::Write |
| Host → guest (input) | host stdin byte → Serial::enqueue_raw_bytes → Trigger::trigger() → IRQ → guest reads RX | the VMM thread / an EventManager subscriber on stdin |
| Guest reboot (i8042) | guest writes the reset command byte → I8042Device → FC initiates shutdown | VcpuExit::IoOut on 0x64 → I8042Device |
Firecracker wraps Serial in its own device type that:
- implements
Triggerover anEventFdregistered withKVM_IRQFDso output readiness / input arrival becomes a guest IRQ, - registers the device on the PIO bus (x86) or MMIO bus (aarch64) so vCPU exits at the serial address dispatch into it, and
- subscribes to host stdin in the
EventManager(event-manager.md) so typed input is enqueued into the UART and the guest is interrupted.
That last point connects three rust-vmm crates in one device: vm-superio is the
UART logic, event-manager delivers the stdin readiness, vmm-sys-util's
EventFd is the Trigger. This is the ecosystem working as designed.
#![allow(unused)] fn main() { // The shape of constructing a Serial Firecracker-style (illustrative). use vm_superio::Serial; use vmm_sys_util::eventfd::EventFd; // A Trigger over an EventFd wired to KVM_IRQFD elsewhere. struct EventFdTrigger(EventFd); impl vm_superio::Trigger for EventFdTrigger { type E = std::io::Error; fn trigger(&self) -> Result<(), Self::E> { self.0.write(1) } } let intr = EventFdTrigger(EventFd::new(libc::EFD_NONBLOCK).unwrap()); let mut serial = Serial::new(intr, std::io::stdout()); // T = trigger, W = stdout // Guest wrote the TX register (from a PIO exit): serial.write(0 /* register offset */, b'H').unwrap(); // -> 'H' to stdout // A host stdin byte arrived (from an EventManager stdin subscriber): serial.enqueue_raw_bytes(b"login: ").unwrap(); // -> raises the guest IRQ via Trigger }
Tip: This is also exactly why
[Lab 1.3 boot](../level-1/labs/lab-03-boot-first-microvm.md) shows kernel output on your terminal even though Firecracker has no graphics, no framebuffer, nothing. The console is one 16550 UART, modeled in ~a few hundred lines of shared Rust, writing to your stdout.
Reading exercise
# 1. The dependency and pinned version.
rg -n "vm-superio" Cargo.toml Cargo.lock
# 2. The Serial / I8042 / Rtc / Trigger API (pinned version).
cargo doc -p vm-superio --no-deps --open
# 3. Where FC constructs and wraps Serial.
rg -n "Serial::|Serial<|SerialWrapper|enqueue_raw_bytes" src/vmm/src/devices/
# 4. FC's Trigger impl over an EventFd / irqfd.
rg -n "impl.*Trigger|fn trigger|register_irqfd|KVM_IRQFD|EventFd" src/vmm/src/devices/
# 5. The bus the serial is registered on (PIO x86 / MMIO aarch64).
rg -n "0x3f8|COM1|PortIODeviceManager|MMIODeviceManager" src/vmm/src/
# 6. The stdin → serial input subscription.
rg -n "stdin|enqueue_raw_bytes|EventSet::IN|process" src/vmm/src/devices/
Answer:
- Name the three device models
vm-superioprovides and which architecture each serves. Why are they so minimal? - What is the
Triggertrait and why does it exist? What does Firecracker implement it over, and how does that reach the guest? Serialis generic over two type parameters. What are they, and what does each let Firecracker vary?- Trace a kernel log line from a guest UART write to a character on your terminal. Which crate does each step?
- Trace a host keystroke from stdin to the guest reading it. Which three rust-vmm crates cooperate?
- What does the i8042 model actually do for Firecracker, and which guest action triggers it?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| No kernel output on the console | serial not registered on the bus, or W not flushed to stdout | the bus registration; the io::Write sink |
| Typed input never reaches the guest | stdin not subscribed in EventManager, or Trigger not firing the IRQ | the stdin subscriber; the Trigger/irqfd wiring |
Guest can't reboot / reboot hangs | i8042 reset path not handled / not on the bus | I8042Device; the 0x64 PIO dispatch |
| Console drops bytes under load | output backpressure / FIFO handling in the UART model | vm-superio Serial — possibly an upstream fix |
| aarch64 guest has wrong time | PL031 Rtc not wired or returning wrong base | Rtc; arch/aarch64 device setup |
Next: event-manager — the epoll loop the VMM thread runs, and the crate that delivers the stdin (and every device) readiness events the serial console depends on. Also a Firecracker donation.