Interrupts and the irqchip
A device that finishes work — a block read completes, a network frame arrives, a virtqueue has new used
entries — must tell the guest, and it does so with an interrupt. Firecracker uses KVM's in-kernel
interrupt controller (the "irqchip"): rather than emulating the PIC/IOAPIC/LAPIC (x86) or GIC (aarch64)
in userspace, it asks KVM to do it in the kernel, and then injects interrupts by writing an eventfd
bound to a guest interrupt line via KVM_IRQFD. This keeps interrupt delivery off the slow userspace
path, exactly as KVM_IOEVENTFD keeps virtqueue kicks off it. This chapter covers creating the irqchip,
how a device's completion turns into a guest interrupt, MSI, and how GSIs/IRQ lines are allocated.
After this chapter you will be able to: explain what the in-kernel irqchip is and why Firecracker uses it; trace a virtio completion from the device through an irqfd to the guest; distinguish legacy IRQ injection from MSI; and find the GSI/IRQ allocation logic.
Note: "In-kernel irqchip" means the interrupt controller (PIC/IOAPIC/LAPIC/GIC) is emulated by KVM, not by Firecracker. Firecracker still decides when to raise an interrupt — it just delivers that decision by poking an eventfd, and KVM's emulated controller does the rest. Less userspace interrupt code = smaller attack surface and faster delivery.
Creating the irqchip
# Where the in-kernel interrupt controller is created.
rg -n "create_irq_chip|KVM_CREATE_IRQCHIP|setup_irqchip|create_pit|KVM_CREATE_PIT|GIC|create_gic" src/vmm/src/ src/vmm/src/arch/
| Arch | What KVM_CREATE_IRQCHIP (and friends) creates in the kernel |
|---|---|
| x86_64 | PIC (8259), IOAPIC, LAPIC per vCPU; plus a PIT (programmable interval timer) via KVM_CREATE_PIT2 |
| aarch64 | GIC (Generic Interrupt Controller) — KVM_CREATE_DEVICE with the GIC type |
This happens early in the-boot-sequence.md, after the VM fd exists and before devices are wired, because devices need interrupt lines to register against. On x86 the PIT is created alongside the irqchip so the guest has a timer.
rg -n "setup_interrupt_controller|create_pit2|KVM_CREATE_PIT2|irq_chip|gic" src/vmm/src/arch/
How a device interrupt reaches the guest
The mechanism is KVM_IRQFD: bind an EventFd to a GSI (global system interrupt number). Whenever
userspace writes that eventfd, KVM injects the corresponding interrupt into the guest through the in-kernel
controller — no VM exit, no userspace round-trip on the vCPU.
rg -n "register_irqfd|KVM_IRQFD|IrqFd|irq_evt|trigger|signal_used_queue|interrupt_evt" src/vmm/src/ src/vmm/src/devices/virtio/
sequenceDiagram
participant VMM as VMM thread (device emulation)
participant EF as irqfd (EventFd)
participant KVM as KVM in-kernel irqchip
participant G as Guest vCPU
VMM->>VMM: virtio: add used-ring entry, work done
VMM->>EF: write(1) (signal the IRQ eventfd)
EF->>KVM: irqfd fires for GSI N
KVM->>G: inject interrupt via IOAPIC/LAPIC (or GIC)
G->>G: ISR runs; guest driver processes the used ring
Walk it:
- At device registration (the-mmio-bus-and-device-manager.md),
each virtio device gets an
EventFdfor its interrupt and that eventfd is registered with KVM viaKVM_IRQFDagainst an allocated GSI/IRQ. - When the device (on the VMM thread) finishes a batch of work, it updates the virtqueue used ring and then writes its interrupt eventfd.
- KVM's irqfd machinery sees the write and injects the interrupt into the guest through the emulated controller.
- The guest's interrupt handler runs and the guest driver drains the used ring.
This is the completion half of the virtio fast path; the submission half is the ioeventfd kick (see
kvm-fundamentals.md and virtio-transport-mmio.md).
Neither half costs a synchronous VM exit on the data path.
Legacy line interrupts vs MSI
There are two ways the injected interrupt is addressed:
| Legacy line (INTx-style) | MSI / MSI-X | |
|---|---|---|
| Model | A device owns an IRQ line (GSI); raising it asserts that line into the IOAPIC | A device "sends a message" — a write to a special address with a vector payload |
| virtio-mmio | Uses a single per-device legacy IRQ line | n/a for the classic mmio transport |
virtio-pci (--enable-pci) | — | Uses MSI-X vectors per queue |
| KVM plumbing | KVM_IRQFD to a GSI | KVM_IRQFD with an MSI route, or KVM_SIGNAL_MSI |
rg -n "MSI|msi|KVM_SIGNAL_MSI|GsiRoute|irq_routing|KVM_SET_GSI_ROUTING|msix" src/vmm/src/
The classic virtio-mmio transport (Firecracker's default) uses one legacy IRQ line per device — simple
and sufficient for a handful of devices. The virtio-pci transport (behind --enable-pci; verify on
your branch) brings MSI-X, which gives each virtqueue its own interrupt vector for better multi-queue
scaling. Note the security history here: CVE-2026-5747 was a vulnerability in the PCI transport, fixed
in 1.14.4 / 1.15.1 (verify) — a reminder that more interrupt machinery is more attack surface, which is
why mmio-with-one-line remains the conservative default.
GSI / IRQ allocation
Each device that can interrupt needs a unique line. Firecracker allocates GSIs/IRQs from a fixed range and hands them out as devices are registered.
rg -n "GsiAllocator|IrqAllocator|allocate_irq|IRQ_BASE|gsi|next_irq|alloc.*irq" src/vmm/src/ src/vmm/src/device_manager/
| Concept | Meaning |
|---|---|
| GSI | Global System Interrupt — the kernel-wide interrupt number the irqchip understands |
| IRQ line | The legacy line a device asserts (maps to a GSI) |
| Allocation | A monotonic allocator hands each registered device the next free IRQ/GSI from a reserved range |
| Advertised to guest | The device's IRQ is told to the guest via the cmdline virtio_mmio.device=SIZE@ADDR:IRQ (x86) or an FDT interrupt property (aarch64) |
The allocated IRQ shows up in two places that must agree: the KVM_IRQFD registration (so the eventfd
injects on the right line) and the device advertisement to the guest (so the guest's driver listens on
the right line). A mismatch means the device signals an interrupt the guest is not waiting on — a classic
"device works but guest never notices completion" bug. See
the-mmio-bus-and-device-manager.md for where the advertisement is
built and virtio-transport-mmio.md for the register block.
Reading exercise
# 1. irqchip creation.
rg -n "create_irq_chip|KVM_CREATE_IRQCHIP|create_pit2|setup_interrupt_controller|create_gic" src/vmm/src/ src/vmm/src/arch/
# 2. irqfd registration and device interrupt eventfds.
rg -n "register_irqfd|KVM_IRQFD|interrupt_evt|irq_evt|signal_used_queue" src/vmm/src/ src/vmm/src/devices/virtio/
# 3. The used-ring + signal path in a device.
rg -n "add_used|signal_used_queue|trigger|write.*1|interrupt_status" src/vmm/src/devices/virtio/
# 4. MSI / routing (mostly the PCI path).
rg -n "MSI|KVM_SIGNAL_MSI|irq_routing|msix" src/vmm/src/
# 5. IRQ/GSI allocation and advertisement.
rg -n "allocate_irq|GsiAllocator|virtio_mmio.device|IRQ" src/vmm/src/ src/vmm/src/device_manager/
# 6. Inside a booted guest, see interrupt counts climb under I/O: cat /proc/interrupts
Answer:
- What does "in-kernel irqchip" mean, and what does Firecracker still decide?
- Trace a virtio-block completion from the VMM thread to the guest's ISR. Where is the eventfd, and where is the VM exit (trick question)?
- Contrast a legacy IRQ line with MSI/MSI-X. Which transport uses which?
- What is a GSI, and which two places must agree on a device's allocated IRQ?
- Why does Firecracker create the irqchip before wiring devices?
- What is the security argument for an in-kernel irqchip and one legacy line per mmio device?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| Device does work but guest never notices completion | IRQ advertised to guest ≠ IRQ registered with KVM_IRQFD | allocation + advertisement agreement |
| Guest hangs waiting on I/O it submitted | Device never writes its interrupt eventfd after updating the used ring | the device's signal_used_queue path |
| Interrupts delivered but to the wrong vCPU | APIC/IOAPIC routing or GSI route misconfigured | irqchip setup; MSI routing |
register_irqfd fails | GSI out of range / already used / irqchip not created yet | allocation order; irqchip-before-devices |
| Multi-queue device underperforms | Single legacy line forces all queues through one interrupt | consider MSI-X (virtio-pci); verify |
| aarch64 device gets no interrupts | GIC not created, or FDT interrupt property wrong | create_gic; FDT in the-boot-sequence.md |
Validation: prove you understand this
- Explain the in-kernel irqchip and name what KVM emulates on x86 vs aarch64.
- Draw the irqfd completion path from device to guest ISR and confirm no synchronous VM exit is involved.
- Contrast legacy line interrupts and MSI/MSI-X and map each to the mmio and pci transports.
- Explain GSI allocation and the two-place agreement that must hold for interrupts to land.
- Why is the irqchip created before devices, and why before the first
KVM_RUN? - Give the security rationale for the conservative default (in-kernel irqchip, one legacy line per device).
Next: The MMIO Bus and the Device Manager — where these interrupt lines get allocated and devices get placed for the guest to find.