diff --git a/src/hyperlight_common/src/arch/aarch64/layout.rs b/src/hyperlight_common/src/arch/aarch64/layout.rs new file mode 100644 index 000000000..20f17026c --- /dev/null +++ b/src/hyperlight_common/src/arch/aarch64/layout.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. + */ + +// TODO(aarch64): change these, they are only provided in order to compile +pub const MAX_GVA: usize = 0xffff_ffff_ffff_efff; +pub const SNAPSHOT_PT_GVA_MIN: usize = 0xffff_8000_0000_0000; +pub const SNAPSHOT_PT_GVA_MAX: usize = 0xffff_80ff_ffff_ffff; +pub const MAX_GPA: usize = 0x0000_000f_ffff_ffff; + +pub fn min_scratch_size(_input_data_size: usize, _output_data_size: usize) -> usize { + unimplemented!("min_scratch_size") +} diff --git a/src/hyperlight_common/src/arch/aarch64/vmem.rs b/src/hyperlight_common/src/arch/aarch64/vmem.rs new file mode 100644 index 000000000..3803251d2 --- /dev/null +++ b/src/hyperlight_common/src/arch/aarch64/vmem.rs @@ -0,0 +1,52 @@ +/* +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. + */ + +// TODO(aarch64): implement real page table operations + +use crate::vmem::{Mapping, TableOps, TableReadOps, Void}; + +pub const PAGE_SIZE: usize = 4096; +pub const PAGE_TABLE_SIZE: usize = 4096; +pub type PageTableEntry = u64; +pub type VirtAddr = u64; +pub type PhysAddr = u64; + +/// # Safety +/// See `TableOps` documentation. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn map(_op: &Op, _mapping: Mapping) { + unimplemented!("map") +} + +/// # Safety +/// See `TableReadOps` documentation. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn virt_to_phys<'a, Op: TableReadOps + 'a>( + _op: impl core::convert::AsRef + Copy + 'a, + _address: u64, + _len: u64, +) -> impl Iterator + 'a { + unimplemented!("virt_to_phys"); + #[allow(unreachable_code)] + core::iter::empty() +} + +pub trait TableMovability {} +impl> TableMovability + for crate::vmem::MayMoveTable +{ +} +impl TableMovability for crate::vmem::MayNotMoveTable {} diff --git a/src/hyperlight_common/src/layout.rs b/src/hyperlight_common/src/layout.rs index 1d40f8fc0..b53897bb1 100644 --- a/src/hyperlight_common/src/layout.rs +++ b/src/hyperlight_common/src/layout.rs @@ -23,10 +23,14 @@ limitations under the License. all(target_arch = "x86_64", feature = "nanvix-unstable"), path = "arch/i686/layout.rs" )] +#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/layout.rs")] mod arch; pub use arch::{MAX_GPA, MAX_GVA}; -#[cfg(all(target_arch = "x86_64", not(feature = "nanvix-unstable")))] +#[cfg(any( + all(target_arch = "x86_64", not(feature = "nanvix-unstable")), + target_arch = "aarch64" +))] pub use arch::{SNAPSHOT_PT_GVA_MAX, SNAPSHOT_PT_GVA_MIN}; // offsets down from the top of scratch memory for various things diff --git a/src/hyperlight_common/src/vmem.rs b/src/hyperlight_common/src/vmem.rs index be67658a3..c72a6b9af 100644 --- a/src/hyperlight_common/src/vmem.rs +++ b/src/hyperlight_common/src/vmem.rs @@ -16,6 +16,7 @@ limitations under the License. #[cfg_attr(target_arch = "x86_64", path = "arch/amd64/vmem.rs")] #[cfg_attr(target_arch = "x86", path = "arch/i686/vmem.rs")] +#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/vmem.rs")] mod arch; /// This is always the page size that the /guest/ is being compiled diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index 031961ed6..28fcba460 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -49,7 +49,7 @@ chrono = { version = "0.4", optional = true } anyhow = "1.0" metrics = "0.24.3" serde_json = "1.0" -elfcore = "2.0" +elfcore = { version = "2.0", optional = true } uuid = { version = "1.22.0", features = ["v4"] } [target.'cfg(windows)'.dependencies] @@ -128,7 +128,7 @@ executable_heap = [] # This feature enables printing of debug information to stdout in debug builds print_debug = [] # Dumps the VM state to a file on unexpected errors or crashes. The path of the file will be printed on stdout and logged. -crashdump = ["dep:chrono"] +crashdump = ["dep:chrono", "dep:elfcore"] trace_guest = ["dep:opentelemetry", "dep:tracing-opentelemetry", "dep:hyperlight-guest-tracing", "hyperlight-common/trace_guest"] mem_profile = [ "trace_guest", "dep:framehop", "dep:fallible-iterator", "hyperlight-common/mem_profile" ] kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"] diff --git a/src/hyperlight_host/build.rs b/src/hyperlight_host/build.rs index 953bfda29..d599bedc1 100644 --- a/src/hyperlight_host/build.rs +++ b/src/hyperlight_host/build.rs @@ -99,10 +99,10 @@ fn main() -> Result<()> { // Essentially the kvm and mshv3 features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")]. // You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv3")] in the codebase. cfg_aliases::cfg_aliases! { - gdb: { all(feature = "gdb", debug_assertions) }, + gdb: { all(feature = "gdb", debug_assertions, target_arch = "x86_64") }, kvm: { all(feature = "kvm", target_os = "linux") }, mshv3: { all(feature = "mshv3", target_os = "linux") }, - crashdump: { all(feature = "crashdump") }, + crashdump: { all(feature = "crashdump", target_arch = "x86_64") }, // print_debug feature is aliased with debug_assertions to make it only available in debug-builds. print_debug: { all(feature = "print_debug", debug_assertions) }, } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs new file mode 100644 index 000000000..c4288ae60 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -0,0 +1,99 @@ +/* +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. +*/ + +// TODO(aarch64): implement arch-specific HyperlightVm methods + +use std::sync::Arc; + +use super::{ + AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm, + InitializeError, +}; +#[cfg(gdb)] +use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse}; +use crate::hypervisor::regs::CommonSpecialRegisters; +use crate::hypervisor::virtual_machine::RegisterError; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; +use crate::sandbox::SandboxConfiguration; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::snapshot::NextAction; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; +#[cfg(crashdump)] +use crate::sandbox::uninitialized::SandboxRuntimeConfig; + +impl HyperlightVm { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + _snapshot_mem: GuestSharedMemory, + _scratch_mem: GuestSharedMemory, + _pml4_addr: u64, + _entrypoint: NextAction, + _rsp_gva: u64, + _config: &SandboxConfiguration, + #[cfg(gdb)] _gdb_conn: Option>, + #[cfg(crashdump)] _rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] _trace_info: MemTraceInfo, + ) -> std::result::Result { + unimplemented!("new") + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn initialise( + &mut self, + _peb_addr: crate::mem::ptr::RawPtr, + _seed: u64, + _page_size: u32, + _mem_mgr: &mut SandboxMemoryManager, + _host_funcs: &Arc>, + _guest_max_log_level: Option, + #[cfg(gdb)] _dbg_mem_access_fn: Arc< + std::sync::Mutex>, + >, + ) -> Result<(), InitializeError> { + unimplemented!("initialise") + } + + pub(crate) fn dispatch_call_from_host( + &mut self, + _mem_mgr: &mut SandboxMemoryManager, + _host_funcs: &Arc>, + #[cfg(gdb)] _dbg_mem_access_fn: Arc< + std::sync::Mutex>, + >, + ) -> Result<(), DispatchGuestCallError> { + unimplemented!("dispatch_call_from_host") + } + + pub(crate) fn get_root_pt(&self) -> Result { + unimplemented!("get_root_pt") + } + + pub(crate) fn get_snapshot_sregs( + &mut self, + ) -> Result { + unimplemented!("get_snapshot_sregs") + } + + pub(crate) fn reset_vcpu( + &mut self, + _cr3: u64, + _sregs: &CommonSpecialRegisters, + ) -> std::result::Result<(), RegisterError> { + unimplemented!("reset_vcpu") + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs new file mode 100644 index 000000000..4fa9fba36 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -0,0 +1,782 @@ +/* +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 = "aarch64")] +mod aarch64; +#[cfg(gdb)] +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; +use hyperlight_common::log_level::GuestLogFilter; +use tracing_core::LevelFilter; + +use crate::HyperlightError; +#[cfg(gdb)] +use crate::hypervisor::gdb::DebuggableVm; +#[cfg(gdb)] +use crate::hypervisor::gdb::arch::VcpuStopReasonError; +#[cfg(gdb)] +use crate::hypervisor::gdb::{ + DebugCommChannel, DebugError, DebugMsg, DebugResponse, GdbTargetError, VcpuStopReason, +}; +#[cfg(gdb)] +use crate::hypervisor::hyperlight_vm::x86_64::debug::ProcessDebugRequestError; +#[cfg(not(gdb))] +use crate::hypervisor::virtual_machine::VirtualMachine; +use crate::hypervisor::virtual_machine::{ + MapMemoryError, RegisterError, RunVcpuError, UnmapMemoryError, VmError, VmExit, +}; +use crate::hypervisor::{InterruptHandle, InterruptHandleImpl}; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory, SharedMemory}; +use crate::metrics::{METRIC_ERRONEOUS_VCPU_KICKS, METRIC_GUEST_CANCELLATION}; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::outb::{HandleOutbError, handle_outb}; +use crate::sandbox::snapshot::NextAction; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; +#[cfg(crashdump)] +use crate::sandbox::uninitialized::SandboxRuntimeConfig; + +/// Get the logging level filter to pass to the guest entrypoint +/// +/// The guest entrypoint uses this to determine the maximum log level to enable for the guest. +/// The `RUST_LOG` environment variable is expected to be in the format of comma-separated +/// key-value pairs, where the key is a log target (e.g., "hyperlight_guest_bin") and the value is +/// a log level (e.g., "debug"). +/// +/// NOTE: This prioritizes the log level for the targets containing "hyperlight_guest" string, then +/// "hyperlight_host", and then general log level. If none of these targets are found, it +/// defaults to "error". +fn get_max_log_level_filter(rust_log: String) -> LevelFilter { + // This is done as the guest will produce logs based on the log level returned here + // producing those logs is expensive and we don't want to do it if the host is not + // going to process them + let level_str = rust_log + .split(',') + // Prioritize targets containing "hyperlight_guest" + .find_map(|part| { + let mut kv = part.splitn(2, '='); + match (kv.next(), kv.next()) { + (Some(k), Some(v)) if k.trim().contains("hyperlight_guest") => Some(v.trim()), + _ => None, + } + }) + // Then check for "hyperlight_host" + .or_else(|| { + rust_log.split(',').find_map(|part| { + let mut kv = part.splitn(2, '='); + match (kv.next(), kv.next()) { + (Some(k), Some(v)) if k.trim().contains("hyperlight_host") => Some(v.trim()), + _ => None, + } + }) + }) + // Finally, check for general log level + .or_else(|| { + rust_log.split(',').find_map(|part| { + if part.contains("=") { + None + } else { + Some(part.trim()) + } + }) + }) + .unwrap_or(""); + + tracing::info!("Determined guest log level: {}", level_str); + + // If no value is found, default to Error + LevelFilter::from_str(level_str).unwrap_or(LevelFilter::ERROR) +} + +/// Converts a given [`Option`] to a `u64` value to be passed to the guest entrypoint +/// If the provided filter is `None`, it uses the `RUST_LOG` environment variable to determine the +/// maximum log level filter for the guest and converts it to a `u64` value. +pub(super) fn get_guest_log_filter(guest_max_log_level: Option) -> u64 { + let guest_log_level_filter = match guest_max_log_level { + Some(level) => level, + None => get_max_log_level_filter(std::env::var("RUST_LOG").unwrap_or_default()), + }; + GuestLogFilter::from(guest_log_level_filter).into() +} + +/// DispatchGuestCall error +#[derive(Debug, thiserror::Error)] +pub enum DispatchGuestCallError { + #[error("Failed to run vm: {0}")] + Run(#[from] RunVmError), + #[error("Failed to setup registers: {0}")] + SetupRegs(RegisterError), + #[error("VM was uninitialized")] + Uninitialized, +} + +impl DispatchGuestCallError { + /// Returns true if this error should poison the sandbox + pub(crate) fn is_poison_error(&self) -> bool { + match self { + // These errors poison the sandbox because they can leave it in an inconsistent state + // by returning before the guest can unwind properly + DispatchGuestCallError::Run(_) => true, + DispatchGuestCallError::SetupRegs(_) | DispatchGuestCallError::Uninitialized => false, + } + } + + /// Converts a `DispatchGuestCallError` to a `HyperlightError`. Used for backwards compatibility. + /// Also determines if the sandbox should be poisoned. + /// + /// Returns a tuple of (error, should_poison) where should_poison indicates whether + /// the sandbox should be marked as poisoned due to incomplete guest execution. + pub(crate) fn promote(self) -> (HyperlightError, bool) { + let should_poison = self.is_poison_error(); + let promoted_error = match self { + DispatchGuestCallError::Run(RunVmError::ExecutionCancelledByHost) => { + HyperlightError::ExecutionCanceledByHost() + } + + DispatchGuestCallError::Run(RunVmError::HandleIo(HandleIoError::Outb( + HandleOutbError::GuestAborted { code, message }, + ))) => HyperlightError::GuestAborted(code, message), + + DispatchGuestCallError::Run(RunVmError::MemoryAccessViolation { + addr, + access_type, + region_flags, + }) => HyperlightError::MemoryAccessViolation(addr, access_type, region_flags), + + // Leave others as is + other => HyperlightVmError::DispatchGuestCall(other).into(), + }; + (promoted_error, should_poison) + } +} + +/// Initialize error +#[derive(Debug, thiserror::Error)] +pub enum InitializeError { + #[error("Failed to convert pointer: {0}")] + ConvertPointer(String), + #[error("Failed to run vm: {0}")] + Run(#[from] RunVmError), + #[error("Failed to setup registers: {0}")] + SetupRegs(#[from] RegisterError), + #[error("Guest initialised stack pointer to architecturally invalid value: {0}")] + InvalidStackPointer(u64), +} + +/// Errors that can occur during VM execution in the run loop +#[derive(Debug, thiserror::Error)] +pub enum RunVmError { + #[cfg(crashdump)] + #[error("Crashdump generation error: {0}")] + CrashdumpGeneration(Box), + #[cfg(gdb)] + #[error("Debug handler error: {0}")] + DebugHandler(#[from] HandleDebugError), + #[error("Execution was cancelled by the host")] + ExecutionCancelledByHost, + #[error("Failed to access page: {0}")] + PageTableAccess(AccessPageTableError), + #[cfg(feature = "trace_guest")] + #[error("Failed to get registers: {0}")] + GetRegs(RegisterError), + #[error("IO handling error: {0}")] + HandleIo(#[from] HandleIoError), + #[error( + "Memory access violation at address {addr:#x}: {access_type} access, but memory is marked as {region_flags}" + )] + MemoryAccessViolation { + addr: u64, + access_type: MemoryRegionFlags, + region_flags: MemoryRegionFlags, + }, + #[error("MMIO READ access to unmapped address {0:#x}")] + MmioReadUnmapped(u64), + #[error("MMIO WRITE access to unmapped address {0:#x}")] + MmioWriteUnmapped(u64), + #[error("vCPU run failed: {0}")] + RunVcpu(#[from] RunVcpuError), + #[error("Unexpected VM exit: {0}")] + UnexpectedVmExit(String), + #[cfg(gdb)] + #[error("vCPU stop reason error: {0}")] + VcpuStopReason(#[from] VcpuStopReasonError), +} + +/// Errors that can occur during IO (outb) handling +#[derive(Debug, thiserror::Error)] +pub enum HandleIoError { + #[cfg(feature = "mem_profile")] + #[error("Failed to get registers: {0}")] + GetRegs(RegisterError), + #[error("No data was given in IO interrupt")] + NoData, + #[error("{0}")] + Outb(#[from] HandleOutbError), +} + +/// Errors that can occur when mapping a memory region +#[derive(Debug, thiserror::Error)] +pub enum MapRegionError { + #[error("VM map memory error: {0}")] + MapMemory(#[from] MapMemoryError), + #[error("Region is not page-aligned (page size: {0:#x})")] + NotPageAligned(usize), +} + +/// Errors that can occur when unmapping a memory region +#[derive(Debug, thiserror::Error)] +pub enum UnmapRegionError { + #[error("Region not found in mapped regions")] + RegionNotFound, + #[error("VM unmap memory error: {0}")] + UnmapMemory(#[from] UnmapMemoryError), +} + +/// Errors that can occur when updating the scratch mapping +#[derive(Debug, thiserror::Error)] +pub enum UpdateRegionError { + #[error("VM map memory error: {0}")] + MapMemory(#[from] MapMemoryError), + #[error("VM unmap memory error: {0}")] + UnmapMemory(#[from] UnmapMemoryError), +} + +/// Errors that can occur when accessing the root page table state +#[derive(Debug, thiserror::Error)] +pub enum AccessPageTableError { + #[error("Failed to get/set registers: {0}")] + AccessRegs(#[from] RegisterError), +} + +#[cfg(crashdump)] +#[derive(Debug, thiserror::Error)] +pub enum CrashDumpError { + #[error("Failed to generate crashdump because of a register error: {0}")] + GetRegs(#[from] RegisterError), + #[error("Failed to get root PT during crashdump generation: {0}")] + GetRootPt(#[from] AccessPageTableError), + #[error("Failed to get guest memory mapping during crashdump generation: {0}")] + AccessPageTable(Box), +} + +/// Errors that can occur during HyperlightVm creation +#[derive(Debug, thiserror::Error)] +pub enum CreateHyperlightVmError { + #[cfg(gdb)] + #[error("Failed to add hardware breakpoint: {0}")] + AddHwBreakpoint(DebugError), + #[error("No hypervisor was found")] + NoHypervisorFound, + #[cfg(gdb)] + #[error("Failed to send debug message: {0}")] + SendDbgMsg(#[from] SendDbgMsgError), + #[error("VM operation error: {0}")] + Vm(#[from] VmError), + #[error("Set scratch error: {0}")] + UpdateRegion(#[from] UpdateRegionError), +} + +/// Errors that can occur during debug exit handling +#[cfg(gdb)] +#[derive(Debug, thiserror::Error)] +pub enum HandleDebugError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("Error processing debug request: {0}")] + ProcessRequest(#[from] ProcessDebugRequestError), + #[error("Failed to receive message from GDB thread: {0}")] + ReceiveMessage(#[from] RecvDbgMsgError), + #[error("Failed to send message to GDB thread: {0}")] + SendMessage(#[from] SendDbgMsgError), +} + +/// Errors that can occur when sending a debug message +#[cfg(gdb)] +#[derive(Debug, thiserror::Error)] +pub enum SendDbgMsgError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("Failed to send message: {0}")] + SendFailed(#[from] GdbTargetError), +} + +/// Errors that can occur when receiving a debug message +#[cfg(gdb)] +#[derive(Debug, thiserror::Error)] +pub enum RecvDbgMsgError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("Failed to receive message: {0}")] + RecvFailed(#[from] GdbTargetError), +} + +/// Unified error type for all HyperlightVm operations +#[derive(Debug, thiserror::Error)] +pub enum HyperlightVmError { + #[error("Create VM error: {0}")] + Create(#[from] CreateHyperlightVmError), + #[error("Dispatch guest call error: {0}")] + DispatchGuestCall(#[from] DispatchGuestCallError), + #[error("Initialize error: {0}")] + Initialize(#[from] InitializeError), + #[error("Map region error: {0}")] + MapRegion(#[from] MapRegionError), + #[error("Restore VM (vcpu) error: {0}")] + Restore(#[from] RegisterError), + #[error("Unmap region error: {0}")] + UnmapRegion(#[from] UnmapRegionError), + #[error("Update region error: {0}")] + UpdateRegion(#[from] UpdateRegionError), + #[error("Access page table error: {0}")] + AccessPageTable(#[from] AccessPageTableError), +} + +/// Represents a Hyperlight Virtual Machine instance. +/// +/// This struct manages the lifecycle of the VM, including: +/// - The underlying hypervisor implementation (e.g., KVM, MSHV, WHP). +/// - Memory management, including initial sandbox regions and dynamic mappings. +/// - The vCPU execution loop and handling of VM exits (I/O, MMIO, interrupts). +pub(crate) struct HyperlightVm { + #[cfg(gdb)] + pub(super) vm: Box, + #[cfg(not(gdb))] + pub(super) vm: Box, + pub(super) page_size: usize, + pub(super) entrypoint: NextAction, // only present if this vm has not yet been initialised + pub(super) rsp_gva: u64, + pub(super) interrupt_handle: Arc, + + pub(super) next_slot: u32, // Monotonically increasing slot number + pub(super) freed_slots: Vec, // Reusable slots from unmapped regions + + pub(super) snapshot_slot: u32, + // The current snapshot region, used to keep it alive as long as + // it is used & when unmapping + pub(super) snapshot_memory: Option, + pub(super) scratch_slot: u32, // The slot number used for the scratch region + // The current scratch region, used to keep it alive as long as it + // is used & when unmapping + pub(super) scratch_memory: Option, + + pub(super) mmap_regions: Vec<(u32, MemoryRegion)>, // Later mapped regions (slot number, region) + + pub(super) pending_tlb_flush: bool, + + #[cfg(gdb)] + pub(super) gdb_conn: Option>, + #[cfg(gdb)] + pub(super) sw_breakpoints: HashMap, // addr -> original instruction + #[cfg(feature = "mem_profile")] + pub(super) trace_info: MemTraceInfo, + #[cfg(crashdump)] + pub(super) rt_cfg: SandboxRuntimeConfig, +} + +impl HyperlightVm { + /// Map a region of host memory into the sandbox. + /// + /// Safety: The caller must ensure that the region points to valid memory and + /// that the memory is valid for the duration of Self's lifetime. + /// Depending on the host platform, there are likely alignment + /// requirements of at least one page for base and len. + pub(crate) unsafe fn map_region( + &mut self, + region: &MemoryRegion, + ) -> std::result::Result<(), MapRegionError> { + if [ + region.guest_region.start, + region.guest_region.end, + #[allow(clippy::useless_conversion)] + region.host_region.start.into(), + #[allow(clippy::useless_conversion)] + region.host_region.end.into(), + ] + .iter() + .any(|x| x % self.page_size != 0) + { + return Err(MapRegionError::NotPageAligned(self.page_size)); + } + + // Try to reuse a freed slot first, otherwise use next_slot + let slot = if let Some(freed_slot) = self.freed_slots.pop() { + freed_slot + } else { + let slot = self.next_slot; + self.next_slot += 1; + slot + }; + + // Safety: slots are unique. It's up to caller to ensure that the region is valid + unsafe { self.vm.map_memory((slot, region))? }; + self.mmap_regions.push((slot, region.clone())); + Ok(()) + } + + /// Unmap a memory region from the sandbox + pub(crate) fn unmap_region( + &mut self, + region: &MemoryRegion, + ) -> std::result::Result<(), UnmapRegionError> { + let pos = self + .mmap_regions + .iter() + .position(|(_, r)| r == region) + .ok_or(UnmapRegionError::RegionNotFound)?; + + let (slot, _) = self.mmap_regions.remove(pos); + self.freed_slots.push(slot); + self.vm.unmap_memory((slot, region))?; + Ok(()) + } + + /// Get the currently mapped dynamic memory regions (not including initial sandbox region) + pub(crate) fn get_mapped_regions(&self) -> impl Iterator { + self.mmap_regions.iter().map(|(_, region)| region) + } + + /// Update the snapshot mapping to point to a new GuestSharedMemory + pub(crate) fn update_snapshot_mapping( + &mut self, + snapshot: GuestSharedMemory, + ) -> Result<(), UpdateRegionError> { + let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; + let rgn = snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); + + if let Some(old_snapshot) = self.snapshot_memory.replace(snapshot) { + let old_rgn = old_snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); + self.vm.unmap_memory((self.snapshot_slot, &old_rgn))?; + } + unsafe { self.vm.map_memory((self.snapshot_slot, &rgn))? }; + + Ok(()) + } + + /// Update the scratch mapping to point to a new GuestSharedMemory + pub(crate) fn update_scratch_mapping( + &mut self, + scratch: GuestSharedMemory, + ) -> Result<(), UpdateRegionError> { + let guest_base = hyperlight_common::layout::scratch_base_gpa(scratch.mem_size()); + let rgn = scratch.mapping_at(guest_base, MemoryRegionType::Scratch); + + if let Some(old_scratch) = self.scratch_memory.replace(scratch) { + let old_base = hyperlight_common::layout::scratch_base_gpa(old_scratch.mem_size()); + let old_rgn = old_scratch.mapping_at(old_base, MemoryRegionType::Scratch); + self.vm.unmap_memory((self.scratch_slot, &old_rgn))?; + } + unsafe { self.vm.map_memory((self.scratch_slot, &rgn))? }; + + Ok(()) + } + + /// Get the current stack top virtual address + pub(crate) fn get_stack_top(&mut self) -> u64 { + self.rsp_gva + } + + /// Set the current stack top virtual address + pub(crate) fn set_stack_top(&mut self, gva: u64) { + self.rsp_gva = gva; + } + + /// Get the current entrypoint action + pub(crate) fn get_entrypoint(&self) -> NextAction { + self.entrypoint + } + + /// Set the current entrypoint action + pub(crate) fn set_entrypoint(&mut self, entrypoint: NextAction) { + self.entrypoint = entrypoint + } + + pub(crate) fn interrupt_handle(&self) -> Arc { + self.interrupt_handle.clone() + } + + pub(crate) fn clear_cancel(&self) { + self.interrupt_handle.clear_cancel(); + } + + pub(super) fn run( + &mut self, + mem_mgr: &mut SandboxMemoryManager, + host_funcs: &Arc>, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, + ) -> std::result::Result<(), RunVmError> { + // Keeps the trace context and open spans + #[cfg(feature = "trace_guest")] + let mut tc = crate::sandbox::trace::TraceContext::new(); + + 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 VmExit::Cancelled() + // without sending any signals/WHV api calls + #[cfg(any(kvm, mshv3))] + self.interrupt_handle.set_tid(); + self.interrupt_handle.set_running(); + // NOTE: `set_running()`` must be called before checking `is_cancelled()` + // otherwise we risk missing a call to `kill()` because the vcpu would not be marked as running yet so signals won't be sent + + let exit_reason = if self.interrupt_handle.is_cancelled() + || self.interrupt_handle.is_debug_interrupted() + { + Ok(VmExit::Cancelled()) + } else { + // ==== KILL() TIMING POINT 3: Before calling run() ==== + // If kill() is called and ran to completion BEFORE this line executes: + // - Will still do a VM entry, but signals will be sent until VM exits + let result = self.vm.run_vcpu( + #[cfg(feature = "trace_guest")] + &mut tc, + ); + + // End current host trace by closing the current span that captures traces + // happening when a guest exits and re-enters. + #[cfg(feature = "trace_guest")] + { + tc.end_host_trace(); + // Handle the guest trace data if any + let regs = self.vm.regs().map_err(RunVmError::GetRegs)?; + + // Only parse the trace if it has reported + if tc.has_trace_data(®s) { + let root_pt = self.get_root_pt().map_err(RunVmError::PageTableAccess)?; + + // If something goes wrong with parsing the trace data, we log the error and + // continue execution instead of returning an error since this is not critical + // to correct execution of the guest + tc.handle_trace(®s, mem_mgr, root_pt) + .unwrap_or_else(|e| { + tracing::error!("Cannot handle trace data: {}", e); + }); + } + } + result + }; + + // ===== KILL() TIMING POINT 4: Before clear_running() ===== + // If kill() is called and ran to completion BEFORE this line executes: + // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. + // - Signals will be sent until `clear_running()` is called, which is ok + self.interrupt_handle.clear_running(); + + // ===== KILL() TIMING POINT 5: Before capturing cancel_requested ===== + // If kill() is called and ran to completion BEFORE this line executes: + // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. + // - Signals will not be sent + let cancel_requested = self.interrupt_handle.is_cancelled(); + let debug_interrupted = self.interrupt_handle.is_debug_interrupted(); + + // ===== KILL() TIMING POINT 6: Before checking exit_reason ===== + // If kill() is called and ran to completion BEFORE this line executes: + // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. + // - Signals will not be sent + match exit_reason { + #[cfg(gdb)] + Ok(VmExit::Debug { dr6, exception }) => { + let initialise = match self.entrypoint { + NextAction::Initialise(initialise) => initialise, + _ => 0, + }; + // Handle debug event (breakpoints) + let stop_reason = crate::hypervisor::gdb::arch::vcpu_stop_reason( + self.vm.as_mut(), + dr6, + initialise, + exception, + )?; + if let Err(e) = self.handle_debug(dbg_mem_access_fn.clone(), stop_reason) { + break Err(e.into()); + } + } + + Ok(VmExit::Halt()) => { + break Ok(()); + } + Ok(VmExit::IoOut(port, data)) => { + self.handle_io(mem_mgr, host_funcs, port, data)?; + } + Ok(VmExit::MmioRead(addr)) => { + let all_regions = self.get_mapped_regions(); + match get_memory_access_violation( + addr as usize, + MemoryRegionFlags::READ, + all_regions, + ) { + Some(MemoryAccess::AccessViolation(region_flags)) => { + break Err(RunVmError::MemoryAccessViolation { + addr, + access_type: MemoryRegionFlags::READ, + region_flags, + }); + } + None => { + break Err(RunVmError::MmioReadUnmapped(addr)); + } + } + } + Ok(VmExit::MmioWrite(addr)) => { + let all_regions = self.get_mapped_regions(); + match get_memory_access_violation( + addr as usize, + MemoryRegionFlags::WRITE, + all_regions, + ) { + Some(MemoryAccess::AccessViolation(region_flags)) => { + break Err(RunVmError::MemoryAccessViolation { + addr, + access_type: MemoryRegionFlags::WRITE, + region_flags, + }); + } + None => { + break Err(RunVmError::MmioWriteUnmapped(addr)); + } + } + } + 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 + // - Windows: WHvCancelRunVirtualProcessor called right after vcpu exits but RUNNING_BIT is still true + 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 VmExit::Retry, the cancel was not meant for this call + continue; + } + + // If the vcpu was interrupted by a debugger, we need to handle it + #[cfg(gdb)] + { + self.interrupt_handle.clear_debug_interrupt(); + if let Err(e) = + self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Interrupt) + { + break Err(e.into()); + } + } + + metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); + break Err(RunVmError::ExecutionCancelledByHost); + } + Ok(VmExit::Unknown(reason)) => { + break Err(RunVmError::UnexpectedVmExit(reason)); + } + Ok(VmExit::Retry()) => continue, + Err(e) => { + break Err(RunVmError::RunVcpu(e)); + } + } + }; + + match result { + Ok(_) => Ok(()), + Err(RunVmError::ExecutionCancelledByHost) => { + // no need to crashdump this + Err(RunVmError::ExecutionCancelledByHost) + } + Err(e) => { + #[cfg(crashdump)] + if self.rt_cfg.guest_core_dump { + crate::hypervisor::crashdump::generate_crashdump(self, mem_mgr, None) + .map_err(|e| RunVmError::CrashdumpGeneration(Box::new(e)))?; + } + + // If GDB is enabled, we handle the debug memory access + // Disregard return value as we want to return the error + #[cfg(gdb)] + if self.gdb_conn.is_some() { + self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash)? + } + Err(e) + } + } + } + + /// Handle an IO exit + fn handle_io( + &mut self, + mem_mgr: &mut SandboxMemoryManager, + host_funcs: &Arc>, + port: u16, + data: Vec, + ) -> std::result::Result<(), HandleIoError> { + if data.is_empty() { + return Err(HandleIoError::NoData); + } + + #[allow(clippy::get_first)] + let val = u32::from_le_bytes([ + data.get(0).copied().unwrap_or(0), + data.get(1).copied().unwrap_or(0), + data.get(2).copied().unwrap_or(0), + data.get(3).copied().unwrap_or(0), + ]); + + #[cfg(feature = "mem_profile")] + { + let regs = self.vm.regs().map_err(HandleIoError::GetRegs)?; + handle_outb(mem_mgr, host_funcs, port, val, ®s, &mut self.trace_info)?; + } + + #[cfg(not(feature = "mem_profile"))] + { + handle_outb(mem_mgr, host_funcs, port, val)?; + } + + Ok(()) + } +} + +impl Drop for HyperlightVm { + fn drop(&mut self) { + self.interrupt_handle.set_dropped(); + } +} + +/// The vCPU tried to access the given addr +enum MemoryAccess { + /// The accessed region has the given flags + AccessViolation(MemoryRegionFlags), +} + +/// Determines if a known memory access violation occurred at the given address with the given action type. +/// Returns Some(reason) if violation reason could be determined, or None if violation occurred but in unmapped region. +fn get_memory_access_violation<'a>( + gpa: usize, + tried: MemoryRegionFlags, + mut mem_regions: impl Iterator, +) -> Option { + let region = mem_regions.find(|region| region.guest_region.contains(&gpa))?; + if !region.flags.contains(tried) { + return Some(MemoryAccess::AccessViolation(region.flags)); + } + // gpa is in `region`, and region allows the tried access, but we got here anyway. + // Treat as a generic access violation for now, unsure if this is reachable. + None +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs similarity index 76% rename from src/hyperlight_host/src/hypervisor/hyperlight_vm.rs rename to src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index d053d15da..57b09fc20 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -18,7 +18,6 @@ limitations under the License. use std::collections::HashMap; #[cfg(crashdump)] use std::path::Path; -use std::str::FromStr; #[cfg(any(kvm, mshv3))] use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU8; @@ -26,29 +25,24 @@ use std::sync::atomic::AtomicU8; use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex}; -use hyperlight_common::log_level::GuestLogFilter; use tracing::{Span, instrument}; use tracing_core::LevelFilter; -#[cfg(gdb)] -use super::gdb::arch::VcpuStopReasonError; -#[cfg(gdb)] -use super::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm, GdbTargetError, VcpuStopReason, arch, -}; -use super::regs::{CommonFpu, CommonRegisters}; -#[cfg(target_os = "windows")] -use super::{PartitionState, WindowsInterruptHandle}; -use crate::HyperlightError; +use super::*; +use crate::hypervisor::InterruptHandleImpl; #[cfg(any(kvm, mshv3))] use crate::hypervisor::LinuxInterruptHandle; #[cfg(crashdump)] use crate::hypervisor::crashdump; #[cfg(gdb)] -use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; +use crate::hypervisor::gdb::{ + DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm, VcpuStopReason, +}; #[cfg(gdb)] -use crate::hypervisor::hyperlight_vm::debug::ProcessDebugRequestError; -use crate::hypervisor::regs::{CommonDebugRegs, CommonSpecialRegisters}; +use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; +use crate::hypervisor::regs::{ + CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, +}; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; #[cfg(kvm)] @@ -58,361 +52,23 @@ use crate::hypervisor::virtual_machine::mshv::MshvVm; #[cfg(target_os = "windows")] use crate::hypervisor::virtual_machine::whp::WhpVm; use crate::hypervisor::virtual_machine::{ - HypervisorType, MapMemoryError, RegisterError, RunVcpuError, UnmapMemoryError, VmError, VmExit, - get_available_hypervisor, + HypervisorType, RegisterError, VmError, get_available_hypervisor, }; -use crate::hypervisor::{InterruptHandle, InterruptHandleImpl}; -use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +#[cfg(target_os = "windows")] +use crate::hypervisor::{PartitionState, WindowsInterruptHandle}; +#[cfg(crashdump)] +use crate::mem::memory_region::MemoryRegion; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::RawPtr; -use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory, SharedMemory}; -use crate::metrics::{METRIC_ERRONEOUS_VCPU_KICKS, METRIC_GUEST_CANCELLATION}; +use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; use crate::sandbox::SandboxConfiguration; use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::outb::{HandleOutbError, handle_outb}; use crate::sandbox::snapshot::NextAction; #[cfg(feature = "mem_profile")] use crate::sandbox::trace::MemTraceInfo; #[cfg(crashdump)] use crate::sandbox::uninitialized::SandboxRuntimeConfig; -/// Get the logging level filter to pass to the guest entrypoint -/// -/// The guest entrypoint uses this to determine the maximum log level to enable for the guest. -/// The `RUST_LOG` environment variable is expected to be in the format of comma-separated -/// key-value pairs, where the key is a log target (e.g., "hyperlight_guest_bin") and the value is -/// a log level (e.g., "debug"). -/// -/// NOTE: This prioritizes the log level for the targets containing "hyperlight_guest" string, then -/// "hyperlight_host", and then general log level. If none of these targets are found, it -/// defaults to "error". -fn get_max_log_level_filter(rust_log: String) -> LevelFilter { - // This is done as the guest will produce logs based on the log level returned here - // producing those logs is expensive and we don't want to do it if the host is not - // going to process them - let level_str = rust_log - .split(',') - // Prioritize targets containing "hyperlight_guest" - .find_map(|part| { - let mut kv = part.splitn(2, '='); - match (kv.next(), kv.next()) { - (Some(k), Some(v)) if k.trim().contains("hyperlight_guest") => Some(v.trim()), - _ => None, - } - }) - // Then check for "hyperlight_host" - .or_else(|| { - rust_log.split(',').find_map(|part| { - let mut kv = part.splitn(2, '='); - match (kv.next(), kv.next()) { - (Some(k), Some(v)) if k.trim().contains("hyperlight_host") => Some(v.trim()), - _ => None, - } - }) - }) - // Finally, check for general log level - .or_else(|| { - rust_log.split(',').find_map(|part| { - if part.contains("=") { - None - } else { - Some(part.trim()) - } - }) - }) - .unwrap_or(""); - - tracing::info!("Determined guest log level: {}", level_str); - - // If no value is found, default to Error - LevelFilter::from_str(level_str).unwrap_or(LevelFilter::ERROR) -} - -/// Converts a given [`Option`] to a `u64` value to be passed to the guest entrypoint -/// If the provided filter is `None`, it uses the `RUST_LOG` environment variable to determine the -/// maximum log level filter for the guest and converts it to a `u64` value. -fn get_guest_log_filter(guest_max_log_level: Option) -> u64 { - let guest_log_level_filter = match guest_max_log_level { - Some(level) => level, - None => get_max_log_level_filter(std::env::var("RUST_LOG").unwrap_or_default()), - }; - GuestLogFilter::from(guest_log_level_filter).into() -} - -/// Represents a Hyperlight Virtual Machine instance. -/// -/// This struct manages the lifecycle of the VM, including: -/// - The underlying hypervisor implementation (e.g., KVM, MSHV, WHP). -/// - Memory management, including initial sandbox regions and dynamic mappings. -/// - The vCPU execution loop and handling of VM exits (I/O, MMIO, interrupts). -pub(crate) struct HyperlightVm { - #[cfg(gdb)] - vm: Box, - #[cfg(not(gdb))] - vm: Box, - page_size: usize, - entrypoint: NextAction, // only present if this vm has not yet been initialised - rsp_gva: u64, - interrupt_handle: Arc, - - next_slot: u32, // Monotonically increasing slot number - freed_slots: Vec, // Reusable slots from unmapped regions - - snapshot_slot: u32, - // The current snapshot region, used to keep it alive as long as - // it is used & when unmapping - snapshot_memory: Option, - scratch_slot: u32, // The slot number used for the scratch region - // The current scratch region, used to keep it alive as long as it - // is used & when unmapping - scratch_memory: Option, - - mmap_regions: Vec<(u32, MemoryRegion)>, // Later mapped regions (slot number, region) - - pending_tlb_flush: bool, - - #[cfg(gdb)] - gdb_conn: Option>, - #[cfg(gdb)] - sw_breakpoints: HashMap, // addr -> original instruction - #[cfg(feature = "mem_profile")] - trace_info: MemTraceInfo, - #[cfg(crashdump)] - rt_cfg: SandboxRuntimeConfig, -} - -/// DispatchGuestCall error -#[derive(Debug, thiserror::Error)] -pub enum DispatchGuestCallError { - #[error("Failed to run vm: {0}")] - Run(#[from] RunVmError), - #[error("Failed to setup registers: {0}")] - SetupRegs(RegisterError), - #[error("VM was uninitialized")] - Uninitialized, -} - -impl DispatchGuestCallError { - /// Returns true if this error should poison the sandbox - pub(crate) fn is_poison_error(&self) -> bool { - match self { - // These errors poison the sandbox because they can leave it in an inconsistent state - // by returning before the guest can unwind properly - DispatchGuestCallError::Run(_) => true, - DispatchGuestCallError::SetupRegs(_) | DispatchGuestCallError::Uninitialized => false, - } - } - - /// Converts a `DispatchGuestCallError` to a `HyperlightError`. Used for backwards compatibility. - /// Also determines if the sandbox should be poisoned. - /// - /// Returns a tuple of (error, should_poison) where should_poison indicates whether - /// the sandbox should be marked as poisoned due to incomplete guest execution. - pub(crate) fn promote(self) -> (HyperlightError, bool) { - let should_poison = self.is_poison_error(); - let promoted_error = match self { - DispatchGuestCallError::Run(RunVmError::ExecutionCancelledByHost) => { - HyperlightError::ExecutionCanceledByHost() - } - - DispatchGuestCallError::Run(RunVmError::HandleIo(HandleIoError::Outb( - HandleOutbError::GuestAborted { code, message }, - ))) => HyperlightError::GuestAborted(code, message), - - DispatchGuestCallError::Run(RunVmError::MemoryAccessViolation { - addr, - access_type, - region_flags, - }) => HyperlightError::MemoryAccessViolation(addr, access_type, region_flags), - - // Leave others as is - other => HyperlightVmError::DispatchGuestCall(other).into(), - }; - (promoted_error, should_poison) - } -} - -/// Initialize error -#[derive(Debug, thiserror::Error)] -pub enum InitializeError { - #[error("Failed to convert pointer: {0}")] - ConvertPointer(String), - #[error("Failed to run vm: {0}")] - Run(#[from] RunVmError), - #[error("Failed to setup registers: {0}")] - SetupRegs(#[from] RegisterError), - #[error("Guest initialised stack pointer to architecturally invalid value: {0}")] - InvalidStackPointer(u64), -} - -/// Errors that can occur during VM execution in the run loop -#[derive(Debug, thiserror::Error)] -pub enum RunVmError { - #[cfg(crashdump)] - #[error("Crashdump generation error: {0}")] - CrashdumpGeneration(Box), - #[cfg(gdb)] - #[error("Debug handler error: {0}")] - DebugHandler(#[from] HandleDebugError), - #[error("Execution was cancelled by the host")] - ExecutionCancelledByHost, - #[error("Failed to access page: {0}")] - PageTableAccess(AccessPageTableError), - #[cfg(feature = "trace_guest")] - #[error("Failed to get registers: {0}")] - GetRegs(RegisterError), - #[error("IO handling error: {0}")] - HandleIo(#[from] HandleIoError), - #[error( - "Memory access violation at address {addr:#x}: {access_type} access, but memory is marked as {region_flags}" - )] - MemoryAccessViolation { - addr: u64, - access_type: MemoryRegionFlags, - region_flags: MemoryRegionFlags, - }, - #[error("MMIO READ access to unmapped address {0:#x}")] - MmioReadUnmapped(u64), - #[error("MMIO WRITE access to unmapped address {0:#x}")] - MmioWriteUnmapped(u64), - #[error("vCPU run failed: {0}")] - RunVcpu(#[from] RunVcpuError), - #[error("Unexpected VM exit: {0}")] - UnexpectedVmExit(String), - #[cfg(gdb)] - #[error("vCPU stop reason error: {0}")] - VcpuStopReason(#[from] VcpuStopReasonError), -} - -/// Errors that can occur during IO (outb) handling -#[derive(Debug, thiserror::Error)] -pub enum HandleIoError { - #[cfg(feature = "mem_profile")] - #[error("Failed to get registers: {0}")] - GetRegs(RegisterError), - #[error("No data was given in IO interrupt")] - NoData, - #[error("{0}")] - Outb(#[from] HandleOutbError), -} - -/// Errors that can occur when mapping a memory region -#[derive(Debug, thiserror::Error)] -pub enum MapRegionError { - #[error("VM map memory error: {0}")] - MapMemory(#[from] MapMemoryError), - #[error("Region is not page-aligned (page size: {0:#x})")] - NotPageAligned(usize), -} - -/// Errors that can occur when unmapping a memory region -#[derive(Debug, thiserror::Error)] -pub enum UnmapRegionError { - #[error("Region not found in mapped regions")] - RegionNotFound, - #[error("VM unmap memory error: {0}")] - UnmapMemory(#[from] UnmapMemoryError), -} - -/// Errors that can occur when updating the scratch mapping -#[derive(Debug, thiserror::Error)] -pub enum UpdateRegionError { - #[error("VM map memory error: {0}")] - MapMemory(#[from] MapMemoryError), - #[error("VM unmap memory error: {0}")] - UnmapMemory(#[from] UnmapMemoryError), -} - -/// Errors that can occur when accessing the root page table state -#[derive(Debug, thiserror::Error)] -pub enum AccessPageTableError { - #[error("Failed to get/set registers: {0}")] - AccessRegs(#[from] RegisterError), -} - -#[cfg(crashdump)] -#[derive(Debug, thiserror::Error)] -pub enum CrashDumpError { - #[error("Failed to generate crashdump because of a register error: {0}")] - GetRegs(#[from] RegisterError), - #[error("Failed to get root PT during crashdump generation: {0}")] - GetRootPt(#[from] AccessPageTableError), - #[error("Failed to get guest memory mapping during crashdump generation: {0}")] - AccessPageTable(Box), -} - -/// Errors that can occur during HyperlightVm creation -#[derive(Debug, thiserror::Error)] -pub enum CreateHyperlightVmError { - #[cfg(gdb)] - #[error("Failed to add hardware breakpoint: {0}")] - AddHwBreakpoint(DebugError), - #[error("No hypervisor was found")] - NoHypervisorFound, - #[cfg(gdb)] - #[error("Failed to send debug message: {0}")] - SendDbgMsg(#[from] SendDbgMsgError), - #[error("VM operation error: {0}")] - Vm(#[from] VmError), - #[error("Set scratch error: {0}")] - UpdateRegion(#[from] UpdateRegionError), -} - -/// Errors that can occur during debug exit handling -#[cfg(gdb)] -#[derive(Debug, thiserror::Error)] -pub enum HandleDebugError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("Error processing debug request: {0}")] - ProcessRequest(#[from] ProcessDebugRequestError), - #[error("Failed to receive message from GDB thread: {0}")] - ReceiveMessage(#[from] RecvDbgMsgError), - #[error("Failed to send message to GDB thread: {0}")] - SendMessage(#[from] SendDbgMsgError), -} - -/// Errors that can occur when sending a debug message -#[cfg(gdb)] -#[derive(Debug, thiserror::Error)] -pub enum SendDbgMsgError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("Failed to send message: {0}")] - SendFailed(#[from] GdbTargetError), -} - -/// Errors that can occur when receiving a debug message -#[cfg(gdb)] -#[derive(Debug, thiserror::Error)] -pub enum RecvDbgMsgError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("Failed to receive message: {0}")] - RecvFailed(#[from] GdbTargetError), -} - -/// Unified error type for all HyperlightVm operations -#[derive(Debug, thiserror::Error)] -pub enum HyperlightVmError { - #[error("Create VM error: {0}")] - Create(#[from] CreateHyperlightVmError), - #[error("Dispatch guest call error: {0}")] - DispatchGuestCall(#[from] DispatchGuestCallError), - #[error("Initialize error: {0}")] - Initialize(#[from] InitializeError), - #[error("Map region error: {0}")] - MapRegion(#[from] MapRegionError), - #[error("Restore VM (vcpu) error: {0}")] - Restore(#[from] RegisterError), - #[error("Unmap region error: {0}")] - UnmapRegion(#[from] UnmapRegionError), - #[error("Update region error: {0}")] - UpdateRegion(#[from] UpdateRegionError), - #[error("Access page table error: {0}")] - AccessPageTable(#[from] AccessPageTableError), -} - impl HyperlightVm { /// Create a new HyperlightVm instance (will not run vm until calling `initialise`) #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] @@ -593,102 +249,6 @@ impl HyperlightVm { Ok(()) } - /// Map a region of host memory into the sandbox. - /// - /// Safety: The caller must ensure that the region points to valid memory and - /// that the memory is valid for the duration of Self's lifetime. - /// Depending on the host platform, there are likely alignment - /// requirements of at least one page for base and len. - pub(crate) unsafe fn map_region( - &mut self, - region: &MemoryRegion, - ) -> std::result::Result<(), MapRegionError> { - if [ - region.guest_region.start, - region.guest_region.end, - #[allow(clippy::useless_conversion)] - region.host_region.start.into(), - #[allow(clippy::useless_conversion)] - region.host_region.end.into(), - ] - .iter() - .any(|x| x % self.page_size != 0) - { - return Err(MapRegionError::NotPageAligned(self.page_size)); - } - - // Try to reuse a freed slot first, otherwise use next_slot - let slot = if let Some(freed_slot) = self.freed_slots.pop() { - freed_slot - } else { - let slot = self.next_slot; - self.next_slot += 1; - slot - }; - - // Safety: slots are unique. It's up to caller to ensure that the region is valid - unsafe { self.vm.map_memory((slot, region))? }; - self.mmap_regions.push((slot, region.clone())); - Ok(()) - } - - /// Unmap a memory region from the sandbox - pub(crate) fn unmap_region( - &mut self, - region: &MemoryRegion, - ) -> std::result::Result<(), UnmapRegionError> { - let pos = self - .mmap_regions - .iter() - .position(|(_, r)| r == region) - .ok_or(UnmapRegionError::RegionNotFound)?; - - let (slot, _) = self.mmap_regions.remove(pos); - self.freed_slots.push(slot); - self.vm.unmap_memory((slot, region))?; - Ok(()) - } - - /// Get the currently mapped dynamic memory regions (not including initial sandbox region) - pub(crate) fn get_mapped_regions(&self) -> impl Iterator { - self.mmap_regions.iter().map(|(_, region)| region) - } - - /// Update the snapshot mapping to point to a new GuestSharedMemory - pub(crate) fn update_snapshot_mapping( - &mut self, - snapshot: GuestSharedMemory, - ) -> Result<(), UpdateRegionError> { - let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; - let rgn = snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); - - if let Some(old_snapshot) = self.snapshot_memory.replace(snapshot) { - let old_rgn = old_snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); - self.vm.unmap_memory((self.snapshot_slot, &old_rgn))?; - } - unsafe { self.vm.map_memory((self.snapshot_slot, &rgn))? }; - - Ok(()) - } - - /// Update the scratch mapping to point to a new GuestSharedMemory - pub(crate) fn update_scratch_mapping( - &mut self, - scratch: GuestSharedMemory, - ) -> Result<(), UpdateRegionError> { - let guest_base = hyperlight_common::layout::scratch_base_gpa(scratch.mem_size()); - let rgn = scratch.mapping_at(guest_base, MemoryRegionType::Scratch); - - if let Some(old_scratch) = self.scratch_memory.replace(scratch) { - let old_base = hyperlight_common::layout::scratch_base_gpa(old_scratch.mem_size()); - let old_rgn = old_scratch.mapping_at(old_base, MemoryRegionType::Scratch); - self.vm.unmap_memory((self.scratch_slot, &old_rgn))?; - } - unsafe { self.vm.map_memory((self.scratch_slot, &rgn))? }; - - Ok(()) - } - /// Get the current base page table physical address. /// /// By default, reads CR3 from the vCPU special registers. @@ -713,26 +273,6 @@ impl HyperlightVm { Ok(self.vm.sregs()?) } - /// Get the current stack top virtual address - pub(crate) fn get_stack_top(&mut self) -> u64 { - self.rsp_gva - } - - /// Set the current stack top virtual address - pub(crate) fn set_stack_top(&mut self, gva: u64) { - self.rsp_gva = gva; - } - - /// Get the current entrypoint action - pub(crate) fn get_entrypoint(&self) -> NextAction { - self.entrypoint - } - - /// Set the current entrypoint action - pub(crate) fn set_entrypoint(&mut self, entrypoint: NextAction) { - self.entrypoint = entrypoint - } - /// Dispatch a call from the host to the guest using the given pointer /// to the dispatch function _in the guest's address space_. /// @@ -789,242 +329,6 @@ impl HyperlightVm { .map_err(DispatchGuestCallError::Run) } - pub(crate) fn interrupt_handle(&self) -> Arc { - self.interrupt_handle.clone() - } - - pub(crate) fn clear_cancel(&self) { - self.interrupt_handle.clear_cancel(); - } - - fn run( - &mut self, - mem_mgr: &mut SandboxMemoryManager, - host_funcs: &Arc>, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> std::result::Result<(), RunVmError> { - // Keeps the trace context and open spans - #[cfg(feature = "trace_guest")] - let mut tc = crate::sandbox::trace::TraceContext::new(); - - 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 VmExit::Cancelled() - // without sending any signals/WHV api calls - #[cfg(any(kvm, mshv3))] - self.interrupt_handle.set_tid(); - self.interrupt_handle.set_running(); - // NOTE: `set_running()`` must be called before checking `is_cancelled()` - // otherwise we risk missing a call to `kill()` because the vcpu would not be marked as running yet so signals won't be sent - - let exit_reason = if self.interrupt_handle.is_cancelled() - || self.interrupt_handle.is_debug_interrupted() - { - Ok(VmExit::Cancelled()) - } else { - // ==== KILL() TIMING POINT 3: Before calling run() ==== - // If kill() is called and ran to completion BEFORE this line executes: - // - Will still do a VM entry, but signals will be sent until VM exits - let result = self.vm.run_vcpu( - #[cfg(feature = "trace_guest")] - &mut tc, - ); - - // End current host trace by closing the current span that captures traces - // happening when a guest exits and re-enters. - #[cfg(feature = "trace_guest")] - { - tc.end_host_trace(); - // Handle the guest trace data if any - let regs = self.vm.regs().map_err(RunVmError::GetRegs)?; - - // Only parse the trace if it has reported - if tc.has_trace_data(®s) { - let root_pt = self.get_root_pt().map_err(RunVmError::PageTableAccess)?; - - // If something goes wrong with parsing the trace data, we log the error and - // continue execution instead of returning an error since this is not critical - // to correct execution of the guest - tc.handle_trace(®s, mem_mgr, root_pt) - .unwrap_or_else(|e| { - tracing::error!("Cannot handle trace data: {}", e); - }); - } - } - result - }; - - // ===== KILL() TIMING POINT 4: Before clear_running() ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. - // - Signals will be sent until `clear_running()` is called, which is ok - self.interrupt_handle.clear_running(); - - // ===== KILL() TIMING POINT 5: Before capturing cancel_requested ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. - // - Signals will not be sent - let cancel_requested = self.interrupt_handle.is_cancelled(); - let debug_interrupted = self.interrupt_handle.is_debug_interrupted(); - - // ===== KILL() TIMING POINT 6: Before checking exit_reason ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. - // - Signals will not be sent - match exit_reason { - #[cfg(gdb)] - Ok(VmExit::Debug { dr6, exception }) => { - let initialise = match self.entrypoint { - NextAction::Initialise(initialise) => initialise, - _ => 0, - }; - // Handle debug event (breakpoints) - let stop_reason = - arch::vcpu_stop_reason(self.vm.as_mut(), dr6, initialise, exception)?; - if let Err(e) = self.handle_debug(dbg_mem_access_fn.clone(), stop_reason) { - break Err(e.into()); - } - } - - Ok(VmExit::Halt()) => { - break Ok(()); - } - Ok(VmExit::IoOut(port, data)) => { - self.handle_io(mem_mgr, host_funcs, port, data)?; - } - Ok(VmExit::MmioRead(addr)) => { - let all_regions = self.get_mapped_regions(); - match get_memory_access_violation( - addr as usize, - MemoryRegionFlags::READ, - all_regions, - ) { - Some(MemoryAccess::AccessViolation(region_flags)) => { - break Err(RunVmError::MemoryAccessViolation { - addr, - access_type: MemoryRegionFlags::READ, - region_flags, - }); - } - None => { - break Err(RunVmError::MmioReadUnmapped(addr)); - } - } - } - Ok(VmExit::MmioWrite(addr)) => { - let all_regions = self.get_mapped_regions(); - match get_memory_access_violation( - addr as usize, - MemoryRegionFlags::WRITE, - all_regions, - ) { - Some(MemoryAccess::AccessViolation(region_flags)) => { - break Err(RunVmError::MemoryAccessViolation { - addr, - access_type: MemoryRegionFlags::WRITE, - region_flags, - }); - } - None => { - break Err(RunVmError::MmioWriteUnmapped(addr)); - } - } - } - 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 - // - Windows: WHvCancelRunVirtualProcessor called right after vcpu exits but RUNNING_BIT is still true - 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 VmExit::Retry, the cancel was not meant for this call - continue; - } - - // If the vcpu was interrupted by a debugger, we need to handle it - #[cfg(gdb)] - { - self.interrupt_handle.clear_debug_interrupt(); - if let Err(e) = - self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Interrupt) - { - break Err(e.into()); - } - } - - metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); - break Err(RunVmError::ExecutionCancelledByHost); - } - Ok(VmExit::Unknown(reason)) => { - break Err(RunVmError::UnexpectedVmExit(reason)); - } - Ok(VmExit::Retry()) => continue, - Err(e) => { - break Err(RunVmError::RunVcpu(e)); - } - } - }; - - match result { - Ok(_) => Ok(()), - Err(RunVmError::ExecutionCancelledByHost) => { - // no need to crashdump this - Err(RunVmError::ExecutionCancelledByHost) - } - Err(e) => { - #[cfg(crashdump)] - if self.rt_cfg.guest_core_dump { - crashdump::generate_crashdump(self, mem_mgr, None) - .map_err(|e| RunVmError::CrashdumpGeneration(Box::new(e)))?; - } - - // If GDB is enabled, we handle the debug memory access - // Disregard return value as we want to return the error - #[cfg(gdb)] - if self.gdb_conn.is_some() { - self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash)? - } - Err(e) - } - } - } - - /// Handle an IO exit - fn handle_io( - &mut self, - mem_mgr: &mut SandboxMemoryManager, - host_funcs: &Arc>, - port: u16, - data: Vec, - ) -> std::result::Result<(), HandleIoError> { - if data.is_empty() { - return Err(HandleIoError::NoData); - } - - #[allow(clippy::get_first)] - let val = u32::from_le_bytes([ - data.get(0).copied().unwrap_or(0), - data.get(1).copied().unwrap_or(0), - data.get(2).copied().unwrap_or(0), - data.get(3).copied().unwrap_or(0), - ]); - - #[cfg(feature = "mem_profile")] - { - let regs = self.vm.regs().map_err(HandleIoError::GetRegs)?; - handle_outb(mem_mgr, host_funcs, port, val, ®s, &mut self.trace_info)?; - } - - #[cfg(not(feature = "mem_profile"))] - { - handle_outb(mem_mgr, host_funcs, port, val)?; - } - - Ok(()) - } - /// Resets the following vCPU state: /// - General purpose registers /// - Debug registers @@ -1066,13 +370,14 @@ impl HyperlightVm { // Handle a debug exit #[cfg(gdb)] - fn handle_debug( + pub(super) fn handle_debug( &mut self, dbg_mem_access_fn: Arc>>, stop_reason: VcpuStopReason, ) -> std::result::Result<(), HandleDebugError> { + use debug::ProcessDebugRequestError; + use crate::hypervisor::gdb::DebugMemoryAccess; - use crate::hypervisor::hyperlight_vm::debug::ProcessDebugRequestError; if self.gdb_conn.is_none() { return Err(HandleDebugError::DebugNotEnabled); @@ -1211,7 +516,8 @@ impl HyperlightVm { pub(crate) fn crashdump_context( &self, mem_mgr: &mut SandboxMemoryManager, - ) -> std::result::Result, CrashDumpError> { + ) -> std::result::Result, CrashDumpError> + { if self.rt_cfg.guest_core_dump { let mut regs = [0; 27]; @@ -1287,36 +593,8 @@ impl HyperlightVm { } } -impl Drop for HyperlightVm { - fn drop(&mut self) { - self.interrupt_handle.set_dropped(); - } -} - -/// The vCPU tried to access the given addr -enum MemoryAccess { - /// The accessed region has the given flags - AccessViolation(MemoryRegionFlags), -} - -/// Determines if a known memory access violation occurred at the given address with the given action type. -/// Returns Some(reason) if violation reason could be determined, or None if violation occurred but in unmapped region. -fn get_memory_access_violation<'a>( - gpa: usize, - tried: MemoryRegionFlags, - mut mem_regions: impl Iterator, -) -> Option { - let region = mem_regions.find(|region| region.guest_region.contains(&gpa))?; - if !region.flags.contains(tried) { - return Some(MemoryAccess::AccessViolation(region.flags)); - } - // gpa is in `region`, and region allows the tried access, but we got here anyway. - // Treat as a generic access violation for now, unsure if this is reachable. - None -} - #[cfg(gdb)] -mod debug { +pub(super) mod debug { use hyperlight_common::mem::PAGE_SIZE; use super::HyperlightVm; diff --git a/src/hyperlight_host/src/hypervisor/regs.rs b/src/hyperlight_host/src/hypervisor/regs.rs index 5d940ba69..828bdf558 100644 --- a/src/hyperlight_host/src/hypervisor/regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs.rs @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -mod debug_regs; -mod fpu; -mod special_regs; -mod standard_regs; +#[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_os = "windows")] use std::collections::HashSet; -pub(crate) use debug_regs::*; -pub(crate) use fpu::*; -pub(crate) use special_regs::*; -pub(crate) use standard_regs::*; +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; #[cfg(target_os = "windows")] #[derive(Debug, PartialEq)] diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs new file mode 100644 index 000000000..8f91c634d --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs @@ -0,0 +1,37 @@ +/* +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. +*/ + +// TODO(aarch64): implement real register definitions + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonRegisters { + _placeholder: u64, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSpecialRegisters { + _placeholder: u64, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonFpu { + _placeholder: u64, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonDebugRegs { + _placeholder: u64, +} diff --git a/src/hyperlight_host/src/hypervisor/regs/debug_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/debug_regs.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/debug_regs.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/debug_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/fpu.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/fpu.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/fpu.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/fpu.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs new file mode 100644 index 000000000..88724d95a --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs @@ -0,0 +1,28 @@ +/* +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. +*/ + +mod debug_regs; +mod fpu; +mod special_regs; +mod standard_regs; + +pub(crate) use debug_regs::*; +pub(crate) use fpu::*; +pub(crate) use special_regs::*; +pub(crate) use standard_regs::*; + +#[cfg(target_os = "windows")] +pub(crate) use super::FromWhpRegisterError; diff --git a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/special_regs.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/standard_regs.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/standard_regs.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/standard_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs new file mode 100644 index 000000000..39ecb775d --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs @@ -0,0 +1,40 @@ +/* +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. +*/ + +// TODO(aarch64): implement KVM backend + +use tracing::{Span, instrument}; + +use crate::hypervisor::virtual_machine::CreateVmError; + +/// Return `true` if the KVM API is available +#[instrument(skip_all, parent = Span::current(), level = "Trace")] +pub(crate) fn is_hypervisor_present() -> bool { + // TODO(aarch64): implement KVM detection + false +} + +/// A KVM implementation of a single-vcpu VM +#[derive(Debug)] +pub(crate) struct KvmVm { + _placeholder: (), +} + +impl KvmVm { + pub(crate) fn new() -> std::result::Result { + unimplemented!("KvmVm::new") + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/mod.rs new file mode 100644 index 000000000..b2adf372c --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/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/kvm.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs rename to src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index d739ad0d4..b3bed769e 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -111,8 +111,8 @@ pub(crate) const XSAVE_MIN_SIZE: usize = 576; #[cfg(all(any(kvm, mshv3), test, not(feature = "nanvix-unstable")))] pub(crate) const XSAVE_BUFFER_SIZE: usize = 4096; -// Compiler error if no hypervisor type is available -#[cfg(not(any(kvm, mshv3, target_os = "windows")))] +// Compiler error if no hypervisor type is available (not applicable on aarch64 yet) +#[cfg(not(any(kvm, mshv3, target_os = "windows", target_arch = "aarch64")))] compile_error!( "No hypervisor type is available for the current platform. Please enable either the `kvm` or `mshv3` cargo feature." ); @@ -121,7 +121,12 @@ compile_error!( pub(crate) enum VmExit { /// The vCPU has exited due to a debug event (usually breakpoint) #[cfg(gdb)] - Debug { dr6: u64, exception: u32 }, + Debug { + #[cfg(target_arch = "x86_64")] + dr6: u64, + #[cfg(target_arch = "x86_64")] + exception: u32, + }, /// The vCPU has halted Halt(), /// The vCPU has issued a write to the given port with the given value diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs new file mode 100644 index 000000000..79d816b15 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs @@ -0,0 +1,38 @@ +/* +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 tracing::{Span, instrument}; + +use crate::hypervisor::virtual_machine::CreateVmError; + +/// Return `true` if the MSHV API is available +#[instrument(skip_all, parent = Span::current(), level = "Trace")] +pub(crate) fn is_hypervisor_present() -> bool { + // TODO(aarch64): implement MSHV detection + false +} + +/// An MSHV implementation of a single-vcpu VM +#[derive(Debug)] +pub(crate) struct MshvVm { + _placeholder: (), +} + +impl MshvVm { + pub(crate) fn new() -> std::result::Result { + unimplemented!("MshvVm::new") + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/mod.rs new file mode 100644 index 000000000..b2adf372c --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/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/mshv.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs rename to src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 1883b0251..ba193e533 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -26,7 +26,9 @@ use kvm_bindings::{KVM_MEM_READONLY, kvm_userspace_memory_region}; use mshv_bindings::{ MSHV_SET_MEM_BIT_EXECUTABLE, MSHV_SET_MEM_BIT_UNMAP, MSHV_SET_MEM_BIT_WRITABLE, }; -#[cfg(mshv3)] +#[cfg(all(mshv3, target_arch = "aarch64"))] +use mshv_bindings::{hv_arm64_memory_intercept_message, mshv_user_mem_region}; +#[cfg(all(mshv3, target_arch = "x86_64"))] use mshv_bindings::{hv_x64_memory_intercept_message, mshv_user_mem_region}; #[cfg(target_os = "windows")] use windows::Win32::System::Hypervisor::{self, WHV_MEMORY_ACCESS_TYPE}; @@ -95,7 +97,7 @@ impl TryFrom for MemoryRegionFlags { } } -#[cfg(mshv3)] +#[cfg(all(mshv3, target_arch = "x86_64"))] impl TryFrom for MemoryRegionFlags { type Error = crate::HyperlightError; @@ -112,6 +114,15 @@ impl TryFrom for MemoryRegionFlags { } } +#[cfg(all(mshv3, target_arch = "aarch64"))] +impl TryFrom for MemoryRegionFlags { + type Error = crate::HyperlightError; + + fn try_from(_msg: hv_arm64_memory_intercept_message) -> crate::Result { + unimplemented!("try_from") + } +} + // NOTE: In the future, all host-side knowledge about memory region types // should collapse down to Snapshot vs Scratch (see shared_mem.rs). // Until then, these variants help distinguish regions for diagnostics