From a5234587e09a644aedbc490814817cdb16d9e15d Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:48:40 -0700 Subject: [PATCH 1/2] Unify register representation Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- .../src/hypervisor/gdb/hyperv_debug.rs | 40 +- .../src/hypervisor/gdb/kvm_debug.rs | 31 +- src/hyperlight_host/src/hypervisor/gdb/mod.rs | 34 +- .../src/hypervisor/gdb/mshv_debug.rs | 30 +- .../src/hypervisor/gdb/x86_64_target.rs | 7 +- .../src/hypervisor/hyperv_linux.rs | 151 ++-- .../src/hypervisor/hyperv_windows.rs | 298 ++------ src/hyperlight_host/src/hypervisor/kvm.rs | 113 ++- src/hyperlight_host/src/hypervisor/mod.rs | 110 ++- .../src/hypervisor/{fpu.rs => regs.rs} | 23 +- .../src/hypervisor/regs/fpu.rs | 466 ++++++++++++ .../src/hypervisor/regs/special_regs.rs | 667 ++++++++++++++++++ .../src/hypervisor/regs/standard_regs.rs | 420 +++++++++++ .../hypervisor/windows_hypervisor_platform.rs | 372 +++------- .../src/hypervisor/wrappers.rs | 79 --- src/hyperlight_host/src/sandbox/outb.rs | 35 +- src/tests/rust_guests/dummyguest/Cargo.lock | 8 +- src/tests/rust_guests/simpleguest/Cargo.lock | 8 +- src/tests/rust_guests/witguest/Cargo.lock | 8 +- typos.toml | 1 + 20 files changed, 1992 insertions(+), 909 deletions(-) rename src/hyperlight_host/src/hypervisor/{fpu.rs => regs.rs} (56%) create mode 100644 src/hyperlight_host/src/hypervisor/regs/fpu.rs create mode 100644 src/hyperlight_host/src/hypervisor/regs/special_regs.rs create mode 100644 src/hyperlight_host/src/hypervisor/regs/standard_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs index 7a902d6de..ed55457fd 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs @@ -19,9 +19,10 @@ use std::collections::HashMap; use windows::Win32::System::Hypervisor::WHV_VP_EXCEPTION_CONTEXT; use super::arch::{MAX_NO_OF_HW_BP, vcpu_stop_reason}; -use super::{GuestDebug, SW_BP_SIZE, VcpuStopReason, X86_64Regs}; +use super::{GuestDebug, SW_BP_SIZE, VcpuStopReason}; +use crate::hypervisor::regs::CommonRegisters; use crate::hypervisor::windows_hypervisor_platform::VMProcessor; -use crate::hypervisor::wrappers::{WHvDebugRegisters, WHvGeneralRegisters}; +use crate::hypervisor::wrappers::WHvDebugRegisters; use crate::{HyperlightError, Result, new_error}; /// KVM Debug struct @@ -54,7 +55,7 @@ impl HypervDebug { /// Returns the instruction pointer from the stopped vCPU fn get_instruction_pointer(&self, vcpu_fd: &VMProcessor) -> Result { let regs = vcpu_fd - .get_regs() + .regs() .map_err(|e| new_error!("Could not retrieve registers from vCPU: {:?}", e))?; Ok(regs.rip) @@ -103,7 +104,7 @@ impl HypervDebug { self.single_step = step; let mut regs = vcpu_fd - .get_regs() + .regs() .map_err(|e| new_error!("Could not get registers: {:?}", e))?; // Set TF Flag to enable Traps @@ -114,7 +115,7 @@ impl HypervDebug { } vcpu_fd - .set_general_purpose_registers(®s) + .set_regs(®s) .map_err(|e| new_error!("Could not set guest registers: {:?}", e))?; Ok(()) @@ -185,10 +186,10 @@ impl GuestDebug for HypervDebug { self.sw_breakpoints.remove(addr) } - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> Result<()> { + fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> Result<()> { log::debug!("Read registers"); let vcpu_regs = vcpu_fd - .get_regs() + .regs() .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; regs.rax = vcpu_regs.rax; @@ -224,32 +225,11 @@ impl GuestDebug for HypervDebug { .map_err(|_| HyperlightError::TranslateGuestAddress(gva)) } - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> Result<()> { + fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> Result<()> { log::debug!("Write registers"); - let regs = WHvGeneralRegisters { - rax: regs.rax, - rbx: regs.rbx, - rcx: regs.rcx, - rdx: regs.rdx, - rsi: regs.rsi, - rdi: regs.rdi, - rbp: regs.rbp, - rsp: regs.rsp, - r8: regs.r8, - r9: regs.r9, - r10: regs.r10, - r11: regs.r11, - r12: regs.r12, - r13: regs.r13, - r14: regs.r14, - r15: regs.r15, - - rip: regs.rip, - rflags: regs.rflags, - }; vcpu_fd - .set_general_purpose_registers(®s) + .set_regs(regs) .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) } } diff --git a/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs index c20b3b279..b0fcf75b4 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs @@ -18,12 +18,13 @@ use std::collections::HashMap; use kvm_bindings::{ KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP, KVM_GUESTDBG_USE_HW_BP, KVM_GUESTDBG_USE_SW_BP, - kvm_debug_exit_arch, kvm_guest_debug, kvm_regs, + kvm_debug_exit_arch, kvm_guest_debug, }; use kvm_ioctls::VcpuFd; use super::arch::{MAX_NO_OF_HW_BP, SW_BP_SIZE, vcpu_stop_reason}; -use super::{GuestDebug, VcpuStopReason, X86_64Regs}; +use super::{GuestDebug, VcpuStopReason}; +use crate::hypervisor::regs::CommonRegisters; use crate::{HyperlightError, Result, new_error}; /// KVM Debug struct @@ -167,7 +168,7 @@ impl GuestDebug for KvmDebug { self.sw_breakpoints.remove(addr) } - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> Result<()> { + fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> Result<()> { log::debug!("Read registers"); let vcpu_regs = vcpu_fd .get_regs() @@ -212,29 +213,9 @@ impl GuestDebug for KvmDebug { } } - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> Result<()> { + fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> Result<()> { log::debug!("Write registers"); - let new_regs = kvm_regs { - rax: regs.rax, - rbx: regs.rbx, - rcx: regs.rcx, - rdx: regs.rdx, - rsi: regs.rsi, - rdi: regs.rdi, - rbp: regs.rbp, - rsp: regs.rsp, - r8: regs.r8, - r9: regs.r9, - r10: regs.r10, - r11: regs.r11, - r12: regs.r12, - r13: regs.r13, - r14: regs.r14, - r15: regs.r15, - - rip: regs.rip, - rflags: regs.rflags, - }; + let new_regs = regs.into(); vcpu_fd .set_regs(&new_regs) diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index eac14f323..7a2234436 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -46,6 +46,7 @@ use thiserror::Error; use x86_64_target::HyperlightSandboxTarget; use super::InterruptHandle; +use crate::hypervisor::regs::CommonRegisters; use crate::mem::layout::SandboxMemoryLayout; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::HostSharedMemory; @@ -88,29 +89,6 @@ impl From for TargetError { } } -/// Struct that contains the x86_64 core registers -#[derive(Debug, Default)] -pub(crate) struct X86_64Regs { - pub(crate) rax: u64, - pub(crate) rbx: u64, - pub(crate) rcx: u64, - pub(crate) rdx: u64, - pub(crate) rsi: u64, - pub(crate) rdi: u64, - pub(crate) rbp: u64, - pub(crate) rsp: u64, - pub(crate) r8: u64, - pub(crate) r9: u64, - pub(crate) r10: u64, - pub(crate) r11: u64, - pub(crate) r12: u64, - pub(crate) r13: u64, - pub(crate) r14: u64, - pub(crate) r15: u64, - pub(crate) rip: u64, - pub(crate) rflags: u64, -} - /// Defines the possible reasons for which a vCPU can be stopped when debugging #[derive(Debug)] pub enum VcpuStopReason { @@ -140,7 +118,7 @@ pub(crate) enum DebugMsg { RemoveSwBreakpoint(u64), Step, WriteAddr(u64, Vec), - WriteRegisters(X86_64Regs), + WriteRegisters(CommonRegisters), } /// Enumerates the possible responses that a hypervisor can provide to a debugger @@ -155,7 +133,7 @@ pub(crate) enum DebugResponse { NotAllowed, InterruptHandle(Arc), ReadAddr(Vec), - ReadRegisters(X86_64Regs), + ReadRegisters(CommonRegisters), RemoveHwBreakpoint(bool), RemoveSwBreakpoint(bool), Step, @@ -183,13 +161,13 @@ pub(super) trait GuestDebug { fn delete_sw_breakpoint_data(&mut self, addr: &u64) -> Option<[u8; 1]>; /// Read registers - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> crate::Result<()>; + fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> crate::Result<()>; /// Enables or disables stepping and sets the vCPU debug configuration fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> crate::Result<()>; /// Translates the guest address to physical address fn translate_gva(&self, vcpu_fd: &Self::Vcpu, gva: u64) -> crate::Result; /// Write registers - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> crate::Result<()>; + fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> crate::Result<()>; /// Adds hardware breakpoint fn add_hw_breakpoint(&mut self, vcpu_fd: &Self::Vcpu, addr: u64) -> crate::Result<()> { @@ -449,7 +427,7 @@ mod tests { let res = gdb_conn.try_recv(); assert!(res.is_err()); - let res = hyp_conn.send(DebugResponse::ReadRegisters(X86_64Regs::default())); + let res = hyp_conn.send(DebugResponse::ReadRegisters(Default::default())); assert!(res.is_ok()); let res = gdb_conn.recv(); diff --git a/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs index 9688d9ed1..338d7aabe 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs @@ -28,12 +28,12 @@ use std::collections::HashMap; use mshv_bindings::{ DebugRegisters, HV_TRANSLATE_GVA_VALIDATE_READ, HV_TRANSLATE_GVA_VALIDATE_WRITE, - StandardRegisters, }; use mshv_ioctls::VcpuFd; use super::arch::{MAX_NO_OF_HW_BP, SW_BP_SIZE, vcpu_stop_reason}; -use super::{GuestDebug, VcpuStopReason, X86_64Regs}; +use super::{GuestDebug, VcpuStopReason}; +use crate::hypervisor::regs::CommonRegisters; use crate::{HyperlightError, Result, new_error}; #[derive(Debug, Default)] @@ -194,7 +194,7 @@ impl GuestDebug for MshvDebug { self.sw_breakpoints.remove(addr) } - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> Result<()> { + fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> Result<()> { log::debug!("Read registers"); let vcpu_regs = vcpu_fd .get_regs() @@ -236,29 +236,9 @@ impl GuestDebug for MshvDebug { Ok(addr) } - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> Result<()> { + fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> Result<()> { log::debug!("Write registers"); - let new_regs = StandardRegisters { - rax: regs.rax, - rbx: regs.rbx, - rcx: regs.rcx, - rdx: regs.rdx, - rsi: regs.rsi, - rdi: regs.rdi, - rbp: regs.rbp, - rsp: regs.rsp, - r8: regs.r8, - r9: regs.r9, - r10: regs.r10, - r11: regs.r11, - r12: regs.r12, - r13: regs.r13, - r14: regs.r14, - r15: regs.r15, - - rip: regs.rip, - rflags: regs.rflags, - }; + let new_regs = regs.into(); vcpu_fd .set_regs(&new_regs) diff --git a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs b/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs index a3e36ebb7..3b86caedf 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs @@ -31,8 +31,9 @@ use gdbstub::target::ext::section_offsets::{Offsets, SectionOffsets}; use gdbstub::target::{Target, TargetError, TargetResult}; use gdbstub_arch::x86::X86_64_SSE as GdbTargetArch; -use super::{DebugCommChannel, DebugMsg, DebugResponse, GdbTargetError, X86_64Regs}; +use super::{DebugCommChannel, DebugMsg, DebugResponse, GdbTargetError}; use crate::hypervisor::InterruptHandle; +use crate::hypervisor::regs::CommonRegisters; /// Gdbstub target used by the gdbstub crate to provide GDB protocol implementation pub(crate) struct HyperlightSandboxTarget { @@ -248,7 +249,7 @@ impl SingleThreadBase for HyperlightSandboxTarget { ) -> TargetResult<(), Self> { log::debug!("Write regs"); - let regs = X86_64Regs { + let regs = CommonRegisters { rax: regs.regs[0], rbx: regs.regs[1], rcx: regs.regs[2], @@ -482,7 +483,7 @@ mod tests { // Check response to read registers - send the response first to not be blocked // by the recv call in the target - let msg = DebugResponse::ReadRegisters(X86_64Regs::default()); + let msg = DebugResponse::ReadRegisters(CommonRegisters::default()); let res = gdb_conn.send(msg); assert!(res.is_ok()); diff --git a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs b/src/hyperlight_host/src/hypervisor/hyperv_linux.rs index aaef9316d..dd9055104 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs +++ b/src/hyperlight_host/src/hypervisor/hyperv_linux.rs @@ -32,7 +32,7 @@ use log::{LevelFilter, error}; #[cfg(mshv2)] use mshv_bindings::hv_message; use mshv_bindings::{ - FloatingPointUnit, SegmentRegister, SpecialRegisters, StandardRegisters, hv_message_type, + FloatingPointUnit, SpecialRegisters, StandardRegisters, hv_message_type, hv_message_type_HVMSG_GPA_INTERCEPT, hv_message_type_HVMSG_UNMAPPED_GPA, hv_message_type_HVMSG_X64_HALT, hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT, hv_register_assoc, hv_register_name_HV_X64_REGISTER_RIP, hv_register_value, mshv_user_mem_region, @@ -48,32 +48,20 @@ use mshv_bindings::{ hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, hv_partition_synthetic_processor_features, }; -#[cfg(feature = "trace_guest")] -use mshv_bindings::{ - hv_register_name, hv_register_name_HV_X64_REGISTER_RAX, hv_register_name_HV_X64_REGISTER_RBP, - hv_register_name_HV_X64_REGISTER_RCX, hv_register_name_HV_X64_REGISTER_RSP, -}; use mshv_ioctls::{Mshv, VcpuFd, VmFd}; use tracing::{Span, instrument}; #[cfg(crashdump)] use {super::crashdump, std::path::Path}; -#[cfg(feature = "trace_guest")] -use super::TraceRegister; -use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT}; #[cfg(gdb)] use super::gdb::{ DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, MshvDebug, VcpuStopReason, }; -#[cfg(feature = "init-paging")] -use super::{ - CR0_AM, CR0_ET, CR0_MP, CR0_NE, CR0_PE, CR0_PG, CR0_WP, CR4_OSFXSR, CR4_OSXMMEXCPT, CR4_PAE, - EFER_LMA, EFER_LME, EFER_NX, EFER_SCE, -}; use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU}; #[cfg(gdb)] use crate::HyperlightError; use crate::hypervisor::get_memory_access_violation; +use crate::hypervisor::regs::CommonFpu; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; @@ -93,7 +81,7 @@ mod debug { use super::mshv_bindings::hv_x64_exception_intercept_message; use super::{HypervLinuxDriver, *}; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason, X86_64Regs}; + use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::HostSharedMemory; use crate::{Result, new_error}; @@ -193,7 +181,7 @@ mod debug { Ok(DebugResponse::ReadAddr(data)) } DebugMsg::ReadRegisters => { - let mut regs = X86_64Regs::default(); + let mut regs = Default::default(); debug .read_regs(&self.vcpu_fd, &mut regs) @@ -370,7 +358,7 @@ impl HypervLinuxDriver { vm_fd }; - let mut vcpu_fd = vm_fd.create_vcpu(0)?; + let vcpu_fd = vm_fd.create_vcpu(0)?; #[cfg(gdb)] let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { @@ -414,8 +402,6 @@ impl HypervLinuxDriver { vm_fd.map_user_memory(mshv_region) })?; - Self::setup_initial_sregs(&mut vcpu_fd, pml4_ptr.absolute()?)?; - let interrupt_handle = Arc::new(LinuxInterruptHandle { running: AtomicU64::new(0), cancel_requested: AtomicBool::new(false), @@ -440,7 +426,6 @@ impl HypervLinuxDriver { dropped: AtomicBool::new(false), }); - #[allow(unused_mut)] let mut hv = Self { _mshv: mshv, page_size: 0, @@ -463,6 +448,8 @@ impl HypervLinuxDriver { trace_info, }; + hv.setup_initial_sregs(pml4_ptr.absolute()?)?; + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] @@ -472,65 +459,6 @@ impl HypervLinuxDriver { Ok(hv) } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn setup_initial_sregs(vcpu: &mut VcpuFd, _pml4_addr: u64) -> Result<()> { - #[cfg(feature = "init-paging")] - let sregs = SpecialRegisters { - cr0: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, - cr4: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, - cr3: _pml4_addr, - efer: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, - cs: SegmentRegister { - type_: 11, - present: 1, - s: 1, - l: 1, - ..Default::default() - }, - tr: SegmentRegister { - limit: 65535, - type_: 11, - present: 1, - ..Default::default() - }, - ..Default::default() - }; - - #[cfg(not(feature = "init-paging"))] - let sregs = SpecialRegisters { - cs: SegmentRegister { - base: 0, - selector: 0, - limit: 0xFFFF, - type_: 11, - present: 1, - s: 1, - ..Default::default() - }, - ds: SegmentRegister { - base: 0, - selector: 0, - limit: 0xFFFF, - type_: 3, - present: 1, - s: 1, - ..Default::default() - }, - tr: SegmentRegister { - base: 0, - selector: 0, - limit: 0xFFFF, - type_: 11, - present: 1, - s: 0, - ..Default::default() - }, - ..Default::default() - }; - vcpu.set_sregs(&sregs)?; - Ok(()) - } } impl Debug for HypervLinuxDriver { @@ -563,19 +491,6 @@ impl Debug for HypervLinuxDriver { } } -#[cfg(feature = "trace_guest")] -impl From for hv_register_name { - fn from(r: TraceRegister) -> Self { - match r { - TraceRegister::RAX => hv_register_name_HV_X64_REGISTER_RAX, - TraceRegister::RCX => hv_register_name_HV_X64_REGISTER_RCX, - TraceRegister::RIP => hv_register_name_HV_X64_REGISTER_RIP, - TraceRegister::RSP => hv_register_name_HV_X64_REGISTER_RSP, - TraceRegister::RBP => hv_register_name_HV_X64_REGISTER_RBP, - } - } -} - impl Hypervisor for HypervLinuxDriver { #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] fn initialise( @@ -670,13 +585,7 @@ impl Hypervisor for HypervLinuxDriver { self.vcpu_fd.set_regs(®s)?; // reset fpu state - let fpu = FloatingPointUnit { - fcw: FP_CONTROL_WORD_DEFAULT, - ftwx: FP_TAG_WORD_DEFAULT, - mxcsr: MXCSR_DEFAULT, - ..Default::default() // zero out the rest - }; - self.vcpu_fd.set_fpu(&fpu)?; + self.set_fpu(&CommonFpu::default())?; // run VirtualCPU::run( @@ -950,6 +859,39 @@ impl Hypervisor for HypervLinuxDriver { Ok(result) } + fn regs(&self) -> Result { + let mshv_regs = self.vcpu_fd.get_regs()?; + Ok((&mshv_regs).into()) + } + + fn set_regs(&mut self, regs: &super::regs::CommonRegisters) -> Result<()> { + let mshv_regs: StandardRegisters = regs.into(); + self.vcpu_fd.set_regs(&mshv_regs)?; + Ok(()) + } + + fn fpu(&self) -> Result { + let mshv_fpu = self.vcpu_fd.get_fpu()?; + Ok((&mshv_fpu).into()) + } + + fn set_fpu(&mut self, fpu: &super::regs::CommonFpu) -> Result<()> { + let mshv_fpu: FloatingPointUnit = fpu.into(); + self.vcpu_fd.set_fpu(&mshv_fpu)?; + Ok(()) + } + + fn sregs(&self) -> Result { + let mshv_sregs = self.vcpu_fd.get_sregs()?; + Ok((&mshv_sregs).into()) + } + + fn set_sregs(&mut self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { + let mshv_sregs: SpecialRegisters = sregs.into(); + self.vcpu_fd.set_sregs(&mshv_sregs)?; + Ok(()) + } + #[instrument(skip_all, parent = Span::current(), level = "Trace")] fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { self as &mut dyn Hypervisor @@ -1159,17 +1101,6 @@ impl Hypervisor for HypervLinuxDriver { } } - #[cfg(feature = "trace_guest")] - fn read_trace_reg(&self, reg: TraceRegister) -> Result { - let mut assoc = [hv_register_assoc { - name: reg.into(), - ..Default::default() - }]; - self.vcpu_fd.get_reg(&mut assoc)?; - // safety: all registers that we currently support are 64-bit - unsafe { Ok(assoc[0].value.reg64) } - } - #[cfg(feature = "trace_guest")] fn trace_info_as_ref(&self) -> &TraceInfo { &self.trace_info diff --git a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs b/src/hyperlight_host/src/hypervisor/hyperv_windows.rs index 620910004..98764106d 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs +++ b/src/hyperlight_host/src/hypervisor/hyperv_windows.rs @@ -23,13 +23,8 @@ use std::sync::{Arc, Mutex}; use log::LevelFilter; use tracing::{Span, instrument}; use windows::Win32::System::Hypervisor::{ - WHV_MEMORY_ACCESS_TYPE, WHV_PARTITION_HANDLE, WHV_REGISTER_VALUE, WHV_RUN_VP_EXIT_CONTEXT, - WHV_RUN_VP_EXIT_REASON, WHV_X64_SEGMENT_REGISTER, WHV_X64_SEGMENT_REGISTER_0, - WHvCancelRunVirtualProcessor, WHvX64RegisterCs, -}; -#[cfg(feature = "init-paging")] -use windows::Win32::System::Hypervisor::{ - WHvX64RegisterCr0, WHvX64RegisterCr3, WHvX64RegisterCr4, WHvX64RegisterEfer, + WHV_MEMORY_ACCESS_TYPE, WHV_PARTITION_HANDLE, WHV_RUN_VP_EXIT_CONTEXT, WHV_RUN_VP_EXIT_REASON, + WHvCancelRunVirtualProcessor, }; #[cfg(crashdump)] use {super::crashdump, std::path::Path}; @@ -41,22 +36,14 @@ use { crate::HyperlightError, }; -#[cfg(feature = "trace_guest")] -use super::TraceRegister; -use super::fpu::{FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT}; +use super::regs::CommonSpecialRegisters; use super::surrogate_process::SurrogateProcess; use super::surrogate_process_manager::*; use super::windows_hypervisor_platform::{VMPartition, VMProcessor}; -use super::wrappers::{HandleWrapper, WHvFPURegisters}; -#[cfg(feature = "init-paging")] -use super::{ - CR0_AM, CR0_ET, CR0_MP, CR0_NE, CR0_PE, CR0_PG, CR0_WP, CR4_OSFXSR, CR4_OSXMMEXCPT, CR4_PAE, - EFER_LMA, EFER_LME, EFER_NX, EFER_SCE, -}; +use super::wrappers::HandleWrapper; use super::{HyperlightExit, Hypervisor, InterruptHandle, VirtualCPU}; -use crate::hypervisor::fpu::FP_CONTROL_WORD_DEFAULT; use crate::hypervisor::get_memory_access_violation; -use crate::hypervisor::wrappers::WHvGeneralRegisters; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; @@ -77,7 +64,7 @@ mod debug { use super::{HypervWindowsDriver, *}; use crate::Result; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason, X86_64Regs}; + use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::HostSharedMemory; @@ -176,7 +163,7 @@ mod debug { Ok(DebugResponse::ReadAddr(data)) } DebugMsg::ReadRegisters => { - let mut regs = X86_64Regs::default(); + let mut regs = Default::default(); debug .read_regs(&self.processor, &mut regs) @@ -328,8 +315,7 @@ impl HypervWindowsDriver { partition.map_gpa_range(&mem_regions, &surrogate_process)?; - let mut proc = VMProcessor::new(partition)?; - Self::setup_initial_sregs(&mut proc, pml4_address)?; + let proc = VMProcessor::new(partition)?; let partition_handle = proc.get_partition_hdl(); #[cfg(gdb)] @@ -351,17 +337,16 @@ impl HypervWindowsDriver { dropped: AtomicBool::new(false), }); - #[allow(unused_mut)] let mut hv = Self { processor: proc, _surrogate_process: surrogate_process, entrypoint, orig_rsp: GuestPtr::try_from(RawPtr::from(rsp))?, - sandbox_regions: mem_regions, - mmap_regions: Vec::new(), interrupt_handle: interrupt_handle.clone(), mem_mgr: None, host_funcs: None, + sandbox_regions: mem_regions, + mmap_regions: Vec::new(), #[cfg(gdb)] debug, #[cfg(gdb)] @@ -372,6 +357,8 @@ impl HypervWindowsDriver { trace_info, }; + hv.setup_initial_sregs(pml4_address)?; + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] @@ -382,62 +369,6 @@ impl HypervWindowsDriver { Ok(hv) } - fn setup_initial_sregs(proc: &mut VMProcessor, _pml4_addr: u64) -> Result<()> { - #[cfg(feature = "init-paging")] - proc.set_registers(&[ - (WHvX64RegisterCr3, WHV_REGISTER_VALUE { Reg64: _pml4_addr }), - ( - WHvX64RegisterCr4, - WHV_REGISTER_VALUE { - Reg64: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, - }, - ), - ( - WHvX64RegisterCr0, - WHV_REGISTER_VALUE { - Reg64: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, - }, - ), - ( - WHvX64RegisterEfer, - WHV_REGISTER_VALUE { - Reg64: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, - }, - ), - ( - WHvX64RegisterCs, - WHV_REGISTER_VALUE { - Segment: WHV_X64_SEGMENT_REGISTER { - Anonymous: WHV_X64_SEGMENT_REGISTER_0 { - Attributes: 0b1011 | (1 << 4) | (1 << 7) | (1 << 13), // Type (11: Execute/Read, accessed) | L (64-bit mode) | P (present) | S (code segment) - }, - ..Default::default() // zero out the rest - }, - }, - ), - ])?; - - #[cfg(not(feature = "init-paging"))] - { - proc.set_registers(&[( - WHvX64RegisterCs, - WHV_REGISTER_VALUE { - Segment: WHV_X64_SEGMENT_REGISTER { - Base: 0, - Selector: 0, - Limit: 0xFFFF, - Anonymous: WHV_X64_SEGMENT_REGISTER_0 { - Attributes: 0b1011 | (1 << 4) | (1 << 7), // Type (11: Execute/Read, accessed) | S (code segment) | P (present) - }, - ..Default::default() - }, - }, - )])?; - } - - Ok(()) - } - #[inline] #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] fn get_exit_details(&self, exit_reason: WHV_RUN_VP_EXIT_REASON) -> Result { @@ -445,7 +376,7 @@ impl HypervWindowsDriver { error.push_str(&format!( "Did not receive a halt from Hypervisor as expected - Received {exit_reason:?}!\n" )); - error.push_str(&format!("Registers: \n{:#?}", self.processor.get_regs()?)); + error.push_str(&format!("Registers: \n{:#?}", self.processor.regs()?)); Ok(error) } } @@ -465,127 +396,14 @@ impl Debug for HypervWindowsDriver { } // Get the registers - - let regs = self.processor.get_regs(); - - if let Ok(regs) = regs { - { - fs.field("Registers", ®s); - } + if let Ok(regs) = self.processor.regs() { + fs.field("Registers", ®s); } // Get the special registers - - let special_regs = self.processor.get_sregs(); - if let Ok(special_regs) = special_regs { - fs.field("CR0", unsafe { &special_regs.cr0.Reg64 }); - fs.field("CR2", unsafe { &special_regs.cr2.Reg64 }); - fs.field("CR3", unsafe { &special_regs.cr3.Reg64 }); - fs.field("CR4", unsafe { &special_regs.cr4.Reg64 }); - fs.field("CR8", unsafe { &special_regs.cr8.Reg64 }); - fs.field("EFER", unsafe { &special_regs.efer.Reg64 }); - fs.field("APIC_BASE", unsafe { &special_regs.apic_base.Reg64 }); - - // Segment registers - fs.field( - "CS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.cs.Segment.Base }, - unsafe { &special_regs.cs.Segment.Limit }, - unsafe { &special_regs.cs.Segment.Selector }, - unsafe { &special_regs.cs.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "DS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.ds.Segment.Base }, - unsafe { &special_regs.ds.Segment.Limit }, - unsafe { &special_regs.ds.Segment.Selector }, - unsafe { &special_regs.ds.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "ES", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.es.Segment.Base }, - unsafe { &special_regs.es.Segment.Limit }, - unsafe { &special_regs.es.Segment.Selector }, - unsafe { &special_regs.es.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "FS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.fs.Segment.Base }, - unsafe { &special_regs.fs.Segment.Limit }, - unsafe { &special_regs.fs.Segment.Selector }, - unsafe { &special_regs.fs.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "GS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.gs.Segment.Base }, - unsafe { &special_regs.gs.Segment.Limit }, - unsafe { &special_regs.gs.Segment.Selector }, - unsafe { &special_regs.gs.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "SS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.ss.Segment.Base }, - unsafe { &special_regs.ss.Segment.Limit }, - unsafe { &special_regs.ss.Segment.Selector }, - unsafe { &special_regs.ss.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "TR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.tr.Segment.Base }, - unsafe { &special_regs.tr.Segment.Limit }, - unsafe { &special_regs.tr.Segment.Selector }, - unsafe { &special_regs.tr.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "LDTR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.ldtr.Segment.Base }, - unsafe { &special_regs.ldtr.Segment.Limit }, - unsafe { &special_regs.ldtr.Segment.Selector }, - unsafe { &special_regs.ldtr.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "GDTR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Pad: {:?} }}", - unsafe { &special_regs.gdtr.Table.Base }, - unsafe { &special_regs.gdtr.Table.Limit }, - unsafe { &special_regs.gdtr.Table.Pad } - ), - ); - fs.field( - "IDTR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Pad: {:?} }}", - unsafe { &special_regs.idtr.Table.Base }, - unsafe { &special_regs.idtr.Table.Limit }, - unsafe { &special_regs.idtr.Table.Pad } - ), - ); - }; + if let Ok(special_regs) = self.processor.sregs() { + fs.field("SpecialRegisters", &special_regs); + } fs.finish() } @@ -611,7 +429,7 @@ impl Hypervisor for HypervWindowsDriver { None => self.get_max_log_level().into(), }; - let regs = WHvGeneralRegisters { + let regs = CommonRegisters { rip: self.entrypoint, rsp: self.orig_rsp.absolute()?, @@ -624,7 +442,7 @@ impl Hypervisor for HypervWindowsDriver { ..Default::default() }; - self.processor.set_general_purpose_registers(®s)?; + self.set_regs(®s)?; VirtualCPU::run( self.as_mut_hypervisor(), @@ -654,21 +472,16 @@ impl Hypervisor for HypervWindowsDriver { #[cfg(gdb)] dbg_mem_access_hdl: Arc>>, ) -> Result<()> { // Reset general purpose registers, then set RIP and RSP - let regs = WHvGeneralRegisters { + let regs = CommonRegisters { rip: dispatch_func_addr.into(), rsp: self.orig_rsp.absolute()?, rflags: 1 << 1, // eflags bit index 1 is reserved and always needs to be 1 ..Default::default() }; - self.processor.set_general_purpose_registers(®s)?; + self.processor.set_regs(®s)?; // reset fpu state - self.processor.set_fpu(&WHvFPURegisters { - fp_control_word: FP_CONTROL_WORD_DEFAULT, - fp_tag_word: FP_TAG_WORD_DEFAULT, - mxcsr: MXCSR_DEFAULT, - ..Default::default() // zero out the rest - })?; + self.processor.set_fpu(&CommonFpu::default())?; VirtualCPU::run( self.as_mut_hypervisor(), @@ -728,9 +541,9 @@ impl Hypervisor for HypervWindowsDriver { handle_outb(mem_mgr, host_funcs, port, val)?; } - let mut regs = self.processor.get_regs()?; + let mut regs = self.regs()?; regs.rip = rip + instruction_length; - self.processor.set_general_purpose_registers(®s) + self.set_regs(®s) } #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] @@ -884,6 +697,35 @@ impl Hypervisor for HypervWindowsDriver { Ok(result) } + /// Get regs + #[allow(dead_code)] + fn regs(&self) -> Result { + self.processor.regs() + } + /// Set regs + fn set_regs(&mut self, regs: &CommonRegisters) -> Result<()> { + self.processor.set_regs(regs) + } + /// Get fpu regs + #[allow(dead_code)] + fn fpu(&self) -> Result { + self.processor.fpu() + } + /// Set fpu regs + fn set_fpu(&mut self, fpu: &CommonFpu) -> Result<()> { + self.processor.set_fpu(fpu) + } + /// Get special regs + #[allow(dead_code)] + fn sregs(&self) -> Result { + self.processor.sregs() + } + /// Set special regs + #[allow(dead_code)] + fn set_sregs(&mut self, sregs: &CommonSpecialRegisters) -> Result<()> { + self.processor.set_sregs(sregs) + } + fn interrupt_handle(&self) -> Arc { self.interrupt_handle.clone() } @@ -898,8 +740,8 @@ impl Hypervisor for HypervWindowsDriver { if self.rt_cfg.guest_core_dump { let mut regs = [0; 27]; - let vcpu_regs = self.processor.get_regs()?; - let sregs = self.processor.get_sregs()?; + let vcpu_regs = self.processor.regs()?; + let sregs = self.processor.sregs()?; let xsave = self.processor.get_xsave()?; // Set the registers in the order expected by the crashdump context @@ -920,16 +762,16 @@ impl Hypervisor for HypervWindowsDriver { regs[14] = vcpu_regs.rdi; // rdi regs[15] = 0; // orig rax regs[16] = vcpu_regs.rip; // rip - regs[17] = unsafe { sregs.cs.Segment.Selector } as u64; // cs + regs[17] = sregs.cs.selector as u64; // cs regs[18] = vcpu_regs.rflags; // eflags regs[19] = vcpu_regs.rsp; // rsp - regs[20] = unsafe { sregs.ss.Segment.Selector } as u64; // ss - regs[21] = unsafe { sregs.fs.Segment.Base }; // fs_base - regs[22] = unsafe { sregs.gs.Segment.Base }; // gs_base - regs[23] = unsafe { sregs.ds.Segment.Selector } as u64; // ds - regs[24] = unsafe { sregs.es.Segment.Selector } as u64; // es - regs[25] = unsafe { sregs.fs.Segment.Selector } as u64; // fs - regs[26] = unsafe { sregs.gs.Segment.Selector } as u64; // gs + regs[20] = sregs.ss.selector as u64; // ss + regs[21] = sregs.fs.base; // fs_base + regs[22] = sregs.gs.base; // gs_base + regs[23] = sregs.ds.selector as u64; // ds + regs[24] = sregs.es.selector as u64; // es + regs[25] = sregs.fs.selector as u64; // fs + regs[26] = sregs.gs.selector as u64; // gs // Get the filename from the config let filename = self.rt_cfg.binary_path.clone().and_then(|path| { @@ -1092,18 +934,6 @@ impl Hypervisor for HypervWindowsDriver { } } - #[cfg(feature = "trace_guest")] - fn read_trace_reg(&self, reg: TraceRegister) -> Result { - let regs = self.processor.get_regs()?; - match reg { - TraceRegister::RAX => Ok(regs.rax), - TraceRegister::RCX => Ok(regs.rcx), - TraceRegister::RIP => Ok(regs.rip), - TraceRegister::RSP => Ok(regs.rsp), - TraceRegister::RBP => Ok(regs.rbp), - } - } - #[cfg(feature = "trace_guest")] fn trace_info_as_ref(&self) -> &TraceInfo { &self.trace_info diff --git a/src/hyperlight_host/src/hypervisor/kvm.rs b/src/hyperlight_host/src/hypervisor/kvm.rs index a69b51f55..3545074ea 100644 --- a/src/hyperlight_host/src/hypervisor/kvm.rs +++ b/src/hyperlight_host/src/hypervisor/kvm.rs @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -use std::convert::TryFrom; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; -use kvm_bindings::{kvm_fpu, kvm_regs, kvm_userspace_memory_region}; +use kvm_bindings::{kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region}; use kvm_ioctls::Cap::UserMemory; use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use log::LevelFilter; @@ -27,20 +26,13 @@ use tracing::{Span, instrument}; #[cfg(crashdump)] use {super::crashdump, std::path::Path}; -#[cfg(feature = "trace_guest")] -use super::TraceRegister; -use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT}; #[cfg(gdb)] use super::gdb::{DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason}; -#[cfg(feature = "init-paging")] -use super::{ - CR0_AM, CR0_ET, CR0_MP, CR0_NE, CR0_PE, CR0_PG, CR0_WP, CR4_OSFXSR, CR4_OSXMMEXCPT, CR4_PAE, - EFER_LMA, EFER_LME, EFER_NX, EFER_SCE, -}; use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU}; #[cfg(gdb)] use crate::HyperlightError; use crate::hypervisor::get_memory_access_violation; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; @@ -83,9 +75,7 @@ mod debug { use kvm_bindings::kvm_debug_exit_arch; use super::KVMDriver; - use crate::hypervisor::gdb::{ - DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason, X86_64Regs, - }; + use crate::hypervisor::gdb::{DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason}; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::HostSharedMemory; use crate::{Result, new_error}; @@ -185,7 +175,7 @@ mod debug { Ok(DebugResponse::ReadAddr(data)) } DebugMsg::ReadRegisters => { - let mut regs = X86_64Regs::default(); + let mut regs = Default::default(); debug .read_regs(&self.vcpu_fd, &mut regs) @@ -337,8 +327,7 @@ impl KVMDriver { unsafe { vm_fd.set_user_memory_region(kvm_region) } })?; - let mut vcpu_fd = vm_fd.create_vcpu(0)?; - Self::setup_initial_sregs(&mut vcpu_fd, pml4_addr)?; + let vcpu_fd = vm_fd.create_vcpu(0)?; #[cfg(gdb)] let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { @@ -377,8 +366,7 @@ impl KVMDriver { sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(), }); - #[allow(unused_mut)] - let mut hv = Self { + let mut kvm = Self { _kvm: kvm, vm_fd, page_size: 0, @@ -402,34 +390,16 @@ impl KVMDriver { trace_info, }; + kvm.setup_initial_sregs(pml4_addr)?; + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] - if hv.debug.is_some() { - hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; + if kvm.debug.is_some() { + kvm.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; } - Ok(hv) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn setup_initial_sregs(vcpu_fd: &mut VcpuFd, _pml4_addr: u64) -> Result<()> { - // setup paging and IA-32e (64-bit) mode - let mut sregs = vcpu_fd.get_sregs()?; - cfg_if::cfg_if! { - if #[cfg(feature = "init-paging")] { - sregs.cr3 = _pml4_addr; - sregs.cr4 = CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT; - sregs.cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP; - sregs.efer = EFER_LME | EFER_LMA | EFER_SCE | EFER_NX; - sregs.cs.l = 1; // required for 64-bit mode - } else { - sregs.cs.base = 0; - sregs.cs.selector = 0; - } - } - vcpu_fd.set_sregs(&sregs)?; - Ok(()) + Ok(kvm) } } @@ -485,7 +455,7 @@ impl Hypervisor for KVMDriver { None => self.get_max_log_level().into(), }; - let regs = kvm_regs { + let regs = CommonRegisters { rip: self.entrypoint, rsp: self.orig_rsp.absolute()?, @@ -497,7 +467,7 @@ impl Hypervisor for KVMDriver { ..Default::default() }; - self.vcpu_fd.set_regs(®s)?; + self.set_regs(®s)?; VirtualCPU::run( self.as_mut_hypervisor(), @@ -572,21 +542,15 @@ impl Hypervisor for KVMDriver { #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()> { // Reset general purpose registers, then set RIP and RSP - let regs = kvm_regs { + let regs = CommonRegisters { rip: dispatch_func_addr.into(), rsp: self.orig_rsp.absolute()?, ..Default::default() }; - self.vcpu_fd.set_regs(®s)?; + self.set_regs(®s)?; // reset fpu state - let fpu = kvm_fpu { - fcw: FP_CONTROL_WORD_DEFAULT, - ftwx: FP_TAG_WORD_DEFAULT, - mxcsr: MXCSR_DEFAULT, - ..Default::default() // zero out the rest - }; - self.vcpu_fd.set_fpu(&fpu)?; + self.set_fpu(&CommonFpu::default())?; // run VirtualCPU::run( @@ -821,6 +785,39 @@ impl Hypervisor for KVMDriver { Ok(result) } + fn regs(&self) -> Result { + let kvm_regs = self.vcpu_fd.get_regs()?; + Ok((&kvm_regs).into()) + } + + fn set_regs(&mut self, regs: &super::regs::CommonRegisters) -> Result<()> { + let kvm_regs: kvm_regs = regs.into(); + self.vcpu_fd.set_regs(&kvm_regs)?; + Ok(()) + } + + fn fpu(&self) -> Result { + let kvm_fpu = self.vcpu_fd.get_fpu()?; + Ok((&kvm_fpu).into()) + } + + fn set_fpu(&mut self, fpu: &super::regs::CommonFpu) -> Result<()> { + let kvm_fpu: kvm_fpu = fpu.into(); + self.vcpu_fd.set_fpu(&kvm_fpu)?; + Ok(()) + } + + fn sregs(&self) -> Result { + let kvm_sregs = self.vcpu_fd.get_sregs()?; + Ok((&kvm_sregs).into()) + } + + fn set_sregs(&mut self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { + let kvm_sregs: kvm_sregs = sregs.into(); + self.vcpu_fd.set_sregs(&kvm_sregs)?; + Ok(()) + } + #[instrument(skip_all, parent = Span::current(), level = "Trace")] fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { self as &mut dyn Hypervisor @@ -1036,18 +1033,6 @@ impl Hypervisor for KVMDriver { } } - #[cfg(feature = "trace_guest")] - fn read_trace_reg(&self, reg: TraceRegister) -> Result { - let regs = self.vcpu_fd.get_regs()?; - Ok(match reg { - TraceRegister::RAX => regs.rax, - TraceRegister::RCX => regs.rcx, - TraceRegister::RIP => regs.rip, - TraceRegister::RSP => regs.rsp, - TraceRegister::RBP => regs.rbp, - }) - } - #[cfg(feature = "trace_guest")] fn trace_info_as_ref(&self) -> &TraceInfo { &self.trace_info diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index 9c04ad3a0..781099134 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -19,16 +19,15 @@ use tracing::{Span, instrument}; use crate::HyperlightError::StackOverflow; use crate::error::HyperlightError::ExecutionCanceledByHost; +use crate::hypervisor::regs::{ + CommonFpu, CommonRegisters, CommonSegmentRegister, CommonSpecialRegisters, +}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::metrics::METRIC_GUEST_CANCELLATION; #[cfg(feature = "trace_guest")] use crate::sandbox::TraceInfo; use crate::{HyperlightError, Result, log_then_return}; -/// Util for handling x87 fpu state -#[cfg(any(kvm, mshv, target_os = "windows"))] -pub mod fpu; - /// HyperV-on-linux functionality #[cfg(mshv)] pub mod hyperv_linux; @@ -40,6 +39,9 @@ pub(crate) mod hyperv_windows; #[cfg(gdb)] pub(crate) mod gdb; +/// Abstracts over different hypervisor register representations +pub(crate) mod regs; + #[cfg(kvm)] /// Functionality to manipulate KVM-based virtual machines pub mod kvm; @@ -116,21 +118,6 @@ pub enum HyperlightExit { Retry(), } -/// Registers which may be useful for tracing/stack unwinding -#[cfg(feature = "trace_guest")] -pub enum TraceRegister { - /// RAX - RAX, - /// RCX - RCX, - /// RIP - RIP, - /// RSP - RSP, - /// RBP - RBP, -} - /// A common set of hypervisor functionality pub(crate) trait Hypervisor: Debug + Send { /// Initialise the internally stored vCPU with the given PEB address and @@ -189,6 +176,87 @@ pub(crate) trait Hypervisor: Debug + Send { /// Get InterruptHandle to underlying VM fn interrupt_handle(&self) -> Arc; + /// Get regs + #[allow(dead_code)] + fn regs(&self) -> Result; + /// Set regs + #[allow(dead_code)] + fn set_regs(&mut self, regs: &CommonRegisters) -> Result<()>; + /// Get fpu regs + #[allow(dead_code)] + fn fpu(&self) -> Result; + /// Set fpu regs + #[allow(dead_code)] + fn set_fpu(&mut self, fpu: &CommonFpu) -> Result<()>; + /// Get special regs + #[allow(dead_code)] + fn sregs(&self) -> Result; + /// Set special regs + #[allow(dead_code)] + fn set_sregs(&mut self, sregs: &CommonSpecialRegisters) -> Result<()>; + + /// Setup initial special registers for the hypervisor + /// This is a default implementation that works for all hypervisors + fn setup_initial_sregs(&mut self, _pml4_addr: u64) -> Result<()> { + #[cfg(feature = "init-paging")] + let sregs = CommonSpecialRegisters { + cr0: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, + cr4: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, + cr3: _pml4_addr, + efer: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, + cs: CommonSegmentRegister { + type_: 11, + present: 1, + s: 1, + l: 1, + ..Default::default() + }, + tr: CommonSegmentRegister { + limit: 65535, + type_: 11, + present: 1, + s: 0, + ..Default::default() + }, + ..Default::default() + }; + + #[cfg(not(feature = "init-paging"))] + let sregs = CommonSpecialRegisters { + cs: CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 11, + present: 1, + s: 1, + ..Default::default() + }, + ds: CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 3, + present: 1, + s: 1, + ..Default::default() + }, + tr: CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 11, + present: 1, + s: 0, + ..Default::default() + }, + ..Default::default() + }; + + self.set_sregs(&sregs)?; + Ok(()) + } + /// Get the logging level to pass to the guest entrypoint fn get_max_log_level(&self) -> u32 { // Check to see if the RUST_LOG environment variable is set @@ -245,10 +313,6 @@ pub(crate) trait Hypervisor: Debug + Send { /// Check stack guard to see if the stack is still valid fn check_stack_guard(&self) -> Result; - /// Read a register for trace/unwind purposes - #[cfg(feature = "trace_guest")] - fn read_trace_reg(&self, reg: TraceRegister) -> Result; - /// Get a reference of the trace info for the guest #[cfg(feature = "trace_guest")] fn trace_info_as_ref(&self) -> &TraceInfo; diff --git a/src/hyperlight_host/src/hypervisor/fpu.rs b/src/hyperlight_host/src/hypervisor/regs.rs similarity index 56% rename from src/hyperlight_host/src/hypervisor/fpu.rs rename to src/hyperlight_host/src/hypervisor/regs.rs index f0b6cb6a2..d29edf4bf 100644 --- a/src/hyperlight_host/src/hypervisor/fpu.rs +++ b/src/hyperlight_host/src/hypervisor/regs.rs @@ -14,6 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -pub(crate) const FP_CONTROL_WORD_DEFAULT: u16 = 0x37f; // mask all fp-exception, set rounding to nearest, set precision to 64-bit -pub(crate) const FP_TAG_WORD_DEFAULT: u8 = 0xff; // each 8 of x87 fpu registers is empty -pub(crate) const MXCSR_DEFAULT: u32 = 0x1f80; // mask simd fp-exceptions, clear exception flags, set rounding to nearest, disable flush-to-zero mode, disable denormals-are-zero mode +mod fpu; +mod special_regs; +mod standard_regs; + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +pub(crate) use fpu::*; +pub(crate) use special_regs::*; +pub(crate) use standard_regs::*; + +#[cfg(target_os = "windows")] +#[derive(Debug, PartialEq)] +pub(crate) enum FromWhpRegisterError { + MissingRegister(HashSet), + InvalidLength(usize), + InvalidEncoding, + DuplicateRegister(i32), + InvalidRegister(i32), +} diff --git a/src/hyperlight_host/src/hypervisor/regs/fpu.rs b/src/hyperlight_host/src/hypervisor/regs/fpu.rs new file mode 100644 index 000000000..243abbbb8 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/fpu.rs @@ -0,0 +1,466 @@ +/* +Copyright 2024 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(mshv2)] +extern crate mshv_bindings2 as mshv_bindings; +#[cfg(mshv2)] +extern crate mshv_ioctls2 as mshv_ioctls; + +#[cfg(mshv3)] +extern crate mshv_bindings3 as mshv_bindings; +#[cfg(mshv3)] +extern crate mshv_ioctls3 as mshv_ioctls; + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +#[cfg(kvm)] +use kvm_bindings::kvm_fpu; +#[cfg(mshv)] +use mshv_bindings::FloatingPointUnit; + +#[cfg(target_os = "windows")] +use super::Align16; +#[cfg(target_os = "windows")] +use crate::hypervisor::regs::FromWhpRegisterError; + +pub(crate) const FP_CONTROL_WORD_DEFAULT: u16 = 0x37f; // mask all fp-exception, set rounding to nearest, set precision to 64-bit +pub(crate) const MXCSR_DEFAULT: u32 = 0x1f80; // mask simd fp-exceptions, clear exception flags, set rounding to nearest, disable flush-to-zero mode, disable denormals-are-zero mode + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct CommonFpu { + pub fpr: [[u8; 16]; 8], + pub fcw: u16, + pub fsw: u16, + pub ftwx: u8, + pub pad1: u8, + pub last_opcode: u16, + pub last_ip: u64, + pub last_dp: u64, + pub xmm: [[u8; 16]; 16], + pub mxcsr: u32, + pub pad2: u32, +} + +impl Default for CommonFpu { + fn default() -> Self { + Self { + fpr: [[0u8; 16]; 8], + fcw: FP_CONTROL_WORD_DEFAULT, + fsw: 0, + ftwx: 0, + pad1: 0, + last_opcode: 0, + last_ip: 0, + last_dp: 0, + xmm: [[0u8; 16]; 16], + mxcsr: MXCSR_DEFAULT, + pad2: 0, + } + } +} + +#[cfg(kvm)] +impl From<&CommonFpu> for kvm_fpu { + fn from(common_fpu: &CommonFpu) -> Self { + kvm_fpu { + fpr: common_fpu.fpr, + fcw: common_fpu.fcw, + fsw: common_fpu.fsw, + ftwx: common_fpu.ftwx, + pad1: common_fpu.pad1, + last_opcode: common_fpu.last_opcode, + last_ip: common_fpu.last_ip, + last_dp: common_fpu.last_dp, + xmm: common_fpu.xmm, + mxcsr: common_fpu.mxcsr, + pad2: common_fpu.pad2, + } + } +} + +#[cfg(mshv)] +impl From<&CommonFpu> for FloatingPointUnit { + fn from(common_fpu: &CommonFpu) -> FloatingPointUnit { + FloatingPointUnit { + fpr: common_fpu.fpr, + fcw: common_fpu.fcw, + fsw: common_fpu.fsw, + ftwx: common_fpu.ftwx, + pad1: common_fpu.pad1, + last_opcode: common_fpu.last_opcode, + last_ip: common_fpu.last_ip, + last_dp: common_fpu.last_dp, + xmm: common_fpu.xmm, + mxcsr: common_fpu.mxcsr, + pad2: common_fpu.pad2, + } + } +} + +#[cfg(kvm)] +impl From<&kvm_fpu> for CommonFpu { + fn from(kvm_fpu: &kvm_fpu) -> Self { + Self { + fpr: kvm_fpu.fpr, + fcw: kvm_fpu.fcw, + fsw: kvm_fpu.fsw, + ftwx: kvm_fpu.ftwx, + pad1: kvm_fpu.pad1, + last_opcode: kvm_fpu.last_opcode, + last_ip: kvm_fpu.last_ip, + last_dp: kvm_fpu.last_dp, + xmm: kvm_fpu.xmm, + mxcsr: kvm_fpu.mxcsr, + pad2: kvm_fpu.pad2, + } + } +} + +#[cfg(mshv)] +impl From<&FloatingPointUnit> for CommonFpu { + fn from(mshv_fpu: &FloatingPointUnit) -> Self { + Self { + fpr: mshv_fpu.fpr, + fcw: mshv_fpu.fcw, + fsw: mshv_fpu.fsw, + ftwx: mshv_fpu.ftwx, + pad1: mshv_fpu.pad1, + last_opcode: mshv_fpu.last_opcode, + last_ip: mshv_fpu.last_ip, + last_dp: mshv_fpu.last_dp, + xmm: mshv_fpu.xmm, + mxcsr: mshv_fpu.mxcsr, + pad2: mshv_fpu.pad2, + } + } +} + +#[cfg(target_os = "windows")] +use windows::Win32::System::Hypervisor::*; + +#[cfg(target_os = "windows")] +impl From<&CommonFpu> for [(WHV_REGISTER_NAME, Align16); 26] { + fn from(fpu: &CommonFpu) -> Self { + let mut regs: [(WHV_REGISTER_NAME, Align16); 26] = + [Default::default(); 26]; + let mut idx = 0; + + // FPU/MMX registers (8 x 128-bit) + for (i, reg) in fpu.fpr.iter().enumerate() { + let mut value = WHV_REGISTER_VALUE::default(); + value.Reg128 = WHV_UINT128 { + Dword: [ + u32::from_le_bytes([reg[0], reg[1], reg[2], reg[3]]), + u32::from_le_bytes([reg[4], reg[5], reg[6], reg[7]]), + u32::from_le_bytes([reg[8], reg[9], reg[10], reg[11]]), + u32::from_le_bytes([reg[12], reg[13], reg[14], reg[15]]), + ], + }; + regs[idx] = ( + WHV_REGISTER_NAME(WHvX64RegisterFpMmx0.0 + i as i32), + Align16(value), + ); + idx += 1; + } + + // FpControlStatus + let mut fp_control_status = WHV_REGISTER_VALUE::default(); + fp_control_status.FpControlStatus = WHV_X64_FP_CONTROL_STATUS_REGISTER { + Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0 { + FpControl: fpu.fcw, + FpStatus: fpu.fsw, + FpTag: fpu.ftwx, + Reserved: fpu.pad1, + LastFpOp: fpu.last_opcode, + Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0_0 { + LastFpRip: fpu.last_ip, + }, + }, + }; + regs[idx] = (WHvX64RegisterFpControlStatus, Align16(fp_control_status)); + idx += 1; + + // XMM registers (16 x 128-bit) + for (i, reg) in fpu.xmm.iter().enumerate() { + let mut value = WHV_REGISTER_VALUE::default(); + value.Reg128 = WHV_UINT128 { + Dword: [ + u32::from_le_bytes([reg[0], reg[1], reg[2], reg[3]]), + u32::from_le_bytes([reg[4], reg[5], reg[6], reg[7]]), + u32::from_le_bytes([reg[8], reg[9], reg[10], reg[11]]), + u32::from_le_bytes([reg[12], reg[13], reg[14], reg[15]]), + ], + }; + regs[idx] = ( + WHV_REGISTER_NAME(WHvX64RegisterXmm0.0 + i as i32), + Align16(value), + ); + idx += 1; + } + + // XmmControlStatus + let mut xmm_control_status = WHV_REGISTER_VALUE::default(); + xmm_control_status.XmmControlStatus = WHV_X64_XMM_CONTROL_STATUS_REGISTER { + Anonymous: WHV_X64_XMM_CONTROL_STATUS_REGISTER_0 { + XmmStatusControl: fpu.mxcsr, + XmmStatusControlMask: !0, + Anonymous: WHV_X64_XMM_CONTROL_STATUS_REGISTER_0_0 { + LastFpRdp: fpu.last_dp, + }, + }, + }; + regs[idx] = (WHvX64RegisterXmmControlStatus, Align16(xmm_control_status)); + + regs + } +} + +#[cfg(target_os = "windows")] +pub(crate) const WHP_FPU_NAMES_LEN: usize = 26; +#[cfg(target_os = "windows")] +pub(crate) const WHP_FPU_NAMES: [WHV_REGISTER_NAME; WHP_FPU_NAMES_LEN] = [ + WHvX64RegisterFpMmx0, + WHvX64RegisterFpMmx1, + WHvX64RegisterFpMmx2, + WHvX64RegisterFpMmx3, + WHvX64RegisterFpMmx4, + WHvX64RegisterFpMmx5, + WHvX64RegisterFpMmx6, + WHvX64RegisterFpMmx7, + WHvX64RegisterFpControlStatus, + WHvX64RegisterXmm0, + WHvX64RegisterXmm1, + WHvX64RegisterXmm2, + WHvX64RegisterXmm3, + WHvX64RegisterXmm4, + WHvX64RegisterXmm5, + WHvX64RegisterXmm6, + WHvX64RegisterXmm7, + WHvX64RegisterXmm8, + WHvX64RegisterXmm9, + WHvX64RegisterXmm10, + WHvX64RegisterXmm11, + WHvX64RegisterXmm12, + WHvX64RegisterXmm13, + WHvX64RegisterXmm14, + WHvX64RegisterXmm15, + WHvX64RegisterXmmControlStatus, +]; + +#[cfg(target_os = "windows")] +impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonFpu { + type Error = FromWhpRegisterError; + + fn try_from( + regs: &[(WHV_REGISTER_NAME, Align16)], + ) -> Result { + if regs.len() != WHP_FPU_NAMES_LEN { + return Err(FromWhpRegisterError::InvalidLength(regs.len())); + } + + let mut fpu = CommonFpu::default(); + let mut seen_registers = HashSet::new(); + + for (name, value) in regs { + let name_id = name.0; + + // Check for duplicates + if !seen_registers.insert(name_id) { + return Err(FromWhpRegisterError::DuplicateRegister(name_id)); + } + + match name_id { + id if (WHvX64RegisterFpMmx0.0..WHvX64RegisterFpMmx0.0 + 8).contains(&id) => { + let idx = (id - WHvX64RegisterFpMmx0.0) as usize; + let dwords = unsafe { value.0.Reg128.Dword }; + fpu.fpr[idx] = [ + dwords[0].to_le_bytes(), + dwords[1].to_le_bytes(), + dwords[2].to_le_bytes(), + dwords[3].to_le_bytes(), + ] + .concat() + .try_into() + .map_err(|_| FromWhpRegisterError::InvalidEncoding)?; + } + + id if id == WHvX64RegisterFpControlStatus.0 => { + let control = unsafe { value.0.FpControlStatus.Anonymous }; + fpu.fcw = control.FpControl; + fpu.fsw = control.FpStatus; + fpu.ftwx = control.FpTag; + fpu.pad1 = control.Reserved; + fpu.last_opcode = control.LastFpOp; + fpu.last_ip = unsafe { control.Anonymous.LastFpRip }; + } + + id if (WHvX64RegisterXmm0.0..WHvX64RegisterXmm0.0 + 16).contains(&id) => { + let idx = (id - WHvX64RegisterXmm0.0) as usize; + let dwords = unsafe { value.0.Reg128.Dword }; + fpu.xmm[idx] = [ + dwords[0].to_le_bytes(), + dwords[1].to_le_bytes(), + dwords[2].to_le_bytes(), + dwords[3].to_le_bytes(), + ] + .concat() + .try_into() + .map_err(|_| FromWhpRegisterError::InvalidEncoding)?; + } + + id if id == WHvX64RegisterXmmControlStatus.0 => { + let control = unsafe { value.0.XmmControlStatus.Anonymous }; + fpu.mxcsr = control.XmmStatusControl; + fpu.last_dp = unsafe { control.Anonymous.LastFpRdp }; + } + + _ => { + return Err(FromWhpRegisterError::InvalidRegister(name_id)); + } + } + } + + // Set of all expected register names + let expected_registers: HashSet = [ + WHvX64RegisterFpMmx0.0, + WHvX64RegisterFpMmx1.0, + WHvX64RegisterFpMmx2.0, + WHvX64RegisterFpMmx3.0, + WHvX64RegisterFpMmx4.0, + WHvX64RegisterFpMmx5.0, + WHvX64RegisterFpMmx6.0, + WHvX64RegisterFpMmx7.0, + WHvX64RegisterFpControlStatus.0, + WHvX64RegisterXmm0.0, + WHvX64RegisterXmm1.0, + WHvX64RegisterXmm2.0, + WHvX64RegisterXmm3.0, + WHvX64RegisterXmm4.0, + WHvX64RegisterXmm5.0, + WHvX64RegisterXmm6.0, + WHvX64RegisterXmm7.0, + WHvX64RegisterXmm8.0, + WHvX64RegisterXmm9.0, + WHvX64RegisterXmm10.0, + WHvX64RegisterXmm11.0, + WHvX64RegisterXmm12.0, + WHvX64RegisterXmm13.0, + WHvX64RegisterXmm14.0, + WHvX64RegisterXmm15.0, + WHvX64RegisterXmmControlStatus.0, + ] + .into_iter() + .collect(); + + // Technically it should not be possible to have any missing registers at this point + // since we are guaranteed to have 18 non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway + let missing: HashSet = expected_registers + .difference(&seen_registers) + .cloned() + .collect(); + + if !missing.is_empty() { + return Err(FromWhpRegisterError::MissingRegister(missing)); + } + + Ok(fpu) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_common_fpu() -> CommonFpu { + CommonFpu { + fpr: [ + [1u8; 16], [2u8; 16], [3u8; 16], [4u8; 16], [5u8; 16], [6u8; 16], [7u8; 16], + [8u8; 16], + ], + fcw: 0x1234, + fsw: 0x5678, + ftwx: 0x9a, + pad1: 0xbc, + last_opcode: 0xdef0, + last_ip: 0xdeadbeefcafebabe, + last_dp: 0xabad1deaf00dbabe, + xmm: [ + [8u8; 16], [9u8; 16], [10u8; 16], [11u8; 16], [12u8; 16], [13u8; 16], [14u8; 16], + [15u8; 16], [16u8; 16], [17u8; 16], [18u8; 16], [19u8; 16], [20u8; 16], [21u8; 16], + [22u8; 16], [23u8; 16], + ], + mxcsr: 0x1f80, + pad2: 0, + } + } + + #[cfg(kvm)] + #[test] + fn round_trip_kvm_fpu() { + use kvm_bindings::kvm_fpu; + + let original = sample_common_fpu(); + let kvm: kvm_fpu = (&original).into(); + let round_tripped = CommonFpu::from(&kvm); + + assert_eq!(original, round_tripped); + } + + #[cfg(mshv)] + #[test] + fn round_trip_mshv_fpu() { + use mshv_bindings::FloatingPointUnit; + + let original = sample_common_fpu(); + let mshv: FloatingPointUnit = (&original).into(); + let round_tripped = CommonFpu::from(&mshv); + + assert_eq!(original, round_tripped); + } + + #[cfg(target_os = "windows")] + #[test] + fn round_trip_windows_fpu() { + use windows::Win32::System::Hypervisor::*; + + let original = sample_common_fpu(); + let windows: [(WHV_REGISTER_NAME, Align16); 26] = (&original).into(); + let round_tripped = CommonFpu::try_from(windows.as_ref()).unwrap(); + assert_eq!(original, round_tripped); + + // test for duplicate register error handling + let original = sample_common_fpu(); + let mut windows: [(WHV_REGISTER_NAME, Align16); 26] = + (&original).into(); + windows[0].0 = WHvX64RegisterFpMmx1; + let err = CommonFpu::try_from(windows.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::DuplicateRegister(WHvX64RegisterFpMmx1.0) + ); + + // test for passing non-fpu register (e.g. RAX) + let original = sample_common_fpu(); + let mut windows: [(WHV_REGISTER_NAME, Align16); 26] = + (&original).into(); + windows[0] = (WHvX64RegisterRax, windows[0].1); + let err = CommonFpu::try_from(windows.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::InvalidRegister(WHvX64RegisterRax.0) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs new file mode 100644 index 000000000..ab14af84b --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs @@ -0,0 +1,667 @@ +/* +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(mshv2)] +extern crate mshv_bindings2 as mshv_bindings; +#[cfg(mshv2)] +extern crate mshv_ioctls2 as mshv_ioctls; + +#[cfg(mshv3)] +extern crate mshv_bindings3 as mshv_bindings; +#[cfg(mshv3)] +extern crate mshv_ioctls3 as mshv_ioctls; + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +#[cfg(kvm)] +use kvm_bindings::{kvm_dtable, kvm_segment, kvm_sregs}; +#[cfg(mshv)] +use mshv_bindings::{SegmentRegister, SpecialRegisters, TableRegister}; +#[cfg(target_os = "windows")] +use windows::Win32::System::Hypervisor::*; + +#[cfg(target_os = "windows")] +use super::FromWhpRegisterError; + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSpecialRegisters { + pub cs: CommonSegmentRegister, + pub ds: CommonSegmentRegister, + pub es: CommonSegmentRegister, + pub fs: CommonSegmentRegister, + pub gs: CommonSegmentRegister, + pub ss: CommonSegmentRegister, + pub tr: CommonSegmentRegister, + pub ldt: CommonSegmentRegister, + pub gdt: CommonTableRegister, + pub idt: CommonTableRegister, + pub cr0: u64, + pub cr2: u64, + pub cr3: u64, + pub cr4: u64, + pub cr8: u64, + pub efer: u64, + pub apic_base: u64, + pub interrupt_bitmap: [u64; 4], +} + +#[cfg(mshv)] +impl From<&SpecialRegisters> for CommonSpecialRegisters { + fn from(value: &SpecialRegisters) -> Self { + CommonSpecialRegisters { + cs: value.cs.into(), + ds: value.ds.into(), + es: value.es.into(), + fs: value.fs.into(), + gs: value.gs.into(), + ss: value.ss.into(), + tr: value.tr.into(), + ldt: value.ldt.into(), + gdt: value.gdt.into(), + idt: value.idt.into(), + cr0: value.cr0, + cr2: value.cr2, + cr3: value.cr3, + cr4: value.cr4, + cr8: value.cr8, + efer: value.efer, + apic_base: value.apic_base, + interrupt_bitmap: value.interrupt_bitmap, + } + } +} + +#[cfg(mshv)] +impl From<&CommonSpecialRegisters> for SpecialRegisters { + fn from(other: &CommonSpecialRegisters) -> Self { + SpecialRegisters { + cs: other.cs.into(), + ds: other.ds.into(), + es: other.es.into(), + fs: other.fs.into(), + gs: other.gs.into(), + ss: other.ss.into(), + tr: other.tr.into(), + ldt: other.ldt.into(), + gdt: other.gdt.into(), + idt: other.idt.into(), + cr0: other.cr0, + cr2: other.cr2, + cr3: other.cr3, + cr4: other.cr4, + cr8: other.cr8, + efer: other.efer, + apic_base: other.apic_base, + interrupt_bitmap: other.interrupt_bitmap, + } + } +} + +#[cfg(kvm)] +impl From<&kvm_sregs> for CommonSpecialRegisters { + fn from(kvm_sregs: &kvm_sregs) -> Self { + CommonSpecialRegisters { + cs: kvm_sregs.cs.into(), + ds: kvm_sregs.ds.into(), + es: kvm_sregs.es.into(), + fs: kvm_sregs.fs.into(), + gs: kvm_sregs.gs.into(), + ss: kvm_sregs.ss.into(), + tr: kvm_sregs.tr.into(), + ldt: kvm_sregs.ldt.into(), + gdt: kvm_sregs.gdt.into(), + idt: kvm_sregs.idt.into(), + cr0: kvm_sregs.cr0, + cr2: kvm_sregs.cr2, + cr3: kvm_sregs.cr3, + cr4: kvm_sregs.cr4, + cr8: kvm_sregs.cr8, + efer: kvm_sregs.efer, + apic_base: kvm_sregs.apic_base, + interrupt_bitmap: kvm_sregs.interrupt_bitmap, + } + } +} + +#[cfg(kvm)] +impl From<&CommonSpecialRegisters> for kvm_sregs { + fn from(common_sregs: &CommonSpecialRegisters) -> Self { + kvm_sregs { + cs: common_sregs.cs.into(), + ds: common_sregs.ds.into(), + es: common_sregs.es.into(), + fs: common_sregs.fs.into(), + gs: common_sregs.gs.into(), + ss: common_sregs.ss.into(), + tr: common_sregs.tr.into(), + ldt: common_sregs.ldt.into(), + gdt: common_sregs.gdt.into(), + idt: common_sregs.idt.into(), + cr0: common_sregs.cr0, + cr2: common_sregs.cr2, + cr3: common_sregs.cr3, + cr4: common_sregs.cr4, + cr8: common_sregs.cr8, + efer: common_sregs.efer, + apic_base: common_sregs.apic_base, + interrupt_bitmap: common_sregs.interrupt_bitmap, + } + } +} + +/// WHV_REGISTER_VALUE must be 16-byte aligned, but the rust struct is incorrectly generated +/// as 8-byte aligned. This is a workaround to ensure that the struct is 16-byte aligned. +#[cfg(target_os = "windows")] +#[repr(C, align(16))] +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct Align16(pub(crate) T); + +#[cfg(target_os = "windows")] +#[allow(clippy::disallowed_macros)] // compile time +const _: () = { + assert!( + std::mem::size_of::>() + == std::mem::size_of::() + ); +}; + +#[cfg(target_os = "windows")] +pub(crate) const WHP_SREGS_NAMES_LEN: usize = 17; +#[cfg(target_os = "windows")] +pub(crate) static WHP_SREGS_NAMES: [WHV_REGISTER_NAME; WHP_SREGS_NAMES_LEN] = [ + WHvX64RegisterCs, + WHvX64RegisterDs, + WHvX64RegisterEs, + WHvX64RegisterFs, + WHvX64RegisterGs, + WHvX64RegisterSs, + WHvX64RegisterTr, + WHvX64RegisterLdtr, + WHvX64RegisterGdtr, + WHvX64RegisterIdtr, + WHvX64RegisterCr0, + WHvX64RegisterCr2, + WHvX64RegisterCr3, + WHvX64RegisterCr4, + WHvX64RegisterCr8, + WHvX64RegisterEfer, + WHvX64RegisterApicBase, +]; + +#[cfg(target_os = "windows")] +impl From<&CommonSpecialRegisters> + for [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] +{ + fn from(other: &CommonSpecialRegisters) -> Self { + [ + (WHvX64RegisterCs, Align16(other.cs.into())), + (WHvX64RegisterDs, Align16(other.ds.into())), + (WHvX64RegisterEs, Align16(other.es.into())), + (WHvX64RegisterFs, Align16(other.fs.into())), + (WHvX64RegisterGs, Align16(other.gs.into())), + (WHvX64RegisterSs, Align16(other.ss.into())), + (WHvX64RegisterTr, Align16(other.tr.into())), + (WHvX64RegisterLdtr, Align16(other.ldt.into())), + (WHvX64RegisterGdtr, Align16(other.gdt.into())), + (WHvX64RegisterIdtr, Align16(other.idt.into())), + ( + WHvX64RegisterCr0, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr0 }), + ), + ( + WHvX64RegisterCr2, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr2 }), + ), + ( + WHvX64RegisterCr3, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr3 }), + ), + ( + WHvX64RegisterCr4, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr4 }), + ), + ( + WHvX64RegisterCr8, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr8 }), + ), + ( + WHvX64RegisterEfer, + Align16(WHV_REGISTER_VALUE { Reg64: other.efer }), + ), + ( + WHvX64RegisterApicBase, + Align16(WHV_REGISTER_VALUE { + Reg64: other.apic_base, + }), + ), + ] + } +} + +#[cfg(target_os = "windows")] +impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonSpecialRegisters { + type Error = FromWhpRegisterError; + + #[expect( + non_upper_case_globals, + reason = "Windows API has lowercase register names" + )] + fn try_from( + regs: &[(WHV_REGISTER_NAME, Align16)], + ) -> Result { + if regs.len() != WHP_SREGS_NAMES_LEN { + return Err(FromWhpRegisterError::InvalidLength(regs.len())); + } + let mut registers = CommonSpecialRegisters::default(); + let mut seen_registers = HashSet::new(); + + for &(name, ref value) in regs { + let name_id = name.0; + + // Check for duplicates + if !seen_registers.insert(name_id) { + return Err(FromWhpRegisterError::DuplicateRegister(name_id)); + } + + unsafe { + match name { + WHvX64RegisterCs => registers.cs = value.0.into(), + WHvX64RegisterDs => registers.ds = value.0.into(), + WHvX64RegisterEs => registers.es = value.0.into(), + WHvX64RegisterFs => registers.fs = value.0.into(), + WHvX64RegisterGs => registers.gs = value.0.into(), + WHvX64RegisterSs => registers.ss = value.0.into(), + WHvX64RegisterTr => registers.tr = value.0.into(), + WHvX64RegisterLdtr => registers.ldt = value.0.into(), + WHvX64RegisterGdtr => registers.gdt = value.0.into(), + WHvX64RegisterIdtr => registers.idt = value.0.into(), + WHvX64RegisterCr0 => registers.cr0 = value.0.Reg64, + WHvX64RegisterCr2 => registers.cr2 = value.0.Reg64, + WHvX64RegisterCr3 => registers.cr3 = value.0.Reg64, + WHvX64RegisterCr4 => registers.cr4 = value.0.Reg64, + WHvX64RegisterCr8 => registers.cr8 = value.0.Reg64, + WHvX64RegisterEfer => registers.efer = value.0.Reg64, + WHvX64RegisterApicBase => registers.apic_base = value.0.Reg64, + _ => { + // Given unexpected register + return Err(FromWhpRegisterError::InvalidRegister(name_id)); + } + } + } + } + + // TODO: I'm not sure how to get this from WHP at the moment + registers.interrupt_bitmap = Default::default(); + + // Set of all expected register names + let expected_registers: HashSet = + WHP_SREGS_NAMES.map(|name| name.0).into_iter().collect(); + + // Technically it should not be possible to have any missing registers at this point + // since we are guaranteed to have 18 non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway + let missing: HashSet<_> = expected_registers + .difference(&seen_registers) + .cloned() + .collect(); + + if !missing.is_empty() { + return Err(FromWhpRegisterError::MissingRegister(missing)); + } + + Ok(registers) + } +} + +// --- Segment Register --- + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSegmentRegister { + pub base: u64, + pub limit: u32, + pub selector: u16, + pub type_: u8, + pub present: u8, + pub dpl: u8, + pub db: u8, + pub s: u8, + pub l: u8, + pub g: u8, + pub avl: u8, + pub unusable: u8, + pub padding: u8, +} + +#[cfg(mshv)] +impl From for CommonSegmentRegister { + fn from(other: SegmentRegister) -> Self { + CommonSegmentRegister { + base: other.base, + limit: other.limit, + selector: other.selector, + type_: other.type_, + present: other.present, + dpl: other.dpl, + db: other.db, + s: other.s, + l: other.l, + g: other.g, + avl: other.avl, + unusable: other.unusable, + padding: other.padding, + } + } +} + +#[cfg(mshv)] +impl From for SegmentRegister { + fn from(other: CommonSegmentRegister) -> Self { + SegmentRegister { + base: other.base, + limit: other.limit, + selector: other.selector, + type_: other.type_, + present: other.present, + dpl: other.dpl, + db: other.db, + s: other.s, + l: other.l, + g: other.g, + avl: other.avl, + unusable: other.unusable, + padding: other.padding, + } + } +} + +#[cfg(kvm)] +impl From for CommonSegmentRegister { + fn from(kvm_segment: kvm_segment) -> Self { + CommonSegmentRegister { + base: kvm_segment.base, + limit: kvm_segment.limit, + selector: kvm_segment.selector, + type_: kvm_segment.type_, + present: kvm_segment.present, + dpl: kvm_segment.dpl, + db: kvm_segment.db, + s: kvm_segment.s, + l: kvm_segment.l, + g: kvm_segment.g, + avl: kvm_segment.avl, + unusable: kvm_segment.unusable, + padding: kvm_segment.padding, + } + } +} + +#[cfg(kvm)] +impl From for kvm_segment { + fn from(common_segment: CommonSegmentRegister) -> Self { + kvm_segment { + base: common_segment.base, + limit: common_segment.limit, + selector: common_segment.selector, + type_: common_segment.type_, + present: common_segment.present, + dpl: common_segment.dpl, + db: common_segment.db, + s: common_segment.s, + l: common_segment.l, + g: common_segment.g, + avl: common_segment.avl, + unusable: common_segment.unusable, + padding: common_segment.padding, + } + } +} + +#[cfg(target_os = "windows")] +impl From for CommonSegmentRegister { + fn from(other: WHV_REGISTER_VALUE) -> Self { + unsafe { + let segment = other.Segment; + let bits = segment.Anonymous.Attributes; + + // Source of bit layout: https://learn.microsoft.com/en-us/virtualization/api/hypervisor-platform/funcs/whvvirtualprocessordatatypes + CommonSegmentRegister { + base: segment.Base, + limit: segment.Limit, + selector: segment.Selector, + type_: (bits & 0b1111) as u8, // bits 0–3: SegmentType + s: ((bits >> 4) & 0b1) as u8, // bit 4: NonSystemSegment + dpl: ((bits >> 5) & 0b11) as u8, // bits 5–6: DPL + present: ((bits >> 7) & 0b1) as u8, // bit 7: Present + // bits 8–11: Reserved + avl: ((bits >> 12) & 0b1) as u8, // bit 12: Available + l: ((bits >> 13) & 0b1) as u8, // bit 13: Long mode + db: ((bits >> 14) & 0b1) as u8, // bit 14: Default + g: ((bits >> 15) & 0b1) as u8, // bit 15: Granularity + unusable: 0, + padding: 0, + } + } + } +} + +#[cfg(target_os = "windows")] +impl From for WHV_REGISTER_VALUE { + fn from(other: CommonSegmentRegister) -> Self { + // Truncate each field to its valid bit width before composing `Attributes`. + let type_ = other.type_ & 0xF; // 4 bits + let s = other.s & 0x1; // 1 bit + let dpl = other.dpl & 0x3; // 2 bits + let present = other.present & 0x1; // 1 bit + let avl = other.avl & 0x1; // 1 bit + let l = other.l & 0x1; // 1 bit + let db = other.db & 0x1; // 1 bit + let g = other.g & 0x1; // 1 bit + + WHV_REGISTER_VALUE { + Segment: WHV_X64_SEGMENT_REGISTER { + Base: other.base, + Limit: other.limit, + Selector: other.selector, + Anonymous: WHV_X64_SEGMENT_REGISTER_0 { + Attributes: (type_ as u16) // bit 0-3 + | ((s as u16) << 4) // bit 4 + | ((dpl as u16) << 5) // bit 5-6 + | ((present as u16) << 7) // bit 7 + | ((avl as u16) << 12) // bit 12 + | ((l as u16) << 13) // bit 13 + | ((db as u16) << 14) // bit 14 + | ((g as u16) << 15), // bit 15 + }, + }, + } + } +} + +// --- Table Register --- + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonTableRegister { + pub base: u64, + pub limit: u16, +} + +#[cfg(mshv)] +impl From for CommonTableRegister { + fn from(other: TableRegister) -> Self { + CommonTableRegister { + base: other.base, + limit: other.limit, + } + } +} + +#[cfg(mshv)] +impl From for TableRegister { + fn from(other: CommonTableRegister) -> Self { + TableRegister { + base: other.base, + limit: other.limit, + } + } +} + +#[cfg(kvm)] +impl From for CommonTableRegister { + fn from(kvm_dtable: kvm_dtable) -> Self { + CommonTableRegister { + base: kvm_dtable.base, + limit: kvm_dtable.limit, + } + } +} + +#[cfg(kvm)] +impl From for kvm_dtable { + fn from(common_dtable: CommonTableRegister) -> Self { + kvm_dtable { + base: common_dtable.base, + limit: common_dtable.limit, + padding: Default::default(), + } + } +} + +#[cfg(target_os = "windows")] +impl From for CommonTableRegister { + fn from(other: WHV_REGISTER_VALUE) -> Self { + unsafe { + let table = other.Table; + CommonTableRegister { + base: table.Base, + limit: table.Limit, + } + } + } +} + +#[cfg(target_os = "windows")] +impl From for WHV_REGISTER_VALUE { + fn from(other: CommonTableRegister) -> Self { + WHV_REGISTER_VALUE { + Table: WHV_X64_TABLE_REGISTER { + Base: other.base, + Limit: other.limit, + Pad: Default::default(), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_common_special_registers() -> CommonSpecialRegisters { + let sample_segment = CommonSegmentRegister { + base: 0x1000, + limit: 0xFFFF, + selector: 0x10, + type_: 0xB, + present: 1, + dpl: 0, + db: 1, + s: 1, + l: 0, + g: 1, + avl: 0, + unusable: 0, + padding: 0, + }; + + let sample_table = CommonTableRegister { + base: 0x2000, + limit: 0x1000, + }; + + CommonSpecialRegisters { + cs: sample_segment, + ds: sample_segment, + es: sample_segment, + fs: sample_segment, + gs: sample_segment, + ss: sample_segment, + tr: sample_segment, + ldt: sample_segment, + gdt: sample_table, + idt: sample_table, + cr0: 0xDEAD_BEEF, + cr2: 0xBAD_C0DE, + cr3: 0xC0FFEE, + cr4: 0xFACE_CAFE, + cr8: 0x1234, + efer: 0x5678, + apic_base: 0x9ABC, + interrupt_bitmap: [0; 4], + } + } + + #[cfg(kvm)] + #[test] + fn round_trip_kvm_sregs() { + let original = sample_common_special_registers(); + let kvm_sregs: kvm_sregs = (&original).into(); + let roundtrip = CommonSpecialRegisters::from(&kvm_sregs); + + assert_eq!(original, roundtrip); + } + + #[cfg(mshv)] + #[test] + fn round_trip_mshv_sregs() { + let original = sample_common_special_registers(); + let mshv_sregs: SpecialRegisters = (&original).into(); + let roundtrip = CommonSpecialRegisters::from(&mshv_sregs); + + assert_eq!(original, roundtrip); + } + + #[cfg(target_os = "windows")] + #[test] + fn round_trip_whp_sregs() { + let original = sample_common_special_registers(); + let whp_sregs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + (&original).into(); + let roundtrip = CommonSpecialRegisters::try_from(whp_sregs.as_ref()).unwrap(); + assert_eq!(original, roundtrip); + + // Test duplicate register error + let original = sample_common_special_registers(); + let mut whp_sregs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + (&original).into(); + whp_sregs[0].0 = WHvX64RegisterDs; + let err = CommonSpecialRegisters::try_from(whp_sregs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::DuplicateRegister(WHvX64RegisterDs.0) + ); + + // Test passing non-sregs register (e.g. RIP) + let original = sample_common_special_registers(); + let mut whp_sregs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + (&original).into(); + whp_sregs[0].0 = WHvX64RegisterRip; + let err = CommonSpecialRegisters::try_from(whp_sregs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::InvalidRegister(WHvX64RegisterRip.0) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs b/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs new file mode 100644 index 000000000..de3a69697 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs @@ -0,0 +1,420 @@ +/* +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(mshv2)] +extern crate mshv_bindings2 as mshv_bindings; +#[cfg(mshv2)] +extern crate mshv_ioctls2 as mshv_ioctls; + +#[cfg(mshv3)] +extern crate mshv_bindings3 as mshv_bindings; +#[cfg(mshv3)] +extern crate mshv_ioctls3 as mshv_ioctls; + +#[cfg(kvm)] +use kvm_bindings::kvm_regs; +#[cfg(mshv)] +use mshv_bindings::StandardRegisters; + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonRegisters { + pub rax: u64, + pub rbx: u64, + pub rcx: u64, + pub rdx: u64, + pub rsi: u64, + pub rdi: u64, + pub rsp: u64, + pub rbp: u64, + pub r8: u64, + pub r9: u64, + pub r10: u64, + pub r11: u64, + pub r12: u64, + pub r13: u64, + pub r14: u64, + pub r15: u64, + pub rip: u64, + pub rflags: u64, +} + +// --- KVM --- +#[cfg(kvm)] +impl From<&kvm_regs> for CommonRegisters { + fn from(kvm_regs: &kvm_regs) -> Self { + CommonRegisters { + rax: kvm_regs.rax, + rbx: kvm_regs.rbx, + rcx: kvm_regs.rcx, + rdx: kvm_regs.rdx, + rsi: kvm_regs.rsi, + rdi: kvm_regs.rdi, + rsp: kvm_regs.rsp, + rbp: kvm_regs.rbp, + r8: kvm_regs.r8, + r9: kvm_regs.r9, + r10: kvm_regs.r10, + r11: kvm_regs.r11, + r12: kvm_regs.r12, + r13: kvm_regs.r13, + r14: kvm_regs.r14, + r15: kvm_regs.r15, + rip: kvm_regs.rip, + rflags: kvm_regs.rflags, + } + } +} + +#[cfg(kvm)] +impl From<&CommonRegisters> for kvm_regs { + fn from(regs: &CommonRegisters) -> Self { + kvm_regs { + rax: regs.rax, + rbx: regs.rbx, + rcx: regs.rcx, + rdx: regs.rdx, + rsi: regs.rsi, + rdi: regs.rdi, + rsp: regs.rsp, + rbp: regs.rbp, + r8: regs.r8, + r9: regs.r9, + r10: regs.r10, + r11: regs.r11, + r12: regs.r12, + r13: regs.r13, + r14: regs.r14, + r15: regs.r15, + rip: regs.rip, + rflags: regs.rflags, + } + } +} + +// --- MSHV --- + +#[cfg(mshv)] +impl From<&StandardRegisters> for CommonRegisters { + fn from(mshv_regs: &StandardRegisters) -> Self { + CommonRegisters { + rax: mshv_regs.rax, + rbx: mshv_regs.rbx, + rcx: mshv_regs.rcx, + rdx: mshv_regs.rdx, + rsi: mshv_regs.rsi, + rdi: mshv_regs.rdi, + rsp: mshv_regs.rsp, + rbp: mshv_regs.rbp, + r8: mshv_regs.r8, + r9: mshv_regs.r9, + r10: mshv_regs.r10, + r11: mshv_regs.r11, + r12: mshv_regs.r12, + r13: mshv_regs.r13, + r14: mshv_regs.r14, + r15: mshv_regs.r15, + rip: mshv_regs.rip, + rflags: mshv_regs.rflags, + } + } +} + +#[cfg(mshv)] +impl From<&CommonRegisters> for StandardRegisters { + fn from(regs: &CommonRegisters) -> Self { + StandardRegisters { + rax: regs.rax, + rbx: regs.rbx, + rcx: regs.rcx, + rdx: regs.rdx, + rsi: regs.rsi, + rdi: regs.rdi, + rsp: regs.rsp, + rbp: regs.rbp, + r8: regs.r8, + r9: regs.r9, + r10: regs.r10, + r11: regs.r11, + r12: regs.r12, + r13: regs.r13, + r14: regs.r14, + r15: regs.r15, + rip: regs.rip, + rflags: regs.rflags, + } + } +} + +#[cfg(target_os = "windows")] +use windows::Win32::System::Hypervisor::*; + +#[cfg(target_os = "windows")] +impl From<&CommonRegisters> for [(WHV_REGISTER_NAME, Align16); 18] { + fn from(regs: &CommonRegisters) -> Self { + [ + ( + WHvX64RegisterRax, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rax }), + ), + ( + WHvX64RegisterRbx, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rbx }), + ), + ( + WHvX64RegisterRcx, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rcx }), + ), + ( + WHvX64RegisterRdx, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rdx }), + ), + ( + WHvX64RegisterRsi, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rsi }), + ), + ( + WHvX64RegisterRdi, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rdi }), + ), + ( + WHvX64RegisterRsp, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rsp }), + ), + ( + WHvX64RegisterRbp, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rbp }), + ), + ( + WHvX64RegisterR8, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r8 }), + ), + ( + WHvX64RegisterR9, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r9 }), + ), + ( + WHvX64RegisterR10, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r10 }), + ), + ( + WHvX64RegisterR11, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r11 }), + ), + ( + WHvX64RegisterR12, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r12 }), + ), + ( + WHvX64RegisterR13, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r13 }), + ), + ( + WHvX64RegisterR14, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r14 }), + ), + ( + WHvX64RegisterR15, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r15 }), + ), + ( + WHvX64RegisterRip, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rip }), + ), + ( + WHvX64RegisterRflags, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rflags }), + ), + ] + } +} + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +#[cfg(target_os = "windows")] +use super::{Align16, FromWhpRegisterError}; + +#[cfg(target_os = "windows")] +pub(crate) const WHP_REGS_NAMES_LEN: usize = 18; +#[cfg(target_os = "windows")] +pub(crate) const WHP_REGS_NAMES: [WHV_REGISTER_NAME; WHP_REGS_NAMES_LEN] = [ + WHvX64RegisterRax, + WHvX64RegisterRbx, + WHvX64RegisterRcx, + WHvX64RegisterRdx, + WHvX64RegisterRsi, + WHvX64RegisterRdi, + WHvX64RegisterRsp, + WHvX64RegisterRbp, + WHvX64RegisterR8, + WHvX64RegisterR9, + WHvX64RegisterR10, + WHvX64RegisterR11, + WHvX64RegisterR12, + WHvX64RegisterR13, + WHvX64RegisterR14, + WHvX64RegisterR15, + WHvX64RegisterRip, + WHvX64RegisterRflags, +]; + +#[cfg(target_os = "windows")] +impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonRegisters { + type Error = FromWhpRegisterError; + + #[expect( + non_upper_case_globals, + reason = "Windows API has lowercase register names" + )] + fn try_from( + regs: &[(WHV_REGISTER_NAME, Align16)], + ) -> Result { + if regs.len() != WHP_REGS_NAMES_LEN { + return Err(FromWhpRegisterError::InvalidLength(regs.len())); + } + let mut registers = CommonRegisters::default(); + let mut seen_registers = HashSet::new(); + + for &(name, value) in regs { + let name_id = name.0; + + // Check for duplicates + if !seen_registers.insert(name_id) { + return Err(FromWhpRegisterError::DuplicateRegister(name_id)); + } + + unsafe { + match name { + WHvX64RegisterRax => registers.rax = value.0.Reg64, + WHvX64RegisterRbx => registers.rbx = value.0.Reg64, + WHvX64RegisterRcx => registers.rcx = value.0.Reg64, + WHvX64RegisterRdx => registers.rdx = value.0.Reg64, + WHvX64RegisterRsi => registers.rsi = value.0.Reg64, + WHvX64RegisterRdi => registers.rdi = value.0.Reg64, + WHvX64RegisterRsp => registers.rsp = value.0.Reg64, + WHvX64RegisterRbp => registers.rbp = value.0.Reg64, + WHvX64RegisterR8 => registers.r8 = value.0.Reg64, + WHvX64RegisterR9 => registers.r9 = value.0.Reg64, + WHvX64RegisterR10 => registers.r10 = value.0.Reg64, + WHvX64RegisterR11 => registers.r11 = value.0.Reg64, + WHvX64RegisterR12 => registers.r12 = value.0.Reg64, + WHvX64RegisterR13 => registers.r13 = value.0.Reg64, + WHvX64RegisterR14 => registers.r14 = value.0.Reg64, + WHvX64RegisterR15 => registers.r15 = value.0.Reg64, + WHvX64RegisterRip => registers.rip = value.0.Reg64, + WHvX64RegisterRflags => registers.rflags = value.0.Reg64, + _ => { + // Given unexpected register + return Err(FromWhpRegisterError::InvalidRegister(name_id)); + } + } + } + } + + // Set of all expected register names + let expected_registers: HashSet = + WHP_REGS_NAMES.map(|name| name.0).into_iter().collect(); + + // Technically it should not be possible to have any missing registers at this point + // since we are guaranteed to have 18 non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway + let missing: HashSet<_> = expected_registers + .difference(&seen_registers) + .cloned() + .collect(); + + if !missing.is_empty() { + return Err(FromWhpRegisterError::MissingRegister(missing)); + } + + Ok(registers) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn common_regs() -> CommonRegisters { + 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: 18, + } + } + #[cfg(kvm)] + #[test] + fn round_trip_kvm_regs() { + let original = common_regs(); + let kvm_regs: kvm_regs = (&original).into(); + let converted: CommonRegisters = (&kvm_regs).into(); + assert_eq!(original, converted); + } + + #[cfg(mshv)] + #[test] + fn round_trip_mshv_regs() { + let original = common_regs(); + let mshv_regs: StandardRegisters = (&original).into(); + let converted: CommonRegisters = (&mshv_regs).into(); + assert_eq!(original, converted); + } + + #[cfg(target_os = "windows")] + #[test] + fn round_trip_whp_regs() { + let original = common_regs(); + let whp_regs: [(WHV_REGISTER_NAME, Align16); 18] = (&original).into(); + let converted: CommonRegisters = whp_regs.as_ref().try_into().unwrap(); + assert_eq!(original, converted); + + // test for duplicate register error handling + let original = common_regs(); + let mut whp_regs: [(WHV_REGISTER_NAME, Align16); 18] = + (&original).into(); + whp_regs[0].0 = WHvX64RegisterRbx; + let err = CommonRegisters::try_from(whp_regs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::DuplicateRegister(WHvX64RegisterRbx.0) + ); + + // test for passing non-standard register (e.g. CR8) + let original = common_regs(); + let mut whp_regs: [(WHV_REGISTER_NAME, Align16); 18] = + (&original).into(); + whp_regs[0].0 = WHvX64RegisterCr8; + let err = CommonRegisters::try_from(whp_regs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::InvalidRegister(WHvX64RegisterCr8.0) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs b/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs index 3d97de9ea..4f4e82555 100644 --- a/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs +++ b/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs @@ -24,12 +24,16 @@ use windows::Win32::System::LibraryLoader::*; use windows::core::s; use windows_result::HRESULT; +use super::regs::{ + Align16, CommonFpu, CommonRegisters, CommonSpecialRegisters, WHP_FPU_NAMES_LEN, WHP_REGS_NAMES, + WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, WHP_SREGS_NAMES_LEN, +}; use super::surrogate_process::SurrogateProcess; #[cfg(crashdump)] use crate::HyperlightError; +use crate::hypervisor::regs::WHP_FPU_NAMES; #[cfg(gdb)] use crate::hypervisor::wrappers::WHvDebugRegisters; -use crate::hypervisor::wrappers::{WHvFPURegisters, WHvGeneralRegisters, WHvSpecialRegisters}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::{Result, new_error}; @@ -303,15 +307,16 @@ impl VMProcessor { part.0 } + /// Helper for setting arbitrary registers. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] pub(super) fn set_registers( &self, - registers: &[(WHV_REGISTER_NAME, WHV_REGISTER_VALUE)], + registers: &[(WHV_REGISTER_NAME, Align16)], ) -> Result<()> { - let partition_handle = self.get_partition_hdl(); let register_count = registers.len(); - let mut register_names: Vec = vec![]; - let mut register_values: Vec = vec![]; + + let mut register_names = Vec::with_capacity(register_count); + let mut register_values = Vec::with_capacity(register_count); for (key, value) in registers.iter() { register_names.push(*key); @@ -320,188 +325,115 @@ impl VMProcessor { unsafe { WHvSetVirtualProcessorRegisters( - partition_handle, + self.get_partition_hdl(), 0, register_names.as_ptr(), register_count as u32, - register_values.as_ptr(), + register_values.as_ptr() as *const WHV_REGISTER_VALUE, )?; } Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn get_sregs(&self) -> Result { - const LEN: usize = 17; - - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterCr0, - WHvX64RegisterCr2, - WHvX64RegisterCr3, - WHvX64RegisterCr4, - WHvX64RegisterCr8, - WHvX64RegisterEfer, - WHvX64RegisterApicBase, - WHvX64RegisterCs, - WHvX64RegisterDs, - WHvX64RegisterEs, - WHvX64RegisterFs, - WHvX64RegisterGs, - WHvX64RegisterSs, - WHvX64RegisterTr, - WHvX64RegisterLdtr, - WHvX64RegisterGdtr, - WHvX64RegisterIdtr, - ]; + pub(super) fn regs(&self) -> Result { + let mut whv_regs_values: [Align16; WHP_REGS_NAMES_LEN] = + unsafe { std::mem::zeroed() }; - let mut out: [WHV_REGISTER_VALUE; LEN] = unsafe { std::mem::zeroed() }; unsafe { WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, - names.as_ptr(), - LEN as u32, - out.as_mut_ptr(), + WHP_REGS_NAMES.as_ptr(), + whv_regs_values.len() as u32, + whv_regs_values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; } - let res: WHvSpecialRegisters = WHvSpecialRegisters { - cr0: out[0], - cr2: out[1], - cr3: out[2], - cr4: out[3], - cr8: out[4], - efer: out[5], - apic_base: out[6], - cs: out[7], - ds: out[8], - es: out[9], - fs: out[10], - gs: out[11], - ss: out[12], - tr: out[13], - ldtr: out[14], - gdtr: out[15], - idtr: out[16], - }; - - Ok(res) + WHP_REGS_NAMES + .into_iter() + .zip(whv_regs_values) + .collect::)>>() + .as_slice() + .try_into() + .map_err(|e| { + new_error!( + "Failed to convert WHP registers to CommonRegisters: {:?}", + e + ) + }) } - // Sets the registers for the VMProcessor to the given general purpose registers. - // If you want to set other registers, use `set_registers` instead. - pub(super) fn set_general_purpose_registers(&self, regs: &WHvGeneralRegisters) -> Result<()> { - const LEN: usize = 18; - - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterRax, - WHvX64RegisterRbx, - WHvX64RegisterRcx, - WHvX64RegisterRdx, - WHvX64RegisterRsi, - WHvX64RegisterRdi, - WHvX64RegisterRsp, - WHvX64RegisterRbp, - WHvX64RegisterR8, - WHvX64RegisterR9, - WHvX64RegisterR10, - WHvX64RegisterR11, - WHvX64RegisterR12, - WHvX64RegisterR13, - WHvX64RegisterR14, - WHvX64RegisterR15, - WHvX64RegisterRip, - WHvX64RegisterRflags, - ]; + pub(super) fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { + let whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_REGS_NAMES_LEN] = + regs.into(); + self.set_registers(&whp_regs)?; + Ok(()) + } - let values: [WHV_REGISTER_VALUE; LEN] = [ - WHV_REGISTER_VALUE { Reg64: regs.rax }, - WHV_REGISTER_VALUE { Reg64: regs.rbx }, - WHV_REGISTER_VALUE { Reg64: regs.rcx }, - WHV_REGISTER_VALUE { Reg64: regs.rdx }, - WHV_REGISTER_VALUE { Reg64: regs.rsi }, - WHV_REGISTER_VALUE { Reg64: regs.rdi }, - WHV_REGISTER_VALUE { Reg64: regs.rsp }, - WHV_REGISTER_VALUE { Reg64: regs.rbp }, - WHV_REGISTER_VALUE { Reg64: regs.r8 }, - WHV_REGISTER_VALUE { Reg64: regs.r9 }, - WHV_REGISTER_VALUE { Reg64: regs.r10 }, - WHV_REGISTER_VALUE { Reg64: regs.r11 }, - WHV_REGISTER_VALUE { Reg64: regs.r12 }, - WHV_REGISTER_VALUE { Reg64: regs.r13 }, - WHV_REGISTER_VALUE { Reg64: regs.r14 }, - WHV_REGISTER_VALUE { Reg64: regs.r15 }, - WHV_REGISTER_VALUE { Reg64: regs.rip }, - WHV_REGISTER_VALUE { Reg64: regs.rflags }, - ]; + pub(super) fn sregs(&self) -> Result { + let mut whp_sregs_values: [Align16; WHP_SREGS_NAMES_LEN] = + unsafe { std::mem::zeroed() }; unsafe { - WHvSetVirtualProcessorRegisters( + WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, - names.as_ptr(), - LEN as u32, - values.as_ptr(), + WHP_SREGS_NAMES.as_ptr(), + whp_sregs_values.len() as u32, + whp_sregs_values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; } - Ok(()) + + WHP_SREGS_NAMES + .into_iter() + .zip(whp_sregs_values) + .collect::)>>() + .as_slice() + .try_into() + .map_err(|e| { + new_error!( + "Failed to convert WHP registers to CommonSpecialRegisters: {:?}", + e + ) + }) } - pub(super) fn get_regs(&self) -> Result { - const LEN: usize = 18; + pub(super) fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { + let whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + sregs.into(); + self.set_registers(&whp_regs)?; + Ok(()) + } - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterRax, - WHvX64RegisterRbx, - WHvX64RegisterRcx, - WHvX64RegisterRdx, - WHvX64RegisterRsi, - WHvX64RegisterRdi, - WHvX64RegisterRsp, - WHvX64RegisterRbp, - WHvX64RegisterR8, - WHvX64RegisterR9, - WHvX64RegisterR10, - WHvX64RegisterR11, - WHvX64RegisterR12, - WHvX64RegisterR13, - WHvX64RegisterR14, - WHvX64RegisterR15, - WHvX64RegisterRip, - WHvX64RegisterRflags, - ]; + pub(super) fn fpu(&self) -> Result { + let mut whp_fpu_values: [Align16; WHP_FPU_NAMES_LEN] = + unsafe { std::mem::zeroed() }; - let mut out: [WHV_REGISTER_VALUE; LEN] = unsafe { std::mem::zeroed() }; unsafe { WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, - names.as_ptr(), - LEN as u32, - out.as_mut_ptr(), + WHP_FPU_NAMES.as_ptr(), + whp_fpu_values.len() as u32, + whp_fpu_values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; - Ok(WHvGeneralRegisters { - rax: out[0].Reg64, - rbx: out[1].Reg64, - rcx: out[2].Reg64, - rdx: out[3].Reg64, - rsi: out[4].Reg64, - rdi: out[5].Reg64, - rsp: out[6].Reg64, - rbp: out[7].Reg64, - r8: out[8].Reg64, - r9: out[9].Reg64, - r10: out[10].Reg64, - r11: out[11].Reg64, - r12: out[12].Reg64, - r13: out[13].Reg64, - r14: out[14].Reg64, - r15: out[15].Reg64, - rip: out[16].Reg64, - rflags: out[17].Reg64, - }) } + + WHP_FPU_NAMES + .into_iter() + .zip(whp_fpu_values) + .collect::)>>() + .as_slice() + .try_into() + .map_err(|e| new_error!("Failed to convert WHP registers to CommonFpu: {:?}", e)) + } + + pub(super) fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { + let whp_fpu: [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] = + fpu.into(); + self.set_registers(&whp_fpu)?; + Ok(()) } #[cfg(crashdump)] @@ -560,12 +492,30 @@ impl VMProcessor { #[cfg(gdb)] pub(super) fn set_debug_regs(&self, regs: &WHvDebugRegisters) -> Result<()> { let registers = vec![ - (WHvX64RegisterDr0, WHV_REGISTER_VALUE { Reg64: regs.dr0 }), - (WHvX64RegisterDr1, WHV_REGISTER_VALUE { Reg64: regs.dr1 }), - (WHvX64RegisterDr2, WHV_REGISTER_VALUE { Reg64: regs.dr2 }), - (WHvX64RegisterDr3, WHV_REGISTER_VALUE { Reg64: regs.dr3 }), - (WHvX64RegisterDr6, WHV_REGISTER_VALUE { Reg64: regs.dr6 }), - (WHvX64RegisterDr7, WHV_REGISTER_VALUE { Reg64: regs.dr7 }), + ( + WHvX64RegisterDr0, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr0 }), + ), + ( + WHvX64RegisterDr1, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr1 }), + ), + ( + WHvX64RegisterDr2, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr2 }), + ), + ( + WHvX64RegisterDr3, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr3 }), + ), + ( + WHvX64RegisterDr6, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr6 }), + ), + ( + WHvX64RegisterDr7, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr7 }), + ), ]; self.set_registers(®isters) @@ -584,116 +534,24 @@ impl VMProcessor { WHvX64RegisterDr7, ]; - let mut out: [WHV_REGISTER_VALUE; LEN] = unsafe { std::mem::zeroed() }; + let mut out: [Align16; LEN] = unsafe { std::mem::zeroed() }; unsafe { WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, names.as_ptr(), LEN as u32, - out.as_mut_ptr(), + out.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; Ok(WHvDebugRegisters { - dr0: out[0].Reg64, - dr1: out[1].Reg64, - dr2: out[2].Reg64, - dr3: out[3].Reg64, - dr6: out[4].Reg64, - dr7: out[5].Reg64, - }) - } - } - - pub(super) fn set_fpu(&self, regs: &WHvFPURegisters) -> Result<()> { - const LEN: usize = 26; - - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterXmm0, - WHvX64RegisterXmm1, - WHvX64RegisterXmm2, - WHvX64RegisterXmm3, - WHvX64RegisterXmm4, - WHvX64RegisterXmm5, - WHvX64RegisterXmm6, - WHvX64RegisterXmm7, - WHvX64RegisterXmm8, - WHvX64RegisterXmm9, - WHvX64RegisterXmm10, - WHvX64RegisterXmm11, - WHvX64RegisterXmm12, - WHvX64RegisterXmm13, - WHvX64RegisterXmm14, - WHvX64RegisterXmm15, - WHvX64RegisterFpMmx0, - WHvX64RegisterFpMmx1, - WHvX64RegisterFpMmx2, - WHvX64RegisterFpMmx3, - WHvX64RegisterFpMmx4, - WHvX64RegisterFpMmx5, - WHvX64RegisterFpMmx6, - WHvX64RegisterFpMmx7, - WHvX64RegisterFpControlStatus, - WHvX64RegisterXmmControlStatus, - ]; - - let xmm_regs = [ - regs.xmm0, regs.xmm1, regs.xmm2, regs.xmm3, regs.xmm4, regs.xmm5, regs.xmm6, regs.xmm7, - regs.xmm8, regs.xmm9, regs.xmm10, regs.xmm11, regs.xmm12, regs.xmm13, regs.xmm14, - regs.xmm15, - ]; - - let mut values: Vec = xmm_regs - .iter() - .map(|®| WHV_REGISTER_VALUE { - Fp: WHV_X64_FP_REGISTER { - AsUINT128: WHV_UINT128 { - Anonymous: WHV_UINT128_0 { - Low64: reg as u64, - High64: (reg >> 64) as u64, - }, - }, - }, + dr0: out[0].0.Reg64, + dr1: out[1].0.Reg64, + dr2: out[2].0.Reg64, + dr3: out[3].0.Reg64, + dr6: out[4].0.Reg64, + dr7: out[5].0.Reg64, }) - .collect(); - - values.extend_from_slice(&[ - WHV_REGISTER_VALUE { Reg64: regs.mmx0 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx1 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx2 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx3 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx4 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx5 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx6 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx7 }, - WHV_REGISTER_VALUE { - FpControlStatus: WHV_X64_FP_CONTROL_STATUS_REGISTER { - Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0 { - FpControl: regs.fp_control_word, - FpTag: regs.fp_tag_word, - ..Default::default() - }, - }, - }, - WHV_REGISTER_VALUE { - XmmControlStatus: WHV_X64_XMM_CONTROL_STATUS_REGISTER { - Anonymous: WHV_X64_XMM_CONTROL_STATUS_REGISTER_0 { - XmmStatusControl: regs.mxcsr, - ..Default::default() - }, - }, - }, - ]); - - unsafe { - WHvSetVirtualProcessorRegisters( - self.get_partition_hdl(), - 0, - names.as_ptr(), - LEN as u32, - values.as_ptr(), - )?; } - Ok(()) } #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] diff --git a/src/hyperlight_host/src/hypervisor/wrappers.rs b/src/hyperlight_host/src/hypervisor/wrappers.rs index 4a2a1ff8b..135ae2b82 100644 --- a/src/hyperlight_host/src/hypervisor/wrappers.rs +++ b/src/hyperlight_host/src/hypervisor/wrappers.rs @@ -18,7 +18,6 @@ use std::ffi::CString; use tracing::{Span, instrument}; use windows::Win32::Foundation::{HANDLE, HMODULE}; -use windows::Win32::System::Hypervisor::WHV_REGISTER_VALUE; use windows::core::PSTR; use crate::{HyperlightError, Result}; @@ -57,29 +56,6 @@ impl From<&PSTRWrapper> for PSTR { } } -// only used on windows. mshv and kvm already has this implemented -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(super) struct WHvGeneralRegisters { - pub rax: u64, - pub rbx: u64, - pub rcx: u64, - pub rdx: u64, - pub rsi: u64, - pub rdi: u64, - pub rsp: u64, - pub rbp: u64, - pub r8: u64, - pub r9: u64, - pub r10: u64, - pub r11: u64, - pub r12: u64, - pub r13: u64, - pub r14: u64, - pub r15: u64, - pub rip: u64, - pub rflags: u64, -} - /// only used on widos for handling debug registers with the VMProcessor #[cfg(gdb)] #[derive(Debug, Default, Copy, Clone, PartialEq)] @@ -92,61 +68,6 @@ pub(super) struct WHvDebugRegisters { pub dr7: u64, } -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(super) struct WHvFPURegisters { - pub xmm0: u128, - pub xmm1: u128, - pub xmm2: u128, - pub xmm3: u128, - pub xmm4: u128, - pub xmm5: u128, - pub xmm6: u128, - pub xmm7: u128, - pub xmm8: u128, - pub xmm9: u128, - pub xmm10: u128, - pub xmm11: u128, - pub xmm12: u128, - pub xmm13: u128, - pub xmm14: u128, - pub xmm15: u128, - - pub mmx0: u64, - pub mmx1: u64, - pub mmx2: u64, - pub mmx3: u64, - pub mmx4: u64, - pub mmx5: u64, - pub mmx6: u64, - pub mmx7: u64, - - pub fp_control_word: u16, - pub fp_tag_word: u8, - - pub mxcsr: u32, -} - -#[derive(Default, Copy, Clone)] -pub(super) struct WHvSpecialRegisters { - pub cr0: WHV_REGISTER_VALUE, - pub cr2: WHV_REGISTER_VALUE, - pub cr3: WHV_REGISTER_VALUE, - pub cr4: WHV_REGISTER_VALUE, - pub cr8: WHV_REGISTER_VALUE, - pub efer: WHV_REGISTER_VALUE, - pub apic_base: WHV_REGISTER_VALUE, - pub cs: WHV_REGISTER_VALUE, - pub ds: WHV_REGISTER_VALUE, - pub es: WHV_REGISTER_VALUE, - pub fs: WHV_REGISTER_VALUE, - pub gs: WHV_REGISTER_VALUE, - pub ss: WHV_REGISTER_VALUE, - pub tr: WHV_REGISTER_VALUE, - pub ldtr: WHV_REGISTER_VALUE, - pub gdtr: WHV_REGISTER_VALUE, - pub idtr: WHV_REGISTER_VALUE, -} - /// Wrapper for HANDLE, required since HANDLE is no longer Send. #[derive(Debug, Copy, Clone)] pub struct HandleWrapper(HANDLE); diff --git a/src/hyperlight_host/src/sandbox/outb.rs b/src/hyperlight_host/src/sandbox/outb.rs index 8f9c1166f..2697a6a5e 100644 --- a/src/hyperlight_host/src/sandbox/outb.rs +++ b/src/hyperlight_host/src/sandbox/outb.rs @@ -170,13 +170,10 @@ fn unwind( .unwind_cache .try_lock() .map_err(|e| new_error!("could not lock unwinder cache {}\n", e))?; + let regs = hv.regs()?; let iter = trace_info.unwinder.iter_frames( - hv.read_trace_reg(crate::hypervisor::TraceRegister::RIP)?, - framehop::x86_64::UnwindRegsX86_64::new( - hv.read_trace_reg(crate::hypervisor::TraceRegister::RIP)?, - hv.read_trace_reg(crate::hypervisor::TraceRegister::RSP)?, - hv.read_trace_reg(crate::hypervisor::TraceRegister::RBP)?, - ), + regs.rip, + framehop::x86_64::UnwindRegsX86_64::new(regs.rip, regs.rsp, regs.rbp), &mut *cache, &mut read_stack, ); @@ -311,13 +308,15 @@ pub(crate) fn handle_outb( } #[cfg(feature = "mem_profile")] OutBAction::TraceMemoryAlloc => { + use crate::hypervisor::regs::CommonRegisters; + let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info_as_ref()) else { return Ok(()); }; - let Ok(amt) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RAX) else { - return Ok(()); - }; - let Ok(ptr) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RCX) else { + let Ok(CommonRegisters { + rax: amt, rcx: ptr, .. + }) = _hv.regs() + else { return Ok(()); }; record_trace_frame(_hv.trace_info_as_ref(), 2u64, |f| { @@ -328,23 +327,27 @@ pub(crate) fn handle_outb( } #[cfg(feature = "mem_profile")] OutBAction::TraceMemoryFree => { + use crate::hypervisor::regs::CommonRegisters; + let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info_as_ref()) else { return Ok(()); }; - let Ok(ptr) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RCX) else { + let Ok(CommonRegisters { rcx, .. }) = _hv.regs() else { return Ok(()); }; record_trace_frame(_hv.trace_info_as_ref(), 3u64, |f| { - let _ = f.write_all(&ptr.to_ne_bytes()); + let _ = f.write_all(&rcx.to_ne_bytes()); write_stack(f, &stack); }) } #[cfg(feature = "trace_guest")] OutBAction::TraceRecord => { - let Ok(len) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RAX) else { - return Ok(()); - }; - let Ok(ptr) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RCX) else { + use crate::hypervisor::regs::CommonRegisters; + + let Ok(CommonRegisters { + rax: len, rcx: ptr, .. + }) = _hv.regs() + else { return Ok(()); }; let mut buffer = vec![0u8; len as usize * std::mem::size_of::()]; diff --git a/src/tests/rust_guests/dummyguest/Cargo.lock b/src/tests/rust_guests/dummyguest/Cargo.lock index fa3de189b..a62c14f21 100644 --- a/src/tests/rust_guests/dummyguest/Cargo.lock +++ b/src/tests/rust_guests/dummyguest/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "autocfg" @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "flatbuffers" -version = "25.2.10" +version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ "bitflags", "rustc_version", diff --git a/src/tests/rust_guests/simpleguest/Cargo.lock b/src/tests/rust_guests/simpleguest/Cargo.lock index 9081da46b..4498df099 100644 --- a/src/tests/rust_guests/simpleguest/Cargo.lock +++ b/src/tests/rust_guests/simpleguest/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "autocfg" @@ -46,9 +46,9 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "flatbuffers" -version = "25.2.10" +version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ "bitflags", "rustc_version", diff --git a/src/tests/rust_guests/witguest/Cargo.lock b/src/tests/rust_guests/witguest/Cargo.lock index 6a33cb503..6d4a14fa0 100644 --- a/src/tests/rust_guests/witguest/Cargo.lock +++ b/src/tests/rust_guests/witguest/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "autocfg" @@ -146,9 +146,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "flatbuffers" -version = "25.2.10" +version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ "bitflags", "rustc_version", diff --git a/typos.toml b/typos.toml index c05ec808d..07ffa9dee 100644 --- a/typos.toml +++ b/typos.toml @@ -8,3 +8,4 @@ extend-exclude = ["**/*.patch", "src/hyperlight_guest_bin/third_party/**/*", "NO # typ is used for field name as type is a reserved keyword typ="typ" mmaped="mmapped" +fpr="fpr" From 3a66f30159d2a4c9a0d704df9d267dec029ade0b Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:35:01 -0700 Subject: [PATCH 2/2] Rebase. Moved files back Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- .../src/hypervisor/crashdump.rs | 54 +- .../src/hypervisor/gdb/arch.rs | 46 +- .../src/hypervisor/gdb/hyperv_debug.rs | 235 --- .../src/hypervisor/gdb/kvm_debug.rs | 224 --- src/hyperlight_host/src/hypervisor/gdb/mod.rs | 205 +-- .../src/hypervisor/gdb/mshv_debug.rs | 247 ---- .../src/hypervisor/hyperlight_vm.rs | 1041 +++++++++++++ .../src/hypervisor/hyperv_linux.rs | 1289 ++++------------- .../src/hypervisor/hyperv_windows.rs | 1205 ++++----------- src/hyperlight_host/src/hypervisor/kvm.rs | 1115 +++----------- src/hyperlight_host/src/hypervisor/mod.rs | 444 +----- .../src/hypervisor/regs/special_regs.rs | 53 +- src/hyperlight_host/src/hypervisor/vm.rs | 130 ++ src/hyperlight_host/src/mem/memory_region.rs | 8 +- src/hyperlight_host/src/mem/shared_mem.rs | 1 + .../src/sandbox/initialized_multi_use.rs | 53 +- src/hyperlight_host/src/sandbox/outb.rs | 55 +- .../src/sandbox/uninitialized_evolve.rs | 124 +- src/hyperlight_host/tests/integration_test.rs | 6 +- 19 files changed, 2153 insertions(+), 4382 deletions(-) delete mode 100644 src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs delete mode 100644 src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs delete mode 100644 src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs create mode 100644 src/hyperlight_host/src/hypervisor/hyperlight_vm.rs create mode 100644 src/hyperlight_host/src/hypervisor/vm.rs diff --git a/src/hyperlight_host/src/hypervisor/crashdump.rs b/src/hyperlight_host/src/hypervisor/crashdump.rs index ae865ef56..7ee838d3a 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -23,7 +23,7 @@ use elfcore::{ ReadProcessMemory, ThreadView, VaProtection, VaRegion, }; -use super::Hypervisor; +use crate::hypervisor::hyperlight_vm::HyperlightVm; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::{Result, new_error}; @@ -262,7 +262,7 @@ impl ReadProcessMemory for GuestMemReader { /// /// # Returns /// * `Result<()>`: Success or error -pub(crate) fn generate_crashdump(hv: &dyn Hypervisor) -> Result<()> { +pub(crate) fn generate_crashdump(hv: &HyperlightVm) -> Result<()> { // Get crash context from hypervisor let ctx = hv .crashdump_context() @@ -349,28 +349,23 @@ fn core_dump_file_path(dump_dir: Option) -> String { /// Returns: /// * `Result`: The number of bytes written to the core dump file. fn checked_core_dump( - ctx: Option, + ctx: CrashDumpContext, get_writer: impl FnOnce() -> Result>, ) -> Result { - let mut nbytes = 0; - // If the HV returned a context it means we can create a core dump - // This is the case when the sandbox has been configured at runtime to allow core dumps - if let Some(ctx) = ctx { - log::info!("Creating core dump file..."); - - // Set up data sources for the core dump - let guest_view = GuestView::new(&ctx); - let memory_reader = GuestMemReader::new(&ctx); - - // Create and write core dump - let core_builder = CoreDumpBuilder::from_source(guest_view, memory_reader); - - let writer = get_writer()?; - // Write the core dump directly to the file - nbytes = core_builder - .write(writer) - .map_err(|e| new_error!("Failed to write core dump: {:?}", e))?; - } + log::info!("Creating core dump file..."); + + // Set up data sources for the core dump + let guest_view = GuestView::new(&ctx); + let memory_reader = GuestMemReader::new(&ctx); + + // Create and write core dump + let core_builder = CoreDumpBuilder::from_source(guest_view, memory_reader); + + let writer = get_writer()?; + // Write the core dump directly to the file + let nbytes = core_builder + .write(writer) + .map_err(|e| new_error!("Failed to write core dump: {:?}", e))?; Ok(nbytes) } @@ -424,17 +419,6 @@ mod test { assert!(path.starts_with(&temp_dir)); } - /// Test core is not created when the context is None - #[test] - fn test_crashdump_not_created_when_context_is_none() { - // Call the function with None context - let result = checked_core_dump(None, || Ok(Box::new(std::io::empty()))); - - // Check if the result is ok and the number of bytes is 0 - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 0); - } - /// Test the core dump creation with no regions fails #[test] fn test_crashdump_write_fails_when_no_regions() { @@ -451,7 +435,7 @@ mod test { let get_writer = || Ok(Box::new(std::io::empty()) as Box); // Call the function - let result = checked_core_dump(Some(ctx), get_writer); + let result = checked_core_dump(ctx, get_writer); // Check if the result is an error // This should fail because there are no regions @@ -482,7 +466,7 @@ mod test { let get_writer = || Ok(Box::new(std::io::empty()) as Box); // Call the function - let result = checked_core_dump(Some(ctx), get_writer); + let result = checked_core_dump(ctx, get_writer); // Check if the result is ok and the number of bytes is 0 assert!(result.is_ok()); diff --git a/src/hyperlight_host/src/hypervisor/gdb/arch.rs b/src/hyperlight_host/src/hypervisor/gdb/arch.rs index e75eade74..71035f0e3 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/arch.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/arch.rs @@ -16,25 +16,22 @@ limitations under the License. //! This file contains architecture specific code for the x86_64 -use std::collections::HashMap; - use super::VcpuStopReason; +use crate::Result; +use crate::hypervisor::regs::CommonRegisters; +use crate::hypervisor::vm::Vm; // Described in Table 6-1. Exceptions and Interrupts at Page 6-13 Vol. 1 // of Intel 64 and IA-32 Architectures Software Developer's Manual /// Exception id for #DB -const DB_EX_ID: u32 = 1; +pub(crate) const DB_EX_ID: u32 = 1; /// Exception id for #BP - triggered by the INT3 instruction -const BP_EX_ID: u32 = 3; +pub(crate) const BP_EX_ID: u32 = 3; -/// Software Breakpoint size in memory -pub(crate) const SW_BP_SIZE: usize = 1; /// Software Breakpoint opcode - INT3 /// Check page 7-28 Vol. 3A of Intel 64 and IA-32 /// Architectures Software Developer's Manual pub(crate) const SW_BP_OP: u8 = 0xCC; -/// Software Breakpoint written to memory -pub(crate) const SW_BP: [u8; SW_BP_SIZE] = [SW_BP_OP]; /// Maximum number of supported hardware breakpoints pub(crate) const MAX_NO_OF_HW_BP: usize = 4; @@ -54,58 +51,51 @@ pub(crate) const DR6_HW_BP_FLAGS_MASK: u64 = 0x0F << DR6_HW_BP_FLAGS_POS; /// NOTE: Additional checks are done for the entrypoint, stored hw_breakpoints /// and sw_breakpoints to ensure the stop reason is valid with internal state pub(crate) fn vcpu_stop_reason( - single_step: bool, - rip: u64, - dr6: u64, + vm: &mut dyn Vm, entrypoint: u64, + dr6: u64, exception: u32, - hw_breakpoints: &[u64], - sw_breakpoints: &HashMap, -) -> VcpuStopReason { +) -> Result { + let CommonRegisters { rip, .. } = vm.regs()?; if DB_EX_ID == exception { // If the BS flag in DR6 register is set, it means a single step // instruction triggered the exit // Check page 19-4 Vol. 3B of Intel 64 and IA-32 // Architectures Software Developer's Manual - if dr6 & DR6_BS_FLAG_MASK != 0 && single_step { - return VcpuStopReason::DoneStep; + if dr6 & DR6_BS_FLAG_MASK != 0 { + return Ok(VcpuStopReason::DoneStep); } // If any of the B0-B3 flags in DR6 register is set, it means a // hardware breakpoint triggered the exit // Check page 19-4 Vol. 3B of Intel 64 and IA-32 // Architectures Software Developer's Manual - if DR6_HW_BP_FLAGS_MASK & dr6 != 0 && hw_breakpoints.contains(&rip) { + if DR6_HW_BP_FLAGS_MASK & dr6 != 0 { if rip == entrypoint { - return VcpuStopReason::EntryPointBp; + vm.remove_hw_breakpoint(entrypoint)?; + return Ok(VcpuStopReason::EntryPointBp); } - return VcpuStopReason::HwBp; + return Ok(VcpuStopReason::HwBp); } } - if BP_EX_ID == exception && sw_breakpoints.contains_key(&rip) { - return VcpuStopReason::SwBp; + if BP_EX_ID == exception { + return Ok(VcpuStopReason::SwBp); } // Log an error and provide internal debugging info log::error!( r"The vCPU exited because of an unknown reason: - single_step: {:?} rip: {:?} dr6: {:?} entrypoint: {:?} exception: {:?} - hw_breakpoints: {:?} - sw_breakpoints: {:?} ", - single_step, rip, dr6, entrypoint, exception, - hw_breakpoints, - sw_breakpoints, ); - VcpuStopReason::Unknown + Ok(VcpuStopReason::Unknown) } diff --git a/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs deleted file mode 100644 index ed55457fd..000000000 --- a/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs +++ /dev/null @@ -1,235 +0,0 @@ -/* -Copyright 2024 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::collections::HashMap; - -use windows::Win32::System::Hypervisor::WHV_VP_EXCEPTION_CONTEXT; - -use super::arch::{MAX_NO_OF_HW_BP, vcpu_stop_reason}; -use super::{GuestDebug, SW_BP_SIZE, VcpuStopReason}; -use crate::hypervisor::regs::CommonRegisters; -use crate::hypervisor::windows_hypervisor_platform::VMProcessor; -use crate::hypervisor::wrappers::WHvDebugRegisters; -use crate::{HyperlightError, Result, new_error}; - -/// KVM Debug struct -/// This struct is used to abstract the internal details of the kvm -/// guest debugging settings -#[derive(Default)] -pub(crate) struct HypervDebug { - /// vCPU stepping state - single_step: bool, - - /// Array of addresses for HW breakpoints - hw_breakpoints: Vec, - /// Saves the bytes modified to enable SW breakpoints - sw_breakpoints: HashMap, - - /// Debug registers - dbg_cfg: WHvDebugRegisters, -} - -impl HypervDebug { - pub(crate) fn new() -> Self { - Self { - single_step: false, - hw_breakpoints: vec![], - sw_breakpoints: HashMap::new(), - dbg_cfg: WHvDebugRegisters::default(), - } - } - - /// Returns the instruction pointer from the stopped vCPU - fn get_instruction_pointer(&self, vcpu_fd: &VMProcessor) -> Result { - let regs = vcpu_fd - .regs() - .map_err(|e| new_error!("Could not retrieve registers from vCPU: {:?}", e))?; - - Ok(regs.rip) - } - - /// This method sets the kvm debugreg fields to enable breakpoints at - /// specific addresses - /// - /// The first 4 debug registers are used to set the addresses - /// The 4th and 5th debug registers are obsolete and not used - /// The 7th debug register is used to enable the breakpoints - /// For more information see: DEBUG REGISTERS chapter in the architecture - /// manual - fn set_debug_config(&mut self, vcpu_fd: &VMProcessor, step: bool) -> Result<()> { - let addrs = &self.hw_breakpoints; - - let mut dbg_cfg = WHvDebugRegisters::default(); - - for (k, addr) in addrs.iter().enumerate() { - match k { - 0 => { - dbg_cfg.dr0 = *addr; - } - 1 => { - dbg_cfg.dr1 = *addr; - } - 2 => { - dbg_cfg.dr2 = *addr; - } - 3 => { - dbg_cfg.dr3 = *addr; - } - _ => { - Err(new_error!("Tried to set more than 4 HW breakpoints"))?; - } - } - dbg_cfg.dr7 |= 1 << (k * 2); - } - - self.dbg_cfg = dbg_cfg; - - vcpu_fd - .set_debug_regs(&self.dbg_cfg) - .map_err(|e| new_error!("Could not set guest debug: {:?}", e))?; - - self.single_step = step; - - let mut regs = vcpu_fd - .regs() - .map_err(|e| new_error!("Could not get registers: {:?}", e))?; - - // Set TF Flag to enable Traps - if self.single_step { - regs.rflags |= 1 << 8; // Set the TF flag - } else { - regs.rflags &= !(1 << 8); // Clear the TF flag - } - - vcpu_fd - .set_regs(®s) - .map_err(|e| new_error!("Could not set guest registers: {:?}", e))?; - - Ok(()) - } - - /// Get the reason the vCPU has stopped - pub(crate) fn get_stop_reason( - &mut self, - vcpu_fd: &VMProcessor, - exception: WHV_VP_EXCEPTION_CONTEXT, - entrypoint: u64, - ) -> Result { - let rip = self.get_instruction_pointer(vcpu_fd)?; - let rip = self.translate_gva(vcpu_fd, rip)?; - - let debug_regs = vcpu_fd - .get_debug_regs() - .map_err(|e| new_error!("Could not retrieve registers from vCPU: {:?}", e))?; - - // Check if the vCPU stopped because of a hardware breakpoint - let reason = vcpu_stop_reason( - self.single_step, - rip, - debug_regs.dr6, - entrypoint, - exception.ExceptionType as u32, - &self.hw_breakpoints, - &self.sw_breakpoints, - ); - - if let VcpuStopReason::EntryPointBp = reason { - // In case the hw breakpoint is the entry point, remove it to - // avoid hanging here as gdb does not remove breakpoints it - // has not set. - // Gdb expects the target to be stopped when connected. - self.remove_hw_breakpoint(vcpu_fd, entrypoint)?; - } - - Ok(reason) - } -} - -impl GuestDebug for HypervDebug { - type Vcpu = VMProcessor; - - fn is_hw_breakpoint(&self, addr: &u64) -> bool { - self.hw_breakpoints.contains(addr) - } - fn is_sw_breakpoint(&self, addr: &u64) -> bool { - self.sw_breakpoints.contains_key(addr) - } - fn save_hw_breakpoint(&mut self, addr: &u64) -> bool { - if self.hw_breakpoints.len() >= MAX_NO_OF_HW_BP { - false - } else { - self.hw_breakpoints.push(*addr); - - true - } - } - fn save_sw_breakpoint_data(&mut self, addr: u64, data: [u8; 1]) { - _ = self.sw_breakpoints.insert(addr, data); - } - fn delete_hw_breakpoint(&mut self, addr: &u64) { - self.hw_breakpoints.retain(|&a| a != *addr); - } - fn delete_sw_breakpoint_data(&mut self, addr: &u64) -> Option<[u8; 1]> { - self.sw_breakpoints.remove(addr) - } - - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> Result<()> { - log::debug!("Read registers"); - let vcpu_regs = vcpu_fd - .regs() - .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; - - regs.rax = vcpu_regs.rax; - regs.rbx = vcpu_regs.rbx; - regs.rcx = vcpu_regs.rcx; - regs.rdx = vcpu_regs.rdx; - regs.rsi = vcpu_regs.rsi; - regs.rdi = vcpu_regs.rdi; - regs.rbp = vcpu_regs.rbp; - regs.rsp = vcpu_regs.rsp; - regs.r8 = vcpu_regs.r8; - regs.r9 = vcpu_regs.r9; - regs.r10 = vcpu_regs.r10; - regs.r11 = vcpu_regs.r11; - regs.r12 = vcpu_regs.r12; - regs.r13 = vcpu_regs.r13; - regs.r14 = vcpu_regs.r14; - regs.r15 = vcpu_regs.r15; - - regs.rip = vcpu_regs.rip; - regs.rflags = vcpu_regs.rflags; - - Ok(()) - } - - fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> Result<()> { - self.set_debug_config(vcpu_fd, enable) - } - - fn translate_gva(&self, vcpu_fd: &Self::Vcpu, gva: u64) -> Result { - vcpu_fd - .translate_gva(gva) - .map_err(|_| HyperlightError::TranslateGuestAddress(gva)) - } - - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> Result<()> { - log::debug!("Write registers"); - - vcpu_fd - .set_regs(regs) - .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) - } -} diff --git a/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs deleted file mode 100644 index b0fcf75b4..000000000 --- a/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs +++ /dev/null @@ -1,224 +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::collections::HashMap; - -use kvm_bindings::{ - KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP, KVM_GUESTDBG_USE_HW_BP, KVM_GUESTDBG_USE_SW_BP, - kvm_debug_exit_arch, kvm_guest_debug, -}; -use kvm_ioctls::VcpuFd; - -use super::arch::{MAX_NO_OF_HW_BP, SW_BP_SIZE, vcpu_stop_reason}; -use super::{GuestDebug, VcpuStopReason}; -use crate::hypervisor::regs::CommonRegisters; -use crate::{HyperlightError, Result, new_error}; - -/// KVM Debug struct -/// This struct is used to abstract the internal details of the kvm -/// guest debugging settings -#[derive(Default)] -pub(crate) struct KvmDebug { - /// vCPU stepping state - single_step: bool, - - /// Array of addresses for HW breakpoints - hw_breakpoints: Vec, - /// Saves the bytes modified to enable SW breakpoints - sw_breakpoints: HashMap, - - /// Sent to KVM for enabling guest debug - dbg_cfg: kvm_guest_debug, -} - -impl KvmDebug { - pub(crate) fn new() -> Self { - let dbg = kvm_guest_debug { - control: KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_SW_BP, - ..Default::default() - }; - - Self { - single_step: false, - hw_breakpoints: vec![], - sw_breakpoints: HashMap::new(), - dbg_cfg: dbg, - } - } - - /// Returns the instruction pointer from the stopped vCPU - fn get_instruction_pointer(&self, vcpu_fd: &VcpuFd) -> Result { - let regs = vcpu_fd - .get_regs() - .map_err(|e| new_error!("Could not retrieve registers from vCPU: {:?}", e))?; - - Ok(regs.rip) - } - - /// This method sets the kvm debugreg fields to enable breakpoints at - /// specific addresses - /// - /// The first 4 debug registers are used to set the addresses - /// The 4th and 5th debug registers are obsolete and not used - /// The 7th debug register is used to enable the breakpoints - /// For more information see: DEBUG REGISTERS chapter in the architecture - /// manual - fn set_debug_config(&mut self, vcpu_fd: &VcpuFd, step: bool) -> Result<()> { - let addrs = &self.hw_breakpoints; - - self.dbg_cfg.arch.debugreg = [0; 8]; - for (k, addr) in addrs.iter().enumerate() { - self.dbg_cfg.arch.debugreg[k] = *addr; - self.dbg_cfg.arch.debugreg[7] |= 1 << (k * 2); - } - - if !addrs.is_empty() { - self.dbg_cfg.control |= KVM_GUESTDBG_USE_HW_BP; - } else { - self.dbg_cfg.control &= !KVM_GUESTDBG_USE_HW_BP; - } - - if step { - self.dbg_cfg.control |= KVM_GUESTDBG_SINGLESTEP; - } else { - self.dbg_cfg.control &= !KVM_GUESTDBG_SINGLESTEP; - } - - log::debug!("Setting bp: {:?} cfg: {:?}", addrs, self.dbg_cfg); - vcpu_fd - .set_guest_debug(&self.dbg_cfg) - .map_err(|e| new_error!("Could not set guest debug: {:?}", e))?; - - self.single_step = step; - - Ok(()) - } - - /// Get the reason the vCPU has stopped - pub(crate) fn get_stop_reason( - &mut self, - vcpu_fd: &VcpuFd, - debug_exit: kvm_debug_exit_arch, - entrypoint: u64, - ) -> Result { - let rip = self.get_instruction_pointer(vcpu_fd)?; - let rip = self.translate_gva(vcpu_fd, rip)?; - - // Check if the vCPU stopped because of a hardware breakpoint - let reason = vcpu_stop_reason( - self.single_step, - rip, - debug_exit.dr6, - entrypoint, - debug_exit.exception, - &self.hw_breakpoints, - &self.sw_breakpoints, - ); - - if let VcpuStopReason::EntryPointBp = reason { - // In case the hw breakpoint is the entry point, remove it to - // avoid hanging here as gdb does not remove breakpoints it - // has not set. - // Gdb expects the target to be stopped when connected. - self.remove_hw_breakpoint(vcpu_fd, entrypoint)?; - } - - Ok(reason) - } -} - -impl GuestDebug for KvmDebug { - type Vcpu = VcpuFd; - - fn is_hw_breakpoint(&self, addr: &u64) -> bool { - self.hw_breakpoints.contains(addr) - } - fn is_sw_breakpoint(&self, addr: &u64) -> bool { - self.sw_breakpoints.contains_key(addr) - } - fn save_hw_breakpoint(&mut self, addr: &u64) -> bool { - if self.hw_breakpoints.len() >= MAX_NO_OF_HW_BP { - false - } else { - self.hw_breakpoints.push(*addr); - - true - } - } - fn save_sw_breakpoint_data(&mut self, addr: u64, data: [u8; 1]) { - _ = self.sw_breakpoints.insert(addr, data); - } - fn delete_hw_breakpoint(&mut self, addr: &u64) { - self.hw_breakpoints.retain(|&a| a != *addr); - } - fn delete_sw_breakpoint_data(&mut self, addr: &u64) -> Option<[u8; 1]> { - self.sw_breakpoints.remove(addr) - } - - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> Result<()> { - log::debug!("Read registers"); - let vcpu_regs = vcpu_fd - .get_regs() - .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; - - regs.rax = vcpu_regs.rax; - regs.rbx = vcpu_regs.rbx; - regs.rcx = vcpu_regs.rcx; - regs.rdx = vcpu_regs.rdx; - regs.rsi = vcpu_regs.rsi; - regs.rdi = vcpu_regs.rdi; - regs.rbp = vcpu_regs.rbp; - regs.rsp = vcpu_regs.rsp; - regs.r8 = vcpu_regs.r8; - regs.r9 = vcpu_regs.r9; - regs.r10 = vcpu_regs.r10; - regs.r11 = vcpu_regs.r11; - regs.r12 = vcpu_regs.r12; - regs.r13 = vcpu_regs.r13; - regs.r14 = vcpu_regs.r14; - regs.r15 = vcpu_regs.r15; - - regs.rip = vcpu_regs.rip; - regs.rflags = vcpu_regs.rflags; - - Ok(()) - } - - fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> Result<()> { - self.set_debug_config(vcpu_fd, enable) - } - - fn translate_gva(&self, vcpu_fd: &Self::Vcpu, gva: u64) -> Result { - let tr = vcpu_fd - .translate_gva(gva) - .map_err(|_| HyperlightError::TranslateGuestAddress(gva))?; - - if tr.valid == 0 { - Err(HyperlightError::TranslateGuestAddress(gva)) - } else { - Ok(tr.physical_address) - } - } - - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> Result<()> { - log::debug!("Write registers"); - let new_regs = regs.into(); - - vcpu_fd - .set_regs(&new_regs) - .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) - } -} diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 7a2234436..4a06753b3 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -14,43 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -mod arch; +pub(crate) mod arch; mod event_loop; -#[cfg(target_os = "windows")] -mod hyperv_debug; -#[cfg(kvm)] -mod kvm_debug; -#[cfg(mshv)] -mod mshv_debug; mod x86_64_target; use std::io::{self, ErrorKind}; use std::net::TcpListener; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::thread; -use arch::{SW_BP, SW_BP_SIZE}; use crossbeam_channel::{Receiver, Sender, TryRecvError}; use event_loop::event_loop_thread; use gdbstub::conn::ConnectionExt; use gdbstub::stub::GdbStub; use gdbstub::target::TargetError; -use hyperlight_common::mem::PAGE_SIZE; -#[cfg(target_os = "windows")] -pub(crate) use hyperv_debug::HypervDebug; -#[cfg(kvm)] -pub(crate) use kvm_debug::KvmDebug; -#[cfg(mshv)] -pub(crate) use mshv_debug::MshvDebug; use thiserror::Error; use x86_64_target::HyperlightSandboxTarget; use super::InterruptHandle; -use crate::hypervisor::regs::CommonRegisters; -use crate::mem::layout::SandboxMemoryLayout; -use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::shared_mem::HostSharedMemory; -use crate::{HyperlightError, new_error}; +use super::regs::CommonRegisters; #[derive(Debug, Error)] pub(crate) enum GdbTargetError { @@ -142,185 +124,6 @@ pub(crate) enum DebugResponse { WriteRegisters, } -/// This trait is used to define common debugging functionality for Hypervisors -pub(super) trait GuestDebug { - /// Type that wraps the vCPU functionality - type Vcpu; - - /// Returns true whether the provided address is a hardware breakpoint - fn is_hw_breakpoint(&self, addr: &u64) -> bool; - /// Returns true whether the provided address is a software breakpoint - fn is_sw_breakpoint(&self, addr: &u64) -> bool; - /// Stores the address of the hw breakpoint - fn save_hw_breakpoint(&mut self, addr: &u64) -> bool; - /// Stores the data that the sw breakpoint op code replaces - fn save_sw_breakpoint_data(&mut self, addr: u64, data: [u8; 1]); - /// Deletes the address of the hw breakpoint from storage - fn delete_hw_breakpoint(&mut self, addr: &u64); - /// Retrieves the saved data that the sw breakpoint op code replaces - fn delete_sw_breakpoint_data(&mut self, addr: &u64) -> Option<[u8; 1]>; - - /// Read registers - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> crate::Result<()>; - /// Enables or disables stepping and sets the vCPU debug configuration - fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> crate::Result<()>; - /// Translates the guest address to physical address - fn translate_gva(&self, vcpu_fd: &Self::Vcpu, gva: u64) -> crate::Result; - /// Write registers - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> crate::Result<()>; - - /// Adds hardware breakpoint - fn add_hw_breakpoint(&mut self, vcpu_fd: &Self::Vcpu, addr: u64) -> crate::Result<()> { - let addr = self.translate_gva(vcpu_fd, addr)?; - - if self.is_hw_breakpoint(&addr) { - return Ok(()); - } - - self.save_hw_breakpoint(&addr) - .then(|| self.set_single_step(vcpu_fd, false)) - .ok_or_else(|| new_error!("Failed to save hw breakpoint"))? - } - /// Overwrites the guest memory with the SW Breakpoint op code that instructs - /// the vCPU to stop when is executed and stores the overwritten data to be - /// able to restore it - fn add_sw_breakpoint( - &mut self, - vcpu_fd: &Self::Vcpu, - addr: u64, - dbg_mem_access_fn: Arc>>, - ) -> crate::Result<()> { - let addr = self.translate_gva(vcpu_fd, addr)?; - - if self.is_sw_breakpoint(&addr) { - return Ok(()); - } - - // Write breakpoint OP code to write to guest memory - let mut save_data = [0; SW_BP_SIZE]; - self.read_addrs(vcpu_fd, addr, &mut save_data[..], dbg_mem_access_fn.clone())?; - self.write_addrs(vcpu_fd, addr, &SW_BP, dbg_mem_access_fn)?; - - // Save guest memory to restore when breakpoint is removed - self.save_sw_breakpoint_data(addr, save_data); - - Ok(()) - } - /// Copies the data from the guest memory address to the provided slice - /// The address is checked to be a valid guest address - fn read_addrs( - &mut self, - vcpu_fd: &Self::Vcpu, - mut gva: u64, - mut data: &mut [u8], - dbg_mem_access_fn: Arc>>, - ) -> crate::Result<()> { - let data_len = data.len(); - log::debug!("Read addr: {:X} len: {:X}", gva, data_len); - - while !data.is_empty() { - let gpa = self.translate_gva(vcpu_fd, gva)?; - - let read_len = std::cmp::min( - data.len(), - (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(), - ); - let offset = (gpa as usize) - .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) - .ok_or_else(|| { - log::warn!( - "gva=0x{:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", - gva, gpa, SandboxMemoryLayout::BASE_ADDRESS); - HyperlightError::TranslateGuestAddress(gva) - })?; - - dbg_mem_access_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .get_shared_mem_mut() - .copy_to_slice(&mut data[..read_len], offset)?; - - data = &mut data[read_len..]; - gva += read_len as u64; - } - - Ok(()) - } - /// Removes hardware breakpoint - fn remove_hw_breakpoint(&mut self, vcpu_fd: &Self::Vcpu, addr: u64) -> crate::Result<()> { - let addr = self.translate_gva(vcpu_fd, addr)?; - - self.is_hw_breakpoint(&addr) - .then(|| { - self.delete_hw_breakpoint(&addr); - self.set_single_step(vcpu_fd, false) - }) - .ok_or_else(|| new_error!("The address: {:?} is not a hw breakpoint", addr))? - } - /// Restores the overwritten data to the guest memory - fn remove_sw_breakpoint( - &mut self, - vcpu_fd: &Self::Vcpu, - addr: u64, - dbg_mem_access_fn: Arc>>, - ) -> crate::Result<()> { - let addr = self.translate_gva(vcpu_fd, addr)?; - - if self.is_sw_breakpoint(&addr) { - let save_data = self - .delete_sw_breakpoint_data(&addr) - .ok_or_else(|| new_error!("Expected to contain the sw breakpoint address"))?; - - // Restore saved data to the guest's memory - self.write_addrs(vcpu_fd, addr, &save_data, dbg_mem_access_fn)?; - - Ok(()) - } else { - Err(new_error!("The address: {:?} is not a sw breakpoint", addr)) - } - } - /// Copies the data from the provided slice to the guest memory address - /// The address is checked to be a valid guest address - fn write_addrs( - &mut self, - vcpu_fd: &Self::Vcpu, - mut gva: u64, - mut data: &[u8], - dbg_mem_access_fn: Arc>>, - ) -> crate::Result<()> { - let data_len = data.len(); - log::debug!("Write addr: {:X} len: {:X}", gva, data_len); - - while !data.is_empty() { - let gpa = self.translate_gva(vcpu_fd, gva)?; - - let write_len = std::cmp::min( - data.len(), - (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(), - ); - let offset = (gpa as usize) - .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) - .ok_or_else(|| { - log::warn!( - "gva=0x{:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", - gva, gpa, SandboxMemoryLayout::BASE_ADDRESS); - HyperlightError::TranslateGuestAddress(gva) - })?; - - dbg_mem_access_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .get_shared_mem_mut() - .copy_from_slice(&data[..write_len], offset)?; - - data = &data[write_len..]; - gva += write_len as u64; - } - - Ok(()) - } -} - /// Debug communication channel that is used for sending a request type and /// receive a different response type pub(crate) struct DebugCommChannel { @@ -427,7 +230,7 @@ mod tests { let res = gdb_conn.try_recv(); assert!(res.is_err()); - let res = hyp_conn.send(DebugResponse::ReadRegisters(Default::default())); + let res = hyp_conn.send(DebugResponse::ReadRegisters(CommonRegisters::default())); assert!(res.is_ok()); let res = gdb_conn.recv(); diff --git a/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs deleted file mode 100644 index 338d7aabe..000000000 --- a/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs +++ /dev/null @@ -1,247 +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. -*/ - -#[cfg(mshv2)] -extern crate mshv_bindings2 as mshv_bindings; -#[cfg(mshv2)] -extern crate mshv_ioctls2 as mshv_ioctls; - -#[cfg(mshv3)] -extern crate mshv_bindings3 as mshv_bindings; -#[cfg(mshv3)] -extern crate mshv_ioctls3 as mshv_ioctls; - -use std::collections::HashMap; - -use mshv_bindings::{ - DebugRegisters, HV_TRANSLATE_GVA_VALIDATE_READ, HV_TRANSLATE_GVA_VALIDATE_WRITE, -}; -use mshv_ioctls::VcpuFd; - -use super::arch::{MAX_NO_OF_HW_BP, SW_BP_SIZE, vcpu_stop_reason}; -use super::{GuestDebug, VcpuStopReason}; -use crate::hypervisor::regs::CommonRegisters; -use crate::{HyperlightError, Result, new_error}; - -#[derive(Debug, Default)] -pub(crate) struct MshvDebug { - /// vCPU stepping state - single_step: bool, - - /// Array of addresses for HW breakpoints - hw_breakpoints: Vec, - /// Saves the bytes modified to enable SW breakpoints - sw_breakpoints: HashMap, - - /// Debug registers - dbg_cfg: DebugRegisters, -} - -impl MshvDebug { - pub(crate) fn new() -> Self { - Self { - single_step: false, - hw_breakpoints: vec![], - sw_breakpoints: HashMap::new(), - dbg_cfg: DebugRegisters::default(), - } - } - - /// Returns the instruction pointer from the stopped vCPU - fn get_instruction_pointer(&self, vcpu_fd: &VcpuFd) -> Result { - let regs = vcpu_fd - .get_regs() - .map_err(|e| new_error!("Could not retrieve registers from vCPU: {:?}", e))?; - - Ok(regs.rip) - } - - /// This method sets the vCPU debug register fields to enable breakpoints at - /// specific addresses - /// - /// The first 4 debug registers are used to set the addresses - /// The 4th and 5th debug registers are obsolete and not used - /// The 7th debug register is used to enable the breakpoints - /// For more information see: DEBUG REGISTERS chapter in the architecture - /// manual - fn set_debug_config(&mut self, vcpu_fd: &VcpuFd, step: bool) -> Result<()> { - let addrs = &self.hw_breakpoints; - - let mut dbg_cfg = DebugRegisters::default(); - for (k, addr) in addrs.iter().enumerate() { - match k { - 0 => { - dbg_cfg.dr0 = *addr; - } - 1 => { - dbg_cfg.dr1 = *addr; - } - 2 => { - dbg_cfg.dr2 = *addr; - } - 3 => { - dbg_cfg.dr3 = *addr; - } - _ => { - Err(new_error!("Tried to set more than 4 HW breakpoints"))?; - } - } - dbg_cfg.dr7 |= 1 << (k * 2); - } - - self.dbg_cfg = dbg_cfg; - vcpu_fd - .set_debug_regs(&self.dbg_cfg) - .map_err(|e| new_error!("Could not set guest debug: {:?}", e))?; - - self.single_step = step; - - let mut regs = vcpu_fd - .get_regs() - .map_err(|e| new_error!("Could not get registers: {:?}", e))?; - - // Set TF Flag to enable Traps - if self.single_step { - regs.rflags |= 1 << 8; - } else { - regs.rflags &= !(1 << 8); - } - - vcpu_fd - .set_regs(®s) - .map_err(|e| new_error!("Could not set registers: {:?}", e))?; - - Ok(()) - } - - /// Returns the vCPU stop reason - pub(crate) fn get_stop_reason( - &mut self, - vcpu_fd: &VcpuFd, - exception: u16, - entrypoint: u64, - ) -> Result { - let regs = vcpu_fd - .get_debug_regs() - .map_err(|e| new_error!("Cannot retrieve debug registers from vCPU: {}", e))?; - - // DR6 register contains debug state related information - let debug_status = regs.dr6; - - let rip = self.get_instruction_pointer(vcpu_fd)?; - let rip = self.translate_gva(vcpu_fd, rip)?; - - let reason = vcpu_stop_reason( - self.single_step, - rip, - debug_status, - entrypoint, - exception as u32, - &self.hw_breakpoints, - &self.sw_breakpoints, - ); - - if let VcpuStopReason::EntryPointBp = reason { - // In case the hw breakpoint is the entry point, remove it to - // avoid hanging here as gdb does not remove breakpoints it - // has not set. - // Gdb expects the target to be stopped when connected. - self.remove_hw_breakpoint(vcpu_fd, entrypoint)?; - } - - Ok(reason) - } -} - -impl GuestDebug for MshvDebug { - type Vcpu = VcpuFd; - - fn is_hw_breakpoint(&self, addr: &u64) -> bool { - self.hw_breakpoints.contains(addr) - } - fn is_sw_breakpoint(&self, addr: &u64) -> bool { - self.sw_breakpoints.contains_key(addr) - } - fn save_hw_breakpoint(&mut self, addr: &u64) -> bool { - if self.hw_breakpoints.len() >= MAX_NO_OF_HW_BP { - false - } else { - self.hw_breakpoints.push(*addr); - - true - } - } - fn save_sw_breakpoint_data(&mut self, addr: u64, data: [u8; 1]) { - _ = self.sw_breakpoints.insert(addr, data); - } - fn delete_hw_breakpoint(&mut self, addr: &u64) { - self.hw_breakpoints.retain(|&a| a != *addr); - } - fn delete_sw_breakpoint_data(&mut self, addr: &u64) -> Option<[u8; 1]> { - self.sw_breakpoints.remove(addr) - } - - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut CommonRegisters) -> Result<()> { - log::debug!("Read registers"); - let vcpu_regs = vcpu_fd - .get_regs() - .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; - - regs.rax = vcpu_regs.rax; - regs.rbx = vcpu_regs.rbx; - regs.rcx = vcpu_regs.rcx; - regs.rdx = vcpu_regs.rdx; - regs.rsi = vcpu_regs.rsi; - regs.rdi = vcpu_regs.rdi; - regs.rbp = vcpu_regs.rbp; - regs.rsp = vcpu_regs.rsp; - regs.r8 = vcpu_regs.r8; - regs.r9 = vcpu_regs.r9; - regs.r10 = vcpu_regs.r10; - regs.r11 = vcpu_regs.r11; - regs.r12 = vcpu_regs.r12; - regs.r13 = vcpu_regs.r13; - regs.r14 = vcpu_regs.r14; - regs.r15 = vcpu_regs.r15; - - regs.rip = vcpu_regs.rip; - regs.rflags = vcpu_regs.rflags; - - Ok(()) - } - - fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> Result<()> { - self.set_debug_config(vcpu_fd, enable) - } - - fn translate_gva(&self, vcpu_fd: &Self::Vcpu, gva: u64) -> Result { - let flags = (HV_TRANSLATE_GVA_VALIDATE_READ | HV_TRANSLATE_GVA_VALIDATE_WRITE) as u64; - let (addr, _) = vcpu_fd - .translate_gva(gva, flags) - .map_err(|_| HyperlightError::TranslateGuestAddress(gva))?; - - Ok(addr) - } - - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &CommonRegisters) -> Result<()> { - log::debug!("Write registers"); - let new_regs = regs.into(); - - vcpu_fd - .set_regs(&new_regs) - .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) - } -} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs new file mode 100644 index 000000000..5923529fc --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -0,0 +1,1041 @@ +/* +Copyright 2024 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::convert::TryFrom; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +use log::LevelFilter; +use tracing::{Span, instrument}; + +#[cfg(gdb)] +use super::gdb::{DebugCommChannel, DebugMsg, DebugResponse, VcpuStopReason, arch}; +use super::regs::{CommonFpu, CommonRegisters}; +use super::{LinuxInterruptHandle, get_max_log_level}; +use crate::HyperlightError::{ExecutionCanceledByHost, NoHypervisorFound}; +#[cfg(crashdump)] +use crate::hypervisor::crashdump; +#[cfg(mshv)] +use crate::hypervisor::hyperv_linux::MshvVm; +#[cfg(kvm)] +use crate::hypervisor::kvm::KvmVm; +use crate::hypervisor::regs::CommonSpecialRegisters; +#[cfg(gdb)] +use crate::hypervisor::vm::DebugExit; +use crate::hypervisor::vm::{Vm, VmExit}; +#[cfg(target_os = "windows")] +use crate::hypervisor::whp::WhpVm; +#[cfg(target_os = "windows")] +use crate::hypervisor::wrappers::HandleWrapper; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::ptr::{GuestPtr, RawPtr}; +use crate::mem::shared_mem::HostSharedMemory; +use crate::metrics::METRIC_GUEST_CANCELLATION; +use crate::sandbox::SandboxConfiguration; +#[cfg(feature = "trace_guest")] +use crate::sandbox::TraceInfo; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::hypervisor::{HypervisorType, get_available_hypervisor}; +use crate::sandbox::outb::handle_outb; +#[cfg(crashdump)] +use crate::sandbox::uninitialized::SandboxRuntimeConfig; +use crate::{HyperlightError, Result, log_then_return, new_error}; + +pub(crate) struct HyperlightVm { + vm: Box, + page_size: usize, + entrypoint: u64, + orig_rsp: GuestPtr, + interrupt_handle: Arc, + mem_mgr: Option>, + host_funcs: Option>>, + + sandbox_regions: Vec, // Initially mapped regions when sandbox is created + mmap_regions: Vec<(u32, MemoryRegion)>, // Later mapped regions (slot number, region) + next_slot: u32, // Monotonically increasing slot number + freed_slots: Vec, // Reusable slots from unmapped regions + + #[cfg(gdb)] + gdb_conn: Option>, + #[cfg(feature = "trace_guest")] + trace_info: TraceInfo, + #[cfg(crashdump)] + rt_cfg: SandboxRuntimeConfig, +} + +impl HyperlightVm { + /// Create a new HyperlightVm instance (will not run vm until calling `initialise`) + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + mem_regions: Vec, + pml4_addr: u64, + entrypoint: u64, + rsp: u64, + config: &SandboxConfiguration, + #[cfg(target_os = "windows")] handle: HandleWrapper, + #[cfg(gdb)] gdb_conn: Option>, + #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "trace_guest")] trace_info: TraceInfo, + ) -> Result { + #[allow(unused_mut)] // needs to be mutable when gdb is enabled + let mut vm: Box = match get_available_hypervisor() { + #[cfg(kvm)] + Some(HypervisorType::Kvm) => Box::new(KvmVm::new()?), + #[cfg(mshv)] + Some(HypervisorType::Mshv) => Box::new(MshvVm::new()?), + #[cfg(target_os = "windows")] + Some(HypervisorType::Whp) => Box::new(WhpVm::new(handle)?), + None => return Err(NoHypervisorFound()), + }; + + for (i, region) in mem_regions.iter().enumerate() { + // Safety: slots are unique and region points to valid memory since we created the regions + unsafe { vm.map_memory((i as u32, region))? }; + } + + vm.set_sregs(&CommonSpecialRegisters { + cr3: pml4_addr, + ..Default::default() + })?; + + #[cfg(gdb)] + let gdb_conn = if let Some(gdb_conn) = gdb_conn { + // Add breakpoint to the entry point address + vm.set_debug(true)?; + vm.add_hw_breakpoint(entrypoint)?; + + Some(gdb_conn) + } else { + None + }; + + let rsp_gp = GuestPtr::try_from(RawPtr::from(rsp))?; + let interrupt_handle = Arc::new(LinuxInterruptHandle { + running: AtomicU64::new(0), + cancel_requested: AtomicBool::new(false), + #[cfg(gdb)] + debug_interrupt: AtomicBool::new(false), + #[cfg(all( + target_arch = "x86_64", + target_vendor = "unknown", + target_os = "linux", + target_env = "musl" + ))] + tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }), + #[cfg(not(all( + target_arch = "x86_64", + target_vendor = "unknown", + target_os = "linux", + target_env = "musl" + )))] + tid: AtomicU64::new(unsafe { libc::pthread_self() }), + retry_delay: config.get_interrupt_retry_delay(), + sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(), + dropped: AtomicBool::new(false), + }); + + #[allow(unused_mut)] // needs to be mutable when gdb is enabled + let mut ret = Self { + vm, + entrypoint, + orig_rsp: rsp_gp, + interrupt_handle, + page_size: 0, // Will be set in `initialise` + mem_mgr: None, + host_funcs: None, + + next_slot: mem_regions.len() as u32, + sandbox_regions: mem_regions, + mmap_regions: Vec::new(), + freed_slots: Vec::new(), + + #[cfg(gdb)] + gdb_conn, + #[cfg(feature = "trace_guest")] + trace_info, + #[cfg(crashdump)] + rt_cfg, + }; + + // Send the interrupt handle to the GDB thread if debugging is enabled + // This is used to allow the GDB thread to stop the vCPU + #[cfg(gdb)] + if ret.gdb_conn.is_some() { + ret.send_dbg_msg(DebugResponse::InterruptHandle(ret.interrupt_handle.clone()))?; + } + + Ok(ret) + } + + /// Initialise the HyperlightVm (will run vm). + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + #[allow(clippy::too_many_arguments)] + pub(crate) fn initialise( + &mut self, + peb_addr: RawPtr, + seed: u64, + page_size: u32, + mem_mgr: SandboxMemoryManager, + host_funcs: Arc>, + max_guest_log_level: Option, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + self.mem_mgr = Some(mem_mgr); + self.host_funcs = Some(host_funcs); + self.page_size = page_size as usize; + + let max_guest_log_level: u64 = match max_guest_log_level { + Some(level) => level as u64, + None => get_max_log_level().into(), + }; + + let regs = CommonRegisters { + rip: self.entrypoint, + rsp: self.orig_rsp.absolute()?, + + // function args + rdi: peb_addr.into(), + rsi: seed, + rdx: page_size.into(), + rcx: max_guest_log_level, + rflags: 1 << 1, + + ..Default::default() + }; + self.vm.set_regs(®s)?; + + self.run( + #[cfg(gdb)] + dbg_mem_access_fn, + )?; + + Ok(()) + } + + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + pub(crate) fn dispatch_call_from_host( + &mut self, + dispatch_func_addr: RawPtr, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + // set RIP and RSP, reset others + let regs = CommonRegisters { + rip: dispatch_func_addr.into(), + rsp: self.orig_rsp.absolute()?, + rflags: 1 << 1, + ..Default::default() + }; + self.vm.set_regs(®s)?; + + // reset fpu + self.vm.set_fpu(&CommonFpu::default())?; + + self.run( + #[cfg(gdb)] + dbg_mem_access_fn, + )?; + + Ok(()) + } + + // Safety: The caller must ensure that the memory region is valid and points to valid memory, + pub(crate) unsafe fn map_region(&mut self, region: &MemoryRegion) -> Result<()> { + // 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(()) + } + + pub(crate) fn unmap_region(&mut self, region: &MemoryRegion) -> Result<()> { + if let Some(pos) = self.mmap_regions.iter().position(|(_, r)| r == region) { + let (slot, _) = self.mmap_regions.remove(pos); + self.freed_slots.push(slot); + self.vm.unmap_memory((slot, region))?; + } else { + return Err(new_error!("Region not found in mapped regions")); + } + + Ok(()) + } + + pub(crate) fn get_mapped_regions(&self) -> &[(u32, MemoryRegion)] { + &self.mmap_regions + } + + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + fn handle_io(&mut self, port: u16, data: Vec) -> Result<()> { + if data.is_empty() { + log_then_return!("no data was given in IO interrupt"); + } + + #[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 = "trace_guest")] + { + // We need to handle the borrow checker issue where we need both: + // - &mut MemMgrWrapper (from self.mem_mgr.as_mut()) + // - &mut dyn Hypervisor (from self) + // We'll use a temporary approach to extract the mem_mgr temporarily + let mem_mgr_option = self.mem_mgr.take(); + let mut mem_mgr = + mem_mgr_option.ok_or_else(|| new_error!("mem_mgr not initialized"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs not initialized"))? + .clone(); + + handle_outb(&mut mem_mgr, host_funcs, self, port, val)?; + + // Put the mem_mgr back + self.mem_mgr = Some(mem_mgr); + } + + #[cfg(not(feature = "trace_guest"))] + { + let mem_mgr = self + .mem_mgr + .as_mut() + .ok_or_else(|| new_error!("mem_mgr not initialized"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs not initialized"))? + .clone(); + + handle_outb(mem_mgr, host_funcs, port, val)?; + } + + Ok(()) + } + + fn run( + &mut self, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + let result = loop { + self.interrupt_handle + .tid + .store(unsafe { libc::pthread_self() as u64 }, Ordering::Relaxed); + // Note: if a `InterruptHandle::kill()` called while this thread is **here** + // Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call + self.interrupt_handle + .set_running_and_increment_generation() + .map_err(|e| { + new_error!( + "Error setting running state and incrementing generation: {}", + e + ) + })?; + + // Don't run the vcpu if `cancel_requested` is true + // + // Note: if a `InterruptHandle::kill()` called while this thread is **here** + // Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call + let exit_reason = if self + .interrupt_handle + .cancel_requested + .load(Ordering::Relaxed) + { + Ok(VmExit::Cancelled()) + } else { + self.vm.run_vcpu() + }; + // Note: if a `InterruptHandle::kill()` called while this thread is **here** + // Then signals will be sent to this thread until `running` is set to false. + // This is fine since the signal handler is a no-op. + let cancel_requested = self + .interrupt_handle + .cancel_requested + .load(Ordering::Relaxed); + // Note: if a `InterruptHandle::kill()` called while this thread is **here** + // Then `cancel_requested` will be set to true again, which will cancel the **next vcpu run**. + // Additionally signals will be sent to this thread until `running` is set to false. + // This is fine since the signal handler is a no-op. + self.interrupt_handle.clear_running_bit(); + // At this point, `running` is `false` so no more signals will be sent to this thread, + // but we may still receive async signals that were sent before this point. + // To prevent those signals from interrupting subsequent calls to `run()` (on other vms!), + // we make sure to check `cancel_requested` before cancelling (see `libc::EINTR` match-arm below). + match exit_reason { + #[cfg(gdb)] + Ok(VmExit::Debug(debug_exit)) => { + match debug_exit { + DebugExit::Debug { dr6, exception } => { + // Handle debug event (breakpoints) + let stop_reason = arch::vcpu_stop_reason( + self.vm.as_mut(), + self.entrypoint, + dr6, + exception, + )?; + if let Err(e) = + self.handle_debug(dbg_mem_access_fn.clone(), stop_reason) + { + break Err(e); + } + } + DebugExit::Interrupt => { + if let Err(e) = self + .handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Interrupt) + { + break Err(e); + } + } + } + } + + Ok(VmExit::Halt()) => { + break Ok(()); + } + Ok(VmExit::IoOut(port, data)) => self.handle_io(port, data)?, + Ok(VmExit::MmioRead(addr)) => { + let all_regions = self + .sandbox_regions + .iter() + .chain(self.mmap_regions.iter().map(|(_, r)| r)); + match get_memory_access_violation( + addr as usize, + MemoryRegionFlags::WRITE, + all_regions, + ) { + Some(MemoryAccess::StackGuardPageViolation) => { + break Err(HyperlightError::StackOverflow()); + } + Some(MemoryAccess::AccessViolation(region_flags)) => { + break Err(HyperlightError::MemoryAccessViolation( + addr, + MemoryRegionFlags::READ, + region_flags, + )); + } + None => { + match &self.mem_mgr { + Some(mem_mgr) => { + if !mem_mgr.check_stack_guard()? { + break Err(HyperlightError::StackOverflow()); + } + } + None => { + break Err(new_error!("Memory manager not initialized")); + } + } + + break Err(new_error!("MMIO READ access address {:#x}", addr)); + } + } + } + Ok(VmExit::MmioWrite(addr)) => { + let all_regions = self + .sandbox_regions + .iter() + .chain(self.mmap_regions.iter().map(|(_, r)| r)); + match get_memory_access_violation( + addr as usize, + MemoryRegionFlags::WRITE, + all_regions, + ) { + Some(MemoryAccess::StackGuardPageViolation) => { + break Err(HyperlightError::StackOverflow()); + } + Some(MemoryAccess::AccessViolation(region_flags)) => { + break Err(HyperlightError::MemoryAccessViolation( + addr, + MemoryRegionFlags::WRITE, + region_flags, + )); + } + None => { + match &self.mem_mgr { + Some(mem_mgr) => { + if !mem_mgr.check_stack_guard()? { + break Err(HyperlightError::StackOverflow()); + } + } + None => { + break Err(new_error!("Memory manager not initialized")); + } + } + + break Err(new_error!("MMIO WRITE access address {:#x}", addr)); + } + } + } + Ok(VmExit::Cancelled()) => { + // If cancellation was not requested for this specific vm, the vcpu was interrupted because of debug interrupt or + // a stale signal that meant to be delivered to a previous/other vcpu on this same thread, so let's ignore it + if cancel_requested { + self.interrupt_handle + .cancel_requested + .store(false, Ordering::Relaxed); + metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); + break Err(ExecutionCanceledByHost()); + } else { + // treat this the same as a VmExit::Retry, the cancel was not meant for this vcpu + continue; + } + } + Ok(VmExit::Unknown(reason)) => { + break Err(new_error!("Unexpected VM Exit: {:?}", reason)); + } + Ok(VmExit::Retry()) => continue, + Err(e) => { + break Err(e); + } + } + }; + + match result { + Ok(_) => Ok(()), + Err(HyperlightError::ExecutionCanceledByHost()) => { + // no need to crashdump this + Err(HyperlightError::ExecutionCanceledByHost()) + } + Err(e) => { + #[cfg(crashdump)] + if self.rt_cfg.guest_core_dump { + crashdump::generate_crashdump(self)?; + } + + // If GDB is enabled, we handle the debug memory access + // Disregard return value as we want to return the error + #[cfg(gdb)] + self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash)?; + + log_then_return!(e); + } + } + } + + pub(crate) fn interrupt_handle(&self) -> Arc { + self.interrupt_handle.clone() + } + + #[cfg(gdb)] + fn handle_debug( + &mut self, + dbg_mem_access_fn: Arc>>, + stop_reason: VcpuStopReason, + ) -> Result<()> { + match stop_reason { + // If the vCPU stopped because of a crash, we need to handle it differently + // We do not want to allow resuming execution or placing breakpoints + // because the guest has crashed. + // We only allow reading registers and memory + VcpuStopReason::Crash => { + self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) + .map_err(|e| { + new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) + })?; + + loop { + log::debug!("Debug wait for event to resume vCPU"); + // Wait for a message from gdb + let req = self.recv_dbg_msg()?; + + // Flag to store if we should deny continue or step requests + let mut deny_continue = false; + // Flag to store if we should detach from the gdb session + let mut detach = false; + + let response = match req { + // Allow the detach request to disable debugging by continuing resuming + // hypervisor crash error reporting + DebugMsg::DisableDebug => { + detach = true; + DebugResponse::DisableDebug + } + // Do not allow continue or step requests + DebugMsg::Continue | DebugMsg::Step => { + deny_continue = true; + DebugResponse::NotAllowed + } + // Do not allow adding/removing breakpoints and writing to memory or registers + DebugMsg::AddHwBreakpoint(_) + | DebugMsg::AddSwBreakpoint(_) + | DebugMsg::RemoveHwBreakpoint(_) + | DebugMsg::RemoveSwBreakpoint(_) + | DebugMsg::WriteAddr(_, _) + | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, + + // For all other requests, we will process them normally + _ => { + let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + match result { + Ok(response) => response, + Err(HyperlightError::TranslateGuestAddress(_)) => { + // Treat non fatal errors separately so the guest doesn't fail + DebugResponse::ErrorOccurred + } + Err(e) => { + log::error!("Error processing debug request: {:?}", e); + return Err(e); + } + } + } + }; + + // Send the response to the request back to gdb + self.send_dbg_msg(response) + .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; + + // If we are denying continue or step requests, the debugger assumes the + // execution started so we need to report a stop reason as a crash and let + // it request to read registers/memory to figure out what happened + if deny_continue { + self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash)) + .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; + } + + // If we are detaching, we will break the loop and the Hypervisor will continue + // to handle the Crash reason + if detach { + break; + } + } + } + // If the vCPU stopped because of any other reason except a crash, we can handle it + // normally + _ => { + // Send the stop reason to the gdb thread + self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) + .map_err(|e| { + new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) + })?; + + loop { + log::debug!("Debug wait for event to resume vCPU"); + // Wait for a message from gdb + let req = self.recv_dbg_msg()?; + + let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + + let response = match result { + Ok(response) => response, + // Treat non fatal errors separately so the guest doesn't fail + Err(HyperlightError::TranslateGuestAddress(_)) => { + DebugResponse::ErrorOccurred + } + Err(e) => { + return Err(e); + } + }; + + let cont = matches!( + response, + DebugResponse::Continue | DebugResponse::Step | DebugResponse::DisableDebug + ); + + self.send_dbg_msg(response) + .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; + + // Check if we should continue execution + // We continue if the response is one of the following: Step, Continue, or DisableDebug + if cont { + break; + } + } + } + } + + Ok(()) + } + + #[cfg(target_os = "windows")] + fn get_partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE { + self.vm.get_partition_handle() + } + + // -------------------------- + // --- CRASHDUMP BELOW ------ + // -------------------------- + + #[cfg(crashdump)] + pub(super) fn crashdump_context(&self) -> Result> { + use crate::hypervisor::crashdump; + + let mut regs = [0; 27]; + + let vcpu_regs = self.vm.regs()?; + let sregs = self.vm.sregs()?; + let xsave = self.vm.xsave()?; + + // Set up the registers for the crash dump + regs[0] = vcpu_regs.r15; // r15 + regs[1] = vcpu_regs.r14; // r14 + regs[2] = vcpu_regs.r13; // r13 + regs[3] = vcpu_regs.r12; // r12 + regs[4] = vcpu_regs.rbp; // rbp + regs[5] = vcpu_regs.rbx; // rbx + regs[6] = vcpu_regs.r11; // r11 + regs[7] = vcpu_regs.r10; // r10 + regs[8] = vcpu_regs.r9; // r9 + regs[9] = vcpu_regs.r8; // r8 + regs[10] = vcpu_regs.rax; // rax + regs[11] = vcpu_regs.rcx; // rcx + regs[12] = vcpu_regs.rdx; // rdx + regs[13] = vcpu_regs.rsi; // rsi + regs[14] = vcpu_regs.rdi; // rdi + regs[15] = 0; // orig rax + regs[16] = vcpu_regs.rip; // rip + regs[17] = sregs.cs.selector as u64; // cs + regs[18] = vcpu_regs.rflags; // eflags + regs[19] = vcpu_regs.rsp; // rsp + regs[20] = sregs.ss.selector as u64; // ss + regs[21] = sregs.fs.base; // fs_base + regs[22] = sregs.gs.base; // gs_base + regs[23] = sregs.ds.selector as u64; // ds + regs[24] = sregs.es.selector as u64; // es + regs[25] = sregs.fs.selector as u64; // fs + regs[26] = sregs.gs.selector as u64; // gs + + // Get the filename from the binary path + let filename = self.rt_cfg.binary_path.clone().and_then(|path| { + use std::path::Path; + + Path::new(&path) + .file_name() + .and_then(|name| name.to_os_string().into_string().ok()) + }); + + Ok(crashdump::CrashDumpContext::new( + &self.sandbox_regions, + regs, + xsave, + self.entrypoint, + self.rt_cfg.binary_path.clone(), + filename, + )) + } + + #[cfg(feature = "trace_guest")] + pub(crate) fn vm_regs(&self) -> Result { + self.vm.regs() + } + + #[cfg(feature = "trace_guest")] + pub(crate) fn trace_info(&self) -> &TraceInfo { + &self.trace_info + } + + #[cfg(feature = "trace_guest")] + pub(crate) fn trace_info_as_mut(&mut self) -> &mut TraceInfo { + &mut self.trace_info + } +} + +impl Drop for HyperlightVm { + fn drop(&mut self) { + self.interrupt_handle.dropped.store(true, Ordering::Relaxed); + } +} + +/// The vCPU tried to access the given addr +enum MemoryAccess { + /// The accessed region has the given flags + AccessViolation(MemoryRegionFlags), + /// The accessed region is a stack guard page + StackGuardPageViolation, +} + +/// Determines if a memory access violation occurred at the given address with the given action type. +fn get_memory_access_violation<'a>( + gpa: usize, + tried: MemoryRegionFlags, + mut mem_regions: impl Iterator, +) -> Option { + // find the region containing the given gpa + let region = mem_regions.find(|region| region.guest_region.contains(&gpa)); + + if let Some(region) = region { + if region.region_type == MemoryRegionType::GuardPage { + return Some(MemoryAccess::StackGuardPageViolation); + } else if !region.flags.contains(tried) { + return Some(MemoryAccess::AccessViolation(region.flags)); + } + } + None +} + +#[cfg(gdb)] +mod debug { + use std::sync::{Arc, Mutex}; + + use hyperlight_common::mem::PAGE_SIZE; + + use super::HyperlightVm; + use crate::hypervisor::gdb::{DebugMsg, DebugResponse}; + use crate::mem::layout::SandboxMemoryLayout; + use crate::mem::mgr::SandboxMemoryManager; + use crate::mem::shared_mem::HostSharedMemory; + use crate::{HyperlightError, Result, new_error}; + + impl HyperlightVm { + pub(crate) fn process_dbg_request( + &mut self, + req: DebugMsg, + dbg_mem_access_fn: Arc>>, + ) -> Result { + if self.gdb_conn.is_some() { + match req { + DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( + self.vm + .add_hw_breakpoint(addr) + .map_err(|e| { + log::error!("Failed to add hw breakpoint: {:?}", e); + + e + }) + .is_ok(), + )), + DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( + self.vm + .add_sw_breakpoint(addr, dbg_mem_access_fn) + .map_err(|e| { + log::error!("Failed to add sw breakpoint: {:?}", e); + + e + }) + .is_ok(), + )), + DebugMsg::Continue => { + self.vm.set_single_step(false).map_err(|e| { + log::error!("Failed to continue execution: {:?}", e); + + e + })?; + + Ok(DebugResponse::Continue) + } + DebugMsg::DisableDebug => { + self.vm.set_debug(false).map_err(|e| { + log::error!("Failed to disable debugging: {:?}", e); + e + })?; + + Ok(DebugResponse::DisableDebug) + } + DebugMsg::GetCodeSectionOffset => { + let offset = dbg_mem_access_fn + .try_lock() + .map_err(|e| { + new_error!("Error locking at {}:{}: {}", file!(), line!(), e) + })? + .layout + .get_guest_code_address(); + + Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) + } + DebugMsg::ReadAddr(addr, len) => { + let mut data = vec![0u8; len]; + + self.read_addrs(addr, &mut data, dbg_mem_access_fn) + .map_err(|e| { + log::error!("Failed to read from address: {:?}", e); + + e + })?; + + Ok(DebugResponse::ReadAddr(data)) + } + DebugMsg::ReadRegisters => self + .vm + .regs() + .map_err(|e| { + log::error!("Failed to read registers: {:?}", e); + + e + }) + .map(DebugResponse::ReadRegisters), + DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( + self.vm + .remove_hw_breakpoint(addr) + .map_err(|e| { + log::error!("Failed to remove hw breakpoint: {:?}", e); + + e + }) + .is_ok(), + )), + DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( + self.vm + .remove_sw_breakpoint(addr, dbg_mem_access_fn) + .map_err(|e| { + log::error!("Failed to remove sw breakpoint: {:?}", e); + + e + }) + .is_ok(), + )), + DebugMsg::Step => { + self.vm.set_single_step(true).map_err(|e| { + log::error!("Failed to enable step instruction: {:?}", e); + + e + })?; + + Ok(DebugResponse::Step) + } + DebugMsg::WriteAddr(addr, data) => { + self.write_addrs(addr, &data, dbg_mem_access_fn) + .map_err(|e| { + log::error!("Failed to write to address: {:?}", e); + + e + })?; + + Ok(DebugResponse::WriteAddr) + } + DebugMsg::WriteRegisters(regs) => self + .vm + .set_regs(®s) + .map_err(|e| { + log::error!("Failed to write registers: {:?}", e); + + e + }) + .map(|_| DebugResponse::WriteRegisters), + } + } else { + Err(new_error!("Debugging is not enabled")) + } + } + + pub(crate) fn recv_dbg_msg(&mut self) -> Result { + let gdb_conn = self + .gdb_conn + .as_mut() + .ok_or_else(|| new_error!("Debug is not enabled"))?; + + gdb_conn.recv().map_err(|e| { + new_error!( + "Got an error while waiting to receive a message from the gdb thread: {:?}", + e + ) + }) + } + + pub(crate) fn send_dbg_msg(&mut self, cmd: DebugResponse) -> Result<()> { + log::debug!("Sending {:?}", cmd); + + let gdb_conn = self + .gdb_conn + .as_mut() + .ok_or_else(|| new_error!("Debug is not enabled"))?; + + gdb_conn.send(cmd).map_err(|e| { + new_error!( + "Got an error while sending a response message to the gdb thread: {:?}", + e + ) + }) + } + + fn read_addrs( + &mut self, + mut gva: u64, + mut data: &mut [u8], + dbg_mem_access_fn: Arc>>, + ) -> crate::Result<()> { + let data_len = data.len(); + log::debug!("Read addr: {:X} len: {:X}", gva, data_len); + + while !data.is_empty() { + let gpa = self.vm.translate_gva(gva)?; + + let read_len = std::cmp::min( + data.len(), + (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(), + ); + let offset = (gpa as usize) + .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) + .ok_or_else(|| { + log::warn!( + "gva=0x{:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", + gva, gpa, SandboxMemoryLayout::BASE_ADDRESS); + HyperlightError::TranslateGuestAddress(gva) + })?; + + dbg_mem_access_fn + .try_lock() + .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? + .get_shared_mem_mut() + .copy_to_slice(&mut data[..read_len], offset)?; + + data = &mut data[read_len..]; + gva += read_len as u64; + } + + Ok(()) + } + + /// Copies the data from the provided slice to the guest memory address + /// The address is checked to be a valid guest address + fn write_addrs( + &mut self, + mut gva: u64, + mut data: &[u8], + dbg_mem_access_fn: Arc>>, + ) -> crate::Result<()> { + let data_len = data.len(); + log::debug!("Write addr: {:X} len: {:X}", gva, data_len); + + while !data.is_empty() { + let gpa = self.vm.translate_gva(gva)?; + + let write_len = std::cmp::min( + data.len(), + (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(), + ); + let offset = (gpa as usize) + .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) + .ok_or_else(|| { + log::warn!( + "gva=0x{:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", + gva, gpa, SandboxMemoryLayout::BASE_ADDRESS); + HyperlightError::TranslateGuestAddress(gva) + })?; + + dbg_mem_access_fn + .try_lock() + .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? + .get_shared_mem_mut() + .copy_from_slice(&data[..write_len], offset)?; + + data = &data[write_len..]; + gva += write_len as u64; + } + + Ok(()) + } + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs b/src/hyperlight_host/src/hypervisor/hyperv_linux.rs index dd9055104..1386bed77 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs +++ b/src/hyperlight_host/src/hypervisor/hyperv_linux.rs @@ -1,5 +1,5 @@ /* -Copyright 2025 The Hyperlight Authors. +Copyright 2024 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. @@ -24,25 +24,25 @@ extern crate mshv_bindings3 as mshv_bindings; #[cfg(mshv3)] extern crate mshv_ioctls3 as mshv_ioctls; -use std::fmt::{Debug, Formatter}; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +#[cfg(gdb)] +use std::collections::HashMap; +#[cfg(gdb)] +use std::fmt::Debug; +use std::sync::LazyLock; +#[cfg(gdb)] use std::sync::{Arc, Mutex}; -use log::{LevelFilter, error}; +#[cfg(gdb)] +use mshv_bindings::DebugRegisters; #[cfg(mshv2)] use mshv_bindings::hv_message; +#[cfg(gdb)] +use mshv_bindings::hv_message_type_HVMSG_X64_EXCEPTION_INTERCEPT; use mshv_bindings::{ - FloatingPointUnit, SpecialRegisters, StandardRegisters, hv_message_type, - hv_message_type_HVMSG_GPA_INTERCEPT, hv_message_type_HVMSG_UNMAPPED_GPA, + hv_message_type, hv_message_type_HVMSG_GPA_INTERCEPT, hv_message_type_HVMSG_UNMAPPED_GPA, hv_message_type_HVMSG_X64_HALT, hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT, hv_register_assoc, hv_register_name_HV_X64_REGISTER_RIP, hv_register_value, mshv_user_mem_region, }; -#[cfg(gdb)] -use mshv_bindings::{ - HV_INTERCEPT_ACCESS_MASK_EXECUTE, hv_intercept_parameters, - hv_intercept_type_HV_INTERCEPT_TYPE_EXCEPTION, hv_message_type_HVMSG_X64_EXCEPTION_INTERCEPT, - mshv_install_intercept, -}; #[cfg(mshv3)] use mshv_bindings::{ hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, @@ -50,231 +50,15 @@ use mshv_bindings::{ }; use mshv_ioctls::{Mshv, VcpuFd, VmFd}; use tracing::{Span, instrument}; -#[cfg(crashdump)] -use {super::crashdump, std::path::Path}; -#[cfg(gdb)] -use super::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, MshvDebug, VcpuStopReason, -}; -use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU}; -#[cfg(gdb)] -use crate::HyperlightError; -use crate::hypervisor::get_memory_access_violation; -use crate::hypervisor::regs::CommonFpu; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::hypervisor::vm::{Vm, VmExit}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +#[cfg(gdb)] use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::ptr::{GuestPtr, RawPtr}; -use crate::mem::shared_mem::HostSharedMemory; -use crate::sandbox::SandboxConfiguration; -#[cfg(feature = "trace_guest")] -use crate::sandbox::TraceInfo; -use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::outb::handle_outb; -#[cfg(crashdump)] -use crate::sandbox::uninitialized::SandboxRuntimeConfig; -use crate::{Result, log_then_return, new_error}; - +use crate::{Result, new_error}; #[cfg(gdb)] -mod debug { - use std::sync::{Arc, Mutex}; - - use super::mshv_bindings::hv_x64_exception_intercept_message; - use super::{HypervLinuxDriver, *}; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason}; - use crate::mem::mgr::SandboxMemoryManager; - use crate::mem::shared_mem::HostSharedMemory; - use crate::{Result, new_error}; - - impl HypervLinuxDriver { - /// Resets the debug information to disable debugging - fn disable_debug(&mut self) -> Result<()> { - let mut debug = MshvDebug::default(); - - debug.set_single_step(&self.vcpu_fd, false)?; - - self.debug = Some(debug); - - Ok(()) - } - - /// Get the reason the vCPU has stopped - pub(crate) fn get_stop_reason( - &mut self, - ex_info: hv_x64_exception_intercept_message, - ) -> Result { - let debug = self - .debug - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - debug.get_stop_reason(&self.vcpu_fd, ex_info.exception_vector, self.entrypoint) - } - - pub(crate) fn process_dbg_request( - &mut self, - req: DebugMsg, - dbg_mem_access_fn: Arc>>, - ) -> Result { - if let Some(debug) = self.debug.as_mut() { - match req { - DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( - debug - .add_hw_breakpoint(&self.vcpu_fd, addr) - .map_err(|e| { - log::error!("Failed to add hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( - debug - .add_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to add sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Continue => { - debug.set_single_step(&self.vcpu_fd, false).map_err(|e| { - log::error!("Failed to continue execution: {:?}", e); - - e - })?; - - Ok(DebugResponse::Continue) - } - DebugMsg::DisableDebug => { - self.disable_debug().map_err(|e| { - log::error!("Failed to disable debugging: {:?}", e); - - e - })?; - - Ok(DebugResponse::DisableDebug) - } - DebugMsg::GetCodeSectionOffset => { - let offset = dbg_mem_access_fn - .try_lock() - .map_err(|e| { - new_error!("Error locking at {}:{}: {}", file!(), line!(), e) - })? - .layout - .get_guest_code_address(); - - Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) - } - DebugMsg::ReadAddr(addr, len) => { - let mut data = vec![0u8; len]; - - debug - .read_addrs(&self.vcpu_fd, addr, &mut data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to read from address: {:?}", e); - - e - })?; - - Ok(DebugResponse::ReadAddr(data)) - } - DebugMsg::ReadRegisters => { - let mut regs = Default::default(); - - debug - .read_regs(&self.vcpu_fd, &mut regs) - .map_err(|e| { - log::error!("Failed to read registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::ReadRegisters(regs)) - } - DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( - debug - .remove_hw_breakpoint(&self.vcpu_fd, addr) - .map_err(|e| { - log::error!("Failed to remove hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( - debug - .remove_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to remove sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Step => { - debug.set_single_step(&self.vcpu_fd, true).map_err(|e| { - log::error!("Failed to enable step instruction: {:?}", e); - - e - })?; - - Ok(DebugResponse::Step) - } - DebugMsg::WriteAddr(addr, data) => { - debug - .write_addrs(&self.vcpu_fd, addr, &data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to write to address: {:?}", e); - - e - })?; - - Ok(DebugResponse::WriteAddr) - } - DebugMsg::WriteRegisters(regs) => debug - .write_regs(&self.vcpu_fd, ®s) - .map_err(|e| { - log::error!("Failed to write registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::WriteRegisters), - } - } else { - Err(new_error!("Debugging is not enabled")) - } - } - - pub(crate) fn recv_dbg_msg(&mut self) -> Result { - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - gdb_conn.recv().map_err(|e| { - new_error!( - "Got an error while waiting to receive a - message: {:?}", - e - ) - }) - } - - pub(crate) fn send_dbg_msg(&mut self, cmd: DebugResponse) -> Result<()> { - log::debug!("Sending {:?}", cmd); - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - gdb_conn - .send(cmd) - .map_err(|e| new_error!("Got an error while sending a response message {:?}", e)) - } - } -} +use crate::{hypervisor::vm::DebugExit, mem::shared_mem::HostSharedMemory}; /// Determine whether the HyperV for Linux hypervisor API is present /// and functional. @@ -289,66 +73,43 @@ pub(crate) fn is_hypervisor_present() -> bool { } } -/// A Hypervisor driver for HyperV-on-Linux. This hypervisor is often -/// called the Microsoft Hypervisor (MSHV) -pub(crate) struct HypervLinuxDriver { - _mshv: Mshv, - page_size: usize, +/// A MSHV implementation of a single-vcpu VM +#[derive(Debug)] +pub(crate) struct MshvVm { vm_fd: VmFd, vcpu_fd: VcpuFd, - orig_rsp: GuestPtr, - entrypoint: u64, - interrupt_handle: Arc, - mem_mgr: Option>, - host_funcs: Option>>, - - sandbox_regions: Vec, // Initially mapped regions when sandbox is created - mmap_regions: Vec, // Later mapped regions #[cfg(gdb)] - debug: Option, - #[cfg(gdb)] - gdb_conn: Option>, - #[cfg(crashdump)] - rt_cfg: SandboxRuntimeConfig, - #[cfg(feature = "trace_guest")] - #[allow(dead_code)] - trace_info: TraceInfo, + debug: MshvDebug, } -impl HypervLinuxDriver { - /// Create a new `HypervLinuxDriver`, complete with all registers - /// set up to execute a Hyperlight binary inside a HyperV-powered - /// sandbox on Linux. - /// - /// While registers are set up, they will not have been applied to - /// the underlying virtual CPU after this function returns. Call the - /// `apply_registers` method to do that, or more likely call - /// `initialise` to do it for you. - #[allow(clippy::too_many_arguments)] - // TODO: refactor this function to take fewer arguments. Add trace_info to rt_cfg +#[cfg(gdb)] +#[derive(Debug, Default)] +struct MshvDebug { + regs: DebugRegisters, + sw_breakpoints: HashMap, // addr -> original instruction +} + +static MSHV: LazyLock> = + LazyLock::new(|| Mshv::new().map_err(|e| new_error!("Failed to open /dev/mshv: {}", e))); + +impl MshvVm { + /// Create a new instance of a MshvVm #[instrument(skip_all, parent = Span::current(), level = "Trace")] - pub(crate) fn new( - mem_regions: Vec, - entrypoint_ptr: GuestPtr, - rsp_ptr: GuestPtr, - pml4_ptr: GuestPtr, - config: &SandboxConfiguration, - #[cfg(gdb)] gdb_conn: Option>, - #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, - #[cfg(feature = "trace_guest")] trace_info: TraceInfo, - ) -> Result { - let mshv = Mshv::new()?; + pub(crate) fn new() -> Result { + let hv = MSHV + .as_ref() + .map_err(|e| new_error!("Failed to create MSHV instance: {}", e))?; let pr = Default::default(); #[cfg(mshv2)] - let vm_fd = mshv.create_vm_with_config(&pr)?; + let vm_fd = hv.create_vm_with_config(&pr)?; #[cfg(mshv3)] let vm_fd = { // It's important to avoid create_vm() and explicitly use // create_vm_with_args() with an empty arguments structure // here, because otherwise the partition is set up with a SynIC. - let vm_fd = mshv.create_vm_with_args(&pr)?; + let vm_fd = hv.create_vm_with_args(&pr)?; let features: hv_partition_synthetic_processor_features = Default::default(); vm_fd.set_partition_property( hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, @@ -360,850 +121,288 @@ impl HypervLinuxDriver { let vcpu_fd = vm_fd.create_vcpu(0)?; - #[cfg(gdb)] - let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { - let mut debug = MshvDebug::new(); - debug.add_hw_breakpoint(&vcpu_fd, entrypoint_ptr.absolute()?)?; - - // The bellow intercepts make the vCPU exit with the Exception Intercept exit code - // Check Table 6-1. Exceptions and Interrupts at Page 6-13 Vol. 1 - // of Intel 64 and IA-32 Architectures Software Developer's Manual - // Install intercept for #DB (1) exception - vm_fd - .install_intercept(mshv_install_intercept { - access_type_mask: HV_INTERCEPT_ACCESS_MASK_EXECUTE, - intercept_type: hv_intercept_type_HV_INTERCEPT_TYPE_EXCEPTION, - // Exception handler #DB (1) - intercept_parameter: hv_intercept_parameters { - exception_vector: 0x1, - }, - }) - .map_err(|e| new_error!("Cannot install debug exception intercept: {}", e))?; - - // Install intercept for #BP (3) exception - vm_fd - .install_intercept(mshv_install_intercept { - access_type_mask: HV_INTERCEPT_ACCESS_MASK_EXECUTE, - intercept_type: hv_intercept_type_HV_INTERCEPT_TYPE_EXCEPTION, - // Exception handler #BP (3) - intercept_parameter: hv_intercept_parameters { - exception_vector: 0x3, - }, - }) - .map_err(|e| new_error!("Cannot install breakpoint exception intercept: {}", e))?; - - (Some(debug), Some(gdb_conn)) - } else { - (None, None) - }; - - mem_regions.iter().try_for_each(|region| { - let mshv_region = region.to_owned().into(); - vm_fd.map_user_memory(mshv_region) - })?; - - let interrupt_handle = Arc::new(LinuxInterruptHandle { - running: AtomicU64::new(0), - cancel_requested: AtomicBool::new(false), - #[cfg(gdb)] - debug_interrupt: AtomicBool::new(false), - #[cfg(all( - target_arch = "x86_64", - target_vendor = "unknown", - target_os = "linux", - target_env = "musl" - ))] - tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }), - #[cfg(not(all( - target_arch = "x86_64", - target_vendor = "unknown", - target_os = "linux", - target_env = "musl" - )))] - tid: AtomicU64::new(unsafe { libc::pthread_self() }), - retry_delay: config.get_interrupt_retry_delay(), - sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(), - dropped: AtomicBool::new(false), - }); - - let mut hv = Self { - _mshv: mshv, - page_size: 0, + Ok(Self { vm_fd, vcpu_fd, - sandbox_regions: mem_regions, - mmap_regions: Vec::new(), - entrypoint: entrypoint_ptr.absolute()?, - orig_rsp: rsp_ptr, - interrupt_handle: interrupt_handle.clone(), - mem_mgr: None, - host_funcs: None, #[cfg(gdb)] - debug, - #[cfg(gdb)] - gdb_conn, - #[cfg(crashdump)] - rt_cfg, - #[cfg(feature = "trace_guest")] - trace_info, - }; - - hv.setup_initial_sregs(pml4_ptr.absolute()?)?; - - // Send the interrupt handle to the GDB thread if debugging is enabled - // This is used to allow the GDB thread to stop the vCPU - #[cfg(gdb)] - if hv.debug.is_some() { - hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; - } - - Ok(hv) + debug: MshvDebug::default(), + }) } } -impl Debug for HypervLinuxDriver { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut f = f.debug_struct("Hyperv Linux Driver"); - - f.field("Entrypoint", &self.entrypoint) - .field("Original RSP", &self.orig_rsp); - - for region in &self.sandbox_regions { - f.field("Sandbox Memory Region", ®ion); - } - for region in &self.mmap_regions { - f.field("Mapped Memory Region", ®ion); - } - - let regs = self.vcpu_fd.get_regs(); - - if let Ok(regs) = regs { - f.field("Registers", ®s); - } - - let sregs = self.vcpu_fd.get_sregs(); - - if let Ok(sregs) = sregs { - f.field("Special Registers", &sregs); - } - - f.finish() +impl Vm for MshvVm { + fn regs(&self) -> Result { + Ok((&self.vcpu_fd.get_regs()?).into()) } -} - -impl Hypervisor for HypervLinuxDriver { - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn initialise( - &mut self, - peb_addr: RawPtr, - seed: u64, - page_size: u32, - mem_mgr: SandboxMemoryManager, - host_funcs: Arc>, - max_guest_log_level: Option, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()> { - self.mem_mgr = Some(mem_mgr); - self.host_funcs = Some(host_funcs); - self.page_size = page_size as usize; - - let max_guest_log_level: u64 = match max_guest_log_level { - Some(level) => level as u64, - None => self.get_max_log_level().into(), - }; - - let regs = StandardRegisters { - rip: self.entrypoint, - rsp: self.orig_rsp.absolute()?, - rflags: 2, //bit 1 of rlags is required to be set - - // function args - rdi: peb_addr.into(), - rsi: seed, - rdx: page_size.into(), - rcx: max_guest_log_level, - ..Default::default() - }; - self.vcpu_fd.set_regs(®s)?; + fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { + Ok(self.vcpu_fd.set_regs(®s.into())?) + } - VirtualCPU::run( - self.as_mut_hypervisor(), - #[cfg(gdb)] - dbg_mem_access_fn, - ) + fn sregs(&self) -> Result { + Ok((&self.vcpu_fd.get_sregs()?).into()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> { - if [ - rgn.guest_region.start, - rgn.guest_region.end, - rgn.host_region.start, - rgn.host_region.end, - ] - .iter() - .any(|x| x % self.page_size != 0) - { - log_then_return!("region is not page-aligned"); - } - let mshv_region: mshv_user_mem_region = rgn.to_owned().into(); - self.vm_fd.map_user_memory(mshv_region)?; - self.mmap_regions.push(rgn.to_owned()); + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { + self.vcpu_fd.set_sregs(&sregs.into())?; Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - unsafe fn unmap_region(&mut self, region: &MemoryRegion) -> Result<()> { - if let Some(pos) = self.mmap_regions.iter().position(|r| r == region) { - let removed_region = self.mmap_regions.remove(pos); - let mshv_region: mshv_user_mem_region = removed_region.into(); - self.vm_fd.unmap_user_memory(mshv_region)?; - Ok(()) - } else { - Err(new_error!("Tried to unmap region that is not mapped")) - } + fn fpu(&self) -> Result { + Ok((&self.vcpu_fd.get_fpu()?).into()) } - fn get_mapped_regions(&self) -> Box + '_> { - Box::new(self.mmap_regions.iter()) + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { + self.vcpu_fd.set_fpu(&fpu.into())?; + Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn dispatch_call_from_host( - &mut self, - dispatch_func_addr: RawPtr, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()> { - // Reset general purpose registers, then set RIP and RSP - let regs = StandardRegisters { - rip: dispatch_func_addr.into(), - rsp: self.orig_rsp.absolute()?, - rflags: 2, //bit 1 of rlags is required to be set - ..Default::default() - }; - self.vcpu_fd.set_regs(®s)?; - - // reset fpu state - self.set_fpu(&CommonFpu::default())?; - - // run - VirtualCPU::run( - self.as_mut_hypervisor(), - #[cfg(gdb)] - dbg_mem_access_fn, - )?; + #[cfg(crashdump)] + fn xsave(&self) -> Result> { + let xsave = self.vcpu_fd.get_xsave()?; + Ok(xsave.buffer.to_vec()) + } + /// # 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. + 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)?; Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn handle_io( - &mut self, - port: u16, - data: Vec, - rip: u64, - instruction_length: u64, - ) -> Result<()> { - let mut padded = [0u8; 4]; - let copy_len = data.len().min(4); - padded[..copy_len].copy_from_slice(&data[..copy_len]); - let val = u32::from_le_bytes(padded); - - #[cfg(feature = "trace_guest")] - { - // We need to handle the borrow checker issue where we need both: - // - &mut SandboxMemoryManager (from self.mem_mgr) - // - &mut dyn Hypervisor (from self) - // We'll use a temporary approach to extract the mem_mgr temporarily - let mem_mgr_option = self.mem_mgr.take(); - let mut mem_mgr = mem_mgr_option - .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; - let host_funcs = self - .host_funcs - .as_ref() - .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? - .clone(); - - handle_outb(&mut mem_mgr, host_funcs, self, port, val)?; - - // Put the mem_mgr back - self.mem_mgr = Some(mem_mgr); - } - - #[cfg(not(feature = "trace_guest"))] - { - let mem_mgr = self - .mem_mgr - .as_mut() - .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; - let host_funcs = self - .host_funcs - .as_ref() - .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? - .clone(); - - handle_outb(mem_mgr, host_funcs, port, val)?; - } - - // update rip - self.vcpu_fd.set_reg(&[hv_register_assoc { - name: hv_register_name_HV_X64_REGISTER_RIP, - value: hv_register_value { - reg64: rip + instruction_length, - }, - ..Default::default() - }])?; + fn unmap_memory(&mut self, (_slot, region): (u32, &MemoryRegion)) -> Result<()> { + let mshv_region: mshv_user_mem_region = region.into(); + self.vm_fd.unmap_user_memory(mshv_region)?; Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn run(&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; - const UNMAPPED_GPA_MESSAGE: hv_message_type = hv_message_type_HVMSG_UNMAPPED_GPA; - const INVALID_GPA_ACCESS_MESSAGE: hv_message_type = hv_message_type_HVMSG_GPA_INTERCEPT; + fn run_vcpu(&mut self) -> Result { + const HALT: hv_message_type = hv_message_type_HVMSG_X64_HALT; + const IO_PORT: hv_message_type = hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT; + const UNMAPPED_GPA: hv_message_type = hv_message_type_HVMSG_UNMAPPED_GPA; + const INVALID_GPA: hv_message_type = hv_message_type_HVMSG_GPA_INTERCEPT; #[cfg(gdb)] const EXCEPTION_INTERCEPT: hv_message_type = hv_message_type_HVMSG_X64_EXCEPTION_INTERCEPT; - self.interrupt_handle - .tid - .store(unsafe { libc::pthread_self() as u64 }, Ordering::Relaxed); - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call - self.interrupt_handle - .set_running_and_increment_generation() - .map_err(|e| { - new_error!( - "Error setting running state and incrementing generation: {}", - e - ) - })?; - #[cfg(not(gdb))] - let debug_interrupt = false; - #[cfg(gdb)] - let debug_interrupt = self - .interrupt_handle - .debug_interrupt - .load(Ordering::Relaxed); - - // Don't run the vcpu if `cancel_requested` is true - // - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call - let exit_reason = if self - .interrupt_handle - .cancel_requested - .load(Ordering::Relaxed) - || debug_interrupt - { - Err(mshv_ioctls::MshvError::from(libc::EINTR)) - } else { - #[cfg(feature = "trace_guest")] - if self.trace_info.guest_start_epoch.is_none() { - // Store the guest start epoch and cycles to trace the guest execution time - crate::debug!("MSHV - Guest Start Epoch set"); - self.trace_info.guest_start_tsc = - Some(hyperlight_guest_tracing::invariant_tsc::read_tsc()); - self.trace_info.guest_start_epoch = Some(std::time::Instant::now()); - } - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then the vcpu will run, but we will keep sending signals to this thread - // to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will - // return either normally with an exit reason, or from being "kicked" by out signal handler, with an EINTR error, - // both of which are fine. - #[cfg(mshv2)] - { - let hv_message: hv_message = Default::default(); - self.vcpu_fd.run(hv_message) - } - #[cfg(mshv3)] - self.vcpu_fd.run() + #[cfg(mshv2)] + let run_result = { + let hv_message: hv_message = Default::default(); + self.vcpu_fd.run(hv_message) }; - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then signals will be sent to this thread until `running` is set to false. - // This is fine since the signal handler is a no-op. - let cancel_requested = self - .interrupt_handle - .cancel_requested - .load(Ordering::Relaxed); - #[cfg(gdb)] - let debug_interrupt = self - .interrupt_handle - .debug_interrupt - .load(Ordering::Relaxed); - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then `cancel_requested` will be set to true again, which will cancel the **next vcpu run**. - // Additionally signals will be sent to this thread until `running` is set to false. - // This is fine since the signal handler is a no-op. - self.interrupt_handle.clear_running_bit(); - // At this point, `running` is false so no more signals will be sent to this thread, - // but we may still receive async signals that were sent before this point. - // To prevent those signals from interrupting subsequent calls to `run()`, - // we make sure to check `cancel_requested` before cancelling (see `libc::EINTR` match-arm below). - let result = match exit_reason { + #[cfg(mshv3)] + let run_result = self.vcpu_fd.run(); + + let result = match run_result { Ok(m) => match m.header.message_type { - HALT_MESSAGE => { - crate::debug!("mshv - Halt Details : {:#?}", &self); - HyperlightExit::Halt() - } - IO_PORT_INTERCEPT_MESSAGE => { + HALT => VmExit::Halt(), + IO_PORT => { let io_message = m.to_ioport_info().map_err(mshv_ioctls::MshvError::from)?; let port_number = io_message.port_number; - let rip = io_message.header.rip; let rax = io_message.rax; - let instruction_length = io_message.header.instruction_length() as u64; - crate::debug!("mshv IO Details : \nPort : {}\n{:#?}", port_number, &self); - HyperlightExit::IoOut( - port_number, - rax.to_le_bytes().to_vec(), - rip, - instruction_length, - ) + // mshv, unlike kvm, does not automatically increment RIP + self.vcpu_fd.set_reg(&[hv_register_assoc { + name: hv_register_name_HV_X64_REGISTER_RIP, + value: hv_register_value { + reg64: io_message.header.rip + + io_message.header.instruction_length() as u64, + }, + ..Default::default() + }])?; + VmExit::IoOut(port_number, rax.to_le_bytes().to_vec()) } - UNMAPPED_GPA_MESSAGE => { + UNMAPPED_GPA => { let mimo_message = m.to_memory_info().map_err(mshv_ioctls::MshvError::from)?; let addr = mimo_message.guest_physical_address; - crate::debug!( - "mshv MMIO unmapped GPA -Details: Address: {} \n {:#?}", - addr, - &self - ); - HyperlightExit::Mmio(addr) + match MemoryRegionFlags::try_from(mimo_message)? { + MemoryRegionFlags::READ => VmExit::MmioRead(addr), + MemoryRegionFlags::WRITE => VmExit::MmioWrite(addr), + _ => VmExit::Unknown("Unknown MMIO access".to_string()), + } } - INVALID_GPA_ACCESS_MESSAGE => { + INVALID_GPA => { let mimo_message = m.to_memory_info().map_err(mshv_ioctls::MshvError::from)?; let gpa = mimo_message.guest_physical_address; let access_info = MemoryRegionFlags::try_from(mimo_message)?; - crate::debug!( - "mshv MMIO invalid GPA access -Details: Address: {} \n {:#?}", - gpa, - &self - ); - match get_memory_access_violation( - gpa as usize, - self.sandbox_regions.iter().chain(self.mmap_regions.iter()), - access_info, - ) { - Some(access_info_violation) => access_info_violation, - None => HyperlightExit::Mmio(gpa), + match access_info { + MemoryRegionFlags::READ => VmExit::MmioRead(gpa), + MemoryRegionFlags::WRITE => VmExit::MmioWrite(gpa), + _ => VmExit::Unknown("Unknown MMIO access".to_string()), } } - // The only case an intercept exit is expected is when debugging is enabled - // and the intercepts are installed. - // Provide the extra information about the exception to accurately determine - // the stop reason #[cfg(gdb)] EXCEPTION_INTERCEPT => { - // Extract exception info from the message so we can figure out - // more information about the vCPU state - let ex_info = match m.to_exception_info().map_err(mshv_ioctls::MshvError::from) - { - Ok(info) => info, - Err(e) => { - log_then_return!("Error converting to exception info: {:?}", e); - } - }; - - match self.get_stop_reason(ex_info) { - Ok(reason) => HyperlightExit::Debug(reason), - Err(e) => { - log_then_return!("Error getting stop reason: {:?}", e); - } - } - } - other => { - crate::debug!("mshv Other Exit: Exit: {:#?} \n {:#?}", other, &self); - #[cfg(crashdump)] - let _ = crashdump::generate_crashdump(self); - log_then_return!("unknown Hyper-V run message type {:?}", other); + let exception_message = m + .to_exception_info() + .map_err(mshv_ioctls::MshvError::from)?; + let DebugRegisters { dr6, .. } = self.vcpu_fd.get_debug_regs()?; + VmExit::Debug(DebugExit::Debug { + dr6, + exception: exception_message.exception_vector as u32, + }) } + other => VmExit::Unknown(format!("Unknown MSHV VCPU exit: {:?}", other)), }, Err(e) => match e.errno() { - // we send a signal to the thread to cancel execution this results in EINTR being returned by KVM so we return Cancelled - libc::EINTR => { - // If cancellation was not requested for this specific vm, the vcpu was interrupted because of debug interrupt or - // a stale signal that meant to be delivered to a previous/other vcpu on this same thread, so let's ignore it - if cancel_requested { - self.interrupt_handle - .cancel_requested - .store(false, Ordering::Relaxed); - HyperlightExit::Cancelled() - } else { - #[cfg(gdb)] - if debug_interrupt { - self.interrupt_handle - .debug_interrupt - .store(false, Ordering::Relaxed); - - // If the vCPU was stopped because of an interrupt, we need to - // return a special exit reason so that the gdb thread can handle it - // and resume execution - HyperlightExit::Debug(VcpuStopReason::Interrupt) - } else { - HyperlightExit::Retry() - } - - #[cfg(not(gdb))] - HyperlightExit::Retry() - } - } - libc::EAGAIN => HyperlightExit::Retry(), - _ => { - crate::debug!("mshv Error - Details: Error: {} \n {:#?}", e, &self); - log_then_return!("Error running VCPU {:?}", e); - } + // In case of the gdb feature, the timeout is not enabled, this + // exit is because of a signal sent from the gdb thread to the + // hypervisor thread to cancel execution (e.g. Ctrl+C from GDB) + #[cfg(gdb)] + libc::EINTR => VmExit::Debug(DebugExit::Interrupt), + // we send a signal to the thread to cancel execution. This results in EINTR being returned + #[cfg(not(gdb))] + libc::EINTR => VmExit::Cancelled(), + libc::EAGAIN => VmExit::Retry(), + _ => VmExit::Unknown(format!("Unknown MSHV VCPU error: {}", e)), }, }; Ok(result) } - fn regs(&self) -> Result { - let mshv_regs = self.vcpu_fd.get_regs()?; - Ok((&mshv_regs).into()) - } - - fn set_regs(&mut self, regs: &super::regs::CommonRegisters) -> Result<()> { - let mshv_regs: StandardRegisters = regs.into(); - self.vcpu_fd.set_regs(&mshv_regs)?; - Ok(()) - } - - fn fpu(&self) -> Result { - let mshv_fpu = self.vcpu_fd.get_fpu()?; - Ok((&mshv_fpu).into()) - } - - fn set_fpu(&mut self, fpu: &super::regs::CommonFpu) -> Result<()> { - let mshv_fpu: FloatingPointUnit = fpu.into(); - self.vcpu_fd.set_fpu(&mshv_fpu)?; - Ok(()) - } - - fn sregs(&self) -> Result { - let mshv_sregs = self.vcpu_fd.get_sregs()?; - Ok((&mshv_sregs).into()) - } + // -- DEBUGGING RELATED BELOW --- - fn set_sregs(&mut self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { - let mshv_sregs: SpecialRegisters = sregs.into(); - self.vcpu_fd.set_sregs(&mshv_sregs)?; - Ok(()) - } - - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { - self as &mut dyn Hypervisor - } - - fn interrupt_handle(&self) -> Arc { - self.interrupt_handle.clone() - } - - #[cfg(crashdump)] - fn crashdump_context(&self) -> Result>> { - if self.rt_cfg.guest_core_dump { - let mut regs = [0; 27]; - - let vcpu_regs = self.vcpu_fd.get_regs()?; - let sregs = self.vcpu_fd.get_sregs()?; - let xsave = self.vcpu_fd.get_xsave()?; + #[cfg(gdb)] + fn translate_gva(&self, gva: u64) -> Result { + use mshv_bindings::{HV_TRANSLATE_GVA_VALIDATE_READ, HV_TRANSLATE_GVA_VALIDATE_WRITE}; - // Set up the registers for the crash dump - regs[0] = vcpu_regs.r15; // r15 - regs[1] = vcpu_regs.r14; // r14 - regs[2] = vcpu_regs.r13; // r13 - regs[3] = vcpu_regs.r12; // r12 - regs[4] = vcpu_regs.rbp; // rbp - regs[5] = vcpu_regs.rbx; // rbx - regs[6] = vcpu_regs.r11; // r11 - regs[7] = vcpu_regs.r10; // r10 - regs[8] = vcpu_regs.r9; // r9 - regs[9] = vcpu_regs.r8; // r8 - regs[10] = vcpu_regs.rax; // rax - regs[11] = vcpu_regs.rcx; // rcx - regs[12] = vcpu_regs.rdx; // rdx - regs[13] = vcpu_regs.rsi; // rsi - regs[14] = vcpu_regs.rdi; // rdi - regs[15] = 0; // orig rax - regs[16] = vcpu_regs.rip; // rip - regs[17] = sregs.cs.selector as u64; // cs - regs[18] = vcpu_regs.rflags; // eflags - regs[19] = vcpu_regs.rsp; // rsp - regs[20] = sregs.ss.selector as u64; // ss - regs[21] = sregs.fs.base; // fs_base - regs[22] = sregs.gs.base; // gs_base - regs[23] = sregs.ds.selector as u64; // ds - regs[24] = sregs.es.selector as u64; // es - regs[25] = sregs.fs.selector as u64; // fs - regs[26] = sregs.gs.selector as u64; // gs + use crate::HyperlightError; - // Get the filename from the binary path - let filename = self.rt_cfg.binary_path.clone().and_then(|path| { - Path::new(&path) - .file_name() - .and_then(|name| name.to_os_string().into_string().ok()) - }); + let flags = (HV_TRANSLATE_GVA_VALIDATE_READ | HV_TRANSLATE_GVA_VALIDATE_WRITE) as u64; + let (addr, _) = self + .vcpu_fd + .translate_gva(gva, flags) + .map_err(|_| HyperlightError::TranslateGuestAddress(gva))?; - Ok(Some(crashdump::CrashDumpContext::new( - &self.sandbox_regions, - regs, - xsave.buffer.to_vec(), - self.entrypoint, - self.rt_cfg.binary_path.clone(), - filename, - ))) - } else { - Ok(None) - } + Ok(addr) } #[cfg(gdb)] - fn handle_debug( - &mut self, - dbg_mem_access_fn: Arc>>, - stop_reason: VcpuStopReason, - ) -> Result<()> { - if self.debug.is_none() { - return Err(new_error!("Debugging is not enabled")); - } - - match stop_reason { - // If the vCPU stopped because of a crash, we need to handle it differently - // We do not want to allow resuming execution or placing breakpoints - // because the guest has crashed. - // We only allow reading registers and memory - VcpuStopReason::Crash => { - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) - .map_err(|e| { - new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) - })?; - - loop { - log::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - // Flag to store if we should deny continue or step requests - let mut deny_continue = false; - // Flag to store if we should detach from the gdb session - let mut detach = false; - - let response = match req { - // Allow the detach request to disable debugging by continuing resuming - // hypervisor crash error reporting - DebugMsg::DisableDebug => { - detach = true; - DebugResponse::DisableDebug - } - // Do not allow continue or step requests - DebugMsg::Continue | DebugMsg::Step => { - deny_continue = true; - DebugResponse::NotAllowed - } - // Do not allow adding/removing breakpoints and writing to memory or registers - DebugMsg::AddHwBreakpoint(_) - | DebugMsg::AddSwBreakpoint(_) - | DebugMsg::RemoveHwBreakpoint(_) - | DebugMsg::RemoveSwBreakpoint(_) - | DebugMsg::WriteAddr(_, _) - | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, - - // For all other requests, we will process them normally - _ => { - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); - match result { - Ok(response) => response, - Err(HyperlightError::TranslateGuestAddress(_)) => { - // Treat non fatal errors separately so the guest doesn't fail - DebugResponse::ErrorOccurred - } - Err(e) => { - log::error!("Error processing debug request: {:?}", e); - return Err(e); - } - } - } - }; - - // Send the response to the request back to gdb - self.send_dbg_msg(response) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; - - // If we are denying continue or step requests, the debugger assumes the - // execution started so we need to report a stop reason as a crash and let - // it request to read registers/memory to figure out what happened - if deny_continue { - self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash)) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; - } - - // If we are detaching, we will break the loop and the Hypervisor will continue - // to handle the Crash reason - if detach { - break; - } - } - } - // If the vCPU stopped because of any other reason except a crash, we can handle it - // normally - _ => { - // Send the stop reason to the gdb thread - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) - .map_err(|e| { - new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) - })?; - - loop { - log::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); - - let response = match result { - Ok(response) => response, - // Treat non fatal errors separately so the guest doesn't fail - Err(HyperlightError::TranslateGuestAddress(_)) => { - DebugResponse::ErrorOccurred - } - Err(e) => { - return Err(e); - } - }; + fn set_debug(&mut self, enabled: bool) -> Result<()> { + use mshv_bindings::{ + HV_INTERCEPT_ACCESS_MASK_EXECUTE, hv_intercept_parameters, + hv_intercept_type_HV_INTERCEPT_TYPE_EXCEPTION, mshv_install_intercept, + }; - let cont = matches!( - response, - DebugResponse::Continue | DebugResponse::Step | DebugResponse::DisableDebug - ); + use crate::hypervisor::gdb::arch::{BP_EX_ID, DB_EX_ID}; + use crate::new_error; - self.send_dbg_msg(response) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; + if enabled { + self.vm_fd + .install_intercept(mshv_install_intercept { + access_type_mask: HV_INTERCEPT_ACCESS_MASK_EXECUTE, + intercept_type: hv_intercept_type_HV_INTERCEPT_TYPE_EXCEPTION, + // Exception handler #DB (1) + intercept_parameter: hv_intercept_parameters { + exception_vector: DB_EX_ID as u16, + }, + }) + .map_err(|e| new_error!("Cannot install debug exception intercept: {}", e))?; - // Check if we should continue execution - // We continue if the response is one of the following: Step, Continue, or DisableDebug - if cont { - break; - } - } - } + // Install intercept for #BP (3) exception + self.vm_fd + .install_intercept(mshv_install_intercept { + access_type_mask: HV_INTERCEPT_ACCESS_MASK_EXECUTE, + intercept_type: hv_intercept_type_HV_INTERCEPT_TYPE_EXCEPTION, + // Exception handler #BP (3) + intercept_parameter: hv_intercept_parameters { + exception_vector: BP_EX_ID as u16, + }, + }) + .map_err(|e| new_error!("Cannot install breakpoint exception intercept: {}", e))?; + } else { + // There doesn't seem to be any way to remove installed intercepts. But that's okay. } - Ok(()) } - fn check_stack_guard(&self) -> Result { - if let Some(mgr) = self.mem_mgr.as_ref() { - mgr.check_stack_guard() + #[cfg(gdb)] + fn set_single_step(&mut self, enable: bool) -> Result<()> { + let mut regs = self.regs()?; + if enable { + regs.rflags |= 1 << 8; } else { - Err(new_error!("Memory manager is not initialized")) + regs.rflags &= !(1 << 8); } + self.set_regs(®s)?; + Ok(()) } - #[cfg(feature = "trace_guest")] - fn trace_info_as_ref(&self) -> &TraceInfo { - &self.trace_info - } - #[cfg(feature = "trace_guest")] - fn trace_info_as_mut(&mut self) -> &mut TraceInfo { - &mut self.trace_info + #[cfg(gdb)] + fn add_hw_breakpoint(&mut self, addr: u64) -> Result<()> { + use crate::hypervisor::gdb::arch::MAX_NO_OF_HW_BP; + use crate::new_error; + + // Find the first available LOCAL (L0–L3) slot + let i = (0..MAX_NO_OF_HW_BP) + .position(|i| self.debug.regs.dr7 & (1 << (i * 2)) == 0) + .ok_or_else(|| new_error!("Tried to add more than 4 hardware breakpoints"))?; + + // Assign to corresponding debug register + *[ + &mut self.debug.regs.dr0, + &mut self.debug.regs.dr1, + &mut self.debug.regs.dr2, + &mut self.debug.regs.dr3, + ][i] = addr; + + // Enable LOCAL bit + self.debug.regs.dr7 |= 1 << (i * 2); + + self.vcpu_fd.set_debug_regs(&self.debug.regs)?; + Ok(()) } -} -impl Drop for HypervLinuxDriver { - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn drop(&mut self) { - self.interrupt_handle.dropped.store(true, Ordering::Relaxed); - for region in self.sandbox_regions.iter().chain(self.mmap_regions.iter()) { - let mshv_region: mshv_user_mem_region = region.to_owned().into(); - match self.vm_fd.unmap_user_memory(mshv_region) { - Ok(_) => (), - Err(e) => error!("Failed to unmap user memory in HyperVOnLinux ({:?})", e), - } + #[cfg(gdb)] + fn remove_hw_breakpoint(&mut self, addr: u64) -> Result<()> { + use crate::new_error; + + let regs = [ + &mut self.debug.regs.dr0, + &mut self.debug.regs.dr1, + &mut self.debug.regs.dr2, + &mut self.debug.regs.dr3, + ]; + + if let Some(i) = regs.iter().position(|&&mut reg| reg == addr) { + // Clear the address + *regs[i] = 0; + // Disable LOCAL bit + self.debug.regs.dr7 &= !(1 << (i * 2)); + self.vcpu_fd.set_debug_regs(&self.debug.regs)?; + Ok(()) + } else { + Err(new_error!("Tried to remove non-existing hw-breakpoint")) } } -} - -#[cfg(test)] -mod tests { - use super::*; - #[cfg(feature = "unwind_guest")] - use crate::mem::exe::DummyUnwindInfo; - use crate::mem::memory_region::MemoryRegionVecBuilder; - use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory}; - #[rustfmt::skip] - const CODE: [u8; 12] = [ - 0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */ - 0x00, 0xd8, /* add %bl, %al */ - 0x04, b'0', /* add $'0', %al */ - 0xee, /* out %al, (%dx) */ - /* send a 0 to indicate we're done */ - 0xb0, b'\0', /* mov $'\0', %al */ - 0xee, /* out %al, (%dx) */ - 0xf4, /* HLT */ - ]; + #[cfg(gdb)] + fn add_sw_breakpoint( + &mut self, + addr: u64, + dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + use crate::hypervisor::gdb::arch::SW_BP_OP; - fn shared_mem_with_code( - code: &[u8], - mem_size: usize, - load_offset: usize, - ) -> Result> { - if load_offset > mem_size { - log_then_return!( - "code load offset ({}) > memory size ({})", - load_offset, - mem_size - ); - } - let mut shared_mem = ExclusiveSharedMemory::new(mem_size)?; - shared_mem.copy_from_slice(code, load_offset)?; - Ok(Box::new(shared_mem)) + let mut guard = dbg_mem_access_fn.lock().unwrap(); + let orig_instr: u8 = guard.get_shared_mem_mut().read(addr as usize)?; + guard.get_shared_mem_mut().write(addr as usize, SW_BP_OP)?; + self.debug.sw_breakpoints.insert(addr, orig_instr); + Ok(()) } - #[test] - fn create_driver() { - if !super::is_hypervisor_present() { - return; - } - const MEM_SIZE: usize = 0x3000; - let gm = shared_mem_with_code(CODE.as_slice(), MEM_SIZE, 0).unwrap(); - let rsp_ptr = GuestPtr::try_from(0).unwrap(); - let pml4_ptr = GuestPtr::try_from(0).unwrap(); - let entrypoint_ptr = GuestPtr::try_from(0).unwrap(); - let mut regions = MemoryRegionVecBuilder::new(0, gm.base_addr()); - regions.push_page_aligned( - MEM_SIZE, - MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE, - crate::mem::memory_region::MemoryRegionType::Code, - ); - let config: SandboxConfiguration = Default::default(); - - super::HypervLinuxDriver::new( - regions.build(), - entrypoint_ptr, - rsp_ptr, - pml4_ptr, - &config, - #[cfg(gdb)] - None, - #[cfg(crashdump)] - SandboxRuntimeConfig { - #[cfg(crashdump)] - binary_path: None, - #[cfg(gdb)] - debug_info: None, - #[cfg(crashdump)] - guest_core_dump: true, - }, - #[cfg(feature = "trace_guest")] - TraceInfo::new( - #[cfg(feature = "unwind_guest")] - Arc::new(DummyUnwindInfo {}), - ) - .unwrap(), - ) - .unwrap(); + #[cfg(gdb)] + fn remove_sw_breakpoint( + &mut self, + addr: u64, + dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + let original_instr = self.debug.sw_breakpoints.remove(&addr).unwrap(); + dbg_mem_access_fn + .lock() + .unwrap() + .get_shared_mem_mut() + .write(addr as usize, original_instr)?; + Ok(()) } } diff --git a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs b/src/hyperlight_host/src/hypervisor/hyperv_windows.rs index 98764106d..acfd35c07 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs +++ b/src/hyperlight_host/src/hypervisor/hyperv_windows.rs @@ -1,5 +1,5 @@ /* -Copyright 2025 The Hyperlight Authors. +Copyright 2024 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. @@ -14,618 +14,336 @@ See the License for the specific language governing permissions and limitations under the License. */ -use std::fmt; -use std::fmt::{Debug, Formatter}; -use std::string::String; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -use log::LevelFilter; -use tracing::{Span, instrument}; -use windows::Win32::System::Hypervisor::{ - WHV_MEMORY_ACCESS_TYPE, WHV_PARTITION_HANDLE, WHV_RUN_VP_EXIT_CONTEXT, WHV_RUN_VP_EXIT_REASON, - WHvCancelRunVirtualProcessor, -}; -#[cfg(crashdump)] -use {super::crashdump, std::path::Path}; +use std::os::raw::c_void; + +use hyperlight_common::mem::PAGE_SIZE_USIZE; +use windows::Win32::Foundation::{FreeLibrary, HANDLE}; +use windows::Win32::System::Hypervisor::*; +use windows::Win32::System::LibraryLoader::*; +use windows::core::s; +use windows_result::HRESULT; + #[cfg(gdb)] -use { - super::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, HypervDebug, VcpuStopReason, - }, - crate::HyperlightError, +use super::handlers::DbgMemAccessHandlerWrapper; +use super::regs::{ + AlignedRegisterValues, WHP_FPU_NAMES, WHP_FPU_NAMES_LEN, WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, + WHP_SREGS_NAMES, WHP_SREGS_NAMES_LEN, }; - -use super::regs::CommonSpecialRegisters; -use super::surrogate_process::SurrogateProcess; -use super::surrogate_process_manager::*; -use super::windows_hypervisor_platform::{VMPartition, VMProcessor}; +use super::vm::HyperlightExit; use super::wrappers::HandleWrapper; -use super::{HyperlightExit, Hypervisor, InterruptHandle, VirtualCPU}; -use crate::hypervisor::get_memory_access_violation; -use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::hypervisor::surrogate_process::SurrogateProcess; +use crate::hypervisor::surrogate_process_manager::get_surrogate_process_manager; +use crate::hypervisor::vm::Vm; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; -use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::ptr::{GuestPtr, RawPtr}; -use crate::mem::shared_mem::HostSharedMemory; -#[cfg(feature = "trace_guest")] -use crate::sandbox::TraceInfo; -use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::outb::handle_outb; -#[cfg(crashdump)] -use crate::sandbox::uninitialized::SandboxRuntimeConfig; -use crate::{Result, debug, log_then_return, new_error}; +use crate::{Result, new_error}; + +pub(crate) fn is_hypervisor_present() -> bool { + let mut capability: WHV_CAPABILITY = Default::default(); + let written_size: Option<*mut u32> = None; + + match unsafe { + WHvGetCapability( + WHvCapabilityCodeHypervisorPresent, + &mut capability as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + written_size, + ) + } { + Ok(_) => unsafe { capability.HypervisorPresent.as_bool() }, + Err(_) => { + log::info!("Windows Hypervisor Platform is not available on this system"); + false + } + } +} -#[cfg(gdb)] -mod debug { - use std::sync::{Arc, Mutex}; +// This function dynamically loads the WHvMapGpaRange2 function from the winhvplatform.dll +// WHvMapGpaRange2 only available on Windows 11 or Windows Server 2022 and later +// we do things this way to allow a user trying to load hyperlight on an older version of windows to +// get an error message saying that hyperlight requires a newer version of windows, rather than just failing +// with an error about a missing entrypoint +// This function should always succeed since before we get here we have already checked that the hypervisor is present and +// that we are on a supported version of windows. +type WHvMapGpaRange2Func = unsafe extern "cdecl" fn( + WHV_PARTITION_HANDLE, + HANDLE, + *const c_void, + u64, + u64, + WHV_MAP_GPA_RANGE_FLAGS, +) -> HRESULT; - use windows::Win32::System::Hypervisor::WHV_VP_EXCEPTION_CONTEXT; +/// A Hypervisor driver for HyperV-on-Windows. +#[derive(Debug)] +pub(crate) struct WhpVm { + partition: WHV_PARTITION_HANDLE, + // Lazily create the surrogate process when we need to map memory + surrogate_process: Option, + mmap_file_handle: HandleWrapper, +} - use super::{HypervWindowsDriver, *}; - use crate::Result; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason}; - use crate::mem::mgr::SandboxMemoryManager; - use crate::mem::shared_mem::HostSharedMemory; +unsafe impl Send for WhpVm {} +unsafe impl Sync for WhpVm {} + +/// WHV_REGISTER_VALUE must be 16-byte aligned, but the rust struct is incorrectly generated +/// as 8-byte aligned. This is a workaround to ensure that the struct is 16-byte aligned. +#[repr(C, align(16))] +struct Align16(T); +#[allow(clippy::disallowed_macros)] // compile time +const _: () = { + assert!( + std::mem::size_of::>() + == std::mem::size_of::() + ); +}; - impl HypervWindowsDriver { - /// Resets the debug information to disable debugging - fn disable_debug(&mut self) -> Result<()> { - let mut debug = HypervDebug::default(); +impl WhpVm { + pub(crate) fn new(mmap_file_handle: HandleWrapper) -> Result { + const NUM_CPU: u32 = 1; + let partition = unsafe { + let partition = WHvCreatePartition()?; + WHvSetPartitionProperty( + partition, + WHvPartitionPropertyCodeProcessorCount, + &NUM_CPU as *const _ as *const _, + std::mem::size_of_val(&NUM_CPU) as _, + )?; + WHvSetupPartition(partition)?; + WHvCreateVirtualProcessor(partition, 0, 0)?; + partition + }; - debug.set_single_step(&self.processor, false)?; + Ok(WhpVm { + partition, + surrogate_process: None, + mmap_file_handle, + }) + } - self.debug = Some(debug); + /// Helper for setting arbitrary registers. + fn set_registers(&self, registers: &[(WHV_REGISTER_NAME, WHV_REGISTER_VALUE)]) -> Result<()> { + let register_count = registers.len(); - Ok(()) - } + // Prepare register names (no special alignment needed) + let mut register_names = Vec::with_capacity(register_count); + let mut register_values = Vec::with_capacity(register_count); - /// Get the reason the vCPU has stopped - pub(crate) fn get_stop_reason( - &mut self, - exception: WHV_VP_EXCEPTION_CONTEXT, - ) -> Result { - let debug = self - .debug - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - debug.get_stop_reason(&self.processor, exception, self.entrypoint) + for (key, value) in registers.iter() { + register_names.push(*key); + register_values.push(Align16(*value)); } - pub(crate) fn process_dbg_request( - &mut self, - req: DebugMsg, - dbg_mem_access_fn: Arc>>, - ) -> Result { - if let Some(debug) = self.debug.as_mut() { - match req { - DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( - debug - .add_hw_breakpoint(&self.processor, addr) - .map_err(|e| { - log::error!("Failed to add hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( - debug - .add_sw_breakpoint(&self.processor, addr, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to add sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Continue => { - debug.set_single_step(&self.processor, false).map_err(|e| { - log::error!("Failed to continue execution: {:?}", e); - - e - })?; - - Ok(DebugResponse::Continue) - } - DebugMsg::DisableDebug => { - self.disable_debug().map_err(|e| { - log::error!("Failed to disable debugging: {:?}", e); - - e - })?; - - Ok(DebugResponse::DisableDebug) - } - DebugMsg::GetCodeSectionOffset => { - let offset = dbg_mem_access_fn - .try_lock() - .map_err(|e| { - new_error!("Error locking at {}:{}: {}", file!(), line!(), e) - })? - .layout - .get_guest_code_address(); - - Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) - } - DebugMsg::ReadAddr(addr, len) => { - let mut data = vec![0u8; len]; - - debug - .read_addrs(&self.processor, addr, &mut data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to read from address: {:?}", e); - - e - })?; - - Ok(DebugResponse::ReadAddr(data)) - } - DebugMsg::ReadRegisters => { - let mut regs = Default::default(); - - debug - .read_regs(&self.processor, &mut regs) - .map_err(|e| { - log::error!("Failed to read registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::ReadRegisters(regs)) - } - DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( - debug - .remove_hw_breakpoint(&self.processor, addr) - .map_err(|e| { - log::error!("Failed to remove hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( - debug - .remove_sw_breakpoint(&self.processor, addr, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to remove sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Step => { - debug.set_single_step(&self.processor, true).map_err(|e| { - log::error!("Failed to enable step instruction: {:?}", e); - - e - })?; - - Ok(DebugResponse::Step) - } - DebugMsg::WriteAddr(addr, data) => { - debug - .write_addrs(&self.processor, addr, &data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to write to address: {:?}", e); - - e - })?; - - Ok(DebugResponse::WriteAddr) - } - DebugMsg::WriteRegisters(regs) => debug - .write_regs(&self.processor, ®s) - .map_err(|e| { - log::error!("Failed to write registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::WriteRegisters), - } - } else { - Err(new_error!("Debugging is not enabled")) - } + unsafe { + WHvSetVirtualProcessorRegisters( + self.partition, + 0, + register_names.as_ptr(), + register_count as u32, + register_values.as_ptr() as *const WHV_REGISTER_VALUE, + )?; } - pub(crate) fn recv_dbg_msg(&mut self) -> Result { - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; + Ok(()) + } +} - gdb_conn.recv().map_err(|e| { +impl Vm for WhpVm { + fn get_regs(&self) -> Result { + let mut whv_regs_values = + AlignedRegisterValues::(unsafe { std::mem::zeroed() }); + + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + WHP_REGS_NAMES.as_ptr(), + whv_regs_values.0.len() as u32, + whv_regs_values.0.as_mut_ptr(), + )?; + } + + WHP_REGS_NAMES + .into_iter() + .zip(whv_regs_values.0) + .collect::>() + .as_slice() + .try_into() + .map_err(|e| { new_error!( - "Got an error while waiting to receive a - message: {:?}", + "Failed to convert WHP registers to CommonRegisters: {:?}", e ) }) - } - - pub(crate) fn send_dbg_msg(&mut self, cmd: DebugResponse) -> Result<()> { - log::debug!("Sending {:?}", cmd); - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - gdb_conn - .send(cmd) - .map_err(|e| new_error!("Got an error while sending a response message {:?}", e)) - } } -} -/// A Hypervisor driver for HyperV-on-Windows. -pub(crate) struct HypervWindowsDriver { - processor: VMProcessor, - _surrogate_process: SurrogateProcess, // we need to keep a reference to the SurrogateProcess for the duration of the driver since otherwise it will dropped and the memory mapping will be unmapped and the surrogate process will be returned to the pool - entrypoint: u64, - orig_rsp: GuestPtr, - interrupt_handle: Arc, - mem_mgr: Option>, - host_funcs: Option>>, - - sandbox_regions: Vec, // Initially mapped regions when sandbox is created - mmap_regions: Vec, // Later mapped regions - - #[cfg(gdb)] - debug: Option, - #[cfg(gdb)] - gdb_conn: Option>, - #[cfg(crashdump)] - rt_cfg: SandboxRuntimeConfig, - #[cfg(feature = "trace_guest")] - #[allow(dead_code)] - trace_info: TraceInfo, -} -/* This does not automatically impl Send because the host - * address of the shared memory region is a raw pointer, which are - * marked as !Send (and !Sync). However, the access patterns used - * here are safe. - */ -unsafe impl Send for HypervWindowsDriver {} - -impl HypervWindowsDriver { - #[allow(clippy::too_many_arguments)] - // TODO: refactor this function to take fewer arguments. Add trace_info to rt_cfg - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - pub(crate) fn new( - mem_regions: Vec, - raw_size: usize, - pml4_address: u64, - entrypoint: u64, - rsp: u64, - mmap_file_handle: HandleWrapper, - #[cfg(gdb)] gdb_conn: Option>, - #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, - #[cfg(feature = "trace_guest")] trace_info: TraceInfo, - ) -> Result { - // create and setup hypervisor partition - let mut partition = VMPartition::new(1)?; - - // get a surrogate process with preallocated memory of size SharedMemory::raw_mem_size() - // with guard pages setup - let surrogate_process = { - let mgr = get_surrogate_process_manager()?; - mgr.get_surrogate_process(raw_size, mmap_file_handle) - }?; - - partition.map_gpa_range(&mem_regions, &surrogate_process)?; - - let proc = VMProcessor::new(partition)?; - let partition_handle = proc.get_partition_hdl(); - - #[cfg(gdb)] - let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { - let mut debug = HypervDebug::new(); - debug.add_hw_breakpoint(&proc, entrypoint)?; - - (Some(debug), Some(gdb_conn)) - } else { - (None, None) - }; - - let interrupt_handle = Arc::new(WindowsInterruptHandle { - running: AtomicBool::new(false), - cancel_requested: AtomicBool::new(false), - #[cfg(gdb)] - debug_interrupt: AtomicBool::new(false), - partition_handle, - dropped: AtomicBool::new(false), - }); - - let mut hv = Self { - processor: proc, - _surrogate_process: surrogate_process, - entrypoint, - orig_rsp: GuestPtr::try_from(RawPtr::from(rsp))?, - interrupt_handle: interrupt_handle.clone(), - mem_mgr: None, - host_funcs: None, - sandbox_regions: mem_regions, - mmap_regions: Vec::new(), - #[cfg(gdb)] - debug, - #[cfg(gdb)] - gdb_conn, - #[cfg(crashdump)] - rt_cfg, - #[cfg(feature = "trace_guest")] - trace_info, - }; - - hv.setup_initial_sregs(pml4_address)?; + fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { + let whp_regs: [(WHV_REGISTER_NAME, WHV_REGISTER_VALUE); WHP_REGS_NAMES_LEN] = regs.into(); + self.set_registers(&whp_regs)?; + Ok(()) + } - // Send the interrupt handle to the GDB thread if debugging is enabled - // This is used to allow the GDB thread to stop the vCPU - #[cfg(gdb)] - if hv.debug.is_some() { - hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; + fn get_sregs(&self) -> Result { + let mut whp_sregs_values = + AlignedRegisterValues::(unsafe { std::mem::zeroed() }); + + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + WHP_SREGS_NAMES.as_ptr(), + whp_sregs_values.0.len() as u32, + whp_sregs_values.0.as_mut_ptr(), + )?; } - Ok(hv) + WHP_SREGS_NAMES + .into_iter() + .zip(whp_sregs_values.0) + .collect::>() + .as_slice() + .try_into() + .map_err(|e| { + new_error!( + "Failed to convert WHP registers to CommonSpecialRegisters: {:?}", + e + ) + }) } - #[inline] - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn get_exit_details(&self, exit_reason: WHV_RUN_VP_EXIT_REASON) -> Result { - let mut error = String::new(); - error.push_str(&format!( - "Did not receive a halt from Hypervisor as expected - Received {exit_reason:?}!\n" - )); - error.push_str(&format!("Registers: \n{:#?}", self.processor.regs()?)); - Ok(error) + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { + let whp_regs: [(WHV_REGISTER_NAME, WHV_REGISTER_VALUE); WHP_SREGS_NAMES_LEN] = sregs.into(); + self.set_registers(&whp_regs)?; + Ok(()) } -} - -impl Debug for HypervWindowsDriver { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut fs = f.debug_struct("HyperV Driver"); - - fs.field("Entrypoint", &self.entrypoint) - .field("Original RSP", &self.orig_rsp); - for region in &self.sandbox_regions { - fs.field("Sandbox Memory Region", ®ion); - } - for region in &self.mmap_regions { - fs.field("Mapped Memory Region", ®ion); + fn get_fpu(&self) -> Result { + let mut whp_fpu_values = + AlignedRegisterValues::(unsafe { std::mem::zeroed() }); + + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + WHP_FPU_NAMES.as_ptr(), + whp_fpu_values.0.len() as u32, + whp_fpu_values.0.as_mut_ptr(), + )?; } - // Get the registers - if let Ok(regs) = self.processor.regs() { - fs.field("Registers", ®s); - } - - // Get the special registers - if let Ok(special_regs) = self.processor.sregs() { - fs.field("SpecialRegisters", &special_regs); - } - - fs.finish() + WHP_FPU_NAMES + .into_iter() + .zip(whp_fpu_values.0) + .collect::>() + .as_slice() + .try_into() + .map_err(|e| new_error!("Failed to convert WHP registers to CommonFpu: {:?}", e)) } -} -impl Hypervisor for HypervWindowsDriver { - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn initialise( - &mut self, - peb_address: RawPtr, - seed: u64, - page_size: u32, - mem_mgr: SandboxMemoryManager, - host_funcs: Arc>, - max_guest_log_level: Option, - #[cfg(gdb)] dbg_mem_access_hdl: Arc>>, - ) -> Result<()> { - self.mem_mgr = Some(mem_mgr); - self.host_funcs = Some(host_funcs); - - let max_guest_log_level: u64 = match max_guest_log_level { - Some(level) => level as u64, - None => self.get_max_log_level().into(), - }; - - let regs = CommonRegisters { - rip: self.entrypoint, - rsp: self.orig_rsp.absolute()?, - - // function args - rdi: peb_address.into(), - rsi: seed, - rdx: page_size.into(), - rcx: max_guest_log_level, - rflags: 1 << 1, // eflags bit index 1 is reserved and always needs to be 1 - - ..Default::default() - }; - self.set_regs(®s)?; - - VirtualCPU::run( - self.as_mut_hypervisor(), - #[cfg(gdb)] - dbg_mem_access_hdl, - ) + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { + let whp_fpu: [(WHV_REGISTER_NAME, WHV_REGISTER_VALUE); WHP_FPU_NAMES_LEN] = fpu.into(); + self.set_registers(&whp_fpu)?; + Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - unsafe fn map_region(&mut self, _region: &MemoryRegion) -> Result<()> { - log_then_return!("Mapping host memory into the guest not yet supported on this platform"); - } + unsafe fn map_memory(&mut self, regions: &[MemoryRegion]) -> Result<()> { + if regions.is_empty() { + return Err(new_error!("No memory regions to map")); + } + if self.surrogate_process.is_some() { + return Err(new_error!("Memory has already been mapped")); + } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - unsafe fn unmap_region(&mut self, _region: &MemoryRegion) -> Result<()> { - log_then_return!("Mapping host memory into the guest not yet supported on this platform"); - } + let size: usize = regions.iter().map(|r| r.host_region.len()).sum(); + let raw_size = size + 2 * PAGE_SIZE_USIZE; + let raw_source_address = regions[0].host_region.start - PAGE_SIZE_USIZE; - fn get_mapped_regions(&self) -> Box + '_> { - Box::new(self.mmap_regions.iter()) - } + // get a surrogate process with preallocated memory of size SharedMemory::raw_mem_size() + // with guard pages setup + let surrogate_process = { + let mgr = get_surrogate_process_manager()?; + mgr.get_surrogate_process( + raw_size, + raw_source_address as *const c_void, + self.mmap_file_handle, + ) + }?; - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn dispatch_call_from_host( - &mut self, - dispatch_func_addr: RawPtr, - #[cfg(gdb)] dbg_mem_access_hdl: Arc>>, - ) -> Result<()> { - // Reset general purpose registers, then set RIP and RSP - let regs = CommonRegisters { - rip: dispatch_func_addr.into(), - rsp: self.orig_rsp.absolute()?, - rflags: 1 << 1, // eflags bit index 1 is reserved and always needs to be 1 - ..Default::default() + let process_handle: HANDLE = surrogate_process.process_handle.into(); + // The function pointer to WHvMapGpaRange2 is resolved dynamically to allow us to detect + // when we are running on older versions of windows that do not support this API and + // return a more informative error message, rather than failing with an error about a missing entrypoint + let whvmapgparange2_func = unsafe { + match try_load_whv_map_gpa_range2() { + Ok(func) => func, + Err(e) => return Err(new_error!("Can't find API: {}", e)), + } }; - self.processor.set_regs(®s)?; - // reset fpu state - self.processor.set_fpu(&CommonFpu::default())?; + regions.iter().try_for_each(|region| unsafe { + let flags = region + .flags + .iter() + .map(|flag| match flag { + MemoryRegionFlags::NONE => Ok(WHvMapGpaRangeFlagNone), + MemoryRegionFlags::READ => Ok(WHvMapGpaRangeFlagRead), + MemoryRegionFlags::WRITE => Ok(WHvMapGpaRangeFlagWrite), + MemoryRegionFlags::EXECUTE => Ok(WHvMapGpaRangeFlagExecute), + _ => Err(new_error!("Invalid Memory Region Flag")), + }) + .collect::>>()? + .iter() + .fold(WHvMapGpaRangeFlagNone, |acc, flag| acc | *flag); // collect using bitwise OR + + let res = whvmapgparange2_func( + self.partition, + process_handle, + region.host_region.start as *const c_void, + region.guest_region.start as u64, + region.guest_region.len() as u64, + flags, + ); + if res.is_err() { + return Err(new_error!("Call to WHvMapGpaRange2 failed")); + } + Ok(()) + })?; - VirtualCPU::run( - self.as_mut_hypervisor(), - #[cfg(gdb)] - dbg_mem_access_hdl, - )?; + self.surrogate_process = Some(surrogate_process); Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn handle_io( - &mut self, - port: u16, - data: Vec, - rip: u64, - instruction_length: u64, - ) -> Result<()> { - let mut padded = [0u8; 4]; - let copy_len = data.len().min(4); - padded[..copy_len].copy_from_slice(&data[..copy_len]); - let val = u32::from_le_bytes(padded); - - #[cfg(feature = "trace_guest")] - { - // We need to handle the borrow checker issue where we need both: - // - &mut SandboxMemoryManager (from self.mem_mgr.as_mut()) - // - &mut dyn Hypervisor (from self) - // We'll use a temporary approach to extract the mem_mgr temporarily - let mem_mgr_option = self.mem_mgr.take(); - let mut mem_mgr = mem_mgr_option - .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; - let host_funcs = self - .host_funcs - .as_ref() - .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? - .clone(); - - handle_outb(&mut mem_mgr, host_funcs, self, port, val)?; - - // Put the mem_mgr back - self.mem_mgr = Some(mem_mgr); + #[expect(non_upper_case_globals, reason = "Windows API constant are lower case")] + fn run_vcpu(&mut self) -> Result { + let mut exit_context: WHV_RUN_VP_EXIT_CONTEXT = Default::default(); + + unsafe { + WHvRunVirtualProcessor( + self.partition, + 0, + &mut exit_context as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + )?; } - #[cfg(not(feature = "trace_guest"))] - { - let mem_mgr = self - .mem_mgr - .as_mut() - .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; - let host_funcs = self - .host_funcs - .as_ref() - .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? - .clone(); - - handle_outb(mem_mgr, host_funcs, port, val)?; - } - - let mut regs = self.regs()?; - regs.rip = rip + instruction_length; - self.set_regs(®s) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn run(&mut self) -> Result { - self.interrupt_handle.running.store(true, Ordering::Relaxed); - - #[cfg(not(gdb))] - let debug_interrupt = false; - #[cfg(gdb)] - let debug_interrupt = self - .interrupt_handle - .debug_interrupt - .load(Ordering::Relaxed); - - // Don't run the vcpu if `cancel_requested` is true - let exit_context = if self - .interrupt_handle - .cancel_requested - .load(Ordering::Relaxed) - || debug_interrupt - { - WHV_RUN_VP_EXIT_CONTEXT { - ExitReason: WHV_RUN_VP_EXIT_REASON(8193i32), // WHvRunVpExitReasonCanceled - VpContext: Default::default(), - Anonymous: Default::default(), - Reserved: Default::default(), - } - } else { - #[cfg(feature = "trace_guest")] - if self.trace_info.guest_start_epoch.is_none() { - // Store the guest start epoch and cycles to trace the guest execution time - crate::debug!("HyperV - Guest Start Epoch set"); - self.trace_info.guest_start_tsc = - Some(hyperlight_guest_tracing::invariant_tsc::read_tsc()); - self.trace_info.guest_start_epoch = Some(std::time::Instant::now()); - } - self.processor.run()? - }; - self.interrupt_handle - .cancel_requested - .store(false, Ordering::Relaxed); - self.interrupt_handle - .running - .store(false, Ordering::Relaxed); - - #[cfg(gdb)] - let debug_interrupt = self - .interrupt_handle - .debug_interrupt - .load(Ordering::Relaxed); - let result = match exit_context.ExitReason { - // WHvRunVpExitReasonX64IoPortAccess - WHV_RUN_VP_EXIT_REASON(2i32) => { - // size of current instruction is in lower byte of _bitfield - // see https://learn.microsoft.com/en-us/virtualization/api/hypervisor-platform/funcs/whvexitcontextdatatypes) + WHvRunVpExitReasonX64IoPortAccess => unsafe { let instruction_length = exit_context.VpContext._bitfield & 0xF; - unsafe { - debug!( - "HyperV IO Details :\n Port: {:#x} \n {:#?}", - exit_context.Anonymous.IoPortAccess.PortNumber, &self - ); - HyperlightExit::IoOut( - exit_context.Anonymous.IoPortAccess.PortNumber, - exit_context - .Anonymous - .IoPortAccess - .Rax - .to_le_bytes() - .to_vec(), - exit_context.VpContext.Rip, - instruction_length as u64, - ) - } - } - // HvRunVpExitReasonX64Halt - WHV_RUN_VP_EXIT_REASON(8i32) => { - debug!("HyperV Halt Details :\n {:#?}", &self); - HyperlightExit::Halt() - } - // WHvRunVpExitReasonMemoryAccess - WHV_RUN_VP_EXIT_REASON(1i32) => { + let rip = exit_context.VpContext.Rip + instruction_length as u64; + self.set_registers(&[(WHvX64RegisterRip, WHV_REGISTER_VALUE { Reg64: rip })])?; + HyperlightExit::IoOut( + exit_context.Anonymous.IoPortAccess.PortNumber, + exit_context + .Anonymous + .IoPortAccess + .Rax + .to_le_bytes() + .to_vec(), + ) + }, + WHvRunVpExitReasonX64Halt => HyperlightExit::Halt(), + WHvRunVpExitReasonMemoryAccess => { let gpa = unsafe { exit_context.Anonymous.MemoryAccess.Gpa }; let access_info = unsafe { WHV_MEMORY_ACCESS_TYPE( @@ -634,348 +352,61 @@ impl Hypervisor for HypervWindowsDriver { ) }; let access_info = MemoryRegionFlags::try_from(access_info)?; - debug!( - "HyperV Memory Access Details :\n GPA: {:#?}\n Access Info :{:#?}\n {:#?} ", - gpa, access_info, &self - ); - - match get_memory_access_violation( - gpa as usize, - self.sandbox_regions.iter().chain(self.mmap_regions.iter()), - access_info, - ) { - Some(access_info) => access_info, - None => HyperlightExit::Mmio(gpa), + match access_info { + MemoryRegionFlags::READ => HyperlightExit::MmioRead(gpa), + MemoryRegionFlags::WRITE => HyperlightExit::MmioWrite(gpa), + _ => HyperlightExit::Unknown("Unknown memory access type".to_string()), } } - // WHvRunVpExitReasonCanceled // Execution was cancelled by the host. // This will happen when guest code runs for too long - WHV_RUN_VP_EXIT_REASON(8193i32) => { - debug!("HyperV Cancelled Details :\n {:#?}", &self); - #[cfg(gdb)] - if debug_interrupt { - self.interrupt_handle - .debug_interrupt - .store(false, Ordering::Relaxed); - - // If the vCPU was stopped because of an interrupt, we need to - // return a special exit reason so that the gdb thread can handle it - // and resume execution - HyperlightExit::Debug(VcpuStopReason::Interrupt) - } else { - HyperlightExit::Cancelled() - } - - #[cfg(not(gdb))] - HyperlightExit::Cancelled() - } - #[cfg(gdb)] - WHV_RUN_VP_EXIT_REASON(4098i32) => { - // Get information about the exception that triggered the exit - let exception = unsafe { exit_context.Anonymous.VpException }; - - match self.get_stop_reason(exception) { - Ok(reason) => HyperlightExit::Debug(reason), - Err(e) => { - log_then_return!("Error getting stop reason: {}", e); - } - } - } - WHV_RUN_VP_EXIT_REASON(_) => { - debug!( - "HyperV Unexpected Exit Details :#nReason {:#?}\n {:#?}", - exit_context.ExitReason, &self - ); - match self.get_exit_details(exit_context.ExitReason) { - Ok(error) => HyperlightExit::Unknown(error), - Err(e) => HyperlightExit::Unknown(format!("Error getting exit details: {}", e)), - } - } + WHvRunVpExitReasonCanceled => HyperlightExit::Cancelled(), + WHV_RUN_VP_EXIT_REASON(_) => HyperlightExit::Unknown(format!( + "Unknown exit reason '{}'", + exit_context.ExitReason.0 + )), }; - Ok(result) } - /// Get regs - #[allow(dead_code)] - fn regs(&self) -> Result { - self.processor.regs() - } - /// Set regs - fn set_regs(&mut self, regs: &CommonRegisters) -> Result<()> { - self.processor.set_regs(regs) - } - /// Get fpu regs - #[allow(dead_code)] - fn fpu(&self) -> Result { - self.processor.fpu() - } - /// Set fpu regs - fn set_fpu(&mut self, fpu: &CommonFpu) -> Result<()> { - self.processor.set_fpu(fpu) - } - /// Get special regs - #[allow(dead_code)] - fn sregs(&self) -> Result { - self.processor.sregs() - } - /// Set special regs - #[allow(dead_code)] - fn set_sregs(&mut self, sregs: &CommonSpecialRegisters) -> Result<()> { - self.processor.set_sregs(sregs) - } - - fn interrupt_handle(&self) -> Arc { - self.interrupt_handle.clone() - } - - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { - self as &mut dyn Hypervisor - } - - #[cfg(crashdump)] - fn crashdump_context(&self) -> Result>> { - if self.rt_cfg.guest_core_dump { - let mut regs = [0; 27]; - - let vcpu_regs = self.processor.regs()?; - let sregs = self.processor.sregs()?; - let xsave = self.processor.get_xsave()?; - - // Set the registers in the order expected by the crashdump context - regs[0] = vcpu_regs.r15; // r15 - regs[1] = vcpu_regs.r14; // r14 - regs[2] = vcpu_regs.r13; // r13 - regs[3] = vcpu_regs.r12; // r12 - regs[4] = vcpu_regs.rbp; // rbp - regs[5] = vcpu_regs.rbx; // rbx - regs[6] = vcpu_regs.r11; // r11 - regs[7] = vcpu_regs.r10; // r10 - regs[8] = vcpu_regs.r9; // r9 - regs[9] = vcpu_regs.r8; // r8 - regs[10] = vcpu_regs.rax; // rax - regs[11] = vcpu_regs.rcx; // rcx - regs[12] = vcpu_regs.rdx; // rdx - regs[13] = vcpu_regs.rsi; // rsi - regs[14] = vcpu_regs.rdi; // rdi - regs[15] = 0; // orig rax - regs[16] = vcpu_regs.rip; // rip - regs[17] = sregs.cs.selector as u64; // cs - regs[18] = vcpu_regs.rflags; // eflags - regs[19] = vcpu_regs.rsp; // rsp - regs[20] = sregs.ss.selector as u64; // ss - regs[21] = sregs.fs.base; // fs_base - regs[22] = sregs.gs.base; // gs_base - regs[23] = sregs.ds.selector as u64; // ds - regs[24] = sregs.es.selector as u64; // es - regs[25] = sregs.fs.selector as u64; // fs - regs[26] = sregs.gs.selector as u64; // gs - - // Get the filename from the config - let filename = self.rt_cfg.binary_path.clone().and_then(|path| { - Path::new(&path) - .file_name() - .and_then(|name| name.to_os_string().into_string().ok()) - }); - - Ok(Some(crashdump::CrashDumpContext::new( - &self.sandbox_regions, - regs, - xsave, - self.entrypoint, - self.rt_cfg.binary_path.clone(), - filename, - ))) - } else { - Ok(None) - } + fn get_partition_handle(&self) -> WHV_PARTITION_HANDLE { + self.partition } +} - #[cfg(gdb)] - fn handle_debug( - &mut self, - dbg_mem_access_fn: Arc>>, - stop_reason: super::gdb::VcpuStopReason, - ) -> Result<()> { - if self.debug.is_none() { - return Err(new_error!("Debugging is not enabled")); - } - match stop_reason { - // If the vCPU stopped because of a crash, we need to handle it differently - // We do not want to allow resuming execution or placing breakpoints - // because the guest has crashed. - // We only allow reading registers and memory - VcpuStopReason::Crash => { - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) - .map_err(|e| { - new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) - })?; - - loop { - log::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - // Flag to store if we should deny continue or step requests - let mut deny_continue = false; - // Flag to store if we should detach from the gdb session - let mut detach = false; - - let response = match req { - // Allow the detach request to disable debugging by continuing resuming - // hypervisor crash error reporting - DebugMsg::DisableDebug => { - detach = true; - DebugResponse::DisableDebug - } - // Do not allow continue or step requests - DebugMsg::Continue | DebugMsg::Step => { - deny_continue = true; - DebugResponse::NotAllowed - } - // Do not allow adding/removing breakpoints and writing to memory or registers - DebugMsg::AddHwBreakpoint(_) - | DebugMsg::AddSwBreakpoint(_) - | DebugMsg::RemoveHwBreakpoint(_) - | DebugMsg::RemoveSwBreakpoint(_) - | DebugMsg::WriteAddr(_, _) - | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, - - // For all other requests, we will process them normally - _ => { - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); - match result { - Ok(response) => response, - Err(HyperlightError::TranslateGuestAddress(_)) => { - // Treat non fatal errors separately so the guest doesn't fail - DebugResponse::ErrorOccurred - } - Err(e) => { - log::error!("Error processing debug request: {:?}", e); - return Err(e); - } - } - } - }; - - // Send the response to the request back to gdb - self.send_dbg_msg(response) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; - - // If we are denying continue or step requests, the debugger assumes the - // execution started so we need to report a stop reason as a crash and let - // it request to read registers/memory to figure out what happened - if deny_continue { - self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash)) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; - } - - // If we are detaching, we will break the loop and the Hypervisor will continue - // to handle the Crash reason - if detach { - break; - } - } - } - - // If the vCPU stopped because of any other reason except a crash, we can handle it - // normally - _ => { - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) - .map_err(|e| { - new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) - })?; - - loop { - log::debug!("Debug wait for event to resume vCPU"); - - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); - - let response = match result { - Ok(response) => response, - // Treat non fatal errors separately so the guest doesn't fail - Err(HyperlightError::TranslateGuestAddress(_)) => { - DebugResponse::ErrorOccurred - } - Err(e) => { - return Err(e); - } - }; - - // If the command was either step or continue, we need to run the vcpu - let cont = matches!( - response, - DebugResponse::Step | DebugResponse::Continue | DebugResponse::DisableDebug - ); - - self.send_dbg_msg(response) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; - - if cont { - break; - } - } - } +impl Drop for WhpVm { + fn drop(&mut self) { + if let Err(e) = unsafe { WHvDeletePartition(self.partition) } { + log::error!("Failed to delete partition: {}", e); } - - Ok(()) } +} - fn check_stack_guard(&self) -> Result { - if let Some(mgr) = self.mem_mgr.as_ref() { - mgr.check_stack_guard() - } else { - Err(new_error!("Memory manager is not initialized")) - } - } +unsafe fn try_load_whv_map_gpa_range2() -> Result { + let library = unsafe { + LoadLibraryExA( + s!("winhvplatform.dll"), + None, + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, + ) + }; - #[cfg(feature = "trace_guest")] - fn trace_info_as_ref(&self) -> &TraceInfo { - &self.trace_info + if let Err(e) = library { + return Err(new_error!("{}", e)); } - #[cfg(feature = "trace_guest")] - fn trace_info_as_mut(&mut self) -> &mut TraceInfo { - &mut self.trace_info - } -} -impl Drop for HypervWindowsDriver { - fn drop(&mut self) { - self.interrupt_handle.dropped.store(true, Ordering::Relaxed); - } -} + #[allow(clippy::unwrap_used)] + // We know this will succeed because we just checked for an error above + let library = library.unwrap(); -#[derive(Debug)] -pub struct WindowsInterruptHandle { - // `WHvCancelRunVirtualProcessor()` will return Ok even if the vcpu is not running, which is the reason we need this flag. - running: AtomicBool, - cancel_requested: AtomicBool, - // This is used to signal the GDB thread to stop the vCPU - #[cfg(gdb)] - debug_interrupt: AtomicBool, - partition_handle: WHV_PARTITION_HANDLE, - dropped: AtomicBool, -} + let address = unsafe { GetProcAddress(library, s!("WHvMapGpaRange2")) }; -impl InterruptHandle for WindowsInterruptHandle { - fn kill(&self) -> bool { - self.cancel_requested.store(true, Ordering::Relaxed); - self.running.load(Ordering::Relaxed) - && unsafe { WHvCancelRunVirtualProcessor(self.partition_handle, 0, 0).is_ok() } - } - #[cfg(gdb)] - fn kill_from_debugger(&self) -> bool { - self.debug_interrupt.store(true, Ordering::Relaxed); - self.running.load(Ordering::Relaxed) - && unsafe { WHvCancelRunVirtualProcessor(self.partition_handle, 0, 0).is_ok() } + if address.is_none() { + unsafe { FreeLibrary(library)? }; + return Err(new_error!( + "Failed to find WHvMapGpaRange2 in winhvplatform.dll" + )); } - fn dropped(&self) -> bool { - self.dropped.load(Ordering::Relaxed) - } + unsafe { Ok(std::mem::transmute_copy(&address)) } } diff --git a/src/hyperlight_host/src/hypervisor/kvm.rs b/src/hyperlight_host/src/hypervisor/kvm.rs index 3545074ea..acd55bc72 100644 --- a/src/hyperlight_host/src/hypervisor/kvm.rs +++ b/src/hyperlight_host/src/hypervisor/kvm.rs @@ -1,5 +1,5 @@ /* -Copyright 2025 The Hyperlight Authors. +Copyright 2024 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. @@ -14,40 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -use std::fmt::Debug; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +#[cfg(gdb)] +use std::collections::HashMap; +use std::sync::LazyLock; +#[cfg(gdb)] use std::sync::{Arc, Mutex}; -use kvm_bindings::{kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region}; +#[cfg(gdb)] +use kvm_bindings::kvm_guest_debug; +use kvm_bindings::kvm_userspace_memory_region; use kvm_ioctls::Cap::UserMemory; use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; -use log::LevelFilter; use tracing::{Span, instrument}; -#[cfg(crashdump)] -use {super::crashdump, std::path::Path}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; #[cfg(gdb)] -use super::gdb::{DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason}; -use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU}; +use crate::hypervisor::vm::DebugExit; +use crate::hypervisor::vm::{Vm, VmExit}; +use crate::mem::memory_region::MemoryRegion; #[cfg(gdb)] -use crate::HyperlightError; -use crate::hypervisor::get_memory_access_violation; -use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; -use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::ptr::{GuestPtr, RawPtr}; +#[cfg(gdb)] use crate::mem::shared_mem::HostSharedMemory; -use crate::sandbox::SandboxConfiguration; -#[cfg(feature = "trace_guest")] -use crate::sandbox::TraceInfo; -use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::outb::handle_outb; -#[cfg(crashdump)] -use crate::sandbox::uninitialized::SandboxRuntimeConfig; -use crate::{Result, log_then_return, new_error}; +use crate::{Result, new_error}; /// Return `true` if the KVM API is available, version 12, and has UserMemory capability, or `false` otherwise -#[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn is_hypervisor_present() -> bool { if let Ok(kvm) = Kvm::new() { let api_version = kvm.get_api_version(); @@ -68,983 +59,243 @@ pub(crate) fn is_hypervisor_present() -> bool { } } -#[cfg(gdb)] -mod debug { - use std::sync::{Arc, Mutex}; - - use kvm_bindings::kvm_debug_exit_arch; - - use super::KVMDriver; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason}; - use crate::mem::mgr::SandboxMemoryManager; - use crate::mem::shared_mem::HostSharedMemory; - use crate::{Result, new_error}; - - impl KVMDriver { - /// Resets the debug information to disable debugging - fn disable_debug(&mut self) -> Result<()> { - let mut debug = KvmDebug::default(); - - debug.set_single_step(&self.vcpu_fd, false)?; - - self.debug = Some(debug); - - Ok(()) - } - - /// Get the reason the vCPU has stopped - pub(crate) fn get_stop_reason( - &mut self, - debug_exit: kvm_debug_exit_arch, - ) -> Result { - let debug = self - .debug - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - debug.get_stop_reason(&self.vcpu_fd, debug_exit, self.entrypoint) - } - - pub(crate) fn process_dbg_request( - &mut self, - req: DebugMsg, - dbg_mem_access_fn: Arc>>, - ) -> Result { - if let Some(debug) = self.debug.as_mut() { - match req { - DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( - debug - .add_hw_breakpoint(&self.vcpu_fd, addr) - .map_err(|e| { - log::error!("Failed to add hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( - debug - .add_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to add sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Continue => { - debug.set_single_step(&self.vcpu_fd, false).map_err(|e| { - log::error!("Failed to continue execution: {:?}", e); - - e - })?; - - Ok(DebugResponse::Continue) - } - DebugMsg::DisableDebug => { - self.disable_debug().map_err(|e| { - log::error!("Failed to disable debugging: {:?}", e); - - e - })?; - - Ok(DebugResponse::DisableDebug) - } - DebugMsg::GetCodeSectionOffset => { - let offset = dbg_mem_access_fn - .try_lock() - .map_err(|e| { - new_error!("Error locking at {}:{}: {}", file!(), line!(), e) - })? - .layout - .get_guest_code_address(); - - Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) - } - DebugMsg::ReadAddr(addr, len) => { - let mut data = vec![0u8; len]; - - debug - .read_addrs(&self.vcpu_fd, addr, &mut data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to read from address: {:?}", e); - - e - })?; - - Ok(DebugResponse::ReadAddr(data)) - } - DebugMsg::ReadRegisters => { - let mut regs = Default::default(); - - debug - .read_regs(&self.vcpu_fd, &mut regs) - .map_err(|e| { - log::error!("Failed to read registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::ReadRegisters(regs)) - } - DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( - debug - .remove_hw_breakpoint(&self.vcpu_fd, addr) - .map_err(|e| { - log::error!("Failed to remove hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( - debug - .remove_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to remove sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Step => { - debug.set_single_step(&self.vcpu_fd, true).map_err(|e| { - log::error!("Failed to enable step instruction: {:?}", e); - - e - })?; - - Ok(DebugResponse::Step) - } - DebugMsg::WriteAddr(addr, data) => { - debug - .write_addrs(&self.vcpu_fd, addr, &data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to write to address: {:?}", e); - - e - })?; - - Ok(DebugResponse::WriteAddr) - } - DebugMsg::WriteRegisters(regs) => debug - .write_regs(&self.vcpu_fd, ®s) - .map_err(|e| { - log::error!("Failed to write registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::WriteRegisters), - } - } else { - Err(new_error!("Debugging is not enabled")) - } - } - - pub(crate) fn recv_dbg_msg(&mut self) -> Result { - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - gdb_conn.recv().map_err(|e| { - new_error!( - "Got an error while waiting to receive a message from the gdb thread: {:?}", - e - ) - }) - } - - pub(crate) fn send_dbg_msg(&mut self, cmd: DebugResponse) -> Result<()> { - log::debug!("Sending {:?}", cmd); - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or_else(|| new_error!("Debug is not enabled"))?; - - gdb_conn.send(cmd).map_err(|e| { - new_error!( - "Got an error while sending a response message to the gdb thread: {:?}", - e - ) - }) - } - } -} - -/// A Hypervisor driver for KVM on Linux -pub(crate) struct KVMDriver { - _kvm: Kvm, +/// A KVM implementation of a single-vcpu VM +#[derive(Debug)] +pub(crate) struct KvmVm { vm_fd: VmFd, - page_size: usize, vcpu_fd: VcpuFd, - entrypoint: u64, - orig_rsp: GuestPtr, - interrupt_handle: Arc, - mem_mgr: Option>, - host_funcs: Option>>, - sandbox_regions: Vec, // Initially mapped regions when sandbox is created - mmap_regions: Vec<(MemoryRegion, u32)>, // Later mapped regions (region, slot number) - next_slot: u32, // Monotonically increasing slot number - freed_slots: Vec, // Reusable slots from unmapped regions - - #[cfg(gdb)] - debug: Option, #[cfg(gdb)] - gdb_conn: Option>, - #[cfg(crashdump)] - rt_cfg: SandboxRuntimeConfig, - #[cfg(feature = "trace_guest")] - #[allow(dead_code)] - trace_info: TraceInfo, + debug: KvmDebug, } -impl KVMDriver { - /// Create a new instance of a `KVMDriver`, with only control registers - /// set. Standard registers will not be set, and `initialise` must - /// be called to do so. - #[allow(clippy::too_many_arguments)] - // TODO: refactor this function to take fewer arguments. Add trace_info to rt_cfg - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - pub(crate) fn new( - mem_regions: Vec, - pml4_addr: u64, - entrypoint: u64, - rsp: u64, - config: &SandboxConfiguration, - #[cfg(gdb)] gdb_conn: Option>, - #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, - #[cfg(feature = "trace_guest")] trace_info: TraceInfo, - ) -> Result { - let kvm = Kvm::new()?; - - let vm_fd = kvm.create_vm_with_type(0)?; +#[cfg(gdb)] +#[derive(Debug, Default)] +struct KvmDebug { + regs: kvm_guest_debug, + sw_breakpoints: HashMap, // addr -> original instruction +} - mem_regions.iter().enumerate().try_for_each(|(i, region)| { - let mut kvm_region: kvm_userspace_memory_region = region.clone().into(); - kvm_region.slot = i as u32; - unsafe { vm_fd.set_user_memory_region(kvm_region) } - })?; +static KVM: LazyLock> = + LazyLock::new(|| Kvm::new().map_err(|e| new_error!("Failed to open /dev/kvm: {}", e))); +impl KvmVm { + /// Create a new instance of a `KvmVm` + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + pub(crate) fn new() -> Result { + let hv = KVM + .as_ref() + .map_err(|e| new_error!("Failed to create KVM instance: {}", e))?; + let vm_fd = hv.create_vm_with_type(0)?; let vcpu_fd = vm_fd.create_vcpu(0)?; - #[cfg(gdb)] - let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { - let mut debug = KvmDebug::new(); - // Add breakpoint to the entry point address - debug.add_hw_breakpoint(&vcpu_fd, entrypoint)?; - - (Some(debug), Some(gdb_conn)) - } else { - (None, None) - }; - - let rsp_gp = GuestPtr::try_from(RawPtr::from(rsp))?; - - let interrupt_handle = Arc::new(LinuxInterruptHandle { - running: AtomicU64::new(0), - cancel_requested: AtomicBool::new(false), - #[cfg(gdb)] - debug_interrupt: AtomicBool::new(false), - #[cfg(all( - target_arch = "x86_64", - target_vendor = "unknown", - target_os = "linux", - target_env = "musl" - ))] - tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }), - #[cfg(not(all( - target_arch = "x86_64", - target_vendor = "unknown", - target_os = "linux", - target_env = "musl" - )))] - tid: AtomicU64::new(unsafe { libc::pthread_self() }), - retry_delay: config.get_interrupt_retry_delay(), - dropped: AtomicBool::new(false), - sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(), - }); - - let mut kvm = Self { - _kvm: kvm, + Ok(Self { vm_fd, - page_size: 0, vcpu_fd, - entrypoint, - orig_rsp: rsp_gp, - next_slot: mem_regions.len() as u32, - sandbox_regions: mem_regions, - mmap_regions: Vec::new(), - freed_slots: Vec::new(), - interrupt_handle: interrupt_handle.clone(), - mem_mgr: None, - host_funcs: None, - #[cfg(gdb)] - debug, #[cfg(gdb)] - gdb_conn, - #[cfg(crashdump)] - rt_cfg, - #[cfg(feature = "trace_guest")] - trace_info, - }; - - kvm.setup_initial_sregs(pml4_addr)?; - - // Send the interrupt handle to the GDB thread if debugging is enabled - // This is used to allow the GDB thread to stop the vCPU - #[cfg(gdb)] - if kvm.debug.is_some() { - kvm.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; - } - - Ok(kvm) + debug: KvmDebug::default(), + }) } } -impl Debug for KVMDriver { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut f = f.debug_struct("KVM Driver"); - // Output each memory region - - for region in &self.sandbox_regions { - f.field("Sandbox Memory Region", ®ion); - } - for region in &self.mmap_regions { - f.field("Mapped Memory Region", ®ion); - } - let regs = self.vcpu_fd.get_regs(); - // check that regs is OK and then set field in debug struct - - if let Ok(regs) = regs { - f.field("Registers", ®s); - } - - let sregs = self.vcpu_fd.get_sregs(); - - // check that sregs is OK and then set field in debug struct - - if let Ok(sregs) = sregs { - f.field("Special Registers", &sregs); - } - - f.finish() +impl Vm for KvmVm { + fn regs(&self) -> Result { + Ok((&self.vcpu_fd.get_regs()?).into()) } -} - -impl Hypervisor for KVMDriver { - /// Implementation of initialise for Hypervisor trait. - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn initialise( - &mut self, - peb_addr: RawPtr, - seed: u64, - page_size: u32, - mem_mgr: SandboxMemoryManager, - host_funcs: Arc>, - max_guest_log_level: Option, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()> { - self.mem_mgr = Some(mem_mgr); - self.host_funcs = Some(host_funcs); - self.page_size = page_size as usize; - let max_guest_log_level: u64 = match max_guest_log_level { - Some(level) => level as u64, - None => self.get_max_log_level().into(), - }; - - let regs = CommonRegisters { - rip: self.entrypoint, - rsp: self.orig_rsp.absolute()?, - - // function args - rdi: peb_addr.into(), - rsi: seed, - rdx: page_size.into(), - rcx: max_guest_log_level, - - ..Default::default() - }; - self.set_regs(®s)?; - - VirtualCPU::run( - self.as_mut_hypervisor(), - #[cfg(gdb)] - dbg_mem_access_fn, - ) + fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { + Ok(self.vcpu_fd.set_regs(®s.into())?) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - unsafe fn map_region(&mut self, region: &MemoryRegion) -> Result<()> { - if [ - region.guest_region.start, - region.guest_region.end, - region.host_region.start, - region.host_region.end, - ] - .iter() - .any(|x| x % self.page_size != 0) - { - log_then_return!( - "region is not page-aligned {:x}, {region:?}", - self.page_size - ); - } - - let mut kvm_region: kvm_userspace_memory_region = region.clone().into(); - - // 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 - }; - - kvm_region.slot = slot; - unsafe { self.vm_fd.set_user_memory_region(kvm_region) }?; - self.mmap_regions.push((region.to_owned(), slot)); - Ok(()) + fn sregs(&self) -> Result { + Ok((&self.vcpu_fd.get_sregs()?).into()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - unsafe fn unmap_region(&mut self, region: &MemoryRegion) -> Result<()> { - if let Some(idx) = self.mmap_regions.iter().position(|(r, _)| r == region) { - let (region, slot) = self.mmap_regions.remove(idx); - let mut kvm_region: kvm_userspace_memory_region = region.into(); - kvm_region.slot = slot; - // Setting memory_size to 0 unmaps the slot's region - // From https://docs.kernel.org/virt/kvm/api.html - // > Deleting a slot is done by passing zero for memory_size. - kvm_region.memory_size = 0; - unsafe { self.vm_fd.set_user_memory_region(kvm_region) }?; - - // Add the freed slot to the reuse list - self.freed_slots.push(slot); - - Ok(()) - } else { - Err(new_error!("Tried to unmap region that is not mapped")) - } + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { + Ok(self.vcpu_fd.set_sregs(&sregs.into())?) } - fn get_mapped_regions(&self) -> Box + '_> { - Box::new(self.mmap_regions.iter().map(|(region, _)| region)) + fn fpu(&self) -> Result { + Ok((&self.vcpu_fd.get_fpu()?).into()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn dispatch_call_from_host( - &mut self, - dispatch_func_addr: RawPtr, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()> { - // Reset general purpose registers, then set RIP and RSP - let regs = CommonRegisters { - rip: dispatch_func_addr.into(), - rsp: self.orig_rsp.absolute()?, - ..Default::default() - }; - self.set_regs(®s)?; - - // reset fpu state - self.set_fpu(&CommonFpu::default())?; + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { + Ok(self.vcpu_fd.set_fpu(&fpu.into())?) + } - // run - VirtualCPU::run( - self.as_mut_hypervisor(), - #[cfg(gdb)] - dbg_mem_access_fn, - )?; + #[cfg(crashdump)] + fn xsave(&self) -> Result> { + let xsave = self.vcpu_fd.get_xsave()?; + Ok(xsave + .region + .into_iter() + .flat_map(u32::to_le_bytes) + .collect()) + } + 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; + unsafe { self.vm_fd.set_user_memory_region(kvm_region)? }; Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn handle_io( - &mut self, - port: u16, - data: Vec, - _rip: u64, - _instruction_length: u64, - ) -> Result<()> { - // KVM does not need RIP or instruction length, as it automatically sets the RIP - - // The payload param for the outb_handle_fn is the first byte - // of the data array cast to an u64. Thus, we need to make sure - // the data array has at least one u8, then convert that to an u64 - if data.is_empty() { - log_then_return!("no data was given in IO interrupt"); - } else { - let mut padded = [0u8; 4]; - let copy_len = data.len().min(4); - padded[..copy_len].copy_from_slice(&data[..copy_len]); - let value = u32::from_le_bytes(padded); - - #[cfg(feature = "trace_guest")] - { - // We need to handle the borrow checker issue where we need both: - // - &mut SandboxMemoryManager (from self.mem_mgr.as_mut()) - // - &mut dyn Hypervisor (from self) - // We'll use a temporary approach to extract the mem_mgr temporarily - let mem_mgr_option = self.mem_mgr.take(); - let mut mem_mgr = - mem_mgr_option.ok_or_else(|| new_error!("mem_mgr not initialized"))?; - let host_funcs = self - .host_funcs - .as_ref() - .ok_or_else(|| new_error!("host_funcs not initialized"))? - .clone(); - - handle_outb(&mut mem_mgr, host_funcs, self, port, value)?; - - // Put the mem_mgr back - self.mem_mgr = Some(mem_mgr); - } - - #[cfg(not(feature = "trace_guest"))] - { - let mem_mgr = self - .mem_mgr - .as_mut() - .ok_or_else(|| new_error!("mem_mgr not initialized"))?; - let host_funcs = self - .host_funcs - .as_ref() - .ok_or_else(|| new_error!("host_funcs not initialized"))? - .clone(); - - handle_outb(mem_mgr, host_funcs, port, value)?; - } - } - + fn unmap_memory(&mut self, (slot, region): (u32, &MemoryRegion)) -> Result<()> { + let mut kvm_region: kvm_userspace_memory_region = region.into(); + kvm_region.slot = slot; + // Setting memory_size to 0 unmaps the slot's region + // From https://docs.kernel.org/virt/kvm/api.html + // > Deleting a slot is done by passing zero for memory_size. + kvm_region.memory_size = 0; + unsafe { self.vm_fd.set_user_memory_region(kvm_region) }?; Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn run(&mut self) -> Result { - self.interrupt_handle - .tid - .store(unsafe { libc::pthread_self() as u64 }, Ordering::Relaxed); - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call - self.interrupt_handle - .set_running_and_increment_generation() - .map_err(|e| { - new_error!( - "Error setting running state and incrementing generation: {}", - e - ) - })?; - #[cfg(not(gdb))] - let debug_interrupt = false; - #[cfg(gdb)] - let debug_interrupt = self - .interrupt_handle - .debug_interrupt - .load(Ordering::Relaxed); - // Don't run the vcpu if `cancel_requested` is true - // - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call - let exit_reason = if self - .interrupt_handle - .cancel_requested - .load(Ordering::Relaxed) - || debug_interrupt - { - Err(kvm_ioctls::Error::new(libc::EINTR)) - } else { - #[cfg(feature = "trace_guest")] - if self.trace_info.guest_start_epoch.is_none() { - // Store the guest start epoch and cycles to trace the guest execution time - crate::debug!("KVM - Guest Start Epoch set"); - self.trace_info.guest_start_epoch = Some(std::time::Instant::now()); - self.trace_info.guest_start_tsc = - Some(hyperlight_guest_tracing::invariant_tsc::read_tsc()); - } - - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then the vcpu will run, but we will keep sending signals to this thread - // to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will - // return either normally with an exit reason, or from being "kicked" by out signal handler, with an EINTR error, - // both of which are fine. - self.vcpu_fd.run() - }; - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then signals will be sent to this thread until `running` is set to false. - // This is fine since the signal handler is a no-op. - let cancel_requested = self - .interrupt_handle - .cancel_requested - .load(Ordering::Relaxed); - #[cfg(gdb)] - let debug_interrupt = self - .interrupt_handle - .debug_interrupt - .load(Ordering::Relaxed); - // Note: if a `InterruptHandle::kill()` called while this thread is **here** - // Then `cancel_requested` will be set to true again, which will cancel the **next vcpu run**. - // Additionally signals will be sent to this thread until `running` is set to false. - // This is fine since the signal handler is a no-op. - self.interrupt_handle.clear_running_bit(); - // At this point, `running` is false so no more signals will be sent to this thread, - // but we may still receive async signals that were sent before this point. - // To prevent those signals from interrupting subsequent calls to `run()` (on other vms!), - // we make sure to check `cancel_requested` before cancelling (see `libc::EINTR` match-arm below). - let result = match exit_reason { - Ok(VcpuExit::Hlt) => { - crate::debug!("KVM - Halt Details : {:#?}", &self); - HyperlightExit::Halt() - } - Ok(VcpuExit::IoOut(port, data)) => { - // because vcpufd.run() mutably borrows self we cannot pass self to crate::debug! macro here - crate::debug!("KVM IO Details : \nPort : {}\nData : {:?}", port, data); - // KVM does not need to set RIP or instruction length so these are set to 0 - HyperlightExit::IoOut(port, data.to_vec(), 0, 0) - } - Ok(VcpuExit::MmioRead(addr, _)) => { - crate::debug!("KVM MMIO Read -Details: Address: {} \n {:#?}", addr, &self); - - match get_memory_access_violation( - addr as usize, - self.sandbox_regions - .iter() - .chain(self.mmap_regions.iter().map(|(r, _)| r)), - MemoryRegionFlags::READ, - ) { - Some(access_violation_exit) => access_violation_exit, - None => HyperlightExit::Mmio(addr), - } - } - Ok(VcpuExit::MmioWrite(addr, _)) => { - crate::debug!("KVM MMIO Write -Details: Address: {} \n {:#?}", addr, &self); - - match get_memory_access_violation( - addr as usize, - self.sandbox_regions - .iter() - .chain(self.mmap_regions.iter().map(|(r, _)| r)), - MemoryRegionFlags::WRITE, - ) { - Some(access_violation_exit) => access_violation_exit, - None => HyperlightExit::Mmio(addr), - } - } + fn run_vcpu(&mut self) -> Result { + match self.vcpu_fd.run() { + 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)] - // KVM provides architecture specific information about the vCPU state when exiting - Ok(VcpuExit::Debug(debug_exit)) => match self.get_stop_reason(debug_exit) { - Ok(reason) => HyperlightExit::Debug(reason), - Err(e) => { - log_then_return!("Error getting stop reason: {:?}", e); - } - }, + Ok(VcpuExit::Debug(debug_exit)) => Ok(VmExit::Debug(DebugExit::Debug { + dr6: debug_exit.dr6, + exception: debug_exit.exception, + })), Err(e) => match e.errno() { - // we send a signal to the thread to cancel execution this results in EINTR being returned by KVM so we return Cancelled - libc::EINTR => { - // If cancellation was not requested for this specific vm, the vcpu was interrupted because of debug interrupt or - // a stale signal that meant to be delivered to a previous/other vcpu on this same thread, so let's ignore it - if cancel_requested { - self.interrupt_handle - .cancel_requested - .store(false, Ordering::Relaxed); - HyperlightExit::Cancelled() - } else { - #[cfg(gdb)] - if debug_interrupt { - self.interrupt_handle - .debug_interrupt - .store(false, Ordering::Relaxed); - - // If the vCPU was stopped because of an interrupt, we need to - // return a special exit reason so that the gdb thread can handle it - // and resume execution - HyperlightExit::Debug(VcpuStopReason::Interrupt) - } else { - HyperlightExit::Retry() - } + libc::EINTR => Ok(VmExit::Cancelled()), + libc::EAGAIN => Ok(VmExit::Retry()), - #[cfg(not(gdb))] - HyperlightExit::Retry() - } - } - libc::EAGAIN => HyperlightExit::Retry(), - _ => { - crate::debug!("KVM Error -Details: Address: {} \n {:#?}", e, &self); - log_then_return!("Error running VCPU {:?}", e); - } + other => Ok(VmExit::Unknown(format!( + "Unknown KVM VCPU error: {}", + other + ))), }, - Ok(other) => { - let err_msg = format!("Unexpected KVM Exit {:?}", other); - crate::debug!("KVM Other Exit Details: {:#?}", &self); - HyperlightExit::Unknown(err_msg) - } - }; - Ok(result) - } - - fn regs(&self) -> Result { - let kvm_regs = self.vcpu_fd.get_regs()?; - Ok((&kvm_regs).into()) + Ok(other) => Ok(VmExit::Unknown(format!( + "Unknown KVM VCPU exit: {:?}", + other + ))), + } } - fn set_regs(&mut self, regs: &super::regs::CommonRegisters) -> Result<()> { - let kvm_regs: kvm_regs = regs.into(); - self.vcpu_fd.set_regs(&kvm_regs)?; - Ok(()) - } + // --- DEBUGGING RELATED BELOW --- - fn fpu(&self) -> Result { - let kvm_fpu = self.vcpu_fd.get_fpu()?; - Ok((&kvm_fpu).into()) - } + #[cfg(gdb)] + fn translate_gva(&self, gva: u64) -> Result { + use crate::HyperlightError; - fn set_fpu(&mut self, fpu: &super::regs::CommonFpu) -> Result<()> { - let kvm_fpu: kvm_fpu = fpu.into(); - self.vcpu_fd.set_fpu(&kvm_fpu)?; - Ok(()) + let gpa = self.vcpu_fd.translate_gva(gva)?; + if gpa.valid == 0 { + Err(HyperlightError::TranslateGuestAddress(gva)) + } else { + Ok(gpa.physical_address) + } } - fn sregs(&self) -> Result { - let kvm_sregs = self.vcpu_fd.get_sregs()?; - Ok((&kvm_sregs).into()) - } + #[cfg(gdb)] + fn set_debug(&mut self, enable: bool) -> Result<()> { + use kvm_bindings::{KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_USE_HW_BP, KVM_GUESTDBG_USE_SW_BP}; - fn set_sregs(&mut self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { - let kvm_sregs: kvm_sregs = sregs.into(); - self.vcpu_fd.set_sregs(&kvm_sregs)?; + log::info!("Setting debug to {}", enable); + if enable { + self.debug.regs.control |= + KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW_BP | KVM_GUESTDBG_USE_SW_BP; + } else { + self.debug.regs.control &= + !(KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW_BP | KVM_GUESTDBG_USE_SW_BP); + } + self.vcpu_fd.set_guest_debug(&self.debug.regs)?; Ok(()) } - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { - self as &mut dyn Hypervisor - } - - fn interrupt_handle(&self) -> Arc { - self.interrupt_handle.clone() - } - - #[cfg(crashdump)] - fn crashdump_context(&self) -> Result>> { - if self.rt_cfg.guest_core_dump { - let mut regs = [0; 27]; - - let vcpu_regs = self.vcpu_fd.get_regs()?; - let sregs = self.vcpu_fd.get_sregs()?; - let xsave = self.vcpu_fd.get_xsave()?; - - // Set the registers in the order expected by the crashdump context - regs[0] = vcpu_regs.r15; // r15 - regs[1] = vcpu_regs.r14; // r14 - regs[2] = vcpu_regs.r13; // r13 - regs[3] = vcpu_regs.r12; // r12 - regs[4] = vcpu_regs.rbp; // rbp - regs[5] = vcpu_regs.rbx; // rbx - regs[6] = vcpu_regs.r11; // r11 - regs[7] = vcpu_regs.r10; // r10 - regs[8] = vcpu_regs.r9; // r9 - regs[9] = vcpu_regs.r8; // r8 - regs[10] = vcpu_regs.rax; // rax - regs[11] = vcpu_regs.rcx; // rcx - regs[12] = vcpu_regs.rdx; // rdx - regs[13] = vcpu_regs.rsi; // rsi - regs[14] = vcpu_regs.rdi; // rdi - regs[15] = 0; // orig rax - regs[16] = vcpu_regs.rip; // rip - regs[17] = sregs.cs.selector as u64; // cs - regs[18] = vcpu_regs.rflags; // eflags - regs[19] = vcpu_regs.rsp; // rsp - regs[20] = sregs.ss.selector as u64; // ss - regs[21] = sregs.fs.base; // fs_base - regs[22] = sregs.gs.base; // gs_base - regs[23] = sregs.ds.selector as u64; // ds - regs[24] = sregs.es.selector as u64; // es - regs[25] = sregs.fs.selector as u64; // fs - regs[26] = sregs.gs.selector as u64; // gs - - // Get the filename from the runtime config - let filename = self.rt_cfg.binary_path.clone().and_then(|path| { - Path::new(&path) - .file_name() - .and_then(|name| name.to_os_string().into_string().ok()) - }); + #[cfg(gdb)] + fn set_single_step(&mut self, enable: bool) -> Result<()> { + use kvm_bindings::KVM_GUESTDBG_SINGLESTEP; - // The [`CrashDumpContext`] accepts xsave as a vector of u8, so we need to convert the - // xsave region to a vector of u8 - Ok(Some(crashdump::CrashDumpContext::new( - &self.sandbox_regions, - regs, - xsave - .region - .iter() - .flat_map(|item| item.to_le_bytes()) - .collect::>(), - self.entrypoint, - self.rt_cfg.binary_path.clone(), - filename, - ))) + log::info!("Setting single step to {}", enable); + if enable { + self.debug.regs.control |= KVM_GUESTDBG_SINGLESTEP; + } else { + self.debug.regs.control &= !KVM_GUESTDBG_SINGLESTEP; + } + // Set TF Flag to enable Traps + let mut regs = self.regs()?; + if enable { + regs.rflags |= 1 << 8; } else { - Ok(None) + regs.rflags &= !(1 << 8); } + self.set_regs(®s)?; + Ok(()) } #[cfg(gdb)] - fn handle_debug( - &mut self, - dbg_mem_access_fn: Arc>>, - stop_reason: VcpuStopReason, - ) -> Result<()> { - if self.debug.is_none() { - return Err(new_error!("Debugging is not enabled")); - } - - match stop_reason { - // If the vCPU stopped because of a crash, we need to handle it differently - // We do not want to allow resuming execution or placing breakpoints - // because the guest has crashed. - // We only allow reading registers and memory - VcpuStopReason::Crash => { - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) - .map_err(|e| { - new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) - })?; - - loop { - log::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - // Flag to store if we should deny continue or step requests - let mut deny_continue = false; - // Flag to store if we should detach from the gdb session - let mut detach = false; + fn add_hw_breakpoint(&mut self, addr: u64) -> Result<()> { + use crate::hypervisor::gdb::arch::MAX_NO_OF_HW_BP; + use crate::new_error; - let response = match req { - // Allow the detach request to disable debugging by continuing resuming - // hypervisor crash error reporting - DebugMsg::DisableDebug => { - detach = true; - DebugResponse::DisableDebug - } - // Do not allow continue or step requests - DebugMsg::Continue | DebugMsg::Step => { - deny_continue = true; - DebugResponse::NotAllowed - } - // Do not allow adding/removing breakpoints and writing to memory or registers - DebugMsg::AddHwBreakpoint(_) - | DebugMsg::AddSwBreakpoint(_) - | DebugMsg::RemoveHwBreakpoint(_) - | DebugMsg::RemoveSwBreakpoint(_) - | DebugMsg::WriteAddr(_, _) - | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, + // Find the first available LOCAL (L0–L3) slot + let i = (0..MAX_NO_OF_HW_BP) + .position(|i| self.debug.regs.arch.debugreg[7] & (1 << (i * 2)) == 0) + .ok_or_else(|| new_error!("Tried to add more than 4 hardware breakpoints"))?; - // For all other requests, we will process them normally - _ => { - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); - match result { - Ok(response) => response, - Err(HyperlightError::TranslateGuestAddress(_)) => { - // Treat non fatal errors separately so the guest doesn't fail - DebugResponse::ErrorOccurred - } - Err(e) => { - log::error!("Error processing debug request: {:?}", e); - return Err(e); - } - } - } - }; + // Assign to corresponding debug register + self.debug.regs.arch.debugreg[i] = addr; - // Send the response to the request back to gdb - self.send_dbg_msg(response) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; + // Enable LOCAL bit + self.debug.regs.arch.debugreg[7] |= 1 << (i * 2); - // If we are denying continue or step requests, the debugger assumes the - // execution started so we need to report a stop reason as a crash and let - // it request to read registers/memory to figure out what happened - if deny_continue { - self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash)) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; - } - - // If we are detaching, we will break the loop and the Hypervisor will continue - // to handle the Crash reason - if detach { - break; - } - } - } - // If the vCPU stopped because of any other reason except a crash, we can handle it - // normally - _ => { - // Send the stop reason to the gdb thread - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason)) - .map_err(|e| { - new_error!("Couldn't signal vCPU stopped event to GDB thread: {:?}", e) - })?; - - loop { - log::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + self.vcpu_fd.set_guest_debug(&self.debug.regs)?; + Ok(()) + } - let response = match result { - Ok(response) => response, - // Treat non fatal errors separately so the guest doesn't fail - Err(HyperlightError::TranslateGuestAddress(_)) => { - DebugResponse::ErrorOccurred - } - Err(e) => { - return Err(e); - } - }; + #[cfg(gdb)] + fn remove_hw_breakpoint(&mut self, addr: u64) -> Result<()> { + use crate::new_error; - let cont = matches!( - response, - DebugResponse::Continue | DebugResponse::Step | DebugResponse::DisableDebug - ); + // Find the index of the breakpoint + let index = self.debug.regs.arch.debugreg[..4] + .iter() + .position(|&a| a == addr) + .ok_or_else(|| new_error!("Tried to remove non-existing hw-breakpoint"))?; - self.send_dbg_msg(response) - .map_err(|e| new_error!("Couldn't send response to gdb: {:?}", e))?; + // Clear the address + self.debug.regs.arch.debugreg[index] = 0; - // Check if we should continue execution - // We continue if the response is one of the following: Step, Continue, or DisableDebug - if cont { - break; - } - } - } - } + // Disable LOCAL bit + self.debug.regs.arch.debugreg[7] &= !(1 << (index * 2)); + self.vcpu_fd.set_guest_debug(&self.debug.regs)?; Ok(()) } - fn check_stack_guard(&self) -> Result { - if let Some(mgr) = self.mem_mgr.as_ref() { - mgr.check_stack_guard() - } else { - Err(new_error!("Memory manager is not initialized")) - } - } + #[cfg(gdb)] + fn add_sw_breakpoint( + &mut self, + addr: u64, + dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + use crate::hypervisor::gdb::arch::SW_BP_OP; - #[cfg(feature = "trace_guest")] - fn trace_info_as_ref(&self) -> &TraceInfo { - &self.trace_info - } - #[cfg(feature = "trace_guest")] - fn trace_info_as_mut(&mut self) -> &mut TraceInfo { - &mut self.trace_info + let mut guard = dbg_mem_access_fn.lock().unwrap(); + let orig_instr: u8 = guard.get_shared_mem_mut().read(addr as usize)?; + guard.get_shared_mem_mut().write(addr as usize, SW_BP_OP)?; + self.debug.sw_breakpoints.insert(addr, orig_instr); + Ok(()) } -} -impl Drop for KVMDriver { - fn drop(&mut self) { - self.interrupt_handle.dropped.store(true, Ordering::Relaxed); + #[cfg(gdb)] + fn remove_sw_breakpoint( + &mut self, + addr: u64, + dbg_mem_access_fn: Arc>>, + ) -> Result<()> { + let original_instr = self.debug.sw_breakpoints.remove(&addr).unwrap(); + dbg_mem_access_fn + .lock() + .unwrap() + .get_shared_mem_mut() + .write(addr as usize, original_instr)?; + Ok(()) } } diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index 781099134..adbee8c37 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -1,5 +1,5 @@ /* -Copyright 2025 The Hyperlight Authors. +Copyright 2024 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. @@ -14,37 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ +use std::fmt::Debug; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::time::Duration; + use log::LevelFilter; -use tracing::{Span, instrument}; - -use crate::HyperlightError::StackOverflow; -use crate::error::HyperlightError::ExecutionCanceledByHost; -use crate::hypervisor::regs::{ - CommonFpu, CommonRegisters, CommonSegmentRegister, CommonSpecialRegisters, -}; -use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; -use crate::metrics::METRIC_GUEST_CANCELLATION; -#[cfg(feature = "trace_guest")] -use crate::sandbox::TraceInfo; -use crate::{HyperlightError, Result, log_then_return}; - -/// HyperV-on-linux functionality -#[cfg(mshv)] -pub mod hyperv_linux; -#[cfg(target_os = "windows")] -/// Hyperv-on-windows functionality -pub(crate) mod hyperv_windows; /// GDB debugging support #[cfg(gdb)] pub(crate) mod gdb; +pub(crate) mod vm; /// Abstracts over different hypervisor register representations pub(crate) mod regs; -#[cfg(kvm)] -/// Functionality to manipulate KVM-based virtual machines -pub mod kvm; #[cfg(target_os = "windows")] /// Hyperlight Surrogate Process pub(crate) mod surrogate_process; @@ -61,378 +45,17 @@ pub(crate) mod wrappers; #[cfg(crashdump)] pub(crate) mod crashdump; -use std::fmt::Debug; -use std::str::FromStr; -#[cfg(any(kvm, mshv))] -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -#[cfg(any(kvm, mshv))] -use std::time::Duration; - -#[cfg(gdb)] -use gdb::VcpuStopReason; - -use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::ptr::RawPtr; -use crate::mem::shared_mem::HostSharedMemory; -use crate::sandbox::host_funcs::FunctionRegistry; - -cfg_if::cfg_if! { - if #[cfg(feature = "init-paging")] { - pub(crate) const CR4_PAE: u64 = 1 << 5; - pub(crate) const CR4_OSFXSR: u64 = 1 << 9; - pub(crate) const CR4_OSXMMEXCPT: u64 = 1 << 10; - pub(crate) const CR0_PE: u64 = 1; - pub(crate) const CR0_MP: u64 = 1 << 1; - pub(crate) const CR0_ET: u64 = 1 << 4; - pub(crate) const CR0_NE: u64 = 1 << 5; - pub(crate) const CR0_WP: u64 = 1 << 16; - pub(crate) const CR0_AM: u64 = 1 << 18; - pub(crate) const CR0_PG: u64 = 1 << 31; - pub(crate) const EFER_LME: u64 = 1 << 8; - pub(crate) const EFER_LMA: u64 = 1 << 10; - pub(crate) const EFER_SCE: u64 = 1; - pub(crate) const EFER_NX: u64 = 1 << 11; - } -} - -/// These are the generic exit reasons that we can handle from a Hypervisor the Hypervisors run method is responsible for mapping from -/// the hypervisor specific exit reasons to these generic ones -pub enum HyperlightExit { - #[cfg(gdb)] - /// The vCPU has exited due to a debug event - Debug(VcpuStopReason), - /// The vCPU has halted - Halt(), - /// The vCPU has issued a write to the given port with the given value - IoOut(u16, Vec, u64, u64), - /// The vCPU has attempted to read or write from an unmapped address - Mmio(u64), - /// The vCPU tried to access memory but was missing the required permissions - AccessViolation(u64, MemoryRegionFlags, MemoryRegionFlags), - /// 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 - Retry(), -} - -/// A common set of hypervisor functionality -pub(crate) trait Hypervisor: Debug + Send { - /// Initialise the internally stored vCPU with the given PEB address and - /// random number seed, then run it until a HLT instruction. - #[allow(clippy::too_many_arguments)] - fn initialise( - &mut self, - peb_addr: RawPtr, - seed: u64, - page_size: u32, - mem_mgr: SandboxMemoryManager, - host_funcs: Arc>, - guest_max_log_level: Option, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()>; - - /// Map a region of host memory into the sandbox. - /// - /// Depending on the host platform, there are likely alignment - /// requirements of at least one page for base and len. - unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()>; - - /// Unmap a memory region from the sandbox - unsafe fn unmap_region(&mut self, rgn: &MemoryRegion) -> Result<()>; - - /// Get the currently mapped dynamic memory regions (not including sandbox regions) - /// - /// Note: Box needed for trait to be object-safe :( - fn get_mapped_regions(&self) -> Box + '_>; - - /// Dispatch a call from the host to the guest using the given pointer - /// to the dispatch function _in the guest's address space_. - /// - /// Do this by setting the instruction pointer to `dispatch_func_addr` - /// and then running the execution loop until a halt instruction. - /// - /// Returns `Ok` if the call succeeded, and an `Err` if it failed - fn dispatch_call_from_host( - &mut self, - dispatch_func_addr: RawPtr, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()>; - - /// Handle an IO exit from the internally stored vCPU. - fn handle_io( - &mut self, - port: u16, - data: Vec, - rip: u64, - instruction_length: u64, - ) -> Result<()>; - - /// Run the vCPU - fn run(&mut self) -> Result; - - /// Get InterruptHandle to underlying VM - fn interrupt_handle(&self) -> Arc; - - /// Get regs - #[allow(dead_code)] - fn regs(&self) -> Result; - /// Set regs - #[allow(dead_code)] - fn set_regs(&mut self, regs: &CommonRegisters) -> Result<()>; - /// Get fpu regs - #[allow(dead_code)] - fn fpu(&self) -> Result; - /// Set fpu regs - #[allow(dead_code)] - fn set_fpu(&mut self, fpu: &CommonFpu) -> Result<()>; - /// Get special regs - #[allow(dead_code)] - fn sregs(&self) -> Result; - /// Set special regs - #[allow(dead_code)] - fn set_sregs(&mut self, sregs: &CommonSpecialRegisters) -> Result<()>; - - /// Setup initial special registers for the hypervisor - /// This is a default implementation that works for all hypervisors - fn setup_initial_sregs(&mut self, _pml4_addr: u64) -> Result<()> { - #[cfg(feature = "init-paging")] - let sregs = CommonSpecialRegisters { - cr0: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, - cr4: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, - cr3: _pml4_addr, - efer: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, - cs: CommonSegmentRegister { - type_: 11, - present: 1, - s: 1, - l: 1, - ..Default::default() - }, - tr: CommonSegmentRegister { - limit: 65535, - type_: 11, - present: 1, - s: 0, - ..Default::default() - }, - ..Default::default() - }; - - #[cfg(not(feature = "init-paging"))] - let sregs = CommonSpecialRegisters { - cs: CommonSegmentRegister { - base: 0, - selector: 0, - limit: 0xFFFF, - type_: 11, - present: 1, - s: 1, - ..Default::default() - }, - ds: CommonSegmentRegister { - base: 0, - selector: 0, - limit: 0xFFFF, - type_: 3, - present: 1, - s: 1, - ..Default::default() - }, - tr: CommonSegmentRegister { - base: 0, - selector: 0, - limit: 0xFFFF, - type_: 11, - present: 1, - s: 0, - ..Default::default() - }, - ..Default::default() - }; - - self.set_sregs(&sregs)?; - Ok(()) - } - - /// Get the logging level to pass to the guest entrypoint - fn get_max_log_level(&self) -> u32 { - // Check to see if the RUST_LOG environment variable is set - // and if so, parse it to get the log_level for hyperlight_guest - // if that is not set get the log level for the hyperlight_host - - // 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 val = std::env::var("RUST_LOG").unwrap_or_default(); - - let level = if val.contains("hyperlight_guest") { - val.split(',') - .find(|s| s.contains("hyperlight_guest")) - .unwrap_or("") - .split('=') - .nth(1) - .unwrap_or("") - } else if val.contains("hyperlight_host") { - val.split(',') - .find(|s| s.contains("hyperlight_host")) - .unwrap_or("") - .split('=') - .nth(1) - .unwrap_or("") - } else { - // look for a value string that does not contain "=" - val.split(',').find(|s| !s.contains("=")).unwrap_or("") - }; - - log::info!("Determined guest log level: {}", level); - // Convert the log level string to a LevelFilter - // If no value is found, default to Error - LevelFilter::from_str(level).unwrap_or(LevelFilter::Error) as u32 - } - - /// get a mutable trait object from self - fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor; - - #[cfg(crashdump)] - fn crashdump_context(&self) -> Result>>; - - #[cfg(gdb)] - /// handles the cases when the vCPU stops due to a Debug event - fn handle_debug( - &mut self, - _dbg_mem_access_fn: Arc>>, - _stop_reason: VcpuStopReason, - ) -> Result<()> { - unimplemented!() - } - - /// Check stack guard to see if the stack is still valid - fn check_stack_guard(&self) -> Result; - - /// Get a reference of the trace info for the guest - #[cfg(feature = "trace_guest")] - fn trace_info_as_ref(&self) -> &TraceInfo; - /// Get a mutable reference of the trace info for the guest - #[cfg(feature = "trace_guest")] - fn trace_info_as_mut(&mut self) -> &mut TraceInfo; -} - -/// Returns a Some(HyperlightExit::AccessViolation(..)) if the given gpa doesn't have -/// access its corresponding region. Returns None otherwise, or if the region is not found. -pub(crate) fn get_memory_access_violation<'a>( - gpa: usize, - mut mem_regions: impl Iterator, - access_info: MemoryRegionFlags, -) -> Option { - // find the region containing the given gpa - let region = mem_regions.find(|region| region.guest_region.contains(&gpa)); - - if let Some(region) = region - && (!region.flags.contains(access_info) - || region.flags.contains(MemoryRegionFlags::STACK_GUARD)) - { - return Some(HyperlightExit::AccessViolation( - gpa as u64, - access_info, - region.flags, - )); - } - None -} - -/// A virtual CPU that can be run until an exit occurs -pub struct VirtualCPU {} - -impl VirtualCPU { - /// Run the given hypervisor until a halt instruction is reached - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - pub(crate) fn run( - hv: &mut dyn Hypervisor, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> Result<()> { - loop { - match hv.run() { - #[cfg(gdb)] - Ok(HyperlightExit::Debug(stop_reason)) => { - if let Err(e) = hv.handle_debug(dbg_mem_access_fn.clone(), stop_reason) { - log_then_return!(e); - } - } - - Ok(HyperlightExit::Halt()) => { - break; - } - Ok(HyperlightExit::IoOut(port, data, rip, instruction_length)) => { - hv.handle_io(port, data, rip, instruction_length)? - } - Ok(HyperlightExit::Mmio(addr)) => { - #[cfg(crashdump)] - crashdump::generate_crashdump(hv)?; - - if !hv.check_stack_guard()? { - log_then_return!(StackOverflow()); - } - - log_then_return!("MMIO access address {:#x}", addr); - } - Ok(HyperlightExit::AccessViolation(addr, tried, region_permission)) => { - #[cfg(crashdump)] - crashdump::generate_crashdump(hv)?; - - // If GDB is enabled, we handle the debug memory access - // Disregard return value as we want to return the error - #[cfg(gdb)] - let _ = hv.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash); - - if region_permission.intersects(MemoryRegionFlags::STACK_GUARD) { - return Err(HyperlightError::StackOverflow()); - } - log_then_return!(HyperlightError::MemoryAccessViolation( - addr, - tried, - region_permission - )); - } - Ok(HyperlightExit::Cancelled()) => { - // Shutdown is returned when the host has cancelled execution - // After termination, the main thread will re-initialize the VM - metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); - log_then_return!(ExecutionCanceledByHost()); - } - Ok(HyperlightExit::Unknown(reason)) => { - #[cfg(crashdump)] - crashdump::generate_crashdump(hv)?; - // If GDB is enabled, we handle the debug memory access - // Disregard return value as we want to return the error - #[cfg(gdb)] - let _ = hv.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash); - - log_then_return!("Unexpected VM Exit {:?}", reason); - } - Ok(HyperlightExit::Retry()) => continue, - Err(e) => { - #[cfg(crashdump)] - crashdump::generate_crashdump(hv)?; - // If GDB is enabled, we handle the debug memory access - // Disregard return value as we want to return the error - #[cfg(gdb)] - let _ = hv.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash); - - return Err(e); - } - } - } +#[cfg(mshv)] +pub(crate) mod hyperv_linux; +#[cfg(target_os = "windows")] +mod hyperv_windows; +#[cfg(kvm)] +pub(crate) mod kvm; - Ok(()) - } -} +pub(crate) mod hyperlight_vm; /// A trait for handling interrupts to a sandbox's vcpu -pub trait InterruptHandle: Debug + Send + Sync { +pub trait InterruptHandle: Send + Sync + Debug { /// Interrupt the corresponding sandbox from running. /// /// - If this is called while the vcpu is running, then it will interrupt the vcpu and return `true`. @@ -584,6 +207,43 @@ impl InterruptHandle for LinuxInterruptHandle { } } +/// 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 + // and if so, parse it to get the log_level for hyperlight_guest + // if that is not set get the log level for the hyperlight_host + + // 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 val = std::env::var("RUST_LOG").unwrap_or_default(); + + let level = if val.contains("hyperlight_guest") { + val.split(',') + .find(|s| s.contains("hyperlight_guest")) + .unwrap_or("") + .split('=') + .nth(1) + .unwrap_or("") + } else if val.contains("hyperlight_host") { + val.split(',') + .find(|s| s.contains("hyperlight_host")) + .unwrap_or("") + .split('=') + .nth(1) + .unwrap_or("") + } else { + // look for a value string that does not contain "=" + val.split(',').find(|s| !s.contains("=")).unwrap_or("") + }; + + log::info!("Determined guest log level: {}", level); + // Convert the log level string to a LevelFilter + // If no value is found, default to Error + LevelFilter::from_str(level).unwrap_or(LevelFilter::Error) as u32 +} + #[cfg(all(test, any(target_os = "windows", kvm)))] pub(crate) mod tests { use std::sync::{Arc, Mutex}; diff --git a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs index ab14af84b..b27b7115d 100644 --- a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs @@ -37,7 +37,22 @@ use windows::Win32::System::Hypervisor::*; #[cfg(target_os = "windows")] use super::FromWhpRegisterError; -#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) const CR4_PAE: u64 = 1 << 5; +pub(crate) const CR4_OSFXSR: u64 = 1 << 9; +pub(crate) const CR4_OSXMMEXCPT: u64 = 1 << 10; +pub(crate) const CR0_PE: u64 = 1; +pub(crate) const CR0_MP: u64 = 1 << 1; +pub(crate) const CR0_ET: u64 = 1 << 4; +pub(crate) const CR0_NE: u64 = 1 << 5; +pub(crate) const CR0_WP: u64 = 1 << 16; +pub(crate) const CR0_AM: u64 = 1 << 18; +pub(crate) const CR0_PG: u64 = 1 << 31; +pub(crate) const EFER_LME: u64 = 1 << 8; +pub(crate) const EFER_LMA: u64 = 1 << 10; +pub(crate) const EFER_SCE: u64 = 1; +pub(crate) const EFER_NX: u64 = 1 << 11; + +#[derive(Debug, Copy, Clone, PartialEq)] pub(crate) struct CommonSpecialRegisters { pub cs: CommonSegmentRegister, pub ds: CommonSegmentRegister, @@ -59,6 +74,42 @@ pub(crate) struct CommonSpecialRegisters { pub interrupt_bitmap: [u64; 4], } +impl Default for CommonSpecialRegisters { + fn default() -> Self { + CommonSpecialRegisters { + cs: CommonSegmentRegister { + l: 1, // 64-bit + type_: 0b1011, // Code, Readable, Accessed + present: 1, // Present + s: 1, // Non-system + ..Default::default() + }, + tr: CommonSegmentRegister { + limit: 0xFFFF, + type_: 0b1011, + present: 1, + ..Default::default() + }, + efer: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, + ds: Default::default(), + es: Default::default(), + fs: Default::default(), + gs: Default::default(), + ss: Default::default(), + ldt: Default::default(), + gdt: Default::default(), + idt: Default::default(), + cr0: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_WP | CR0_PG, + cr2: 0, + cr4: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, + cr3: 0, + cr8: 0, + apic_base: 0, + interrupt_bitmap: [0; 4], + } + } +} + #[cfg(mshv)] impl From<&SpecialRegisters> for CommonSpecialRegisters { fn from(value: &SpecialRegisters) -> Self { diff --git a/src/hyperlight_host/src/hypervisor/vm.rs b/src/hyperlight_host/src/hypervisor/vm.rs new file mode 100644 index 000000000..b88583f6d --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/vm.rs @@ -0,0 +1,130 @@ +use std::fmt::Debug; +#[cfg(gdb)] +use std::sync::Arc; +#[cfg(gdb)] +use std::sync::Mutex; + +use super::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::Result; +use crate::mem::memory_region::MemoryRegion; +#[cfg(gdb)] +use crate::mem::mgr::SandboxMemoryManager; +#[cfg(gdb)] +use crate::mem::shared_mem::HostSharedMemory; + +/// 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: Send + Sync + Debug { + /// Get the standard registers of the vCPU + #[allow(dead_code)] + fn regs(&self) -> Result; + /// Set the standard registers of the vCPU + fn set_regs(&self, regs: &CommonRegisters) -> Result<()>; + + /// Get the special registers of the vCPU + #[allow(dead_code)] + fn sregs(&self) -> Result; + /// Set the special registers of the vCPU + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()>; + + /// Get the FPU registers of the vCPU + #[allow(dead_code)] + fn fpu(&self) -> Result; + /// Set the FPU registers of the vCPU + fn set_fpu(&self, fpu: &CommonFpu) -> Result<()>; + /// xsave + #[cfg(crashdump)] + fn xsave(&self) -> Result>; + + /// 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. + 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 + fn run_vcpu(&mut self) -> Result; + + // -------------------------- + // --- DEBUGGING BELOW ------ + // -------------------------- + + /// Translates a guest virtual address to a guest physical address + #[cfg(gdb)] + fn translate_gva(&self, gva: u64) -> Result; + + /// Enable/disable debugging + #[cfg(gdb)] + fn set_debug(&mut self, enable: bool) -> Result<()>; + + /// Enable/disable single stepping + #[cfg(gdb)] + fn set_single_step(&mut self, enable: bool) -> Result<()>; + + /// Add a hardware breakpoint at the given address + #[cfg(gdb)] + fn add_hw_breakpoint(&mut self, addr: u64) -> Result<()>; + + /// Remove a hardware breakpoint at the given address + #[cfg(gdb)] + fn remove_hw_breakpoint(&mut self, addr: u64) -> Result<()>; + + /// Add a software breakpoint at the given address + #[cfg(gdb)] + fn add_sw_breakpoint( + &mut self, + addr: u64, + dbg_mem_access_fn: Arc>>, + ) -> Result<()>; + + /// Remove a software breakpoint at the given address + #[cfg(gdb)] + fn remove_sw_breakpoint( + &mut self, + addr: u64, + dbg_mem_access_fn: Arc>>, + ) -> Result<()>; +} + +/// Possible exit reasons of a VM's vCPU +pub(crate) enum VmExit { + /// 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(), + #[cfg(gdb)] + /// The vCPU has exited due to a debug event + Debug(DebugExit), +} + +#[derive(Debug)] +#[cfg(gdb)] +pub(crate) enum DebugExit { + /// The vCPU has exited due to a debug event (usually breakpoint) + Debug { dr6: u64, exception: u32 }, + /// The user has requested to stop the VM during execution (e.g. via Ctrl+C inside GDB) + Interrupt, +} diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 22f71d65b..454c72e3e 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -262,8 +262,8 @@ impl MemoryRegionVecBuilder { } #[cfg(mshv)] -impl From for mshv_user_mem_region { - fn from(region: MemoryRegion) -> Self { +impl From<&MemoryRegion> for mshv_user_mem_region { + fn from(region: &MemoryRegion) -> Self { let size = (region.guest_region.end - region.guest_region.start) as u64; let guest_pfn = region.guest_region.start as u64 >> PAGE_SHIFT; let userspace_addr = region.host_region.start as u64; @@ -312,8 +312,8 @@ impl From for mshv_user_mem_region { } #[cfg(kvm)] -impl From for kvm_bindings::kvm_userspace_memory_region { - fn from(region: MemoryRegion) -> Self { +impl From<&MemoryRegion> for kvm_bindings::kvm_userspace_memory_region { + fn from(region: &MemoryRegion) -> Self { let perm_flags = MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE; diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index 84cbb4c8e..a54b01aec 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -1228,6 +1228,7 @@ mod tests { // provides a way for running the above tests in a separate process since they expect to crash #[test] + #[ignore] fn guard_page_testing_shim() { let tests = vec!["read", "write", "exec"]; diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index fc860caf6..7f0301be3 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -37,7 +37,8 @@ use super::snapshot::Snapshot; use crate::HyperlightError::SnapshotSandboxMismatch; use crate::func::guest_err::check_for_guest_error; use crate::func::{ParameterTuple, SupportedReturnType}; -use crate::hypervisor::{Hypervisor, InterruptHandle}; +use crate::hypervisor::InterruptHandle; +use crate::hypervisor::hyperlight_vm::HyperlightVm; #[cfg(unix)] use crate::mem::memory_region::MemoryRegionType; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; @@ -60,7 +61,7 @@ pub struct MultiUseSandbox { // We need to keep a reference to the host functions, even if the compiler marks it as unused. The compiler cannot detect our dynamic usages of the host function in `HyperlightFunction::call`. pub(super) _host_funcs: Arc>, pub(crate) mem_mgr: SandboxMemoryManager, - vm: Box, + vm: HyperlightVm, dispatch_ptr: RawPtr, #[cfg(gdb)] dbg_mem_access_fn: Arc>>, @@ -79,7 +80,7 @@ impl MultiUseSandbox { pub(super) fn from_uninit( host_funcs: Arc>, mgr: SandboxMemoryManager, - vm: Box, + vm: HyperlightVm, dispatch_ptr: RawPtr, #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> MultiUseSandbox { @@ -123,13 +124,17 @@ impl MultiUseSandbox { if let Some(snapshot) = &self.snapshot { return Ok(snapshot.clone()); } - let mapped_regions_iter = self.vm.get_mapped_regions(); - let mapped_regions_vec: Vec = mapped_regions_iter.cloned().collect(); - let memory_snapshot = self.mem_mgr.snapshot(self.id, mapped_regions_vec)?; - let inner = Arc::new(memory_snapshot); - let snapshot = Snapshot { inner }; - self.snapshot = Some(snapshot.clone()); - Ok(snapshot) + let mapped_regions: Vec = self + .vm + .get_mapped_regions() + .iter() + .map(|(_, region)| region) + .cloned() + .collect(); + let memory_snapshot = self.mem_mgr.snapshot(self.id, mapped_regions)?; + Ok(Snapshot { + inner: Arc::new(memory_snapshot), + }) } /// Restores the sandbox's memory to a previously captured snapshot state. @@ -178,18 +183,32 @@ impl MultiUseSandbox { self.mem_mgr.restore_snapshot(&snapshot.inner)?; - let current_regions: HashSet<_> = self.vm.get_mapped_regions().cloned().collect(); + let current_regions: HashSet<_> = self + .vm + .get_mapped_regions() + .iter() + .map(|(_, region)| region) + .cloned() + .collect(); let snapshot_regions: HashSet<_> = snapshot.inner.regions().iter().cloned().collect(); - let regions_to_unmap = current_regions.difference(&snapshot_regions); - let regions_to_map = snapshot_regions.difference(¤t_regions); + let regions_to_unmap: Vec<_> = current_regions + .difference(&snapshot_regions) + .cloned() + .collect(); + let regions_to_map: Vec<_> = snapshot_regions + .difference(¤t_regions) + .cloned() + .collect(); for region in regions_to_unmap { - unsafe { self.vm.unmap_region(region)? }; + self.vm.unmap_region(®ion)?; } for region in regions_to_map { - unsafe { self.vm.map_region(region)? }; + // Safety: The region has been mapped before, and at that point the caller promised that the memory region is valid + // in their call to `MultiUseSandbox::map_region` + unsafe { self.vm.map_region(®ion)? }; } // The restored snapshot is now our most current snapshot @@ -976,8 +995,8 @@ mod tests { assert_eq!(sbox.vm.get_mapped_regions().len(), 1); // Verify the region is the same - let mut restored_regions = sbox.vm.get_mapped_regions(); - assert_eq!(*restored_regions.next().unwrap(), region); + let mut restored_regions = sbox.vm.get_mapped_regions().iter(); + assert_eq!(restored_regions.next().unwrap().1, region); assert!(restored_regions.next().is_none()); drop(restored_regions); diff --git a/src/hyperlight_host/src/sandbox/outb.rs b/src/hyperlight_host/src/sandbox/outb.rs index 2697a6a5e..b2fd3b7fb 100644 --- a/src/hyperlight_host/src/sandbox/outb.rs +++ b/src/hyperlight_host/src/sandbox/outb.rs @@ -34,7 +34,7 @@ use tracing_log::format_trace; use super::host_funcs::FunctionRegistry; #[cfg(feature = "trace_guest")] -use crate::hypervisor::Hypervisor; +use crate::hypervisor::hyperlight_vm::HyperlightVm; #[cfg(feature = "trace_guest")] use crate::mem::layout::SandboxMemoryLayout; use crate::mem::mgr::SandboxMemoryManager; @@ -157,7 +157,7 @@ fn outb_abort(mem_mgr: &mut SandboxMemoryManager, data: u32) - #[cfg(feature = "unwind_guest")] fn unwind( - hv: &dyn Hypervisor, + hv: &HyperlightVm, mem: &SandboxMemoryManager, trace_info: &TraceInfo, ) -> Result> { @@ -170,7 +170,7 @@ fn unwind( .unwind_cache .try_lock() .map_err(|e| new_error!("could not lock unwinder cache {}\n", e))?; - let regs = hv.regs()?; + let regs = hv.vm_regs()?; let iter = trace_info.unwinder.iter_frames( regs.rip, framehop::x86_64::UnwindRegsX86_64::new(regs.rip, regs.rsp, regs.rbp), @@ -267,7 +267,7 @@ pub(super) fn record_guest_trace_frame( pub(crate) fn handle_outb( mem_mgr: &mut SandboxMemoryManager, host_funcs: Arc>, - #[cfg(feature = "trace_guest")] _hv: &mut dyn Hypervisor, + #[cfg(feature = "trace_guest")] _hv: &mut HyperlightVm, port: u16, data: u32, ) -> Result<()> { @@ -299,27 +299,22 @@ pub(crate) fn handle_outb( } #[cfg(feature = "unwind_guest")] OutBAction::TraceRecordStack => { - let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info_as_ref()) else { + let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info()) else { return Ok(()); }; - record_trace_frame(_hv.trace_info_as_ref(), 1u64, |f| { + record_trace_frame(_hv.trace_info(), 1u64, |f| { write_stack(f, &stack); }) } #[cfg(feature = "mem_profile")] OutBAction::TraceMemoryAlloc => { - use crate::hypervisor::regs::CommonRegisters; - - let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info_as_ref()) else { - return Ok(()); - }; - let Ok(CommonRegisters { - rax: amt, rcx: ptr, .. - }) = _hv.regs() - else { + let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info()) else { return Ok(()); }; - record_trace_frame(_hv.trace_info_as_ref(), 2u64, |f| { + let regs = _hv.vm_regs()?; + let amt = regs.rax; + let ptr = regs.rcx; + record_trace_frame(_hv.trace_info(), 2u64, |f| { let _ = f.write_all(&ptr.to_ne_bytes()); let _ = f.write_all(&amt.to_ne_bytes()); write_stack(f, &stack); @@ -327,29 +322,21 @@ pub(crate) fn handle_outb( } #[cfg(feature = "mem_profile")] OutBAction::TraceMemoryFree => { - use crate::hypervisor::regs::CommonRegisters; - - let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info_as_ref()) else { - return Ok(()); - }; - let Ok(CommonRegisters { rcx, .. }) = _hv.regs() else { + let Ok(stack) = unwind(_hv, mem_mgr, _hv.trace_info()) else { return Ok(()); }; - record_trace_frame(_hv.trace_info_as_ref(), 3u64, |f| { - let _ = f.write_all(&rcx.to_ne_bytes()); + let regs = _hv.vm_regs()?; + let ptr = regs.rcx; + record_trace_frame(_hv.trace_info(), 3u64, |f| { + let _ = f.write_all(&ptr.to_ne_bytes()); write_stack(f, &stack); }) } #[cfg(feature = "trace_guest")] OutBAction::TraceRecord => { - use crate::hypervisor::regs::CommonRegisters; - - let Ok(CommonRegisters { - rax: len, rcx: ptr, .. - }) = _hv.regs() - else { - return Ok(()); - }; + let regs = _hv.vm_regs()?; + let len = regs.rax; + let ptr = regs.rcx; let mut buffer = vec![0u8; len as usize * std::mem::size_of::()]; let buffer = &mut buffer[..]; @@ -392,10 +379,10 @@ pub(crate) fn handle_outb( } for record in traces { - record_guest_trace_frame(_hv.trace_info_as_ref(), 4u64, record.cycles, |f| { + record_guest_trace_frame(_hv.trace_info(), 4u64, record.cycles, |f| { let _ = f.write_all(&record.msg_len.to_ne_bytes()); let _ = f.write_all(&record.msg[..record.msg_len]); - })? + })?; } Ok(()) diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index f73e334e8..6fdd9d198 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -20,16 +20,13 @@ use rand::Rng; use tracing::{Span, instrument}; use super::SandboxConfiguration; -use super::hypervisor::{HypervisorType, get_available_hypervisor}; #[cfg(any(crashdump, gdb))] use super::uninitialized::SandboxRuntimeConfig; -use crate::HyperlightError::NoHypervisorFound; -use crate::hypervisor::Hypervisor; +use crate::hypervisor::hyperlight_vm::HyperlightVm; use crate::mem::exe::LoadInfo; use crate::mem::layout::SandboxMemoryLayout; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; -use crate::mem::ptr_offset::Offset; use crate::mem::shared_mem::GuestSharedMemory; #[cfg(any(feature = "init-paging", target_os = "windows"))] use crate::mem::shared_mem::SharedMemory; @@ -41,7 +38,7 @@ use crate::sandbox::config::DebugInfo; use crate::sandbox::host_funcs::FunctionRegistry; #[cfg(target_os = "linux")] use crate::signal_handlers::setup_signal_handlers; -use crate::{MultiUseSandbox, Result, UninitializedSandbox, log_then_return, new_error}; +use crate::{MultiUseSandbox, Result, UninitializedSandbox, new_error}; /// The implementation for evolving `UninitializedSandbox`es to /// `Sandbox`es. @@ -63,7 +60,7 @@ where TransformFunc: Fn( Arc>, SandboxMemoryManager, - Box, + HyperlightVm, RawPtr, ) -> Result, { @@ -138,42 +135,23 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, #[cfg(any(crashdump, gdb))] rt_cfg: &SandboxRuntimeConfig, _load_info: LoadInfo, -) -> Result> { +) -> Result { #[cfg(feature = "init-paging")] - let rsp_ptr = { + let rsp = { let mut regions = mgr.layout.get_memory_regions(&mgr.shared_mem)?; let mem_size = u64::try_from(mgr.shared_mem.mem_size())?; - let rsp_u64 = mgr.set_up_shared_memory(mem_size, &mut regions)?; - let rsp_raw = RawPtr::from(rsp_u64); - GuestPtr::try_from(rsp_raw) - }?; + mgr.set_up_shared_memory(mem_size, &mut regions)? + }; #[cfg(not(feature = "init-paging"))] - let rsp_ptr = GuestPtr::try_from(Offset::from(0))?; + let rsp = 0; let regions = mgr.layout.get_memory_regions(&mgr.shared_mem)?; - let base_ptr = GuestPtr::try_from(Offset::from(0))?; - let pml4_ptr = { - let pml4_offset_u64 = u64::try_from(SandboxMemoryLayout::PML4_OFFSET)?; - base_ptr + Offset::from(pml4_offset_u64) - }; + let pml4 = SandboxMemoryLayout::PML4_OFFSET; + let entrypoint_ptr = { let entrypoint_total_offset = mgr.load_addr.clone() + mgr.entrypoint_offset; GuestPtr::try_from(entrypoint_total_offset) - }?; - - if base_ptr != pml4_ptr { - log_then_return!( - "Error: base_ptr ({:#?}) does not equal pml4_ptr ({:#?})", - base_ptr, - pml4_ptr - ); - } - if entrypoint_ptr <= pml4_ptr { - log_then_return!( - "Error: entrypoint_ptr ({:#?}) is not greater than pml4_ptr ({:#?})", - entrypoint_ptr, - pml4_ptr - ); - } + }? + .absolute()?; // Create gdb thread if gdb is enabled and the configuration is provided #[cfg(gdb)] @@ -202,71 +180,19 @@ pub(crate) fn set_up_hypervisor_partition( _load_info, )?; - match *get_available_hypervisor() { - #[cfg(mshv)] - Some(HypervisorType::Mshv) => { - let hv = crate::hypervisor::hyperv_linux::HypervLinuxDriver::new( - regions, - entrypoint_ptr, - rsp_ptr, - pml4_ptr, - config, - #[cfg(gdb)] - gdb_conn, - #[cfg(crashdump)] - rt_cfg.clone(), - #[cfg(feature = "trace_guest")] - trace_info, - )?; - Ok(Box::new(hv)) - } - - #[cfg(kvm)] - Some(HypervisorType::Kvm) => { - let hv = crate::hypervisor::kvm::KVMDriver::new( - regions, - pml4_ptr.absolute()?, - entrypoint_ptr.absolute()?, - rsp_ptr.absolute()?, - config, - #[cfg(gdb)] - gdb_conn, - #[cfg(crashdump)] - rt_cfg.clone(), - #[cfg(feature = "trace_guest")] - trace_info, - )?; - Ok(Box::new(hv)) - } - - #[cfg(target_os = "windows")] - Some(HypervisorType::Whp) => { - use crate::hypervisor::wrappers::HandleWrapper; - - let mmap_file_handle = mgr - .shared_mem - .with_exclusivity(|e| e.get_mmap_file_handle())?; - let hv = crate::hypervisor::hyperv_windows::HypervWindowsDriver::new( - regions, - mgr.shared_mem.raw_mem_size(), // we use raw_* here because windows driver requires 64K aligned addresses, - pml4_ptr.absolute()?, - entrypoint_ptr.absolute()?, - rsp_ptr.absolute()?, - HandleWrapper::from(mmap_file_handle), - #[cfg(gdb)] - gdb_conn, - #[cfg(crashdump)] - rt_cfg.clone(), - #[cfg(feature = "trace_guest")] - trace_info, - )?; - Ok(Box::new(hv)) - } - - _ => { - log_then_return!(NoHypervisorFound()); - } - } + HyperlightVm::new( + regions, + pml4 as u64, + entrypoint_ptr, + rsp, + config, + #[cfg(gdb)] + gdb_conn, + #[cfg(crashdump)] + rt_cfg.clone(), + #[cfg(feature = "trace_guest")] + trace_info, + ) } #[cfg(test)] diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 8b2077a19..0192b0644 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -339,7 +339,11 @@ fn interrupt_spamming_host_call() { .call::("HostCallLoop", "HostFunc1".to_string()) .unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(res, HyperlightError::ExecutionCanceledByHost()), + "Expected ExecutionCanceledByHost error but got: {:?}", + res + ); thread.join().expect("Thread should finish"); }