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/
ArchWhat KVM_CREATE_IRQCHIP (and friends) creates in the kernel
x86_64PIC (8259), IOAPIC, LAPIC per vCPU; plus a PIT (programmable interval timer) via KVM_CREATE_PIT2
aarch64GIC (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:

  1. At device registration (the-mmio-bus-and-device-manager.md), each virtio device gets an EventFd for its interrupt and that eventfd is registered with KVM via KVM_IRQFD against an allocated GSI/IRQ.
  2. When the device (on the VMM thread) finishes a batch of work, it updates the virtqueue used ring and then writes its interrupt eventfd.
  3. KVM's irqfd machinery sees the write and injects the interrupt into the guest through the emulated controller.
  4. 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
ModelA device owns an IRQ line (GSI); raising it asserts that line into the IOAPICA device "sends a message" — a write to a special address with a vector payload
virtio-mmioUses a single per-device legacy IRQ linen/a for the classic mmio transport
virtio-pci (--enable-pci)—Uses MSI-X vectors per queue
KVM plumbingKVM_IRQFD to a GSIKVM_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/
ConceptMeaning
GSIGlobal System Interrupt — the kernel-wide interrupt number the irqchip understands
IRQ lineThe legacy line a device asserts (maps to a GSI)
AllocationA monotonic allocator hands each registered device the next free IRQ/GSI from a reserved range
Advertised to guestThe 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:

  1. What does "in-kernel irqchip" mean, and what does Firecracker still decide?
  2. 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)?
  3. Contrast a legacy IRQ line with MSI/MSI-X. Which transport uses which?
  4. What is a GSI, and which two places must agree on a device's allocated IRQ?
  5. Why does Firecracker create the irqchip before wiring devices?
  6. What is the security argument for an in-kernel irqchip and one legacy line per mmio device?

Common bugs and symptoms

SymptomRoot causeWhere to look
Device does work but guest never notices completionIRQ advertised to guest ≠ IRQ registered with KVM_IRQFDallocation + advertisement agreement
Guest hangs waiting on I/O it submittedDevice never writes its interrupt eventfd after updating the used ringthe device's signal_used_queue path
Interrupts delivered but to the wrong vCPUAPIC/IOAPIC routing or GSI route misconfiguredirqchip setup; MSI routing
register_irqfd failsGSI out of range / already used / irqchip not created yetallocation order; irqchip-before-devices
Multi-queue device underperformsSingle legacy line forces all queues through one interruptconsider MSI-X (virtio-pci); verify
aarch64 device gets no interruptsGIC not created, or FDT interrupt property wrongcreate_gic; FDT in the-boot-sequence.md

Validation: prove you understand this

  1. Explain the in-kernel irqchip and name what KVM emulates on x86 vs aarch64.
  2. Draw the irqfd completion path from device to guest ISR and confirm no synchronous VM exit is involved.
  3. Contrast legacy line interrupts and MSI/MSI-X and map each to the mmio and pci transports.
  4. Explain GSI allocation and the two-place agreement that must hold for interrupts to land.
  5. Why is the irqchip created before devices, and why before the first KVM_RUN?
  6. 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.