From 2ba9a0bcf2fbfbf31e1659ad775cc49e888ff953 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Sat, 11 Jul 2026 07:43:07 -0700 Subject: [PATCH 1/6] feat(aarch64): add WHP (Windows Hypervisor Platform) backend for ARM64 Implement the WHP hypervisor backend for aarch64 Windows, enabling hyperlight to run micro-VMs on Windows ARM64 systems. Changes: - Restructure whp.rs into whp/ directory (whp/mod.rs + whp/x86_64.rs) to support per-architecture implementations (matching kvm/mshv pattern) - Add whp/aarch64.rs with full VirtualMachine trait implementation: - Manual FFI bindings for ARM64 WHP register names, exit reasons, and exit context layout (from Windows SDK WinHvPlatformDefs.h) - Run loop handling MMIO-based I/O (ARM64 has no IO ports) - Register get/set via WHvGet/SetVirtualProcessorRegisters - Surrogate process support (same pattern as x86_64) - Wire WhpVm into hyperlight_vm/aarch64.rs for Windows platform - Fix super::x86_64::hw_interrupts path after directory restructure Resolves: #1544 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .../src/hypervisor/hyperlight_vm/aarch64.rs | 28 +- .../hypervisor/virtual_machine/whp/aarch64.rs | 854 ++++++++++++++++++ .../src/hypervisor/virtual_machine/whp/mod.rs | 25 + .../virtual_machine/{whp.rs => whp/x86_64.rs} | 23 +- 4 files changed, 918 insertions(+), 12 deletions(-) create mode 100644 src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs create mode 100644 src/hyperlight_host/src/hypervisor/virtual_machine/whp/mod.rs rename src/hyperlight_host/src/hypervisor/virtual_machine/{whp.rs => whp/x86_64.rs} (98%) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 770959c14..59770f379 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -29,12 +29,17 @@ use crate::hypervisor::hyperlight_vm::get_guest_log_filter; use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; #[cfg(kvm)] use crate::hypervisor::virtual_machine::kvm::KvmVm; -#[cfg(kvm)] +#[cfg(target_os = "windows")] +use crate::hypervisor::virtual_machine::whp::WhpVm; +#[cfg(any(kvm, mshv3, target_os = "windows"))] use crate::hypervisor::virtual_machine::{HypervisorType, VmError}; use crate::hypervisor::virtual_machine::{ RegisterError, ResetVcpuError, VirtualMachine, get_available_hypervisor, }; +#[cfg(target_os = "linux")] use crate::hypervisor::{InterruptHandleImpl, LinuxInterruptHandle}; +#[cfg(target_os = "windows")] +use crate::hypervisor::{InterruptHandleImpl, PartitionState, WindowsInterruptHandle}; use crate::mem::mgr::{SandboxMemoryManager, SnapshotSharedMemory}; use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; use crate::sandbox::SandboxConfiguration; @@ -54,7 +59,7 @@ impl HyperlightVm { next_action: NextAction, rsp_gva: u64, page_size: usize, - config: &SandboxConfiguration, + #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, #[cfg(gdb)] _gdb_conn: Option>, #[cfg(crashdump)] _rt_cfg: SandboxRuntimeConfig, #[cfg(feature = "mem_profile")] _trace_info: MemTraceInfo, @@ -64,13 +69,19 @@ impl HyperlightVm { let vm: VmType = match get_available_hypervisor() { #[cfg(kvm)] Some(HypervisorType::Kvm) => Box::new(KvmVm::new().map_err(VmError::CreateVm)?), - // TODO: mshv support #[cfg(mshv3)] - Some(HypervisorType::Mshv) => return Err(CreateHyperlightVmError::NoHypervisorFound), + Some(HypervisorType::Mshv) => { + // MSHV aarch64 VirtualMachine impl not yet available on this branch + return Err(CreateHyperlightVmError::NoHypervisorFound); + } + #[cfg(target_os = "windows")] + Some(HypervisorType::Whp) => Box::new(WhpVm::new().map_err(VmError::CreateVm)?), None => return Err(CreateHyperlightVmError::NoHypervisorFound), }; vm.set_sregs(&CommonSpecialRegisters::defaults(root_pt_addr)) .map_err(VmError::Register)?; + + #[cfg(target_os = "linux")] let interrupt_handle: Arc = Arc::new(LinuxInterruptHandle { state: AtomicU8::new(0), tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }), @@ -79,6 +90,15 @@ impl HyperlightVm { dropped: AtomicBool::new(false), }); + #[cfg(target_os = "windows")] + let interrupt_handle: Arc = Arc::new(WindowsInterruptHandle { + state: AtomicU8::new(0), + partition_state: std::sync::RwLock::new(PartitionState { + handle: vm.partition_handle(), + dropped: false, + }), + }); + let snapshot_slot = 0u32; let scratch_slot = 1u32; let vm_can_reset_vcpu = vm.can_reset_vcpu(); diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs new file mode 100644 index 000000000..d1cb5c786 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -0,0 +1,854 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Windows Hypervisor Platform (WHP) backend for AArch64. +//! +//! This module provides the [`VirtualMachine`] trait implementation using the +//! WHP APIs on Windows ARM64 systems. Because the `windows` crate does not yet +//! expose ARM64 WHP structures, we define our own FFI bindings derived from +//! the Windows SDK header `WinHvPlatformDefs.h` (10.0.26100.0). + +use std::os::raw::c_void; +use std::sync::atomic::{AtomicBool, Ordering}; + +use hyperlight_common::outb::VmAction; +#[cfg(feature = "trace_guest")] +use tracing::Span; +#[cfg(feature = "trace_guest")] +use tracing_opentelemetry::OpenTelemetrySpanExt; +use windows::Win32::System::Hypervisor::*; +use windows_result::HRESULT; + +use crate::hypervisor::regs::{ + CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, +}; +use crate::hypervisor::surrogate_process::SurrogateProcess; +use crate::hypervisor::surrogate_process_manager::{ + get_surrogate_process_manager, surrogates_disabled, +}; +use crate::hypervisor::virtual_machine::{ + CreateVmError, MapMemoryError, RegisterError, ResetVcpuError, RunVcpuError, UnmapMemoryError, + VirtualMachine, VmExit, +}; +use crate::hypervisor::wrappers::HandleWrapper; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +#[cfg(feature = "trace_guest")] +use crate::sandbox::trace::TraceContext as SandboxTraceContext; + +// ============================================================================ +// ARM64 WHP FFI bindings +// +// These are manually translated from WinHvPlatformDefs.h (Windows SDK 10.0.26100.0) +// because the `windows` crate does not expose ARM64 WHP types. +// ============================================================================ + +/// ARM64 WHP register name constants. +/// Mapped from `WHV_REGISTER_NAME` enum values in the SDK header under `_ARM64_`. +mod arm64_regs { + use windows::Win32::System::Hypervisor::WHV_REGISTER_NAME; + + // General-purpose registers X0-X28 + pub const WHV_ARM64_REGISTER_X0: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020000); + // Fp = X29 + pub const WHV_ARM64_REGISTER_FP: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0002001D); + // Lr = X30 + pub const WHV_ARM64_REGISTER_LR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0002001E); + pub const WHV_ARM64_REGISTER_PC: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020022); + pub const WHV_ARM64_REGISTER_PSTATE: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020023); + pub const WHV_ARM64_REGISTER_SP: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0002001F); + pub const WHV_ARM64_REGISTER_SP_EL0: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020020); + pub const WHV_ARM64_REGISTER_SP_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020021); + + // Floating-point registers Q0-Q31 + pub const WHV_ARM64_REGISTER_Q0: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00030000); + + // FP status/control + pub const WHV_ARM64_REGISTER_FPCR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040012); + pub const WHV_ARM64_REGISTER_FPSR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040013); + + // System registers + pub const WHV_ARM64_REGISTER_SCTLR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040002); + pub const WHV_ARM64_REGISTER_CPACR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040004); + pub const WHV_ARM64_REGISTER_TTBR0_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040005); + pub const WHV_ARM64_REGISTER_TTBR1_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040006); + pub const WHV_ARM64_REGISTER_TCR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040007); + pub const WHV_ARM64_REGISTER_MAIR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0004000B); + pub const WHV_ARM64_REGISTER_VBAR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0004000C); + + /// Helper: produce the `WHV_REGISTER_NAME` for general-purpose register X. + /// For i in 0..29, uses sequential numbering from X0. + /// i == 29 maps to FP, i == 30 maps to LR. + pub fn xreg(i: u32) -> WHV_REGISTER_NAME { + match i { + 0..=28 => WHV_REGISTER_NAME(WHV_ARM64_REGISTER_X0.0 + i as i32), + 29 => WHV_ARM64_REGISTER_FP, + 30 => WHV_ARM64_REGISTER_LR, + _ => panic!("Invalid ARM64 GP register index: {i}"), + } + } + + /// Helper: produce the `WHV_REGISTER_NAME` for SIMD register Q. + pub fn qreg(i: u32) -> WHV_REGISTER_NAME { + debug_assert!(i < 32, "Invalid ARM64 SIMD register index: {i}"); + WHV_REGISTER_NAME(WHV_ARM64_REGISTER_Q0.0 + i as i32) + } + + // Suppress unused warnings for registers defined for completeness + #[allow(dead_code)] + pub const WHV_ARM64_REGISTER_TTBR1_EL1_: WHV_REGISTER_NAME = WHV_ARM64_REGISTER_TTBR1_EL1; + #[allow(dead_code)] + pub const WHV_ARM64_REGISTER_SP_EL0_: WHV_REGISTER_NAME = WHV_ARM64_REGISTER_SP_EL0; +} + +/// ARM64 WHP exit reasons (from the SDK header under `_ARM64_`). +mod arm64_exit_reasons { + use windows::Win32::System::Hypervisor::WHV_RUN_VP_EXIT_REASON; + + pub const WHV_EXIT_REASON_NONE: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x00000000u32 as i32); + pub const WHV_EXIT_REASON_UNMAPPED_GPA: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x80000000u32 as i32); + pub const WHV_EXIT_REASON_GPA_INTERCEPT: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x80000001u32 as i32); + pub const WHV_EXIT_REASON_UNRECOVERABLE: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x80000021u32 as i32); + pub const WHV_EXIT_REASON_INVALID_VP_REGISTER: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x80000020u32 as i32); + pub const WHV_EXIT_REASON_ARM64_RESET: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x8001000cu32 as i32); + pub const WHV_EXIT_REASON_CANCELLED: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0xFFFFFFFFu32 as i32); +} + +/// ARM64 WHP exit context layout. +/// +/// On ARM64, `WHV_RUN_VP_EXIT_CONTEXT` is: +/// ```c +/// struct { ExitReason: u32, Reserved: u32, Reserved1: u64, union { ... AsUINT64[32] } } +/// ``` +/// Total size = 8 (header) + 8 (reserved1) + 256 (union) = 272 bytes. +/// +/// The union's `MemoryAccess` variant starts with `WHV_INTERCEPT_MESSAGE_HEADER` (24 bytes): +/// ```c +/// struct { VpIndex: u32, InstructionLength: u8, InterceptAccessType: u8, +/// ExecutionState: u16, Pc: u64, Cpsr: u64 } +/// ``` +/// Followed by memory-access-specific fields. +#[repr(C)] +#[derive(Clone, Copy)] +struct Arm64ExitContext { + exit_reason: WHV_RUN_VP_EXIT_REASON, + reserved: u32, + reserved1: u64, + /// Raw payload — union of various context types. We interpret based on exit_reason. + payload: [u64; 32], +} + +impl Default for Arm64ExitContext { + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} + +/// Parsed fields from `WHV_INTERCEPT_MESSAGE_HEADER` (ARM64 version). +struct InterceptHeader { + #[allow(dead_code)] + vp_index: u32, + instruction_length: u8, + intercept_access_type: u8, + #[allow(dead_code)] + execution_state: u16, + pc: u64, + #[allow(dead_code)] + cpsr: u64, +} + +impl Arm64ExitContext { + /// Parse the intercept message header from the start of the payload. + /// This is valid for memory access, unrecoverable, and register intercept exits. + fn intercept_header(&self) -> InterceptHeader { + let bytes = unsafe { core::slice::from_raw_parts(self.payload.as_ptr() as *const u8, 24) }; + InterceptHeader { + vp_index: u32::from_le_bytes(bytes[0..4].try_into().unwrap()), + instruction_length: bytes[4], + intercept_access_type: bytes[5], + execution_state: u16::from_le_bytes(bytes[6..8].try_into().unwrap()), + pc: u64::from_le_bytes(bytes[8..16].try_into().unwrap()), + cpsr: u64::from_le_bytes(bytes[16..24].try_into().unwrap()), + } + } + + /// For memory access exits: get the GPA from the payload. + /// + /// ARM64 `WHV_MEMORY_ACCESS_CONTEXT` layout after the 24-byte header: + /// ```text + /// offset 24: Reserved0 (u32) + /// offset 28: InstructionByteCount (u8) + /// offset 29: AccessInfo (u8 bitfield: GvaValid, GvaGpaValid, HypercallOutputPending) + /// offset 30: Reserved1 (u16) + /// offset 32: InstructionBytes[4] (u32) + /// offset 36: Reserved2 (u32) + /// offset 40: Gva (u64) + /// offset 48: Gpa (u64) + /// offset 56: Syndrome (u64) + /// ``` + fn memory_access_gpa(&self) -> u64 { + let bytes = unsafe { core::slice::from_raw_parts(self.payload.as_ptr() as *const u8, 64) }; + u64::from_le_bytes(bytes[48..56].try_into().unwrap()) + } +} + +// ============================================================================ +// Surrogate process guard (same as x86_64 version) +// ============================================================================ + +use std::sync::atomic::AtomicBool as StdAtomicBool; + +/// RAII guard: when surrogates are disabled, only one no-surrogate WHP VM +/// can exist at a time. This flag prevents a second one from being created. +static NO_SURROGATE_VM_ACTIVE: StdAtomicBool = StdAtomicBool::new(false); + +struct NoSurrogateGuard; + +impl NoSurrogateGuard { + fn acquire() -> Result { + if NO_SURROGATE_VM_ACTIVE + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + return Err(CreateVmError::SurrogateProcess( + "Another no-surrogate WHP VM is already active".to_string(), + )); + } + Ok(NoSurrogateGuard) + } +} + +impl Drop for NoSurrogateGuard { + fn drop(&mut self) { + NO_SURROGATE_VM_ACTIVE.store(false, Ordering::SeqCst); + } +} + +// ============================================================================ +// WhpVm implementation +// ============================================================================ + +/// Determine whether the WHP hypervisor API is available. +#[allow(dead_code)] +pub(crate) fn is_hypervisor_present() -> bool { + let mut capability: WHV_CAPABILITY = Default::default(); + let written_size: Option<*mut u32> = None; + + match unsafe { + WHvGetCapability( + WHvCapabilityCodeHypervisorPresent, + &mut capability as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + written_size, + ) + } { + Ok(_) => unsafe { capability.HypervisorPresent.as_bool() }, + Err(_) => { + tracing::info!("Windows Hypervisor Platform is not available on this system"); + false + } + } +} + +/// Helper: release a host-side file mapping view and its handle. +fn release_file_mapping(view_base: *mut c_void, mapping_handle: HandleWrapper) { + unsafe { + use windows::Win32::Foundation::CloseHandle; + use windows::Win32::System::Memory::{MEMORY_MAPPED_VIEW_ADDRESS, UnmapViewOfFile}; + + if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: view_base }) { + tracing::error!("Failed to unmap view of file: {e:?}"); + } + if let Err(e) = CloseHandle(mapping_handle.into()) { + tracing::error!("Failed to close file mapping handle: {e:?}"); + } + } +} + +/// A WHP-backed single-vCPU VM on ARM64. +#[derive(Debug)] +pub(crate) struct WhpVm { + partition: WHV_PARTITION_HANDLE, + surrogate_process: Option, + /// Tracks host-side file mappings for cleanup. + file_mappings: Vec<(HandleWrapper, *mut c_void)>, + _no_surrogate_guard: Option, + /// Tracks whether the cancel flag is set + cancelled: AtomicBool, +} + +// Safety: same reasoning as x86_64 WhpVm — raw pointers are kernel resource handles, +// not dereferenced, safe to transfer between threads. +unsafe impl Send for WhpVm {} + +impl WhpVm { + pub(crate) fn new() -> Result { + const NUM_CPU: u32 = 1; + + let no_surrogate = surrogates_disabled(); + let no_surrogate_guard = if no_surrogate { + Some(NoSurrogateGuard::acquire()?) + } else { + None + }; + + let partition = unsafe { + let p = WHvCreatePartition().map_err(|e| CreateVmError::CreateVmFd(e.into()))?; + WHvSetPartitionProperty( + p, + WHvPartitionPropertyCodeProcessorCount, + &NUM_CPU as *const _ as *const _, + std::mem::size_of_val(&NUM_CPU) as _, + ) + .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + + WHvSetupPartition(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?; + WHvCreateVirtualProcessor(p, 0, 0) + .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; + + p + }; + + let surrogate_process = if no_surrogate { + None + } else { + let mgr = get_surrogate_process_manager() + .map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?; + Some( + mgr.get_surrogate_process() + .map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?, + ) + }; + + Ok(WhpVm { + partition, + surrogate_process, + file_mappings: Vec::new(), + _no_surrogate_guard: no_surrogate_guard, + cancelled: AtomicBool::new(false), + }) + } + + /// Get a single 64-bit register value. + fn get_reg64(&self, name: WHV_REGISTER_NAME) -> Result { + let names = [name]; + let mut values: [WHV_REGISTER_VALUE; 1] = unsafe { core::mem::zeroed() }; + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + names.as_ptr(), + 1, + values.as_mut_ptr(), + ) + .map_err(|e| RegisterError::GetRegs(e.into()))?; + } + Ok(unsafe { values[0].Reg64 }) + } + + /// Set a single 64-bit register value. + fn set_reg64(&self, name: WHV_REGISTER_NAME, value: u64) -> Result<(), RegisterError> { + let names = [name]; + let values = [WHV_REGISTER_VALUE { Reg64: value }]; + unsafe { + WHvSetVirtualProcessorRegisters(self.partition, 0, names.as_ptr(), 1, values.as_ptr()) + .map_err(|e| RegisterError::SetRegs(e.into()))?; + } + Ok(()) + } + + /// Get a single 128-bit register value (for SIMD Q registers). + fn get_reg128(&self, name: WHV_REGISTER_NAME) -> Result { + let names = [name]; + let mut values: [WHV_REGISTER_VALUE; 1] = unsafe { core::mem::zeroed() }; + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + names.as_ptr(), + 1, + values.as_mut_ptr(), + ) + .map_err(|e| RegisterError::GetFpu(e.into()))?; + } + let v = unsafe { values[0].Reg128 }; + Ok((v.High64 as u128) << 64 | v.Low64 as u128) + } + + /// Set a single 128-bit register value (for SIMD Q registers). + fn set_reg128(&self, name: WHV_REGISTER_NAME, value: u128) -> Result<(), RegisterError> { + let names = [name]; + let values = [WHV_REGISTER_VALUE { + Reg128: WHV_UINT128 { + Low64: value as u64, + High64: (value >> 64) as u64, + }, + }]; + unsafe { + WHvSetVirtualProcessorRegisters(self.partition, 0, names.as_ptr(), 1, values.as_ptr()) + .map_err(|e| RegisterError::SetFpu(e.into()))?; + } + Ok(()) + } + + /// Get the partition handle for this VM. + pub(crate) fn partition_handle(&self) -> WHV_PARTITION_HANDLE { + self.partition + } +} + +impl VirtualMachine for WhpVm { + unsafe fn map_memory( + &mut self, + (_slot, region): (u32, &MemoryRegion), + ) -> Result<(), MapMemoryError> { + let flags = region + .flags + .iter() + .map(|flag| match flag { + MemoryRegionFlags::NONE => Ok(WHvMapGpaRangeFlagNone), + MemoryRegionFlags::READ => Ok(WHvMapGpaRangeFlagRead), + MemoryRegionFlags::WRITE => Ok(WHvMapGpaRangeFlagWrite), + MemoryRegionFlags::EXECUTE => Ok(WHvMapGpaRangeFlagExecute), + _ => Err(MapMemoryError::InvalidFlags(format!( + "Invalid memory region flag: {:?}", + flag + ))), + }) + .collect::, MapMemoryError>>()? + .iter() + .fold(WHvMapGpaRangeFlagNone, |acc, flag| acc | *flag); + + match &mut self.surrogate_process { + None => { + let host_addr = (region.host_region.start.handle_base + + region.host_region.start.offset) + as *const c_void; + let res = unsafe { + WHvMapGpaRange( + self.partition, + host_addr, + region.guest_region.start as u64, + region.guest_region.len() as u64, + flags, + ) + }; + if let Err(e) = res { + return Err(MapMemoryError::Hypervisor( + super::super::HypervisorError::WindowsError(e), + )); + } + } + Some(surrogate) => { + let surrogate_base = surrogate + .map( + region.host_region.start.from_handle, + region.host_region.start.handle_base, + region.host_region.start.handle_size, + ®ion.region_type.surrogate_mapping(), + ) + .map_err(|e| MapMemoryError::SurrogateProcess(e.to_string()))?; + let surrogate_addr = surrogate_base.wrapping_add(region.host_region.start.offset); + + let whvmapgparange2_func = unsafe { + match try_load_whv_map_gpa_range2() { + Ok(func) => func, + Err(e) => { + return Err(MapMemoryError::LoadApi { + api_name: "WHvMapGpaRange2", + source: e, + }); + } + } + }; + + let res = unsafe { + whvmapgparange2_func( + self.partition, + surrogate.process_handle.into(), + surrogate_addr, + region.guest_region.start as u64, + region.guest_region.len() as u64, + flags, + ) + }; + if res.is_err() { + return Err(MapMemoryError::Hypervisor( + super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(res), + ), + )); + } + } + } + + if region.region_type == MemoryRegionType::MappedFile { + self.file_mappings.push(( + region.host_region.start.from_handle, + region.host_region.start.handle_base as *mut c_void, + )); + } + + Ok(()) + } + + fn unmap_memory( + &mut self, + (_slot, region): (u32, &MemoryRegion), + ) -> Result<(), UnmapMemoryError> { + unsafe { + WHvUnmapGpaRange( + self.partition, + region.guest_region.start as u64, + region.guest_region.len() as u64, + ) + .map_err(|e| { + UnmapMemoryError::Hypervisor(super::super::HypervisorError::WindowsError(e)) + })?; + } + if let Some(surrogate) = &mut self.surrogate_process { + surrogate.unmap(region.host_region.start.handle_base); + } + + if region.region_type == MemoryRegionType::MappedFile { + let handle_base = region.host_region.start.handle_base as *mut c_void; + if let Some(pos) = self + .file_mappings + .iter() + .position(|(_, vb)| *vb == handle_base) + { + let (handle, view) = self.file_mappings.swap_remove(pos); + release_file_mapping(view, handle); + } + } + + Ok(()) + } + + fn run_vcpu( + &mut self, + #[cfg(feature = "trace_guest")] tc: &mut SandboxTraceContext, + ) -> Result { + use arm64_exit_reasons::*; + use arm64_regs::*; + + let mut exit_context = Arm64ExitContext::default(); + + #[cfg(feature = "trace_guest")] + tc.setup_guest_trace(Span::current().context()); + + loop { + unsafe { + WHvRunVirtualProcessor( + self.partition, + 0, + &mut exit_context as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + ) + .map_err(|e| RunVcpuError::Unknown(e.into()))?; + } + + match exit_context.exit_reason { + WHV_EXIT_REASON_UNMAPPED_GPA | WHV_EXIT_REASON_GPA_INTERCEPT => { + let header = exit_context.intercept_header(); + let gpa = exit_context.memory_access_gpa(); + + // On ARM64, I/O is performed via MMIO writes to the I/O page. + let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; + let is_write = header.intercept_access_type != 0; + + if is_write + && gpa >= io_page_gpa + && (gpa - io_page_gpa) < hyperlight_common::vmem::PAGE_SIZE as u64 + { + let off = (gpa - io_page_gpa) as usize; + let port = off / core::mem::size_of::(); + + // Advance PC past the faulting instruction. + // WHP ARM64 does not auto-advance PC on intercepts. + let next_pc = header.pc + header.instruction_length as u64; + self.set_reg64(WHV_ARM64_REGISTER_PC, next_pc) + .map_err(|e| match e { + RegisterError::SetRegs(he) => RunVcpuError::IncrementRip(he), + _ => RunVcpuError::Unknown( + super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(HRESULT(0)), + ), + ), + })?; + + if port == VmAction::Halt as usize { + return Ok(VmExit::Halt()); + } else { + return Ok(VmExit::IoOut( + port as u16, + (off as u64).to_le_bytes().to_vec(), + )); + } + } else { + // Non-I/O page memory access + if is_write { + return Ok(VmExit::MmioWrite(gpa)); + } else { + return Ok(VmExit::MmioRead(gpa)); + } + } + } + WHV_EXIT_REASON_CANCELLED => { + return Ok(VmExit::Cancelled()); + } + WHV_EXIT_REASON_ARM64_RESET => { + return Ok(VmExit::Halt()); + } + WHV_EXIT_REASON_UNRECOVERABLE => { + let header = exit_context.intercept_header(); + return Ok(VmExit::Unknown(format!( + "Unrecoverable exception at PC={:#x}", + header.pc + ))); + } + WHV_EXIT_REASON_INVALID_VP_REGISTER => { + return Ok(VmExit::Unknown("Invalid VP register value".to_string())); + } + other => { + return Ok(VmExit::Unknown(format!( + "Unknown WHP ARM64 exit reason: {:#x}", + other.0 as u32 + ))); + } + } + } + } + + fn regs(&self) -> Result { + use arm64_regs::*; + + // Get all 31 GP regs + PC + SP + PSTATE in one batch + const COUNT: usize = 31 + 3; // X0..X30, PC, SP, PSTATE + let mut names = [WHV_REGISTER_NAME(0); COUNT]; + for i in 0..31u32 { + names[i as usize] = xreg(i); + } + names[31] = WHV_ARM64_REGISTER_PC; + names[32] = WHV_ARM64_REGISTER_SP; + names[33] = WHV_ARM64_REGISTER_PSTATE; + + let mut values: [WHV_REGISTER_VALUE; COUNT] = unsafe { core::mem::zeroed() }; + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + names.as_ptr(), + COUNT as u32, + values.as_mut_ptr(), + ) + .map_err(|e| RegisterError::GetRegs(e.into()))?; + } + + let mut x = [0u64; 31]; + for i in 0..31 { + x[i] = unsafe { values[i].Reg64 }; + } + + Ok(CommonRegisters { + x, + pc: unsafe { values[31].Reg64 }, + sp: unsafe { values[32].Reg64 }, + pstate: unsafe { values[33].Reg64 }, + }) + } + + fn set_regs(&self, regs: &CommonRegisters) -> Result<(), RegisterError> { + use arm64_regs::*; + + const COUNT: usize = 31 + 3; + let mut names = [WHV_REGISTER_NAME(0); COUNT]; + let mut values: [WHV_REGISTER_VALUE; COUNT] = unsafe { core::mem::zeroed() }; + + for i in 0..31u32 { + names[i as usize] = xreg(i); + values[i as usize] = WHV_REGISTER_VALUE { + Reg64: regs.x[i as usize], + }; + } + names[31] = WHV_ARM64_REGISTER_PC; + values[31] = WHV_REGISTER_VALUE { Reg64: regs.pc }; + names[32] = WHV_ARM64_REGISTER_SP; + values[32] = WHV_REGISTER_VALUE { Reg64: regs.sp }; + names[33] = WHV_ARM64_REGISTER_PSTATE; + values[33] = WHV_REGISTER_VALUE { Reg64: regs.pstate }; + + unsafe { + WHvSetVirtualProcessorRegisters( + self.partition, + 0, + names.as_ptr(), + COUNT as u32, + values.as_ptr(), + ) + .map_err(|e| RegisterError::SetRegs(e.into()))?; + } + Ok(()) + } + + fn fpu(&self) -> Result { + use arm64_regs::*; + + let mut v = [0u128; 32]; + for i in 0..32u32 { + v[i as usize] = self.get_reg128(qreg(i))?; + } + let fpsr = self.get_reg64(WHV_ARM64_REGISTER_FPSR).map_err(|_| { + RegisterError::GetFpu(super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(HRESULT(0)), + )) + })? as u32; + let fpcr = self.get_reg64(WHV_ARM64_REGISTER_FPCR).map_err(|_| { + RegisterError::GetFpu(super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(HRESULT(0)), + )) + })? as u32; + + Ok(CommonFpu { v, fpsr, fpcr }) + } + + fn set_fpu(&self, fpu: &CommonFpu) -> Result<(), RegisterError> { + use arm64_regs::*; + + for i in 0..32u32 { + self.set_reg128(qreg(i), fpu.v[i as usize])?; + } + self.set_reg64(WHV_ARM64_REGISTER_FPSR, fpu.fpsr as u64) + .map_err(|_| { + RegisterError::SetFpu(super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(HRESULT(0)), + )) + })?; + self.set_reg64(WHV_ARM64_REGISTER_FPCR, fpu.fpcr as u64) + .map_err(|_| { + RegisterError::SetFpu(super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(HRESULT(0)), + )) + })?; + Ok(()) + } + + fn sregs(&self) -> Result { + use arm64_regs::*; + + Ok(CommonSpecialRegisters { + ttbr0_el1: self.get_reg64(WHV_ARM64_REGISTER_TTBR0_EL1)?, + tcr_el1: self.get_reg64(WHV_ARM64_REGISTER_TCR_EL1)?, + mair_el1: self.get_reg64(WHV_ARM64_REGISTER_MAIR_EL1)?, + sctlr_el1: self.get_reg64(WHV_ARM64_REGISTER_SCTLR_EL1)?, + cpacr_el1: self.get_reg64(WHV_ARM64_REGISTER_CPACR_EL1)?, + vbar_el1: self.get_reg64(WHV_ARM64_REGISTER_VBAR_EL1)?, + sp_el1: self.get_reg64(WHV_ARM64_REGISTER_SP_EL1)?, + }) + } + + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<(), RegisterError> { + use arm64_regs::*; + + self.set_reg64(WHV_ARM64_REGISTER_TTBR0_EL1, sregs.ttbr0_el1)?; + self.set_reg64(WHV_ARM64_REGISTER_TCR_EL1, sregs.tcr_el1)?; + self.set_reg64(WHV_ARM64_REGISTER_MAIR_EL1, sregs.mair_el1)?; + self.set_reg64(WHV_ARM64_REGISTER_SCTLR_EL1, sregs.sctlr_el1)?; + self.set_reg64(WHV_ARM64_REGISTER_CPACR_EL1, sregs.cpacr_el1)?; + self.set_reg64(WHV_ARM64_REGISTER_VBAR_EL1, sregs.vbar_el1)?; + self.set_reg64(WHV_ARM64_REGISTER_SP_EL1, sregs.sp_el1)?; + Ok(()) + } + + fn debug_regs(&self) -> Result { + // Debug register support on ARM64 WHP not yet implemented + Ok(CommonDebugRegs::default()) + } + + fn set_debug_regs(&self, _drs: &CommonDebugRegs) -> Result<(), RegisterError> { + // Debug register support on ARM64 WHP not yet implemented + Ok(()) + } + + fn can_reset_vcpu(&self) -> bool { + true + } + + fn reset_vcpu(&mut self) -> Result<(), ResetVcpuError> { + // Reset by zeroing all GP registers, PC, SP, PSTATE + let regs = CommonRegisters::default(); + self.set_regs(®s).map_err(ResetVcpuError::Register)?; + Ok(()) + } + + fn partition_handle(&self) -> WHV_PARTITION_HANDLE { + self.partition + } +} + +impl Drop for WhpVm { + fn drop(&mut self) { + // Clean up file mappings + for (handle, view) in self.file_mappings.drain(..) { + release_file_mapping(view, handle); + } + + unsafe { + if let Err(e) = WHvDeleteVirtualProcessor(self.partition, 0) { + tracing::error!("Failed to delete virtual processor: {e:?}"); + } + if let Err(e) = WHvDeletePartition(self.partition) { + tracing::error!("Failed to delete partition: {e:?}"); + } + } + } +} + +// ============================================================================ +// Helper: dynamically load WHvMapGpaRange2 +// ============================================================================ + +type WhvMapGpaRange2Fn = unsafe extern "system" fn( + WHV_PARTITION_HANDLE, + windows::Win32::Foundation::HANDLE, + *const c_void, + u64, + u64, + WHV_MAP_GPA_RANGE_FLAGS, +) -> HRESULT; + +unsafe fn try_load_whv_map_gpa_range2() -> Result { + use windows::Win32::System::LibraryLoader::*; + use windows::core::s; + + let module = unsafe { LoadLibraryA(s!("winhvplatform.dll"))? }; + let proc = unsafe { GetProcAddress(module, s!("WHvMapGpaRange2")) }; + match proc { + Some(f) => Ok(unsafe { std::mem::transmute(f) }), + None => { + unsafe { + windows::Win32::Foundation::FreeLibrary(module).ok(); + } + Err(windows_result::Error::from_hresult(HRESULT(-1))) + } + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/mod.rs new file mode 100644 index 000000000..b2adf372c --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/mod.rs @@ -0,0 +1,25 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#[cfg(target_arch = "x86_64")] +mod x86_64; +#[cfg(target_arch = "x86_64")] +pub(crate) use x86_64::*; + +#[cfg(target_arch = "aarch64")] +mod aarch64; +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs similarity index 98% rename from src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs rename to src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs index b60777fcc..9c4290f70 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs @@ -586,7 +586,11 @@ impl VirtualMachine for WhpVm { if self.handle_hw_io_out(port, &data) { continue; } - } else if let Some(val) = super::x86_64::hw_interrupts::handle_io_in(port) { + } else if let Some(val) = + crate::hypervisor::virtual_machine::x86_64::hw_interrupts::handle_io_in( + port, + ) + { self.set_registers(&[( WHvX64RegisterRax, Align16(WHV_REGISTER_VALUE { Reg64: val }), @@ -1263,7 +1267,7 @@ impl WhpVm { )); } - super::x86_64::hw_interrupts::init_lapic_registers(&mut state); + crate::hypervisor::virtual_machine::x86_64::hw_interrupts::init_lapic_registers(&mut state); unsafe { WHvSetVirtualProcessorInterruptControllerState2( @@ -1312,7 +1316,7 @@ impl WhpVm { /// delivers through the LAPIC and the guest only acknowledges via PIC. fn do_lapic_eoi(&self) { if let Ok(mut state) = self.get_lapic_state() { - super::x86_64::hw_interrupts::lapic_eoi(&mut state); + crate::hypervisor::virtual_machine::x86_64::hw_interrupts::lapic_eoi(&mut state); if let Err(e) = self.set_lapic_state(&state) { tracing::warn!("WHP set_lapic_state (EOI) failed: {e}"); } @@ -1322,8 +1326,8 @@ impl WhpVm { fn handle_hw_io_out(&mut self, port: u16, data: &[u8]) -> bool { if port == VmAction::PvTimerConfig as u16 { let partition_raw = self.partition.0; - let vector = super::x86_64::hw_interrupts::TIMER_VECTOR; - super::x86_64::hw_interrupts::handle_pv_timer_config( + let vector = crate::hypervisor::virtual_machine::x86_64::hw_interrupts::TIMER_VECTOR; + crate::hypervisor::virtual_machine::x86_64::hw_interrupts::handle_pv_timer_config( &mut self.timer, data, move || { @@ -1345,9 +1349,12 @@ impl WhpVm { return true; } let timer_active = self.timer.as_ref().is_some_and(|t| t.is_active()); - super::x86_64::hw_interrupts::handle_common_io_out(port, data, timer_active, || { - self.do_lapic_eoi() - }) + crate::hypervisor::virtual_machine::x86_64::hw_interrupts::handle_common_io_out( + port, + data, + timer_active, + || self.do_lapic_eoi(), + ) } } From 9b59799083deb0732fa97c7280bf42823ccd7e50 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:12:43 -0700 Subject: [PATCH 2/6] fix: address cross-compilation issues for aarch64-pc-windows-msvc - Move vmm-sys-util to unix-only dependencies (it doesn't compile on Windows) - Fix WHV_UINT128 field access (requires Anonymous wrapper on windows crate) - Add CpuVendor::current() for aarch64 Windows target - Remove redundant partition_handle inherent method and unused cancelled field - Fix unused import warnings - Add #[allow(dead_code)] on exit reason constants module Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- src/hyperlight_host/Cargo.toml | 2 +- .../src/hypervisor/hyperlight_vm/aarch64.rs | 4 +++- .../hypervisor/virtual_machine/whp/aarch64.rs | 20 ++++++++----------- .../src/sandbox/snapshot/file/config.rs | 6 ++++++ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index 60be1555d..6814cefee 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -42,7 +42,6 @@ tracing-core = "0.1.36" tracing-opentelemetry = { version = "0.33.0", optional = true } hyperlight-common = { workspace = true, default-features = true, features = [ "std" ] } hyperlight-guest-tracing = { workspace = true, default-features = true, optional = true } -vmm-sys-util = "0.15.0" crossbeam-channel = "0.5.15" thiserror = "2.0.18" chrono = { version = "0.4", optional = true } @@ -83,6 +82,7 @@ kvm-bindings = { version = "0.14", features = ["fam-wrappers"], optional = true kvm-ioctls = { version = "0.25", optional = true } mshv-bindings = { version = "0.6", optional = true } mshv-ioctls = { version = "0.6", optional = true} +vmm-sys-util = "0.15.0" [dev-dependencies] uuid = { version = "1.23.3", features = ["v4"] } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 59770f379..0d1268b7a 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -17,7 +17,9 @@ limitations under the License. // TODO(aarch64): implement arch-specific HyperlightVm methods use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64}; +use std::sync::atomic::AtomicU8; +#[cfg(target_os = "linux")] +use std::sync::atomic::{AtomicBool, AtomicU64}; use super::{ AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm, diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index d1cb5c786..a4eea3236 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -22,7 +22,7 @@ limitations under the License. //! the Windows SDK header `WinHvPlatformDefs.h` (10.0.26100.0). use std::os::raw::c_void; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::Ordering; use hyperlight_common::outb::VmAction; #[cfg(feature = "trace_guest")] @@ -114,6 +114,7 @@ mod arm64_regs { } /// ARM64 WHP exit reasons (from the SDK header under `_ARM64_`). +#[allow(dead_code)] mod arm64_exit_reasons { use windows::Win32::System::Hypervisor::WHV_RUN_VP_EXIT_REASON; @@ -221,6 +222,7 @@ use std::sync::atomic::AtomicBool as StdAtomicBool; /// can exist at a time. This flag prevents a second one from being created. static NO_SURROGATE_VM_ACTIVE: StdAtomicBool = StdAtomicBool::new(false); +#[derive(Debug)] struct NoSurrogateGuard; impl NoSurrogateGuard { @@ -292,8 +294,6 @@ pub(crate) struct WhpVm { /// Tracks host-side file mappings for cleanup. file_mappings: Vec<(HandleWrapper, *mut c_void)>, _no_surrogate_guard: Option, - /// Tracks whether the cancel flag is set - cancelled: AtomicBool, } // Safety: same reasoning as x86_64 WhpVm — raw pointers are kernel resource handles, @@ -344,7 +344,6 @@ impl WhpVm { surrogate_process, file_mappings: Vec::new(), _no_surrogate_guard: no_surrogate_guard, - cancelled: AtomicBool::new(false), }) } @@ -391,7 +390,7 @@ impl WhpVm { .map_err(|e| RegisterError::GetFpu(e.into()))?; } let v = unsafe { values[0].Reg128 }; - Ok((v.High64 as u128) << 64 | v.Low64 as u128) + Ok((unsafe { v.Anonymous.High64 } as u128) << 64 | unsafe { v.Anonymous.Low64 } as u128) } /// Set a single 128-bit register value (for SIMD Q registers). @@ -399,8 +398,10 @@ impl WhpVm { let names = [name]; let values = [WHV_REGISTER_VALUE { Reg128: WHV_UINT128 { - Low64: value as u64, - High64: (value >> 64) as u64, + Anonymous: WHV_UINT128_0 { + Low64: value as u64, + High64: (value >> 64) as u64, + }, }, }]; unsafe { @@ -409,11 +410,6 @@ impl WhpVm { } Ok(()) } - - /// Get the partition handle for this VM. - pub(crate) fn partition_handle(&self) -> WHV_PARTITION_HANDLE { - self.partition - } } impl VirtualMachine for WhpVm { diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 01c2f501b..bb6df2bb2 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -134,6 +134,12 @@ impl CpuVendor { // `0x` prefix padded to width 4, e.g. Apple `0x61`, Arm `0x41`. Self(format!("{implementer:#04x}")) } + #[cfg(all(target_arch = "aarch64", target_os = "windows"))] + { + // Windows does not expose MIDR_EL1 directly; use a fixed vendor + // string. All current Windows ARM64 devices use Qualcomm/ARM cores. + Self("arm-windows".to_owned()) + } } pub(super) fn as_str(&self) -> &str { From 1d3a29968a104373971a8cefb030c2d4d3f9cfe3 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:48:14 +0000 Subject: [PATCH 3/6] fix(aarch64): make WHP backend operational Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .../src/hypervisor/hyperlight_vm/aarch64.rs | 2 +- src/hyperlight_host/src/hypervisor/mod.rs | 20 ++ src/hyperlight_host/src/hypervisor/regs.rs | 4 +- .../hypervisor/virtual_machine/whp/aarch64.rs | 297 ++++++++++++------ .../src/sandbox/snapshot/file/config.rs | 2 + src/hyperlight_host/tests/integration_test.rs | 10 +- .../tests/sandbox_host_tests.rs | 7 +- 7 files changed, 234 insertions(+), 108 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 0d1268b7a..6e5769e43 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -236,7 +236,7 @@ impl HyperlightVm { self.vm_can_reset_vcpu, "No fallback path for vcpu reset on aarch64" ); - self.vm.reset_vcpu()?; + self.interrupt_handle.reset_vcpu(self.vm.as_mut())?; self.apply_sregs(cr3, sregs)?; Ok(()) } diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index 732f08563..fd2136b9e 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -46,6 +46,8 @@ use std::sync::atomic::{AtomicU8, Ordering}; #[cfg(any(kvm, mshv3))] use std::time::Duration; +use self::virtual_machine::{ResetVcpuError, VirtualMachine}; + /// A trait for platform-specific interrupt handle implementation details pub(crate) trait InterruptHandleImpl: InterruptHandle { /// Set the thread ID for the vcpu thread @@ -67,6 +69,11 @@ pub(crate) trait InterruptHandleImpl: InterruptHandle { /// Clear the cancellation request flag fn clear_cancel(&self); + /// Reset the vCPU while honoring platform lifecycle synchronization. + fn reset_vcpu(&self, vm: &mut dyn VirtualMachine) -> Result<(), ResetVcpuError> { + vm.reset_vcpu() + } + /// Check if debug interrupt was requested (always returns false when gdb feature is disabled) fn is_debug_interrupted(&self) -> bool; @@ -347,6 +354,19 @@ impl InterruptHandleImpl for WindowsInterruptHandle { self.state.fetch_and(!Self::RUNNING_BIT, Ordering::Release); } + fn reset_vcpu(&self, vm: &mut dyn VirtualMachine) -> Result<(), ResetVcpuError> { + let guard = self + .partition_state + .write() + .map_err(|e| ResetVcpuError::Unknown(e.to_string()))?; + if guard.dropped { + return Err(ResetVcpuError::Unknown( + "cannot reset a dropped partition".to_string(), + )); + } + vm.reset_vcpu() + } + fn is_debug_interrupted(&self) -> bool { #[cfg(gdb)] { diff --git a/src/hyperlight_host/src/hypervisor/regs.rs b/src/hyperlight_host/src/hypervisor/regs.rs index 828bdf558..4afad9fa8 100644 --- a/src/hyperlight_host/src/hypervisor/regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs.rs @@ -21,13 +21,13 @@ pub(crate) use x86_64::*; #[cfg(target_arch = "aarch64")] mod aarch64; -#[cfg(target_os = "windows")] +#[cfg(all(target_arch = "x86_64", target_os = "windows"))] use std::collections::HashSet; #[cfg(target_arch = "aarch64")] pub(crate) use aarch64::*; -#[cfg(target_os = "windows")] +#[cfg(all(target_arch = "x86_64", target_os = "windows"))] #[derive(Debug, PartialEq)] pub(crate) enum FromWhpRegisterError { MissingRegister(HashSet), diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index a4eea3236..286b55e52 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -55,6 +55,44 @@ use crate::sandbox::trace::TraceContext as SandboxTraceContext; // because the `windows` crate does not expose ARM64 WHP types. // ============================================================================ +const WHV_PARTITION_PROPERTY_CODE_ARM64_IC_PARAMETERS: WHV_PARTITION_PROPERTY_CODE = + WHV_PARTITION_PROPERTY_CODE(0x00001012); +const WHV_ARM64_REGISTER_GICR_BASE_GPA: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00063000); + +#[repr(C)] +struct Arm64IcGicV3Parameters { + gicd_base_address: u64, + gits_translater_base_address: u64, + reserved: u32, + gic_lpi_int_id_bits: u32, + gic_ppi_overflow_interrupt_from_cntv: u32, + gic_ppi_performance_monitors_interrupt: u32, + reserved1: [u32; 6], +} + +#[repr(C)] +struct Arm64IcParameters { + emulation_mode: i32, + reserved: u32, + gic_v3_parameters: Arm64IcGicV3Parameters, +} + +const ARM64_IC_PARAMETERS: Arm64IcParameters = Arm64IcParameters { + emulation_mode: 1, + reserved: 0, + gic_v3_parameters: Arm64IcGicV3Parameters { + gicd_base_address: 0xffff0000, + gits_translater_base_address: 0xeff68000, + reserved: 0, + gic_lpi_int_id_bits: 1, + gic_ppi_overflow_interrupt_from_cntv: 0x1b, + gic_ppi_performance_monitors_interrupt: 0x17, + reserved1: [0; 6], + }, +}; + +const GICR_BASE_GPA: u64 = 0xeffee000; + /// ARM64 WHP register name constants. /// Mapped from `WHV_REGISTER_NAME` enum values in the SDK header under `_ARM64_`. mod arm64_regs { @@ -68,7 +106,6 @@ mod arm64_regs { pub const WHV_ARM64_REGISTER_LR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0002001E); pub const WHV_ARM64_REGISTER_PC: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020022); pub const WHV_ARM64_REGISTER_PSTATE: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020023); - pub const WHV_ARM64_REGISTER_SP: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0002001F); pub const WHV_ARM64_REGISTER_SP_EL0: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020020); pub const WHV_ARM64_REGISTER_SP_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00020021); @@ -210,6 +247,11 @@ impl Arm64ExitContext { let bytes = unsafe { core::slice::from_raw_parts(self.payload.as_ptr() as *const u8, 64) }; u64::from_le_bytes(bytes[48..56].try_into().unwrap()) } + + fn memory_access_syndrome(&self) -> u64 { + let bytes = unsafe { core::slice::from_raw_parts(self.payload.as_ptr() as *const u8, 64) }; + u64::from_le_bytes(bytes[56..64].try_into().unwrap()) + } } // ============================================================================ @@ -313,38 +355,67 @@ impl WhpVm { let partition = unsafe { let p = WHvCreatePartition().map_err(|e| CreateVmError::CreateVmFd(e.into()))?; - WHvSetPartitionProperty( - p, - WHvPartitionPropertyCodeProcessorCount, - &NUM_CPU as *const _ as *const _, - std::mem::size_of_val(&NUM_CPU) as _, - ) - .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + let mut vcpu_created = false; + let setup_result = (|| { + WHvSetPartitionProperty( + p, + WHvPartitionPropertyCodeProcessorCount, + &NUM_CPU as *const _ as *const _, + std::mem::size_of_val(&NUM_CPU) as _, + ) + .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; - WHvSetupPartition(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?; - WHvCreateVirtualProcessor(p, 0, 0) - .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; + WHvSetPartitionProperty( + p, + WHV_PARTITION_PROPERTY_CODE_ARM64_IC_PARAMETERS, + &ARM64_IC_PARAMETERS as *const _ as *const _, + std::mem::size_of_val(&ARM64_IC_PARAMETERS) as _, + ) + .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + + WHvSetupPartition(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?; + WHvCreateVirtualProcessor(p, 0, 0) + .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; + vcpu_created = true; + + let names = [WHV_ARM64_REGISTER_GICR_BASE_GPA]; + let values = [WHV_REGISTER_VALUE { + Reg64: GICR_BASE_GPA, + }]; + WHvSetVirtualProcessorRegisters(p, 0, names.as_ptr(), 1, values.as_ptr()) + .map_err(|e| CreateVmError::InitializeVm(e.into())) + })(); + + if let Err(error) = setup_result { + if vcpu_created && let Err(cleanup_error) = WHvDeleteVirtualProcessor(p, 0) { + tracing::error!("Failed to delete virtual processor: {cleanup_error:?}"); + } + if let Err(cleanup_error) = WHvDeletePartition(p) { + tracing::error!("Failed to delete partition: {cleanup_error:?}"); + } + return Err(error); + } p }; - let surrogate_process = if no_surrogate { - None - } else { + let mut vm = WhpVm { + partition, + surrogate_process: None, + file_mappings: Vec::new(), + _no_surrogate_guard: no_surrogate_guard, + }; + + if !no_surrogate { let mgr = get_surrogate_process_manager() .map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?; - Some( + vm.surrogate_process = Some( mgr.get_surrogate_process() .map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?, - ) - }; + ); + } - Ok(WhpVm { - partition, - surrogate_process, - file_mappings: Vec::new(), - _no_surrogate_guard: no_surrogate_guard, - }) + Ok(vm) } /// Get a single 64-bit register value. @@ -552,86 +623,94 @@ impl VirtualMachine for WhpVm { #[cfg(feature = "trace_guest")] tc.setup_guest_trace(Span::current().context()); - loop { - unsafe { - WHvRunVirtualProcessor( - self.partition, - 0, - &mut exit_context as *mut _ as *mut c_void, - std::mem::size_of::() as u32, - ) - .map_err(|e| RunVcpuError::Unknown(e.into()))?; - } + unsafe { + WHvRunVirtualProcessor( + self.partition, + 0, + &mut exit_context as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + ) + .map_err(|e| RunVcpuError::Unknown(e.into()))?; + } - match exit_context.exit_reason { - WHV_EXIT_REASON_UNMAPPED_GPA | WHV_EXIT_REASON_GPA_INTERCEPT => { - let header = exit_context.intercept_header(); - let gpa = exit_context.memory_access_gpa(); - - // On ARM64, I/O is performed via MMIO writes to the I/O page. - let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; - let is_write = header.intercept_access_type != 0; - - if is_write - && gpa >= io_page_gpa - && (gpa - io_page_gpa) < hyperlight_common::vmem::PAGE_SIZE as u64 - { - let off = (gpa - io_page_gpa) as usize; - let port = off / core::mem::size_of::(); - - // Advance PC past the faulting instruction. - // WHP ARM64 does not auto-advance PC on intercepts. - let next_pc = header.pc + header.instruction_length as u64; - self.set_reg64(WHV_ARM64_REGISTER_PC, next_pc) - .map_err(|e| match e { - RegisterError::SetRegs(he) => RunVcpuError::IncrementRip(he), - _ => RunVcpuError::Unknown( - super::super::HypervisorError::WindowsError( - windows_result::Error::from_hresult(HRESULT(0)), - ), - ), - })?; - - if port == VmAction::Halt as usize { - return Ok(VmExit::Halt()); - } else { - return Ok(VmExit::IoOut( - port as u16, - (off as u64).to_le_bytes().to_vec(), - )); - } + match exit_context.exit_reason { + WHV_EXIT_REASON_UNMAPPED_GPA | WHV_EXIT_REASON_GPA_INTERCEPT => { + let header = exit_context.intercept_header(); + let gpa = exit_context.memory_access_gpa(); + + // On ARM64, I/O is performed via MMIO writes to the I/O page. + let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; + let is_write = header.intercept_access_type != 0; + + if is_write + && gpa >= io_page_gpa + && (gpa - io_page_gpa) < hyperlight_common::vmem::PAGE_SIZE as u64 + { + let off = (gpa - io_page_gpa) as usize; + let port = off / core::mem::size_of::(); + + // Advance PC past the faulting instruction. + // WHP ARM64 does not auto-advance PC on intercepts. + let next_pc = header.pc + header.instruction_length as u64; + self.set_reg64(WHV_ARM64_REGISTER_PC, next_pc) + .map_err(|e| match e { + RegisterError::SetRegs(he) => RunVcpuError::IncrementRip(he), + _ => { + RunVcpuError::Unknown(super::super::HypervisorError::WindowsError( + windows_result::Error::from_hresult(HRESULT(0)), + )) + } + })?; + + if port == VmAction::Halt as usize { + Ok(VmExit::Halt()) } else { - // Non-I/O page memory access - if is_write { - return Ok(VmExit::MmioWrite(gpa)); - } else { - return Ok(VmExit::MmioRead(gpa)); + let syndrome = exit_context.memory_access_syndrome(); + const ISV: u64 = 1 << 24; + const WNR: u64 = 1 << 6; + if syndrome & (ISV | WNR) != ISV | WNR { + return Err(RunVcpuError::ParseGpaAccessInfo); } + let source_register = ((syndrome >> 16) & 0x1f) as u32; + let access_size = 1usize << ((syndrome >> 22) & 0x3); + let value = if source_register == 31 { + 0 + } else { + self.get_reg64(xreg(source_register)).map_err(|e| match e { + RegisterError::GetRegs(error) => RunVcpuError::Unknown(error), + _ => unreachable!("get_reg64 returned a non-get error"), + })? + }; + Ok(VmExit::IoOut( + port as u16, + value.to_le_bytes()[..access_size].to_vec(), + )) + } + } else { + // Non-I/O page memory access + if is_write { + Ok(VmExit::MmioWrite(gpa)) + } else { + Ok(VmExit::MmioRead(gpa)) } } - WHV_EXIT_REASON_CANCELLED => { - return Ok(VmExit::Cancelled()); - } - WHV_EXIT_REASON_ARM64_RESET => { - return Ok(VmExit::Halt()); - } - WHV_EXIT_REASON_UNRECOVERABLE => { - let header = exit_context.intercept_header(); - return Ok(VmExit::Unknown(format!( - "Unrecoverable exception at PC={:#x}", - header.pc - ))); - } - WHV_EXIT_REASON_INVALID_VP_REGISTER => { - return Ok(VmExit::Unknown("Invalid VP register value".to_string())); - } - other => { - return Ok(VmExit::Unknown(format!( - "Unknown WHP ARM64 exit reason: {:#x}", - other.0 as u32 - ))); - } } + WHV_EXIT_REASON_CANCELLED => Ok(VmExit::Cancelled()), + WHV_EXIT_REASON_ARM64_RESET => Ok(VmExit::Halt()), + WHV_EXIT_REASON_UNRECOVERABLE => { + let header = exit_context.intercept_header(); + Ok(VmExit::Unknown(format!( + "Unrecoverable exception at PC={:#x}", + header.pc + ))) + } + WHV_EXIT_REASON_INVALID_VP_REGISTER => { + Ok(VmExit::Unknown("Invalid VP register value".to_string())) + } + other => Ok(VmExit::Unknown(format!( + "Unknown WHP ARM64 exit reason: {:#x}", + other.0 as u32 + ))), } } @@ -645,7 +724,7 @@ impl VirtualMachine for WhpVm { names[i as usize] = xreg(i); } names[31] = WHV_ARM64_REGISTER_PC; - names[32] = WHV_ARM64_REGISTER_SP; + names[32] = WHV_ARM64_REGISTER_SP_EL0; names[33] = WHV_ARM64_REGISTER_PSTATE; let mut values: [WHV_REGISTER_VALUE; COUNT] = unsafe { core::mem::zeroed() }; @@ -688,7 +767,7 @@ impl VirtualMachine for WhpVm { } names[31] = WHV_ARM64_REGISTER_PC; values[31] = WHV_REGISTER_VALUE { Reg64: regs.pc }; - names[32] = WHV_ARM64_REGISTER_SP; + names[32] = WHV_ARM64_REGISTER_SP_EL0; values[32] = WHV_REGISTER_VALUE { Reg64: regs.sp }; names[33] = WHV_ARM64_REGISTER_PSTATE; values[33] = WHV_REGISTER_VALUE { Reg64: regs.pstate }; @@ -790,9 +869,19 @@ impl VirtualMachine for WhpVm { } fn reset_vcpu(&mut self) -> Result<(), ResetVcpuError> { - // Reset by zeroing all GP registers, PC, SP, PSTATE - let regs = CommonRegisters::default(); - self.set_regs(®s).map_err(ResetVcpuError::Register)?; + unsafe { + WHvDeleteVirtualProcessor(self.partition, 0) + .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; + WHvCreateVirtualProcessor(self.partition, 0, 0) + .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; + + let names = [WHV_ARM64_REGISTER_GICR_BASE_GPA]; + let values = [WHV_REGISTER_VALUE { + Reg64: GICR_BASE_GPA, + }]; + WHvSetVirtualProcessorRegisters(self.partition, 0, names.as_ptr(), 1, values.as_ptr()) + .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; + } Ok(()) } @@ -839,7 +928,9 @@ unsafe fn try_load_whv_map_gpa_range2() -> Result Ok(unsafe { std::mem::transmute(f) }), + Some(f) => Ok(unsafe { + std::mem::transmute:: isize, WhvMapGpaRange2Fn>(f) + }), None => { unsafe { windows::Win32::Foundation::FreeLibrary(module).ok(); diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index bb6df2bb2..e34fdde89 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -745,6 +745,8 @@ mod tests { #[cfg(all(target_arch = "aarch64", target_os = "linux"))] // MIDR_EL1 implementer byte for Apple silicon. assert_eq!(v, "0x61", "unexpected aarch64 CPU implementer"); + #[cfg(all(target_arch = "aarch64", target_os = "windows"))] + assert_eq!(v, "arm-windows", "unexpected Windows ARM64 CPU vendor"); } /// The architecture the current host is not running. diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index b449ea68d..1397994e6 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -935,6 +935,9 @@ fn interrupt_random_kill_stress_test() { use hyperlight_host::sandbox::snapshot::Snapshot; use tracing::{error, trace}; + #[cfg(target_arch = "aarch64")] + const POOL_SIZE: usize = 64; + #[cfg(not(target_arch = "aarch64"))] const POOL_SIZE: usize = 100; const NUM_THREADS: usize = 100; const ITERATIONS_PER_THREAD: usize = 500; @@ -1471,7 +1474,11 @@ fn interrupt_infinite_moving_loop_stress_test() { use std::sync::Arc; use std::thread; - // We have a high thread count to stress test and to have interesting interleavings + // ARM64 WHP partitions include GICv3 state, limiting practical partition + // concurrency. Each thread creates two partitions. + #[cfg(target_arch = "aarch64")] + const NUM_THREADS: usize = 32; + #[cfg(not(target_arch = "aarch64"))] const NUM_THREADS: usize = 200; let mut handles = vec![]; @@ -1737,6 +1744,7 @@ fn fill_heap_and_cause_exception() { /// Based on local observations, "likely" means that if the bug exist, running this test 5 times will catch it at least once. #[test] #[cfg(target_os = "windows")] +#[serial(thread_heavy)] fn interrupt_cancel_delete_race() { const NUM_THREADS: usize = 8; const NUM_KILL_THREADS: usize = 4; diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index e0daf969b..849430c1d 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -330,7 +330,12 @@ fn callback_test() { #[test] fn callback_test_parallel() { - let handles: Vec<_> = (0..100) + #[cfg(target_arch = "aarch64")] + const THREADS: usize = 64; + #[cfg(not(target_arch = "aarch64"))] + const THREADS: usize = 100; + + let handles: Vec<_> = (0..THREADS) .map(|_| { std::thread::spawn(|| { callback_test_helper(); From 5cb75f12c757c487f8f39bff6ad3b0a55c5a5974 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:17:32 +0000 Subject: [PATCH 4/6] fix(aarch64): correct GITS translator field name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .../src/hypervisor/virtual_machine/whp/aarch64.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index 286b55e52..0e6ae1e7e 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -62,7 +62,7 @@ const WHV_ARM64_REGISTER_GICR_BASE_GPA: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x #[repr(C)] struct Arm64IcGicV3Parameters { gicd_base_address: u64, - gits_translater_base_address: u64, + gits_translator_base_address: u64, reserved: u32, gic_lpi_int_id_bits: u32, gic_ppi_overflow_interrupt_from_cntv: u32, @@ -82,7 +82,7 @@ const ARM64_IC_PARAMETERS: Arm64IcParameters = Arm64IcParameters { reserved: 0, gic_v3_parameters: Arm64IcGicV3Parameters { gicd_base_address: 0xffff0000, - gits_translater_base_address: 0xeff68000, + gits_translator_base_address: 0xeff68000, reserved: 0, gic_lpi_int_id_bits: 1, gic_ppi_overflow_interrupt_from_cntv: 0x1b, From af8dafc9470378960568c4303bd6d21d61aaa197 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:58:39 +0000 Subject: [PATCH 5/6] fix: gate architecture-specific vCPU reset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- src/hyperlight_host/src/hypervisor/mod.rs | 3 +++ .../src/hypervisor/virtual_machine/whp/x86_64.rs | 7 +++++++ src/hyperlight_host/src/sandbox/config.rs | 4 +++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index fd2136b9e..0efa94c94 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -46,6 +46,7 @@ use std::sync::atomic::{AtomicU8, Ordering}; #[cfg(any(kvm, mshv3))] use std::time::Duration; +#[cfg(target_arch = "aarch64")] use self::virtual_machine::{ResetVcpuError, VirtualMachine}; /// A trait for platform-specific interrupt handle implementation details @@ -70,6 +71,7 @@ pub(crate) trait InterruptHandleImpl: InterruptHandle { fn clear_cancel(&self); /// Reset the vCPU while honoring platform lifecycle synchronization. + #[cfg(target_arch = "aarch64")] fn reset_vcpu(&self, vm: &mut dyn VirtualMachine) -> Result<(), ResetVcpuError> { vm.reset_vcpu() } @@ -354,6 +356,7 @@ impl InterruptHandleImpl for WindowsInterruptHandle { self.state.fetch_and(!Self::RUNNING_BIT, Ordering::Release); } + #[cfg(target_arch = "aarch64")] fn reset_vcpu(&self, vm: &mut dyn VirtualMachine) -> Result<(), ResetVcpuError> { let guard = self .partition_state diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs index 9c4290f70..b7bff5a33 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs @@ -533,6 +533,13 @@ impl VirtualMachine for WhpVm { #[cfg(feature = "trace_guest")] tc.setup_guest_trace(Span::current().context()); + #[cfg_attr( + not(feature = "hw-interrupts"), + expect( + clippy::never_loop, + reason = "the loop continues when hw-interrupts is enabled" + ) + )] loop { unsafe { WHvRunVirtualProcessor( diff --git a/src/hyperlight_host/src/sandbox/config.rs b/src/hyperlight_host/src/sandbox/config.rs index e7216a8a7..442da8415 100644 --- a/src/hyperlight_host/src/sandbox/config.rs +++ b/src/hyperlight_host/src/sandbox/config.rs @@ -345,7 +345,9 @@ impl Default for SandboxConfiguration { #[cfg(test)] mod tests { - use super::{GuestMsrError, SandboxConfiguration}; + #[cfg(target_arch = "x86_64")] + use super::GuestMsrError; + use super::SandboxConfiguration; #[test] #[cfg(target_arch = "x86_64")] From 05d5350929b299f6f077154f295bf58c4b28a194 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:57:01 +0000 Subject: [PATCH 6/6] fix(aarch64): address WHP review feedback Preserve FPU register errors, use safe DLL lookup, and document the Windows snapshot token accurately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .../hypervisor/virtual_machine/whp/aarch64.rs | 49 +++++++++++-------- .../src/sandbox/snapshot/file/config.rs | 4 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index 0e6ae1e7e..131fbc6b9 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -792,16 +792,18 @@ impl VirtualMachine for WhpVm { for i in 0..32u32 { v[i as usize] = self.get_reg128(qreg(i))?; } - let fpsr = self.get_reg64(WHV_ARM64_REGISTER_FPSR).map_err(|_| { - RegisterError::GetFpu(super::super::HypervisorError::WindowsError( - windows_result::Error::from_hresult(HRESULT(0)), - )) - })? as u32; - let fpcr = self.get_reg64(WHV_ARM64_REGISTER_FPCR).map_err(|_| { - RegisterError::GetFpu(super::super::HypervisorError::WindowsError( - windows_result::Error::from_hresult(HRESULT(0)), - )) - })? as u32; + let fpsr = self + .get_reg64(WHV_ARM64_REGISTER_FPSR) + .map_err(|e| match e { + RegisterError::GetRegs(error) => RegisterError::GetFpu(error), + _ => unreachable!("get_reg64 returned a non-get error"), + })? as u32; + let fpcr = self + .get_reg64(WHV_ARM64_REGISTER_FPCR) + .map_err(|e| match e { + RegisterError::GetRegs(error) => RegisterError::GetFpu(error), + _ => unreachable!("get_reg64 returned a non-get error"), + })? as u32; Ok(CommonFpu { v, fpsr, fpcr }) } @@ -813,16 +815,14 @@ impl VirtualMachine for WhpVm { self.set_reg128(qreg(i), fpu.v[i as usize])?; } self.set_reg64(WHV_ARM64_REGISTER_FPSR, fpu.fpsr as u64) - .map_err(|_| { - RegisterError::SetFpu(super::super::HypervisorError::WindowsError( - windows_result::Error::from_hresult(HRESULT(0)), - )) + .map_err(|e| match e { + RegisterError::SetRegs(error) => RegisterError::SetFpu(error), + _ => unreachable!("set_reg64 returned a non-set error"), })?; self.set_reg64(WHV_ARM64_REGISTER_FPCR, fpu.fpcr as u64) - .map_err(|_| { - RegisterError::SetFpu(super::super::HypervisorError::WindowsError( - windows_result::Error::from_hresult(HRESULT(0)), - )) + .map_err(|e| match e { + RegisterError::SetRegs(error) => RegisterError::SetFpu(error), + _ => unreachable!("set_reg64 returned a non-set error"), })?; Ok(()) } @@ -925,7 +925,13 @@ unsafe fn try_load_whv_map_gpa_range2() -> Result Ok(unsafe { @@ -935,7 +941,10 @@ unsafe fn try_load_whv_map_gpa_range2() -> Result