From 4658338fce87085958bfe28d4da3ad03cb623450 Mon Sep 17 00:00:00 2001 From: Stefan Lankes Date: Thu, 6 Aug 2026 15:09:58 +0200 Subject: [PATCH 1/2] fix(pci): configure legacy interrupts level triggered and active low A legacy PCI interrupt is signalled level triggered and active low, while the IOAPIC defaults to the edge triggered, active high behavior of the ISA interrupts. A passed-through device therefore stops delivering interrupts after its first one, because the host only unmasks it once the guest has completed a level triggered interrupt. Correct the redirection entry where a legacy interrupt line is handed out. --- src/arch/x86_64/kernel/apic.rs | 19 +++++++++++++++++++ src/drivers/pci.rs | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/src/arch/x86_64/kernel/apic.rs b/src/arch/x86_64/kernel/apic.rs index 7c33db2720..1f207c9285 100644 --- a/src/arch/x86_64/kernel/apic.rs +++ b/src/arch/x86_64/kernel/apic.rs @@ -613,6 +613,25 @@ fn ioapic_set_interrupt(irq: u8, apicid: u8, enabled: bool) { ioapic_write(IOAPIC_REG_TABLE + off + 1, ioredirect_upper); } +/// Reconfigures an interrupt line for a legacy PCI interrupt. +/// +/// [`init_ioapic`] sets up every line for the edge triggered, active high +/// behavior of the ISA interrupts. PCI signals its interrupts level triggered +/// and active low instead. +pub(crate) fn ioapic_set_pci_interrupt(irq: u8, apicid: u8) { + assert!(irq <= 24); + + let off = u32::from(irq * 2); + let ioredirect_upper = u32::from(apicid) << 24; + // Vector, active low (bit 13) and level triggered (bit 15). + let ioredirect_lower = u32::from(0x20 + irq) | (1 << 13) | (1 << 15); + + debug!("Configuring irq {irq} as level triggered, active low"); + + ioapic_write(IOAPIC_REG_TABLE + off, ioredirect_lower); + ioapic_write(IOAPIC_REG_TABLE + off + 1, ioredirect_upper); +} + pub fn init_local_apic() { // Mask out all interrupts we don't need right now. local_apic_write(IA32_X2APIC_LVT_TIMER, APIC_LVT_MASK); diff --git a/src/drivers/pci.rs b/src/drivers/pci.rs index a59aaacfe7..b1a6a8af40 100644 --- a/src/drivers/pci.rs +++ b/src/drivers/pci.rs @@ -181,6 +181,15 @@ impl PciDevice { return None; } + // A legacy PCI interrupt is signalled level triggered and active + // low, while the IOAPIC defaults to the edge triggered, active + // high behavior of the ISA interrupts. Without the correction a + // passed-through device stops delivering interrupts after its + // first one, because the host only unmasks the interrupt once the + // guest has completed a level triggered one. + #[cfg(target_arch = "x86_64")] + crate::arch::kernel::apic::ioapic_set_pci_interrupt(line, 0); + Some(line) } 5.. => { From a0756f05d8e62e4bc4fafe358cccf62ccd55efa1 Mon Sep 17 00:00:00 2001 From: Stefan Lankes Date: Thu, 6 Aug 2026 14:07:57 +0200 Subject: [PATCH 2/2] fix(pci): disable decoding while sizing BARs Determining the size of a BAR writes all-ones into it and restores the previous value afterwards. While decoding is enabled, the device decodes those intermediate values as real addresses and relocates itself. For a passed-through device the host follows that relocation and tries to map the BAR outside of the guest, which aborts the VM: kvm_set_user_memory_region: KVM_SET_USER_MEMORY_REGION failed, slot=5, start=0xfffffffffe000000, size=0x400000: Invalid argument Disable decoding around the probe, as the PCI specification requires, and route the informational bus dump through get_bar so that it is covered as well. --- src/arch/x86_64/kernel/apic.rs | 1 + src/drivers/pci.rs | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/arch/x86_64/kernel/apic.rs b/src/arch/x86_64/kernel/apic.rs index 1f207c9285..b70042b7f8 100644 --- a/src/arch/x86_64/kernel/apic.rs +++ b/src/arch/x86_64/kernel/apic.rs @@ -618,6 +618,7 @@ fn ioapic_set_interrupt(irq: u8, apicid: u8, enabled: bool) { /// [`init_ioapic`] sets up every line for the edge triggered, active high /// behavior of the ISA interrupts. PCI signals its interrupts level triggered /// and active low instead. +#[cfg(feature = "pci")] pub(crate) fn ioapic_set_pci_interrupt(irq: u8, apicid: u8) { assert!(irq <= 24); diff --git a/src/drivers/pci.rs b/src/drivers/pci.rs index b1a6a8af40..0a6a68b3d1 100644 --- a/src/drivers/pci.rs +++ b/src/drivers/pci.rs @@ -73,9 +73,23 @@ impl PciDevice { /// Returns the bar at bar-register `slot`. pub fn get_bar(&self, slot: u8) -> Option { - let header = self.header(); - let endpoint = EndpointHeader::from_header(header, &self.access)?; - endpoint.bar(slot, &self.access) + let endpoint = EndpointHeader::from_header(self.header(), &self.access)?; + let mut header = self.header(); + + // Determining the size of a bar writes all-ones into it and restores the + // old value afterwards. While decoding is enabled, the device decodes + // these intermediate values as real addresses and relocates itself. On a + // passed-through device the host follows that relocation and tries to map + // the bar at an address outside of the guest, which kills the VM. The PCI + // specification requires decoding to be disabled while sizing a bar. + let command = header.command(&self.access); + header.update_command(&self.access, |command| { + command & !(CommandRegister::IO_ENABLE | CommandRegister::MEMORY_ENABLE) + }); + let bar = endpoint.bar(slot, &self.access); + header.update_command(&self.access, |_| command); + + bar } /// Configure the bar at register `slot` @@ -181,7 +195,7 @@ impl PciDevice { return None; } - // A legacy PCI interrupt is signalled level triggered and active + // A legacy PCI interrupt is signaled level triggered and active // low, while the IOAPIC defaults to the edge triggered, active // high behavior of the ISA interrupts. Without the correction a // passed-through device stops delivering interrupts after its @@ -277,7 +291,7 @@ impl fmt::Display for PciDevice { let mut slot: u8 = 0; while usize::from(slot) < MAX_BARS { - if let Some(pci_bar) = endpoint.bar(slot, &self.access) { + if let Some(pci_bar) = self.get_bar(slot) { match pci_bar { Bar::Memory64 { address,