diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index cb866d511..d773bf75a 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -31,9 +31,10 @@ use gdbstub::target::TargetError; use thiserror::Error; use x86_64_target::HyperlightSandboxTarget; +use super::InterruptHandle; use super::regs::CommonRegisters; -use super::{Hypervisor, InterruptHandle}; use crate::hypervisor::regs::CommonFpu; +use crate::hypervisor::vm::Vm; use crate::mem::layout::SandboxMemoryLayout; use crate::mem::memory_region::MemoryRegion; use crate::mem::mgr::SandboxMemoryManager; @@ -275,8 +276,8 @@ pub(crate) enum DebugResponse { } /// Trait for VMs that support debugging capabilities. -/// This extends the base Hypervisor trait with GDB-specific functionality. -pub(crate) trait DebuggableVm: Hypervisor { +/// This extends the base Vm trait with GDB-specific functionality. +pub(crate) trait DebuggableVm: Vm { /// Translates a guest virtual address to a guest physical address fn translate_gva(&self, gva: u64) -> crate::Result; diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index 7fdd83b8f..cf11e0f20 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -36,22 +36,23 @@ use super::regs::{CommonFpu, CommonRegisters}; #[cfg(target_os = "windows")] use super::{PartitionState, WindowsInterruptHandle}; use crate::HyperlightError::{ExecutionCanceledByHost, NoHypervisorFound}; -#[cfg(not(gdb))] -use crate::hypervisor::Hypervisor; #[cfg(any(kvm, mshv3))] use crate::hypervisor::LinuxInterruptHandle; #[cfg(crashdump)] use crate::hypervisor::crashdump; +use crate::hypervisor::regs::CommonSpecialRegisters; +#[cfg(not(gdb))] +use crate::hypervisor::vm::Vm; +#[cfg(kvm)] +use crate::hypervisor::vm::kvm::KvmVm; #[cfg(mshv3)] -use crate::hypervisor::hyperv_linux::MshvVm; +use crate::hypervisor::vm::mshv::MshvVm; #[cfg(target_os = "windows")] -use crate::hypervisor::hyperv_windows::WhpVm; -#[cfg(kvm)] -use crate::hypervisor::kvm::KvmVm; -use crate::hypervisor::regs::CommonSpecialRegisters; +use crate::hypervisor::vm::whp::WhpVm; +use crate::hypervisor::vm::{HypervisorType, VmExit, get_available_hypervisor}; #[cfg(target_os = "windows")] use crate::hypervisor::wrappers::HandleWrapper; -use crate::hypervisor::{HyperlightExit, InterruptHandle, InterruptHandleImpl, get_max_log_level}; +use crate::hypervisor::{InterruptHandle, InterruptHandleImpl, get_max_log_level}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; @@ -59,7 +60,6 @@ use crate::mem::shared_mem::HostSharedMemory; use crate::metrics::{METRIC_ERRONEOUS_VCPU_KICKS, METRIC_GUEST_CANCELLATION}; use crate::sandbox::SandboxConfiguration; use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::hypervisor::{HypervisorType, get_available_hypervisor}; use crate::sandbox::outb::handle_outb; #[cfg(feature = "mem_profile")] use crate::sandbox::trace::MemTraceInfo; @@ -77,7 +77,7 @@ pub(crate) struct HyperlightVm { #[cfg(gdb)] vm: Box, #[cfg(not(gdb))] - vm: Box, + vm: Box, page_size: usize, entrypoint: u64, orig_rsp: GuestPtr, @@ -117,7 +117,7 @@ impl HyperlightVm { #[cfg(gdb)] type VmType = Box; #[cfg(not(gdb))] - type VmType = Box; + type VmType = Box; #[cfg_attr(not(gdb), allow(unused_mut))] let mut vm: VmType = match get_available_hypervisor() { @@ -368,7 +368,7 @@ impl HyperlightVm { let result = loop { // ===== KILL() TIMING POINT 2: Before set_tid() ===== // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set and we will return an early HyperlightExit::Cancelled() + // - CANCEL_BIT will be set and we will return an early VmExit::Cancelled() // without sending any signals/WHV api calls #[cfg(any(kvm, mshv3))] self.interrupt_handle.set_tid(); @@ -379,7 +379,7 @@ impl HyperlightVm { let exit_reason = if self.interrupt_handle.is_cancelled() || self.interrupt_handle.is_debug_interrupted() { - Ok(HyperlightExit::Cancelled()) + Ok(VmExit::Cancelled()) } else { #[cfg(feature = "trace_guest")] tc.setup_guest_trace(Span::current().context()); @@ -424,7 +424,7 @@ impl HyperlightVm { // - Signals will not be sent match exit_reason { #[cfg(gdb)] - Ok(HyperlightExit::Debug { dr6, exception }) => { + Ok(VmExit::Debug { dr6, exception }) => { // Handle debug event (breakpoints) let stop_reason = arch::vcpu_stop_reason(self.vm.as_mut(), dr6, self.entrypoint, exception)?; @@ -433,13 +433,11 @@ impl HyperlightVm { } } - Ok(HyperlightExit::Halt()) => { + Ok(VmExit::Halt()) => { break Ok(()); } - Ok(HyperlightExit::IoOut(port, data)) => { - self.handle_io(mem_mgr, host_funcs, port, data)? - } - Ok(HyperlightExit::MmioRead(addr)) => { + Ok(VmExit::IoOut(port, data)) => self.handle_io(mem_mgr, host_funcs, port, data)?, + Ok(VmExit::MmioRead(addr)) => { let all_regions = self.sandbox_regions.iter().chain(self.get_mapped_regions()); match get_memory_access_violation( addr as usize, @@ -465,7 +463,7 @@ impl HyperlightVm { } } } - Ok(HyperlightExit::MmioWrite(addr)) => { + Ok(VmExit::MmioWrite(addr)) => { let all_regions = self.sandbox_regions.iter().chain(self.get_mapped_regions()); match get_memory_access_violation( addr as usize, @@ -491,7 +489,7 @@ impl HyperlightVm { } } } - Ok(HyperlightExit::Cancelled()) => { + Ok(VmExit::Cancelled()) => { // If cancellation was not requested for this specific guest function call, // the vcpu was interrupted by a stale cancellation. This can occur when: // - Linux: A signal from a previous call arrives late @@ -499,7 +497,7 @@ impl HyperlightVm { if !cancel_requested && !debug_interrupted { // Track that an erroneous vCPU kick occurred metrics::counter!(METRIC_ERRONEOUS_VCPU_KICKS).increment(1); - // treat this the same as a HyperlightExit::Retry, the cancel was not meant for this call + // treat this the same as a VmExit::Retry, the cancel was not meant for this call continue; } @@ -517,10 +515,10 @@ impl HyperlightVm { metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); break Err(ExecutionCanceledByHost()); } - Ok(HyperlightExit::Unknown(reason)) => { + Ok(VmExit::Unknown(reason)) => { break Err(new_error!("Unexpected VM Exit: {:?}", reason)); } - Ok(HyperlightExit::Retry()) => continue, + Ok(VmExit::Retry()) => continue, Err(e) => { break Err(e); } diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index 5cdd74b48..767c9c4f5 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -16,16 +16,6 @@ limitations under the License. use log::LevelFilter; -use crate::Result; -use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; -use crate::mem::memory_region::MemoryRegion; - -/// HyperV-on-linux functionality -#[cfg(mshv3)] -pub(crate) mod hyperv_linux; -#[cfg(target_os = "windows")] -pub(crate) mod hyperv_windows; - /// GDB debugging support #[cfg(gdb)] pub(crate) mod gdb; @@ -33,9 +23,7 @@ pub(crate) mod gdb; /// Abstracts over different hypervisor register representations pub(crate) mod regs; -#[cfg(kvm)] -/// Functionality to manipulate KVM-based virtual machines -pub(crate) mod kvm; +pub(crate) mod vm; #[cfg(target_os = "windows")] /// Hyperlight Surrogate Process @@ -61,83 +49,6 @@ use std::sync::atomic::{AtomicU8, Ordering}; #[cfg(any(kvm, mshv3))] use std::time::Duration; -pub(crate) enum HyperlightExit { - /// The vCPU has exited due to a debug event (usually breakpoint) - #[cfg(gdb)] - Debug { dr6: u64, exception: u32 }, - /// The vCPU has halted - Halt(), - /// The vCPU has issued a write to the given port with the given value - IoOut(u16, Vec), - /// The vCPU tried to read from the given (unmapped) addr - MmioRead(u64), - /// The vCPU tried to write to the given (unmapped) addr - MmioWrite(u64), - /// The vCPU execution has been cancelled - Cancelled(), - /// The vCPU has exited for a reason that is not handled by Hyperlight - Unknown(String), - /// The operation should be retried, for example this can happen on Linux where a call to run the CPU can return EAGAIN - #[cfg_attr( - target_os = "windows", - expect( - dead_code, - reason = "Retry() is never constructed on Windows, but it is still matched on (which dead_code lint ignores)" - ) - )] - Retry(), -} - -/// Trait for single-vCPU VMs. Provides a common interface for basic VM operations. -/// Abstracts over differences between KVM, MSHV and WHP implementations. -pub(crate) trait Hypervisor: Debug + Send { - /// Map memory region into this VM - /// - /// # Safety - /// The caller must ensure that the memory region is valid and points to valid memory, - /// and lives long enough for the VM to use it. - /// The caller must ensure that the given u32 is not already mapped, otherwise previously mapped - /// memory regions may be overwritten. - /// The memory region must not overlap with an existing region, and depending on platform, must be aligned to page boundaries. - unsafe fn map_memory(&mut self, region: (u32, &MemoryRegion)) -> Result<()>; - - /// Unmap memory region from this VM that has previously been mapped using `map_memory`. - fn unmap_memory(&mut self, region: (u32, &MemoryRegion)) -> Result<()>; - - /// Runs the vCPU until it exits. - /// Note: this function should not emit any traces or spans as it is called after guest span is setup - fn run_vcpu(&mut self) -> Result; - - /// Get regs - #[allow(dead_code)] - fn regs(&self) -> Result; - /// Set regs - fn set_regs(&self, regs: &CommonRegisters) -> Result<()>; - /// Get fpu regs - #[allow(dead_code)] - fn fpu(&self) -> Result; - /// Set fpu regs - fn set_fpu(&self, fpu: &CommonFpu) -> Result<()>; - /// Get special regs - #[allow(dead_code)] - fn sregs(&self) -> Result; - /// Set special regs - fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()>; - - /// xsave - #[cfg(crashdump)] - fn xsave(&self) -> Result>; - - /// Get partition handle - #[cfg(target_os = "windows")] - fn partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE; - - /// Mark that initial memory setup is complete. After this, map_memory will fail. - /// This is only needed on Windows where dynamic memory mapping is not yet supported. - #[cfg(target_os = "windows")] - fn complete_initial_memory_setup(&mut self); -} - /// Get the logging level to pass to the guest entrypoint fn get_max_log_level() -> u32 { // Check to see if the RUST_LOG environment variable is set diff --git a/src/hyperlight_host/src/hypervisor/kvm.rs b/src/hyperlight_host/src/hypervisor/vm/kvm.rs similarity index 85% rename from src/hyperlight_host/src/hypervisor/kvm.rs rename to src/hyperlight_host/src/hypervisor/vm/kvm.rs index 037b60fc1..c74bfd977 100644 --- a/src/hyperlight_host/src/hypervisor/kvm.rs +++ b/src/hyperlight_host/src/hypervisor/vm/kvm.rs @@ -25,7 +25,8 @@ use tracing::{Span, instrument}; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; -use crate::hypervisor::{HyperlightExit, Hypervisor}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::hypervisor::vm::{Vm, VmExit}; use crate::mem::memory_region::MemoryRegion; use crate::{Result, new_error}; @@ -84,7 +85,7 @@ impl KvmVm { } } -impl Hypervisor for KvmVm { +impl Vm for KvmVm { unsafe fn map_memory(&mut self, (slot, region): (u32, &MemoryRegion)) -> Result<()> { let mut kvm_region: kvm_userspace_memory_region = region.into(); kvm_region.slot = slot; @@ -103,61 +104,58 @@ impl Hypervisor for KvmVm { Ok(()) } - fn run_vcpu(&mut self) -> Result { + fn run_vcpu(&mut self) -> Result { match self.vcpu_fd.run() { - Ok(VcpuExit::Hlt) => Ok(HyperlightExit::Halt()), - Ok(VcpuExit::IoOut(port, data)) => Ok(HyperlightExit::IoOut(port, data.to_vec())), - Ok(VcpuExit::MmioRead(addr, _)) => Ok(HyperlightExit::MmioRead(addr)), - Ok(VcpuExit::MmioWrite(addr, _)) => Ok(HyperlightExit::MmioWrite(addr)), + Ok(VcpuExit::Hlt) => Ok(VmExit::Halt()), + Ok(VcpuExit::IoOut(port, data)) => Ok(VmExit::IoOut(port, data.to_vec())), + Ok(VcpuExit::MmioRead(addr, _)) => Ok(VmExit::MmioRead(addr)), + Ok(VcpuExit::MmioWrite(addr, _)) => Ok(VmExit::MmioWrite(addr)), #[cfg(gdb)] - Ok(VcpuExit::Debug(debug_exit)) => Ok(HyperlightExit::Debug { + Ok(VcpuExit::Debug(debug_exit)) => Ok(VmExit::Debug { dr6: debug_exit.dr6, exception: debug_exit.exception, }), Err(e) => match e.errno() { // InterruptHandle::kill() sends a signal (SIGRTMIN+offset) to interrupt the vcpu, which causes EINTR - libc::EINTR => Ok(HyperlightExit::Cancelled()), - libc::EAGAIN => Ok(HyperlightExit::Retry()), - _ => Ok(HyperlightExit::Unknown(format!( - "Unknown KVM VCPU error: {}", - e - ))), + libc::EINTR => Ok(VmExit::Cancelled()), + libc::EAGAIN => Ok(VmExit::Retry()), + _ => Ok(VmExit::Unknown(format!("Unknown KVM VCPU error: {}", e))), }, - Ok(other) => Ok(HyperlightExit::Unknown(format!( + Ok(other) => Ok(VmExit::Unknown(format!( "Unknown KVM VCPU exit: {:?}", other ))), } } - fn regs(&self) -> Result { + fn regs(&self) -> Result { let kvm_regs = self.vcpu_fd.get_regs()?; Ok((&kvm_regs).into()) } - fn set_regs(&self, regs: &super::regs::CommonRegisters) -> Result<()> { + fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { let kvm_regs: kvm_regs = regs.into(); self.vcpu_fd.set_regs(&kvm_regs)?; Ok(()) } - fn fpu(&self) -> Result { + fn fpu(&self) -> Result { let kvm_fpu = self.vcpu_fd.get_fpu()?; Ok((&kvm_fpu).into()) } - fn set_fpu(&self, fpu: &super::regs::CommonFpu) -> Result<()> { + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { let kvm_fpu: kvm_fpu = fpu.into(); self.vcpu_fd.set_fpu(&kvm_fpu)?; Ok(()) } - fn sregs(&self) -> Result { + fn sregs(&self) -> Result { let kvm_sregs = self.vcpu_fd.get_sregs()?; Ok((&kvm_sregs).into()) } - fn set_sregs(&self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { let kvm_sregs: kvm_sregs = sregs.into(); self.vcpu_fd.set_sregs(&kvm_sregs)?; Ok(()) diff --git a/src/hyperlight_host/src/hypervisor/vm/mod.rs b/src/hyperlight_host/src/hypervisor/vm/mod.rs new file mode 100644 index 000000000..34fbc12f0 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/vm/mod.rs @@ -0,0 +1,442 @@ +/* +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. +*/ + +use std::fmt::Debug; +use std::sync::OnceLock; + +use tracing::{Span, instrument}; + +use crate::Result; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::mem::memory_region::MemoryRegion; + +/// KVM (Kernel-based Virtual Machine) functionality (linux) +#[cfg(kvm)] +pub(crate) mod kvm; +/// MSHV (Microsoft Hypervisor) functionality (linux) +#[cfg(mshv3)] +pub(crate) mod mshv; +/// WHP (Windows Hypervisor Platform) functionality (windows) +#[cfg(target_os = "windows")] +pub(crate) mod whp; + +static AVAILABLE_HYPERVISOR: OnceLock> = OnceLock::new(); + +/// Returns which type of hypervisor is available, if any +pub fn get_available_hypervisor() -> &'static Option { + AVAILABLE_HYPERVISOR.get_or_init(|| { + cfg_if::cfg_if! { + if #[cfg(all(kvm, mshv3))] { + // If both features are enabled, we need to determine hypervisor at runtime. + // Currently /dev/kvm and /dev/mshv cannot exist on the same machine, so the first one + // that works is guaranteed to be correct. + if mshv::is_hypervisor_present() { + Some(HypervisorType::Mshv) + } else if kvm::is_hypervisor_present() { + Some(HypervisorType::Kvm) + } else { + None + } + } else if #[cfg(kvm)] { + if kvm::is_hypervisor_present() { + Some(HypervisorType::Kvm) + } else { + None + } + } else if #[cfg(mshv3)] { + if mshv::is_hypervisor_present() { + Some(HypervisorType::Mshv) + } else { + None + } + } else if #[cfg(target_os = "windows")] { + if whp::is_hypervisor_present() { + Some(HypervisorType::Whp) + } else { + None + } + } else { + None + } + } + }) +} + +/// Returns `true` if a suitable hypervisor is available. +/// If this returns `false`, no hypervisor-backed sandboxes can be created. +#[instrument(skip_all, parent = Span::current())] +pub fn is_hypervisor_present() -> bool { + get_available_hypervisor().is_some() +} + +/// The hypervisor types available for the current platform +#[derive(PartialEq, Eq, Debug)] +pub(crate) enum HypervisorType { + #[cfg(kvm)] + Kvm, + + #[cfg(mshv3)] + Mshv, + + #[cfg(target_os = "windows")] + Whp, +} + +// Compiler error if no hypervisor type is available +#[cfg(not(any(kvm, mshv3, target_os = "windows")))] +compile_error!( + "No hypervisor type is available for the current platform. Please enable either the `kvm` or `mshv3` cargo feature." +); + +/// The various reasons a VM's vCPU can exit +pub(crate) enum VmExit { + /// The vCPU has exited due to a debug event (usually breakpoint) + #[cfg(gdb)] + Debug { dr6: u64, exception: u32 }, + /// The vCPU has halted + Halt(), + /// The vCPU has issued a write to the given port with the given value + IoOut(u16, Vec), + /// The vCPU tried to read from the given (unmapped) addr + MmioRead(u64), + /// The vCPU tried to write to the given (unmapped) addr + MmioWrite(u64), + /// The vCPU execution has been cancelled + Cancelled(), + /// The vCPU has exited for a reason that is not handled by Hyperlight + Unknown(String), + /// The operation should be retried, for example this can happen on Linux where a call to run the CPU can return EAGAIN + #[cfg_attr( + target_os = "windows", + expect( + dead_code, + reason = "Retry() is never constructed on Windows, but it is still matched on (which dead_code lint ignores)" + ) + )] + Retry(), +} + +/// Trait for single-vCPU VMs. Provides a common interface for basic VM operations. +/// Abstracts over differences between KVM, MSHV and WHP implementations. +pub(crate) trait Vm: Debug + Send { + /// Map memory region into this VM + /// + /// # Safety + /// The caller must ensure that the memory region is valid and points to valid memory, + /// and lives long enough for the VM to use it. + /// The caller must ensure that the given u32 is not already mapped, otherwise previously mapped + /// memory regions may be overwritten. + /// The memory region must not overlap with an existing region, and depending on platform, must be aligned to page boundaries. + unsafe fn map_memory(&mut self, region: (u32, &MemoryRegion)) -> Result<()>; + + /// Unmap memory region from this VM that has previously been mapped using `map_memory`. + fn unmap_memory(&mut self, region: (u32, &MemoryRegion)) -> Result<()>; + + /// Runs the vCPU until it exits. + /// Note: this function should not emit any traces or spans as it is called after guest span is setup + fn run_vcpu(&mut self) -> Result; + + /// Get regs + #[allow(dead_code)] + fn regs(&self) -> Result; + /// Set regs + fn set_regs(&self, regs: &CommonRegisters) -> Result<()>; + /// Get fpu regs + #[allow(dead_code)] + fn fpu(&self) -> Result; + /// Set fpu regs + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()>; + /// Get special regs + #[allow(dead_code)] + fn sregs(&self) -> Result; + /// Set special regs + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()>; + + /// xsave + #[cfg(crashdump)] + fn xsave(&self) -> Result>; + + /// Get partition handle + #[cfg(target_os = "windows")] + fn partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE; + + /// Mark that initial memory setup is complete. After this, map_memory will fail. + /// This is only needed on Windows where dynamic memory mapping is not yet supported. + #[cfg(target_os = "windows")] + fn complete_initial_memory_setup(&mut self); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hypervisor::regs::{CommonSegmentRegister, CommonTableRegister}; + + fn boxed_vm() -> Box { + let available_vm = get_available_hypervisor().as_ref().unwrap(); + match available_vm { + #[cfg(kvm)] + HypervisorType::Kvm => { + use crate::hypervisor::vm::kvm::KvmVm; + Box::new(KvmVm::new().unwrap()) + } + #[cfg(mshv3)] + HypervisorType::Mshv => { + use crate::hypervisor::vm::mshv::MshvVm; + Box::new(MshvVm::new().unwrap()) + } + #[cfg(target_os = "windows")] + HypervisorType::Whp => { + use hyperlight_common::mem::PAGE_SIZE_USIZE; + + use crate::hypervisor::vm::whp::WhpVm; + use crate::hypervisor::wrappers::HandleWrapper; + use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory}; + + let mem_size = PAGE_SIZE_USIZE; + let shared_mem = ExclusiveSharedMemory::new(mem_size).unwrap(); + let handle = HandleWrapper::from(shared_mem.get_mmap_file_handle()); + Box::new(WhpVm::new(handle, shared_mem.raw_mem_size()).unwrap()) + } + } + } + + #[test] + // TODO: add support for testing on WHP + #[cfg(target_os = "linux")] + fn is_hypervisor_present() { + use std::path::Path; + + cfg_if::cfg_if! { + if #[cfg(all(kvm, mshv3))] { + assert_eq!(Path::new("/dev/kvm").exists() || Path::new("/dev/mshv").exists(), super::is_hypervisor_present()); + } else if #[cfg(kvm)] { + assert_eq!(Path::new("/dev/kvm").exists(), super::is_hypervisor_present()); + } else if #[cfg(mshv3)] { + assert_eq!(Path::new("/dev/mshv").exists(), super::is_hypervisor_present()); + } else { + assert!(!super::is_hypervisor_present()); + } + } + } + + #[test] + fn regs() { + let vm = boxed_vm(); + + let regs = CommonRegisters { + rax: 1, + rbx: 2, + rcx: 3, + rdx: 4, + rsi: 5, + rdi: 6, + rsp: 7, + rbp: 8, + r8: 9, + r9: 10, + r10: 11, + r11: 12, + r12: 13, + r13: 14, + r14: 15, + r15: 16, + rip: 17, + rflags: 0x2, + }; + + vm.set_regs(®s).unwrap(); + let read_regs = vm.regs().unwrap(); + assert_eq!(regs, read_regs); + } + + #[test] + fn fpu() { + let vm = boxed_vm(); + + // x87 FPU registers are 80-bit (10 bytes), stored in 16-byte slots for alignment. + // Only the first 10 bytes are preserved; the remaining 6 bytes are reserved/zeroed. + // See IntelĀ® 64 and IA-32 Architectures SDM, Vol. 1, Sec. 10.5.1.1 (x87 State) + let fpr_entry: [u8; 16] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]; + let fpu = CommonFpu { + fpr: [fpr_entry; 8], + fcw: 2, + fsw: 3, + ftwx: 4, + last_opcode: 5, + last_ip: 6, + last_dp: 7, + xmm: [[8; 16]; 16], + mxcsr: 9, + pad1: 0, + pad2: 0, + }; + vm.set_fpu(&fpu).unwrap(); + #[cfg_attr(not(kvm), allow(unused_mut))] + let mut read_fpu = vm.fpu().unwrap(); + #[cfg(kvm)] + { + read_fpu.mxcsr = fpu.mxcsr; // KVM get/set fpu does not preserve mxcsr + } + assert_eq!(fpu, read_fpu); + } + + #[test] + fn sregs() { + let vm = boxed_vm(); + + let segment = CommonSegmentRegister { + base: 1, + limit: 2, + selector: 3, + type_: 3, + present: 1, + dpl: 1, + db: 0, + s: 1, + l: 1, + g: 1, + avl: 1, + unusable: 0, + padding: 0, + }; + + let cs_segment = CommonSegmentRegister { + base: 1, + limit: 0xFFFF, + selector: 0x08, + type_: 0b1011, // code segment, execute/read, accessed + present: 1, + dpl: 1, + db: 0, // must be 0 in 64-bit mode + s: 1, + l: 1, // 64-bit mode + g: 1, + avl: 1, + unusable: 0, + padding: 0, + }; + let table = CommonTableRegister { + base: 12, + limit: 13, + }; + let sregs = CommonSpecialRegisters { + cs: cs_segment, + ds: segment, + es: segment, + fs: segment, + gs: segment, + ss: segment, + tr: segment, + ldt: segment, + gdt: table, + idt: table, + cr0: 0x80000011, // bit 0 (PE) + bit 4 (ET) + bit 31 (PG) + cr2: 2, + cr3: 3, + cr4: 0x20, + cr8: 5, + efer: 0x500, + apic_base: 0xFEE00900, + interrupt_bitmap: [0; 4], + }; + vm.set_sregs(&sregs).unwrap(); + let read_sregs = vm.sregs().unwrap(); + assert_eq!(sregs, read_sregs); + } + + /// Helper to create a page-aligned memory region for testing + #[cfg(any(kvm, mshv3))] + fn create_test_memory(size: usize) -> crate::mem::shared_mem::ExclusiveSharedMemory { + use hyperlight_common::mem::PAGE_SIZE_USIZE; + let aligned_size = size.div_ceil(PAGE_SIZE_USIZE) * PAGE_SIZE_USIZE; + crate::mem::shared_mem::ExclusiveSharedMemory::new(aligned_size).unwrap() + } + + /// Helper to create a MemoryRegion from ExclusiveSharedMemory + #[cfg(any(kvm, mshv3))] + fn region_for_test_memory( + mem: &crate::mem::shared_mem::ExclusiveSharedMemory, + guest_base: usize, + flags: crate::mem::memory_region::MemoryRegionFlags, + ) -> MemoryRegion { + use crate::mem::memory_region::MemoryRegionType; + use crate::mem::shared_mem::SharedMemory; + let ptr = mem.base_addr(); + let len = mem.mem_size(); + MemoryRegion { + host_region: ptr..(ptr + len), + guest_region: guest_base..(guest_base + len), + flags, + region_type: MemoryRegionType::Heap, + } + } + + #[test] + #[cfg(any(kvm, mshv3))] // Requires memory mapping support (TODO on WHP) + fn map_memory() { + use crate::mem::memory_region::MemoryRegionFlags; + + let mut vm = boxed_vm(); + + let mem1 = create_test_memory(4096); + let guest_addr: usize = 0x1000; + let region = region_for_test_memory( + &mem1, + guest_addr, + MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, + ); + + // SAFETY: The memory region points to valid memory allocated by ExclusiveSharedMemory, + // and will live until we drop mem1 at the end of the test. + // Slot 0 is not already mapped. + unsafe { + vm.map_memory((0, ®ion)).unwrap(); + } + + // Unmap the region + vm.unmap_memory((0, ®ion)).unwrap(); + + // Unmapping a region that was already unmapped should fail + vm.unmap_memory((0, ®ion)).unwrap_err(); + + // Unmapping a region that was never mapped should fail + vm.unmap_memory((99, ®ion)).unwrap_err(); + + // Re-map the same region to a different slot + // SAFETY: Same as above - memory is still valid and slot 1 is not mapped. + unsafe { + vm.map_memory((1, ®ion)).unwrap(); + } + + // Map a second region to a different slot + let mem2 = create_test_memory(4096); + let guest_addr2: usize = 0x2000; + let region2 = region_for_test_memory( + &mem2, + guest_addr2, + MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, + ); + + // SAFETY: Memory is valid from ExclusiveSharedMemory, slot 2 is not mapped. + unsafe { + vm.map_memory((2, ®ion2)).unwrap(); + } + + // Clean up: unmap both regions + vm.unmap_memory((1, ®ion)).unwrap(); + vm.unmap_memory((2, ®ion2)).unwrap(); + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs b/src/hyperlight_host/src/hypervisor/vm/mshv.rs similarity index 87% rename from src/hyperlight_host/src/hypervisor/hyperv_linux.rs rename to src/hyperlight_host/src/hypervisor/vm/mshv.rs index 8c6feb351..10e254369 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs +++ b/src/hyperlight_host/src/hypervisor/vm/mshv.rs @@ -33,7 +33,8 @@ use tracing::{Span, instrument}; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; -use crate::hypervisor::{HyperlightExit, Hypervisor}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::hypervisor::vm::{Vm, VmExit}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::{Result, new_error}; @@ -89,7 +90,7 @@ impl MshvVm { } } -impl Hypervisor for MshvVm { +impl Vm for MshvVm { unsafe fn map_memory(&mut self, (_slot, region): (u32, &MemoryRegion)) -> Result<()> { let mshv_region: mshv_user_mem_region = region.into(); self.vm_fd.map_user_memory(mshv_region)?; @@ -102,7 +103,7 @@ impl Hypervisor for MshvVm { Ok(()) } - fn run_vcpu(&mut self) -> Result { + fn run_vcpu(&mut self) -> Result { const HALT_MESSAGE: hv_message_type = hv_message_type_HVMSG_X64_HALT; const IO_PORT_INTERCEPT_MESSAGE: hv_message_type = hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT; @@ -115,7 +116,7 @@ impl Hypervisor for MshvVm { let result = match exit_reason { Ok(m) => match m.header.message_type { - HALT_MESSAGE => HyperlightExit::Halt(), + HALT_MESSAGE => VmExit::Halt(), IO_PORT_INTERCEPT_MESSAGE => { let io_message = m.to_ioport_info().map_err(mshv_ioctls::MshvError::from)?; let port_number = io_message.port_number; @@ -131,15 +132,15 @@ impl Hypervisor for MshvVm { }, ..Default::default() }])?; - HyperlightExit::IoOut(port_number, rax.to_le_bytes().to_vec()) + VmExit::IoOut(port_number, rax.to_le_bytes().to_vec()) } UNMAPPED_GPA_MESSAGE => { let mimo_message = m.to_memory_info().map_err(mshv_ioctls::MshvError::from)?; let addr = mimo_message.guest_physical_address; match MemoryRegionFlags::try_from(mimo_message)? { - MemoryRegionFlags::READ => HyperlightExit::MmioRead(addr), - MemoryRegionFlags::WRITE => HyperlightExit::MmioWrite(addr), - _ => HyperlightExit::Unknown("Unknown MMIO access".to_string()), + MemoryRegionFlags::READ => VmExit::MmioRead(addr), + MemoryRegionFlags::WRITE => VmExit::MmioWrite(addr), + _ => VmExit::Unknown("Unknown MMIO access".to_string()), } } INVALID_GPA_ACCESS_MESSAGE => { @@ -147,9 +148,9 @@ impl Hypervisor for MshvVm { let gpa = mimo_message.guest_physical_address; let access_info = MemoryRegionFlags::try_from(mimo_message)?; match access_info { - MemoryRegionFlags::READ => HyperlightExit::MmioRead(gpa), - MemoryRegionFlags::WRITE => HyperlightExit::MmioWrite(gpa), - _ => HyperlightExit::Unknown("Unknown MMIO access".to_string()), + MemoryRegionFlags::READ => VmExit::MmioRead(gpa), + MemoryRegionFlags::WRITE => VmExit::MmioWrite(gpa), + _ => VmExit::Unknown("Unknown MMIO access".to_string()), } } #[cfg(gdb)] @@ -158,51 +159,51 @@ impl Hypervisor for MshvVm { .to_exception_info() .map_err(mshv_ioctls::MshvError::from)?; let DebugRegisters { dr6, .. } = self.vcpu_fd.get_debug_regs()?; - HyperlightExit::Debug { + VmExit::Debug { dr6, exception: ex_info.exception_vector as u32, } } - other => HyperlightExit::Unknown(format!("Unknown MSHV VCPU exit: {:?}", other)), + other => VmExit::Unknown(format!("Unknown MSHV VCPU exit: {:?}", other)), }, Err(e) => match e.errno() { // InterruptHandle::kill() sends a signal (SIGRTMIN+offset) to interrupt the vcpu, which causes EINTR - libc::EINTR => HyperlightExit::Cancelled(), - libc::EAGAIN => HyperlightExit::Retry(), - _ => HyperlightExit::Unknown(format!("Unknown MSHV VCPU error: {}", e)), + libc::EINTR => VmExit::Cancelled(), + libc::EAGAIN => VmExit::Retry(), + _ => VmExit::Unknown(format!("Unknown MSHV VCPU error: {}", e)), }, }; Ok(result) } - fn regs(&self) -> Result { + fn regs(&self) -> Result { let mshv_regs = self.vcpu_fd.get_regs()?; Ok((&mshv_regs).into()) } - fn set_regs(&self, regs: &super::regs::CommonRegisters) -> Result<()> { + fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { let mshv_regs: StandardRegisters = regs.into(); self.vcpu_fd.set_regs(&mshv_regs)?; Ok(()) } - fn fpu(&self) -> Result { + fn fpu(&self) -> Result { let mshv_fpu = self.vcpu_fd.get_fpu()?; Ok((&mshv_fpu).into()) } - fn set_fpu(&self, fpu: &super::regs::CommonFpu) -> Result<()> { + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { let mshv_fpu: FloatingPointUnit = fpu.into(); self.vcpu_fd.set_fpu(&mshv_fpu)?; Ok(()) } - fn sregs(&self) -> Result { + fn sregs(&self) -> Result { let mshv_sregs = self.vcpu_fd.get_sregs()?; Ok((&mshv_sregs).into()) } - fn set_sregs(&self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { let mshv_sregs: SpecialRegisters = sregs.into(); self.vcpu_fd.set_sregs(&mshv_sregs)?; Ok(()) diff --git a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs b/src/hyperlight_host/src/hypervisor/vm/whp.rs similarity index 95% rename from src/hyperlight_host/src/hypervisor/hyperv_windows.rs rename to src/hyperlight_host/src/hypervisor/vm/whp.rs index cc6876a5b..6b012df61 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs +++ b/src/hyperlight_host/src/hypervisor/vm/whp.rs @@ -23,17 +23,16 @@ use windows::Win32::System::LibraryLoader::*; use windows::core::s; use windows_result::HRESULT; -use super::regs::{ - Align16, WHP_FPU_NAMES, WHP_FPU_NAMES_LEN, WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, - WHP_SREGS_NAMES_LEN, -}; -use super::surrogate_process::SurrogateProcess; -use super::surrogate_process_manager::get_surrogate_process_manager; -use super::wrappers::HandleWrapper; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; -use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; -use crate::hypervisor::{HyperlightExit, Hypervisor}; +use crate::hypervisor::regs::{ + Align16, CommonFpu, CommonRegisters, CommonSpecialRegisters, WHP_FPU_NAMES, WHP_FPU_NAMES_LEN, + WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, WHP_SREGS_NAMES_LEN, +}; +use crate::hypervisor::surrogate_process::SurrogateProcess; +use crate::hypervisor::surrogate_process_manager::get_surrogate_process_manager; +use crate::hypervisor::vm::{Vm, VmExit}; +use crate::hypervisor::wrappers::HandleWrapper; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::{Result, log_then_return, new_error}; @@ -130,7 +129,7 @@ impl WhpVm { } } -impl Hypervisor for WhpVm { +impl Vm for WhpVm { unsafe fn map_memory(&mut self, (_slot, region): (u32, &MemoryRegion)) -> Result<()> { // Only allow memory mapping during initial setup (the first batch of regions). // After the initial setup is complete, subsequent calls should fail, @@ -206,7 +205,7 @@ impl Hypervisor for WhpVm { } #[expect(non_upper_case_globals, reason = "Windows API constant are lower case")] - fn run_vcpu(&mut self) -> Result { + fn run_vcpu(&mut self) -> Result { let mut exit_context: WHV_RUN_VP_EXIT_CONTEXT = Default::default(); unsafe { @@ -226,7 +225,7 @@ impl Hypervisor for WhpVm { WHvX64RegisterRip, Align16(WHV_REGISTER_VALUE { Reg64: rip }), )])?; - HyperlightExit::IoOut( + VmExit::IoOut( exit_context.Anonymous.IoPortAccess.PortNumber, exit_context .Anonymous @@ -236,7 +235,7 @@ impl Hypervisor for WhpVm { .to_vec(), ) }, - WHvRunVpExitReasonX64Halt => HyperlightExit::Halt(), + WHvRunVpExitReasonX64Halt => VmExit::Halt(), WHvRunVpExitReasonMemoryAccess => { let gpa = unsafe { exit_context.Anonymous.MemoryAccess.Gpa }; let access_info = unsafe { @@ -247,13 +246,13 @@ impl Hypervisor for WhpVm { }; let access_info = MemoryRegionFlags::try_from(access_info)?; match access_info { - MemoryRegionFlags::READ => HyperlightExit::MmioRead(gpa), - MemoryRegionFlags::WRITE => HyperlightExit::MmioWrite(gpa), - _ => HyperlightExit::Unknown("Unknown memory access type".to_string()), + MemoryRegionFlags::READ => VmExit::MmioRead(gpa), + MemoryRegionFlags::WRITE => VmExit::MmioWrite(gpa), + _ => VmExit::Unknown("Unknown memory access type".to_string()), } } // Execution was cancelled by the host. - WHvRunVpExitReasonCanceled => HyperlightExit::Cancelled(), + WHvRunVpExitReasonCanceled => VmExit::Cancelled(), #[cfg(gdb)] WHvRunVpExitReasonException => { let exception = unsafe { exit_context.Anonymous.VpException }; @@ -274,12 +273,12 @@ impl Hypervisor for WhpVm { unsafe { out[0].0.Reg64 } }; - HyperlightExit::Debug { + VmExit::Debug { dr6, exception: exception.ExceptionType as u32, } } - WHV_RUN_VP_EXIT_REASON(_) => HyperlightExit::Unknown(format!( + WHV_RUN_VP_EXIT_REASON(_) => VmExit::Unknown(format!( "Unknown exit reason '{}'", exit_context.ExitReason.0 )), diff --git a/src/hyperlight_host/src/lib.rs b/src/hyperlight_host/src/lib.rs index 5f034dc79..06771cecc 100644 --- a/src/hyperlight_host/src/lib.rs +++ b/src/hyperlight_host/src/lib.rs @@ -85,13 +85,13 @@ pub(crate) mod testing; /// The re-export for the `HyperlightError` type pub use error::HyperlightError; +/// The re-export for the `is_hypervisor_present` type +pub use hypervisor::vm::is_hypervisor_present; /// A sandbox that can call be used to make multiple calls to guest functions, /// and otherwise reused multiple times pub use sandbox::MultiUseSandbox; /// The re-export for the `UninitializedSandbox` type pub use sandbox::UninitializedSandbox; -/// The re-export for the `is_hypervisor_present` type -pub use sandbox::is_hypervisor_present; /// The re-export for the `GuestBinary` type pub use sandbox::uninitialized::GuestBinary; diff --git a/src/hyperlight_host/src/sandbox/hypervisor.rs b/src/hyperlight_host/src/sandbox/hypervisor.rs deleted file mode 100644 index c72b7efbf..000000000 --- a/src/hyperlight_host/src/sandbox/hypervisor.rs +++ /dev/null @@ -1,85 +0,0 @@ -/* -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. -*/ - -use std::fmt::Debug; -use std::sync::OnceLock; - -#[cfg(mshv3)] -use crate::hypervisor::hyperv_linux; -#[cfg(kvm)] -use crate::hypervisor::kvm; - -static AVAILABLE_HYPERVISOR: OnceLock> = OnceLock::new(); - -pub fn get_available_hypervisor() -> &'static Option { - AVAILABLE_HYPERVISOR.get_or_init(|| { - cfg_if::cfg_if! { - if #[cfg(all(kvm, mshv3))] { - // If both features are enabled, we need to determine hypervisor at runtime. - // Currently /dev/kvm and /dev/mshv cannot exist on the same machine, so the first one - // that works is guaranteed to be correct. - if hyperv_linux::is_hypervisor_present() { - Some(HypervisorType::Mshv) - } else if kvm::is_hypervisor_present() { - Some(HypervisorType::Kvm) - } else { - None - } - } else if #[cfg(kvm)] { - if kvm::is_hypervisor_present() { - Some(HypervisorType::Kvm) - } else { - None - } - } else if #[cfg(mshv3)] { - if hyperv_linux::is_hypervisor_present() { - Some(HypervisorType::Mshv) - } else { - None - } - } else if #[cfg(target_os = "windows")] { - use crate::hypervisor::hyperv_windows; - - if hyperv_windows::is_hypervisor_present() { - Some(HypervisorType::Whp) - } else { - None - } - } else { - None - } - } - }) -} - -/// The hypervisor types available for the current platform -#[derive(PartialEq, Eq, Debug)] -pub(crate) enum HypervisorType { - #[cfg(kvm)] - Kvm, - - #[cfg(mshv3)] - Mshv, - - #[cfg(target_os = "windows")] - Whp, -} - -// Compiler error if no hypervisor type is available -#[cfg(not(any(kvm, mshv3, target_os = "windows")))] -compile_error!( - "No hypervisor type is available for the current platform. Please enable either the `kvm` or `mshv3` cargo feature." -); diff --git a/src/hyperlight_host/src/sandbox/mod.rs b/src/hyperlight_host/src/sandbox/mod.rs index e637dd578..ccb15e3fb 100644 --- a/src/hyperlight_host/src/sandbox/mod.rs +++ b/src/hyperlight_host/src/sandbox/mod.rs @@ -18,8 +18,6 @@ limitations under the License. pub mod config; /// Functionality for reading, but not modifying host functions pub(crate) mod host_funcs; -/// Functionality for dealing with `Sandbox`es that contain Hypervisors -pub(crate) mod hypervisor; /// Functionality for dealing with initialized sandboxes that can /// call 0 or more guest functions pub mod initialized_multi_use; @@ -47,21 +45,11 @@ pub use callable::Callable; pub use config::SandboxConfiguration; /// Re-export for the `MultiUseSandbox` type pub use initialized_multi_use::MultiUseSandbox; -use tracing::{Span, instrument}; /// Re-export for `GuestBinary` type pub use uninitialized::GuestBinary; /// Re-export for `UninitializedSandbox` type pub use uninitialized::UninitializedSandbox; -/// Determine whether a suitable hypervisor is available to run -/// this sandbox. -/// -/// Returns a boolean indicating whether a suitable hypervisor is present. -#[instrument(skip_all, parent = Span::current())] -pub fn is_hypervisor_present() -> bool { - hypervisor::get_available_hypervisor().is_some() -} - #[cfg(test)] mod tests { use std::sync::Arc; @@ -73,25 +61,6 @@ mod tests { use crate::sandbox::uninitialized::GuestBinary; use crate::{MultiUseSandbox, UninitializedSandbox, new_error}; - #[test] - // TODO: add support for testing on WHP - #[cfg(target_os = "linux")] - fn is_hypervisor_present() { - use std::path::Path; - - cfg_if::cfg_if! { - if #[cfg(all(kvm, mshv3))] { - assert_eq!(Path::new("/dev/kvm").exists() || Path::new("/dev/mshv").exists(), super::is_hypervisor_present()); - } else if #[cfg(kvm)] { - assert_eq!(Path::new("/dev/kvm").exists(), super::is_hypervisor_present()); - } else if #[cfg(mshv3)] { - assert_eq!(Path::new("/dev/mshv").exists(), super::is_hypervisor_present()); - } else { - assert!(!super::is_hypervisor_present()); - } - } - } - #[test] fn check_create_and_use_sandbox_on_different_threads() { let unintializedsandbox_queue = Arc::new(ArrayQueue::::new(10));