diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 2b449f2838..587dc10b9c 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -2266,6 +2266,11 @@ async fn new_underhill_vm( ); } + // Set the callback in GET to trigger the debug interrupt. + let p = partition.clone(); + let debug_interrupt_callback = move |vtl: u8| p.assert_debug_interrupt(vtl); + get_client.set_debug_interrupt_callback(Box::new(debug_interrupt_callback)); + let mut input_distributor = InputDistributor::new(remote_console_cfg.input); resolver.add_async_resolver::( input_distributor.client().clone(), diff --git a/openhcl/virt_mshv_vtl/src/lib.rs b/openhcl/virt_mshv_vtl/src/lib.rs index b8e1d9b18a..9aaa728468 100644 --- a/openhcl/virt_mshv_vtl/src/lib.rs +++ b/openhcl/virt_mshv_vtl/src/lib.rs @@ -103,6 +103,7 @@ use user_driver::DmaClient; use virt::IsolationType; use virt::PartitionCapabilities; use virt::VpIndex; +use virt::X86Partition; use virt::irqcon::IoApicRouting; use virt::irqcon::MsiRequest; use virt::x86::apic_software_device::ApicSoftwareDevices; @@ -858,7 +859,7 @@ impl virt::Partition for UhPartition { } } -impl virt::X86Partition for UhPartition { +impl X86Partition for UhPartition { fn ioapic_routing(&self) -> Arc { self.inner.clone() } @@ -1943,6 +1944,13 @@ impl UhPartition { } } + /// Trigger the LINT1 interrupt vector on the LAPIC of the BSP. + #[cfg(guest_arch = "x86_64")] + pub fn assert_debug_interrupt(&self, vtl: u8) { + let bsp_index = VpIndex::new(0); + self.pulse_lint(bsp_index, Vtl::try_from(vtl).unwrap(), 1) + } + /// Enables or disables the PM timer assist. pub fn set_pm_timer_assist(&self, port: Option) -> Result<(), HvError> { self.inner.hcl.set_pm_timer_assist(port) diff --git a/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/apic.rs b/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/apic.rs index 9651495272..77de85ae21 100644 --- a/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/apic.rs +++ b/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/apic.rs @@ -5,6 +5,7 @@ use crate::UhProcessor; use crate::processor::HardwareIsolatedBacking; +use crate::processor::NMI_SUPPRESS_LINT1_REQUESTED; use cvm_tracing::CVM_ALLOWED; use hcl::GuestVtl; use virt::Processor; @@ -28,6 +29,8 @@ pub(crate) trait ApicBacking<'b, B: HardwareIsolatedBacking> { fn handle_extint(&mut self, vtl: GuestVtl) { tracelimit::warn_ratelimited!(CVM_ALLOWED, ?vtl, "extint not supported"); } + + fn supports_nmi_masking(&mut self) -> bool; } pub(crate) fn poll_apic_core<'b, B: HardwareIsolatedBacking, T: ApicBacking<'b, B>>( @@ -53,6 +56,7 @@ pub(crate) fn poll_apic_core<'b, B: HardwareIsolatedBacking, T: ApicBacking<'b, extint, sipi, nmi, + lint1, interrupt, } = vp.backing.cvm_state_mut().lapics[vtl] .lapic @@ -93,10 +97,21 @@ pub(crate) fn poll_apic_core<'b, B: HardwareIsolatedBacking, T: ApicBacking<'b, } // Interrupts are ignored while waiting for SIPI. + let supports_nmi_masking = apic_backing.supports_nmi_masking(); let lapic = &mut apic_backing.vp().backing.cvm_state_mut().lapics[vtl]; if lapic.activity != MpState::WaitForSipi { - if nmi || lapic.nmi_pending { + if lint1 { + if supports_nmi_masking || !lapic.cross_vtl_nmi_requested { + lapic.nmi_suppression |= NMI_SUPPRESS_LINT1_REQUESTED; + lapic.nmi_pending = true; + } + } + + if nmi { lapic.nmi_pending = true; + } + + if lapic.nmi_pending { apic_backing.handle_nmi(vtl); } diff --git a/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs b/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs index 3048faea8a..0a2957d844 100644 --- a/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs @@ -2766,6 +2766,17 @@ impl hv1_hypercall::AssertVirtualInterrupt return Err(HvError::InvalidParameter); } + if matches!( + interrupt_control.interrupt_type(), + HvInterruptType::HvX64InterruptTypeNmi + ) { + // Note whether this is a cross-VTL NMI request. If so, this must be + // visible before any NMIs are requested. + if self.intercepted_vtl == GuestVtl::Vtl1 && target_vtl == GuestVtl::Vtl0 { + self.vp.backing.cvm_state_mut().lapics[target_vtl].cross_vtl_nmi_requested = true; + } + } + self.vp.partition.request_msi( target_vtl, MsiRequest::new_x86( diff --git a/openhcl/virt_mshv_vtl/src/processor/mod.rs b/openhcl/virt_mshv_vtl/src/processor/mod.rs index 46c135ca0b..50a029a731 100644 --- a/openhcl/virt_mshv_vtl/src/processor/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/mod.rs @@ -154,12 +154,21 @@ impl VtlsTlbLocked { } } +// NMI suppression state to prevent duplicate NMI +#[cfg(guest_arch = "x86_64")] +const NMI_SUPPRESS_LINT1_DELIVERED: u32 = 1; +#[cfg(guest_arch = "x86_64")] +const NMI_SUPPRESS_LINT1_REQUESTED: u32 = 1 << 1; + #[cfg(guest_arch = "x86_64")] #[derive(Inspect)] pub(crate) struct LapicState { lapic: LocalApic, activity: MpState, nmi_pending: bool, + lint1_pending: bool, + nmi_suppression: u32, + cross_vtl_nmi_requested: bool, } #[cfg(guest_arch = "x86_64")] @@ -169,6 +178,9 @@ impl LapicState { lapic, activity, nmi_pending: false, + lint1_pending: false, + nmi_suppression: 0, + cross_vtl_nmi_requested: false, } } } diff --git a/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs b/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs index 2da625b9ff..d515be9641 100644 --- a/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs @@ -22,6 +22,8 @@ use crate::UhCvmVpState; use crate::UhPartitionInner; use crate::UhPartitionNewParams; use crate::WakeReason; +use crate::processor::NMI_SUPPRESS_LINT1_DELIVERED; +use crate::processor::NMI_SUPPRESS_LINT1_REQUESTED; use crate::processor::UhHypercallHandler; use crate::processor::UhProcessor; use crate::processor::hardware_cvm::apic::ApicBacking; @@ -884,6 +886,20 @@ impl<'b> ApicBacking<'b, SnpBacked> for UhProcessor<'b, SnpBacked> { // TODO SNP: support virtual NMI injection // For now, just inject an NMI and hope for the best. // Don't forget to update handle_cross_vtl_interrupts if this code changes. + + if (self.backing.cvm.lapics[vtl].nmi_suppression & NMI_SUPPRESS_LINT1_DELIVERED) != 0 { + // Cancel this NMI request since it cannot be delivered. + self.backing.cvm.lapics[vtl].nmi_pending = false; + return; + } + + if (self.backing.cvm.lapics[vtl].nmi_suppression & NMI_SUPPRESS_LINT1_REQUESTED) != 0 { + // If a LINT1 NMI has been requested, then it is being delivered now, + // so no further NMIs can be delivered. + self.backing.cvm.lapics[vtl].nmi_suppression &= !NMI_SUPPRESS_LINT1_REQUESTED; + self.backing.cvm.lapics[vtl].nmi_suppression |= NMI_SUPPRESS_LINT1_DELIVERED; + } + let mut vmsa = self.runner.vmsa_mut(vtl); // TODO GUEST VSM: Don't inject the NMI if there's already an event @@ -905,6 +921,10 @@ impl<'b> ApicBacking<'b, SnpBacked> for UhProcessor<'b, SnpBacked> { vmsa.set_rip(0); self.backing.cvm.lapics[vtl].activity = MpState::Running; } + + fn supports_nmi_masking(&mut self) -> bool { + false + } } impl UhProcessor<'_, SnpBacked> { diff --git a/openhcl/virt_mshv_vtl/src/processor/tdx/mod.rs b/openhcl/virt_mshv_vtl/src/processor/tdx/mod.rs index 116d195e5b..5db4857aae 100644 --- a/openhcl/virt_mshv_vtl/src/processor/tdx/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/tdx/mod.rs @@ -1459,6 +1459,10 @@ impl<'b> hardware_cvm::apic::ApicBacking<'b, TdxBacked> for TdxApicScanner<'_, ' self.vp.backing.vtls[vtl].private_regs.rip = 0; self.vp.backing.cvm.lapics[vtl].activity = MpState::Running; } + + fn supports_nmi_masking(&mut self) -> bool { + true + } } impl UhProcessor<'_, TdxBacked> { diff --git a/vm/devices/get/get_protocol/src/lib.rs b/vm/devices/get/get_protocol/src/lib.rs index e5c54b9262..258952fb59 100644 --- a/vm/devices/get/get_protocol/src/lib.rs +++ b/vm/devices/get/get_protocol/src/lib.rs @@ -94,6 +94,7 @@ open_enum! { MODIFY_VTL2_SETTINGS_REV1 = 6, // --- GE --- BATTERY_STATUS = 7, + INJECT_DEBUG_INTERRUPT = 8, } } @@ -1759,6 +1760,16 @@ impl BatteryStatusNotification { } } +#[repr(C)] +#[derive(Copy, Clone, Debug, IntoBytes, FromBytes, Immutable, KnownLayout)] +pub struct InjectDebugInterruptNotification { + pub message_header: HeaderGuestNotification, + pub vtl: u8, + pub _pad: u8, +} + +const_assert_eq!(6, size_of::()); + #[bitfield(u64)] #[derive(IntoBytes, FromBytes, Immutable, KnownLayout)] pub struct CreateRamGpaRangeFlags { diff --git a/vm/devices/get/guest_emulation_transport/src/client.rs b/vm/devices/get/guest_emulation_transport/src/client.rs index 2647f1cb10..ca05cf82e8 100644 --- a/vm/devices/get/guest_emulation_transport/src/client.rs +++ b/vm/devices/get/guest_emulation_transport/src/client.rs @@ -382,6 +382,12 @@ impl GuestEmulationTransportClient { .notify(msg::Msg::SetGpaAllocator(gpa_allocator)); } + /// Set the the callback to trigger the debug interrupt. + pub fn set_debug_interrupt_callback(&mut self, callback: Box) { + self.control + .notify(msg::Msg::SetDebugInterruptCallback(callback)); + } + /// Send the attestation request to the IGVM agent on the host. pub async fn igvm_attest( &self, diff --git a/vm/devices/get/guest_emulation_transport/src/process_loop.rs b/vm/devices/get/guest_emulation_transport/src/process_loop.rs index b2f02cc878..d76ee0fe1a 100644 --- a/vm/devices/get/guest_emulation_transport/src/process_loop.rs +++ b/vm/devices/get/guest_emulation_transport/src/process_loop.rs @@ -191,6 +191,8 @@ pub(crate) mod msg { Inspect(inspect::Deferred), /// Store the gpa allocator to be used for attestation. SetGpaAllocator(Arc), + /// Store the callback to trigger the debug interrupt. + SetDebugInterruptCallback(Box), // Late bound receivers for Guest Notifications /// Take the late-bound GuestRequest receiver for Generation Id updates. @@ -493,6 +495,8 @@ pub(crate) struct ProcessLoop { #[inspect(skip)] secondary_host_requests_read_send: mesh::Sender>, gpa_allocator: Option>, + #[inspect(skip)] + set_debug_interrupt: Option>, stats: Stats, guest_notification_listeners: GuestNotificationListeners, @@ -700,6 +704,7 @@ impl ProcessLoop { battery_status: GuestNotificationSender::new(), }, gpa_allocator: None, + set_debug_interrupt: None, } } @@ -1029,6 +1034,9 @@ impl ProcessLoop { Msg::SetGpaAllocator(gpa_allocator) => { self.gpa_allocator = Some(gpa_allocator); } + Msg::SetDebugInterruptCallback(callback) => { + self.set_debug_interrupt = Some(callback); + } // Late bound receivers for Guest Notifications Msg::TakeVtl2SettingsReceiver(req) => req.handle_sync(|()| { @@ -1244,8 +1252,8 @@ impl ProcessLoop { ) -> Result<(), FatalError> { use get_protocol::GuestNotifications; - // Version must be latest. Give up if not. - if header.message_version != get_protocol::MessageVersions::HEADER_VERSION_1 { + // Version must be at least version 1. + if header.message_version < get_protocol::MessageVersions::HEADER_VERSION_1 { tracing::error!( msg = ?buf, version = ?header.message_version, @@ -1287,6 +1295,9 @@ impl ProcessLoop { GuestNotifications::BATTERY_STATUS => { self.handle_battery_status_notification(read_guest_notification(id, buf)?)?; } + GuestNotifications::INJECT_DEBUG_INTERRUPT => { + self.handle_debug_interrupt_notification(read_guest_notification(id, buf)?)?; + } invalid_notification => { tracing::error!( ?invalid_notification, @@ -1489,6 +1500,30 @@ impl ProcessLoop { }) } + fn handle_debug_interrupt_notification( + &mut self, + notification: get_protocol::InjectDebugInterruptNotification, + ) -> Result<(), FatalError> { + tracing::debug!( + "Received inject debug interrupt notification, vtl = {}", + notification.vtl, + ); + + if notification.vtl != 0 { + tracing::error!( + ?notification.vtl, + "Debug interrupts can only be injected into VTL 0", + ); + } else { + // Trigger the LINT1 interrupt vector on the LAPIC of the BSP. + self.set_debug_interrupt + .as_ref() + .map(|callback| callback(notification.vtl)); + } + + Ok(()) + } + fn complete_modify_vtl2_settings( &mut self, result: Result<(), RpcError>>, diff --git a/vmm_core/virt_support_apic/src/lib.rs b/vmm_core/virt_support_apic/src/lib.rs index dd390d5121..dbc7886ada 100644 --- a/vmm_core/virt_support_apic/src/lib.rs +++ b/vmm_core/virt_support_apic/src/lib.rs @@ -123,6 +123,7 @@ struct Stats { eoi_level: Counter, spurious_eoi: Counter, lazy_eoi: Counter, + lint1: Counter, interrupt: Counter, nmi: Counter, extint: Counter, @@ -230,7 +231,8 @@ struct WorkFlags { sipi_vector: u8, extint: bool, nmi: bool, - #[bits(20)] + lint1: bool, + #[bits(19)] _rsvd: u32, } @@ -440,13 +442,23 @@ impl LocalApicSet { // Don't know how to manage remote IRR. return; } - slot.request_interrupt( - DeliveryMode(lvt.delivery_mode()), - lvt.vector(), - lvt.trigger_mode_level(), - false, - wake, - ); + if lint_index == 1 { + slot.request_lint1( + DeliveryMode(lvt.delivery_mode()), + lvt.vector(), + lvt.trigger_mode_level(), + false, + wake, + ); + } else { + slot.request_interrupt( + DeliveryMode(lvt.delivery_mode()), + lvt.vector(), + lvt.trigger_mode_level(), + false, + wake, + ); + } } } } @@ -1223,6 +1235,38 @@ impl SharedState { _ => false, } } + + /// Returns true if the VP should be woken up to scan the APIC. + #[must_use] + fn request_lint1( + &self, + software_enabled: bool, + delivery_mode: DeliveryMode, + vector: u8, + level_triggered: bool, + auto_eoi: bool, + ) -> bool { + match delivery_mode { + DeliveryMode::NMI => { + let old = self + .work + .fetch_update(Ordering::Release, Ordering::Relaxed, |w| { + Some(WorkFlags::from(w).with_lint1(true).into()) + }) + .unwrap(); + old == 0 + } + _ => { + return self.request_interrupt( + software_enabled, + delivery_mode, + vector, + level_triggered, + auto_eoi, + ); + } + } + } } impl MutableGlobalState { @@ -1283,6 +1327,29 @@ impl ApicSlot { } } } + + fn request_lint1( + &self, + delivery_mode: DeliveryMode, + vector: u8, + level_triggered: bool, + auto_eoi: bool, + wake: impl FnOnce(VpIndex), + ) { + if let Some(shared) = &self.shared { + if self.hardware_enabled + && shared.request_lint1( + self.software_enabled, + delivery_mode, + vector, + level_triggered, + auto_eoi, + ) + { + wake(shared.vp_index); + } + } + } } /// Work to do as a result of [`LocalApic::scan`] or [`LocalApic::flush`]. @@ -1305,6 +1372,8 @@ pub struct ApicWork { pub extint: bool, /// An NMI was requested. pub nmi: bool, + /// LINT1 was requested. + pub lint1: bool, /// A fixed interrupt was requested. /// /// Call [`LocalApic::acknowledge_interrupt`] after it has been injected. @@ -1574,6 +1643,10 @@ impl LocalApic { self.stats.nmi.increment(); r.nmi = true; } + if work.lint1() { + self.stats.lint1.increment(); + r.lint1 = true; + } if work.extint() { self.stats.extint.increment(); r.extint = true;