GICv3 Virtual Interrupt Implementation

February 15, 2026 · View on GitHub

Version: v0.7.0 Last updated: 2026-02-14 Status: Verified — Linux 6.12.12 boots to BusyBox shell with 4 vCPUs, no RCU stalls


Overview

The hypervisor uses ARM GICv3 hardware virtualization to provide interrupt services to the guest. The implementation combines three strategies:

ComponentStrategyPurpose
GICD (0x08000000)Trap + write-throughGuest writes trapped, forwarded to physical GICD + shadow state
GICR (0x080A0000+)Trap-and-emulateStage-2 unmapped (4KB pages); VirtualGicr emulates per-vCPU state
ICC system regsVirtual redirectICH_HCR_EL2.En=1 redirects ICC_* to ICV_* at EL1
ICC_SGI1R_EL1Trapped (TALL1)Decoded for IPI emulation across vCPUs
ICH_LR_EL2Direct4 List Registers for virtual interrupt injection

List Register Injection

LR Format (64-bit)

Bits [63:62] - State: 00=Invalid, 01=Pending, 10=Active, 11=Pending+Active
Bit  [61]    - HW: 1=physical-virtual linkage (pINTID in [41:32])
Bit  [60]    - Group: 1=Group 1
Bits [55:48] - Priority (0x00 = highest)
Bits [41:32] - pINTID (physical INTID, when HW=1)
Bits [31:0]  - vINTID (virtual INTID)

Injection Paths

  1. SGI (INTID 0-15): Queued in PENDING_SGIS[vcpu_id] atomics, injected into arch_state.ich_lr[] before vcpu.run().
  2. SPI (INTID 32+): Queued in PENDING_SPIS[vcpu_id] atomics, injected before run or flushed immediately via flush_pending_spis_to_hardware().
  3. Virtual Timer (INTID 27): Injected with HW=1 (pINTID=27) in IRQ handler. Guest EOI auto-deactivates physical interrupt.
  4. Direct: GicV3VirtualInterface::inject_interrupt() writes hardware LRs from exception handler context.

EOImode=1

ICC_CTLR_EL1.EOImode=1 is set at EL2, splitting EOI into:

  • EOIR (priority drop): Guest writes ICC_EOIR1_EL1
  • DIR (deactivation): Hypervisor calls GicV3SystemRegs::write_dir() for non-HW interrupts

For HW=1 interrupts (vtimer), the guest's virtual EOI automatically deactivates the physical interrupt — no DIR needed.

GICR Trap-and-Emulate (Phase 7)

Architecture

Each GICv3 Redistributor (GICR) has two 64KB frames:

  • RD frame (offset 0x00000): GICR_CTLR, GICR_WAKER, GICR_TYPER, etc.
  • SGI frame (offset 0x10000): GICR_IGROUPR0, GICR_ISENABLER0, GICR_ICENABLER0, etc.

The hypervisor unmaps all 4 GICRs (0-3) via Stage-2 4KB page unmapping (32 pages per GICR = 128KB). Guest accesses trap as Data Aborts to EL2, where VirtualGicr emulates the registers.

VirtualGicr State

pub struct GicrState {
    pub igroupr0: u32,      // Interrupt Group Register
    pub isenabler0: u32,    // Interrupt Set-Enable
    pub icenabler0: u32,    // Interrupt Clear-Enable (shadow)
    pub ipriorityr: [u8; 32], // Priority for INTIDs 0-31
    pub icfgr0: u32,        // SGI configuration
    pub icfgr1: u32,        // PPI configuration
}

Per-vCPU state array: states: [GicrState; SMP_CPUS]. GICR index computed from IPA:

gicr_index = (ipa - GICR0_RD_BASE) / GICR_FRAME_SIZE
vcpu_id = gicr_index  (identity: GICR N → vCPU N)

Key Emulated Registers

RegisterOffsetBehavior
GICR_CTLR0x0000Returns 0 (RWP=0, no LPIs)
GICR_WAKER0x0014Returns 0 (ProcessorSleep=0, ChildrenAsleep=0)
GICR_TYPER0x0008Returns per-vCPU Aff0, Last bit for final GICR
GICR_IGROUPR00x10080Tracked per-vCPU, read/write
GICR_ISENABLER00x10100Write-1-to-set semantics
GICR_ICENABLER00x10180Write-1-to-clear semantics
GICR_IPRIORITYR0x10400-0x1041FPer-interrupt priority, byte access
GICR_ICFGR0/10x10C00/0x10C04Edge/level configuration

SGI/IPI Emulation

Trap Mechanism

ICH_HCR_EL2.TALL1=1 traps guest writes to ICC_SGI1R_EL1 as MSR exceptions (EC=0x18).

ICC_SGI1R_EL1 Bit Fields

CRITICAL — these differ from some documentation:

