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-superio is 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 in vm-superio upstream — Cloud Hypervisor uses the same Serial.

# 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

TypeEmulatesWhere it lives in the guestArch
Serial<T, W>the 16550A UART (the serial port)PIO 0x3f8 (COM1) on x86 / an MMIO UART on aarch64both
I8042Device<T>the i8042 PS/2 controller (reset path only)PIO 0x60/0x64 on x86x86
Rtc<T>the PL031 ARM real-time clockan MMIO register block on aarch64aarch64

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/
DirectionWhat happensPieces
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 RXthe VMM thread / an EventManager subscriber on stdin
Guest reboot (i8042)guest writes the reset command byte → I8042Device → FC initiates shutdownVcpuExit::IoOut on 0x64 → I8042Device

Firecracker wraps Serial in its own device type that:

  1. implements Trigger over an EventFd registered with KVM_IRQFD so output readiness / input arrival becomes a guest IRQ,
  2. registers the device on the PIO bus (x86) or MMIO bus (aarch64) so vCPU exits at the serial address dispatch into it, and
  3. 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:

  1. Name the three device models vm-superio provides and which architecture each serves. Why are they so minimal?
  2. What is the Trigger trait and why does it exist? What does Firecracker implement it over, and how does that reach the guest?
  3. Serial is generic over two type parameters. What are they, and what does each let Firecracker vary?
  4. Trace a kernel log line from a guest UART write to a character on your terminal. Which crate does each step?
  5. Trace a host keystroke from stdin to the guest reading it. Which three rust-vmm crates cooperate?
  6. What does the i8042 model actually do for Firecracker, and which guest action triggers it?

Common bugs and symptoms

SymptomRoot causeWhere to look
No kernel output on the consoleserial not registered on the bus, or W not flushed to stdoutthe bus registration; the io::Write sink
Typed input never reaches the gueststdin not subscribed in EventManager, or Trigger not firing the IRQthe stdin subscriber; the Trigger/irqfd wiring
Guest can't reboot / reboot hangsi8042 reset path not handled / not on the busI8042Device; the 0x64 PIO dispatch
Console drops bytes under loadoutput backpressure / FIFO handling in the UART modelvm-superio Serial — possibly an upstream fix
aarch64 guest has wrong timePL031 Rtc not wired or returning wrong baseRtc; 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.