From 3d5f06ff03ec8f5986bf93d0da9e446e12bca366 Mon Sep 17 00:00:00 2001 From: doge Date: Sun, 28 Jun 2026 21:53:32 +0800 Subject: [PATCH] vmm: acpi: provide FADT PM1a event/control blocks for nested Hyper-V A Windows guest that launches nested Hyper-V (for example to run WSL2) fails to start its hypervisor on cloud-hypervisor's HW-reduced-ACPI FADT. hvloader's hypervisor-launch path (0x18000f01c -> 0x180015628 -> 0x180015788) registers every legacy PM register block via 0x1800158dc and rejects any block whose GAS address is 0 with status 8 (STATUS_INVALID_DEVICE_REQUEST). hvix64 then never launches and HypervisorPresent stays False. The HW-reduced FADT leaves those blocks zero. Emit valid PM1a event/control blocks (I/O ports, lengths and X_GAS) in the FADT and reserve those ports in the I/O allocator so nothing else claims them. The HW-reduced guest OS ignores the legacy ports; only hvloader's ACPI validation reads them. These blocks are only useful to a guest that itself runs an enlightened hypervisor, so emit them only when both guest nesting and the Hyper-V enlightenments are enabled (--cpu nested=on,kvm_hyperv=on). Signed-off-by: doge --- vmm/src/acpi.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++--- vmm/src/cpu.rs | 10 ++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/vmm/src/acpi.rs b/vmm/src/acpi.rs index 994b9dff2..6c4bb4841 100644 --- a/vmm/src/acpi.rs +++ b/vmm/src/acpi.rs @@ -5,6 +5,7 @@ use std::time::Instant; use acpi_tables::Aml; +use acpi_tables::gas::{AccessSize, AddressSpace, GAS}; use acpi_tables::rsdp::Rsdp; #[cfg(target_arch = "aarch64")] use acpi_tables::sdt::GenericAddress; @@ -274,7 +275,11 @@ pub fn create_dsdt_table( const FACP_DSDT_OFFSET: usize = 140; -fn create_facp_table(dsdt_offset: GuestAddress, device_manager: &DeviceManager) -> Sdt { +fn create_facp_table( + dsdt_offset: GuestAddress, + device_manager: &DeviceManager, + legacy_acpi_pm1a: bool, +) -> Sdt { trace_scoped!("create_facp_table"); // Revision 6 of the ACPI FADT table is 276 bytes long @@ -326,6 +331,50 @@ fn create_facp_table(dsdt_offset: GuestAddress, device_manager: &DeviceManager) // Hypervisor Vendor Identity facp.write_bytes(268, b"CLOUDHYP"); + // Windows' nested-Hyper-V hvloader rejects a HW-reduced FADT whose PM1a GAS is + // zero; point the blocks at unused ACPI I/O ports (conforming guests ignore them). + if legacy_acpi_pm1a { + const PM1A_EVT_PORT: u16 = 0x60c; + facp.write(56, PM1A_EVT_PORT as u32); // PM1a_EVT_BLK (0x38) + facp.write(88, 4u8); // PM1_EVT_LEN (0x58) + facp.write_bytes( + 148, // X_PM1a_EVT_BLK (0x94) + GAS::new( + AddressSpace::SystemIo, + 32, + 0, + AccessSize::WordAccess, + PM1A_EVT_PORT.into(), + ) + .as_bytes(), + ); + + const PM1A_CNT_PORT: u16 = 0x610; + facp.write(64, PM1A_CNT_PORT as u32); // PM1a_CNT_BLK (0x40) + facp.write(89, 2u8); // PM1_CNT_LEN (0x59) + facp.write_bytes( + 172, // X_PM1a_CNT_BLK (0xac) + GAS::new( + AddressSpace::SystemIo, + 16, + 0, + AccessSize::WordAccess, + PM1A_CNT_PORT.into(), + ) + .as_bytes(), + ); + + let mut allocator = device_manager.allocator().lock().unwrap(); + for (port, len) in [(PM1A_EVT_PORT, 4u64), (PM1A_CNT_PORT, 2u64)] { + if allocator + .allocate_io_addresses(Some(GuestAddress(port.into())), len, None) + .is_none() + { + warn!("Could not reserve PM1a I/O port {port:#x} advertised in the FADT"); + } + } + } + facp.update_checksum(); facp @@ -855,7 +904,12 @@ fn create_acpi_tables_internal( tables_bytes.extend_from_slice(dsdt.as_slice()); // FACP aka FADT - let facp = create_facp_table(dsdt_addr, device_manager); + // The legacy PM1a blocks are x86-only; there is nothing to emit on other arches. + #[cfg(target_arch = "x86_64")] + let legacy_acpi_pm1a = cpu_manager.nested() && cpu_manager.kvm_hyperv(); + #[cfg(not(target_arch = "x86_64"))] + let legacy_acpi_pm1a = false; + let facp = create_facp_table(dsdt_addr, device_manager, legacy_acpi_pm1a); let facp_addr = dsdt_addr.checked_add(dsdt.len() as u64).unwrap(); tables_bytes.extend_from_slice(facp.as_slice()); xsdt_table_pointers.push(facp_addr.0); @@ -1130,7 +1184,12 @@ pub fn create_acpi_tables_tdx( )]; // FACP aka FADT - tables.push(create_facp_table(GuestAddress(0), device_manager)); + let legacy_acpi_pm1a = cpu_manager.nested() && cpu_manager.kvm_hyperv(); + tables.push(create_facp_table( + GuestAddress(0), + device_manager, + legacy_acpi_pm1a, + )); // MADT tables.push(cpu_manager.create_madt()); diff --git a/vmm/src/cpu.rs b/vmm/src/cpu.rs index 372a2f999..95a5bb1a1 100644 --- a/vmm/src/cpu.rs +++ b/vmm/src/cpu.rs @@ -1089,6 +1089,16 @@ impl CpuManager { Ok(()) } + #[cfg(target_arch = "x86_64")] + pub fn nested(&self) -> bool { + self.config.nested + } + + #[cfg(target_arch = "x86_64")] + pub fn kvm_hyperv(&self) -> bool { + self.config.kvm_hyperv + } + /// Only create new vCPUs if there aren't any inactive ones to reuse fn create_vcpus( &mut self,