FieldBitsDescription
TargetList[15:0]Bitmap of target PEs (bit N = Aff0=N)
Aff1[23:16]Affinity level 1
INTID[27:24]SGI interrupt ID (0-15)
Aff2[39:32]Affinity level 2
IRM[40]1=target all PEs except self
RS[47:44]Range Selector
Aff3[55:48]Affinity level 3

SGI Flow

Guest writes ICC_SGI1R_EL1
  → TALL1 trap to EL2
  → handle_sgi_trap() decodes TargetList, INTID, IRM
  → Self-targeting: inject directly via hardware LR
  → Cross-vCPU: queue in PENDING_SGIS[target_vcpu] atomic
  → run_smp() loop: wake_pending_vcpus() unblocks targets
  → inject_pending_sgis() drains queue into arch_state.ich_lr[]
  → vcpu.run() → arch_state.restore() → hardware LRs set
  → ERET → guest receives SGI

GICD Shadow State

VirtualGicd intercepts GICD writes to track:

  • GICD_IROUTER[N]: SPI N routing affinity (Aff0 field → target vCPU ID)
  • GICD_ISENABLER[N]: SPI enable state

Used by inject_spi() to route SPIs to the correct vCPU's PENDING_SPIS array based on IROUTER Aff0.

Virtual Timer (INTID 27)

  1. Physical timer fires → IRQ trap to EL2 (HCR_EL2.IMO=1)
  2. handle_irq_exception() acknowledges via ICC_IAR1_EL1
  3. mask_guest_vtimer() disables timer to stop re-firing
  4. inject_hw_interrupt(27, 27, priority) writes LR with HW=1, pINTID=27
  5. Guest acknowledges via ICV_IAR1_EL1 (virtual) → LR state: Pending→Active
  6. Guest EOIs via ICV_EOIR1_EL1 → hardware auto-deactivates physical INTID 27
  7. Timer unmasks on next guest timer write

Preemption Timer (INTID 26)

CNTHP_EL2 (EL2 physical timer) fires every 10ms for preemptive scheduling:

  1. arm_preemption_timer() sets CNTHP_CVAL and enables CNTHP_CTL
  2. Physical IRQ → INTID 26 → handle_irq_exception()
  3. Sets PREEMPTION_EXIT=true → returns false → exits to scheduler
  4. ensure_cnthp_enabled() re-enables INTID 26 in GICR before every vCPU entry (guest may disable it via GICR writes)

Source Files

FileRole
src/arch/aarch64/peripherals/gicv3.rsGicV3SystemRegs, GicV3VirtualInterface, LR management
src/devices/gic/distributor.rsVirtualGicd — GICD trap-and-emulate, IROUTER shadow
src/devices/gic/redistributor.rsVirtualGicr — GICR trap-and-emulate, per-vCPU state
src/arch/aarch64/vcpu_arch_state.rsPer-vCPU ICH_LR/VMCR/HCR save/restore
src/vm.rsinject_pending_sgis/spis, wake_pending_vcpus, ensure_cnthp_enabled
src/arch/aarch64/hypervisor/exception.rshandle_irq_exception, handle_sgi_trap, flush_pending_spis
src/global.rsPENDING_SGIS, PENDING_SPIS, inject_spi()

Implementation Checklist

Core GICv3 (Sprint 1.6)

  • ICC system register interface (ICC_IAR1, ICC_EOIR1, ICC_PMR, ICC_IGRPEN1)
  • ICH virtual interface (ICH_VTR, ICH_HCR, ICH_VMCR, ICH_LR0-3)
  • List Register injection (inject_interrupt, inject_hw_interrupt)
  • EOImode=1 (split priority drop / deactivation)
  • HW=1 for virtual timer (physical-virtual EOI linkage)
  • GICv3 availability detection (ID_AA64PFR0_EL1)

Multi-vCPU GIC (Phase 7 / M2)

  • Per-vCPU LR save/restore (VcpuArchState)
  • Per-vCPU ICH_VMCR/HCR save/restore
  • TALL1 SGI trap (ICC_SGI1R_EL1 emulation)
  • PENDING_SGIS atomic queuing and injection
  • PENDING_SPIS atomic queuing and injection
  • flush_pending_spis_to_hardware() (low-latency SPI delivery)
  • GICR trap-and-emulate (VirtualGicr, 4KB unmap)
  • GICD shadow state (VirtualGicd, IROUTER tracking)
  • SPI routing via GICD_IROUTER Aff0
  • GICR WAKER management for secondary CPUs
  • ensure_cnthp_enabled() (re-enable INTID 26)
  • CNTHP preemption timer (10ms, INTID 26)

Verified

  • Linux 6.12.12 boots with 4 vCPUs, no RCU stalls
  • Virtio-blk detected and functional
  • BusyBox shell interactive