diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 88808fb34..62fd73efd 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -67,7 +67,7 @@ jobs: - docs-pr - build-guests strategy: - fail-fast: true + fail-fast: false matrix: hypervisor: [hyperv, 'hyperv-ws2025', mshv3, kvm] cpu: [amd, intel] diff --git a/Cargo.lock b/Cargo.lock index acb239405..abc03e7b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,6 +1478,7 @@ dependencies = [ "hyperlight-component-macro", "hyperlight-guest-tracing", "hyperlight-testing", + "iced-x86", "kvm-bindings", "kvm-ioctls", "lazy_static", @@ -1573,6 +1574,15 @@ dependencies = [ "cc", ] +[[package]] +name = "iced-x86" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b" +dependencies = [ + "lazy_static", +] + [[package]] name = "icu_collections" version = "2.1.1" diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index 2976a70b0..3e24e358a 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -84,6 +84,7 @@ mshv-ioctls = { version = "0.6", optional = true} uuid = { version = "1.19.0", features = ["v4"] } signal-hook-registry = "1.4.8" serde = "1.0" +iced-x86 = { version = "1.21", default-features = false, features = ["std", "code_asm"] } proptest = "1.9.0" tempfile = "3.24.0" crossbeam-queue = "0.3.12" diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index c836d3d80..47ff1264d 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -40,7 +40,7 @@ use crate::HyperlightError::{ExecutionCanceledByHost, NoHypervisorFound}; use crate::hypervisor::LinuxInterruptHandle; #[cfg(crashdump)] use crate::hypervisor::crashdump; -use crate::hypervisor::regs::CommonSpecialRegisters; +use crate::hypervisor::regs::{CommonDebugRegs, CommonSpecialRegisters}; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; #[cfg(kvm)] @@ -88,6 +88,9 @@ pub(crate) struct HyperlightVm { next_slot: u32, // Monotonically increasing slot number freed_slots: Vec, // Reusable slots from unmapped regions + // pml4 saved to be able to restore it if needed + #[cfg(feature = "init-paging")] + pml4_addr: u64, #[cfg(gdb)] gdb_conn: Option>, #[cfg(gdb)] @@ -189,6 +192,8 @@ impl HyperlightVm { mmap_regions: Vec::new(), freed_slots: Vec::new(), + #[cfg(feature = "init-paging")] + pml4_addr: _pml4_addr, #[cfg(gdb)] gdb_conn, #[cfg(gdb)] @@ -583,6 +588,31 @@ impl HyperlightVm { Ok(()) } + // Resets the following vCPU state: + // - General purpose registers + // - Debug registers + // - XSAVE (includes FPU/SSE state with proper FCW and MXCSR defaults) + // - Special registers (with saved PML4 if feature enabled) + // TODO: check if other state needs to be reset + pub(crate) fn reset_vcpu(&self) -> Result<()> { + self.vm.set_regs(&CommonRegisters { + rflags: 1 << 1, // Reserved bit always set + ..Default::default() + })?; + self.vm.set_debug_regs(&CommonDebugRegs::default())?; + self.vm.reset_xsave()?; + #[cfg(feature = "init-paging")] + self.vm + .set_sregs(&CommonSpecialRegisters::standard_64bit_defaults( + self.pml4_addr, + ))?; + #[cfg(not(feature = "init-paging"))] + self.vm + .set_sregs(&CommonSpecialRegisters::standard_real_mode_defaults())?; + + Ok(()) + } + // Handle a debug exit #[cfg(gdb)] fn handle_debug( @@ -1074,3 +1104,1030 @@ mod debug { } } } + +#[cfg(test)] +#[cfg(feature = "init-paging")] +#[allow(clippy::needless_range_loop)] +mod tests { + use std::sync::{Arc, Mutex}; + + use hyperlight_common::vmem::{self, BasicMapping, Mapping, MappingKind}; + use rand::Rng; + + use super::*; + #[cfg(kvm)] + use crate::hypervisor::regs::FP_CONTROL_WORD_DEFAULT; + use crate::hypervisor::regs::{CommonSegmentRegister, CommonTableRegister, MXCSR_DEFAULT}; + use crate::hypervisor::virtual_machine::VirtualMachine; + use crate::mem::layout::SandboxMemoryLayout; + use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegionFlags}; + use crate::mem::mgr::{GuestPageTableBuffer, SandboxMemoryManager}; + use crate::mem::ptr::RawPtr; + use crate::mem::ptr_offset::Offset; + use crate::mem::shared_mem::ExclusiveSharedMemory; + use crate::sandbox::SandboxConfiguration; + use crate::sandbox::host_funcs::FunctionRegistry; + #[cfg(any(crashdump, gdb))] + use crate::sandbox::uninitialized::SandboxRuntimeConfig; + use crate::sandbox::uninitialized_evolve::set_up_hypervisor_partition; + + /// Build dirty general purpose registers for testing reset_vcpu. + fn dirty_regs() -> CommonRegisters { + CommonRegisters { + rax: 0x1111111111111111, + rbx: 0x2222222222222222, + rcx: 0x3333333333333333, + rdx: 0x4444444444444444, + rsi: 0x5555555555555555, + rdi: 0x6666666666666666, + rsp: 0x7777777777777777, + rbp: 0x8888888888888888, + r8: 0x9999999999999999, + r9: 0xAAAAAAAAAAAAAAAA, + r10: 0xBBBBBBBBBBBBBBBB, + r11: 0xCCCCCCCCCCCCCCCC, + r12: 0xDDDDDDDDDDDDDDDD, + r13: 0xEEEEEEEEEEEEEEEE, + r14: 0xFFFFFFFFFFFFFFFF, + r15: 0x0123456789ABCDEF, + rip: 0xFEDCBA9876543210, + rflags: 0x202, // IF + reserved bit 1 + } + } + + /// Build dirty FPU state for testing reset_vcpu. + fn dirty_fpu() -> CommonFpu { + CommonFpu { + fpr: [[0xAB; 16]; 8], + fcw: 0x0F7F, // Different from default 0x037F + fsw: 0x1234, + ftwx: 0xAB, + last_opcode: 0x0123, + last_ip: 0xDEADBEEF00000000, + last_dp: 0xCAFEBABE00000000, + xmm: [[0xCD; 16]; 16], + mxcsr: 0x3F80, // Different from default 0x1F80 + } + } + + /// Build dirty special registers for testing reset_vcpu. + /// Must be consistent for 64-bit long mode (CR0/CR4/EFER). + fn dirty_sregs(_pml4_addr: u64) -> CommonSpecialRegisters { + let segment = CommonSegmentRegister { + base: 0x1000, + limit: 0xFFFF, + selector: 0x10, + type_: 3, // data segment, read/write, accessed + present: 1, + dpl: 0, + db: 1, + s: 1, + l: 0, + g: 1, + avl: 1, + unusable: 0, + padding: 0, + }; + // CS segment - 64-bit code segment + let cs_segment = CommonSegmentRegister { + base: 0, + limit: 0xFFFF, + selector: 0x08, + type_: 0b1011, // code segment, execute/read, accessed + present: 1, + dpl: 0, + db: 0, // must be 0 in 64-bit mode + s: 1, + l: 1, // 64-bit mode + g: 1, + avl: 0, + unusable: 0, + padding: 0, + }; + let table = CommonTableRegister { + base: 0xDEAD0000, + limit: 0xFFFF, + }; + CommonSpecialRegisters { + cs: cs_segment, + ds: segment, + es: segment, + fs: segment, + gs: segment, + ss: segment, + tr: CommonSegmentRegister { + type_: 0b1011, // busy TSS + present: 1, + ..segment + }, + ldt: segment, + gdt: table, + idt: table, + cr0: 0x80000011, // PE + ET + PG + cr2: 0xBADC0DE, + // MSHV validates cr3 and rejects bogus values; use valid _pml4_addr for MSHV + cr3: match get_available_hypervisor() { + #[cfg(mshv3)] + Some(HypervisorType::Mshv) => _pml4_addr, + _ => 0x12345000, + }, + cr4: 0x20, // PAE + cr8: 0x5, + efer: 0x500, // LME + LMA + apic_base: 0xFEE00900, + interrupt_bitmap: [0; 4], // fails if non-zero on MSHV + } + } + + /// Build dirty debug registers for testing reset_vcpu. + /// + /// DR6 bit layout (Intel SDM / AMD APM): + /// Bits 0-3 (B0-B3): Breakpoint condition detected - software writable/clearable + /// Bits 4-10: Reserved, read as 1s on modern processors (read-only) + /// Bit 11 (BLD): Bus Lock Trap - cleared by processor, read-only on older CPUs + /// Bit 12: Reserved, always 0 + /// Bit 13 (BD): Debug Register Access Detected - software clearable + /// Bit 14 (BS): Single-Step - software clearable + /// Bit 15 (BT): Task Switch breakpoint - software clearable + /// Bit 16 (RTM): TSX-related, read-only (1 if no TSX) + /// Bits 17-31: Reserved, read as 1s on modern processors (read-only) + /// Bits 32-63: Reserved, must be 0 + /// + /// Writable bits: 0-3, 13, 14, 15 = mask 0xE00F + /// Reserved 1s: 4-10, 11 (if no BLD), 16 (if no TSX), 17-31 = ~0xE00F on lower 32 bits + const DR6_WRITABLE_MASK: u64 = 0xE00F; // B0-B3, BD, BS, BT + + /// DR7 bit layout: + /// Bits 0-7 (L0-L3, G0-G3): Local/global breakpoint enables - writable + /// Bits 8-9 (LE, GE): Local/Global Exact (386 only, ignored on modern) - writable + /// Bit 10: Reserved, must be 1 (read-only) + /// Bits 11-12: Reserved (RTM/TSX on some CPUs), must be 0 (read-only) + /// Bit 13 (GD): General Detect Enable - writable + /// Bits 14-15: Reserved, must be 0 (read-only) + /// Bits 16-31 (R/W0-3, LEN0-3): Breakpoint conditions and lengths - writable + /// Bits 32-63: Reserved, must be 0 (read-only) + /// + /// Writable bits: 0-9, 13, 16-31 = mask 0xFFFF23FF + const DR7_WRITABLE_MASK: u64 = 0xFFFF_23FF; + + fn dirty_debug_regs() -> CommonDebugRegs { + CommonDebugRegs { + dr0: 0xDEADBEEF00001000, + dr1: 0xDEADBEEF00002000, + dr2: 0xDEADBEEF00003000, + dr3: 0xDEADBEEF00004000, + // Set all writable bits: B0-B3 (0-3), BD (13), BS (14), BT (15) + dr6: DR6_WRITABLE_MASK, + // Set writable bits: L0-L3, G0-G3 (0-7), LE/GE (8-9), GD (13), conditions (16-31) + dr7: DR7_WRITABLE_MASK, + } + } + + /// Normalize debug registers for comparison by applying writable masks. + /// Reserved bits in DR6/DR7 are read-only (set by CPU), so we copy them from actual. + fn normalize_debug_regs(expected: &mut CommonDebugRegs, actual: &CommonDebugRegs) { + expected.dr6 = (expected.dr6 & DR6_WRITABLE_MASK) | (actual.dr6 & !DR6_WRITABLE_MASK); + expected.dr7 = (expected.dr7 & DR7_WRITABLE_MASK) | (actual.dr7 & !DR7_WRITABLE_MASK); + } + + /// Normalize special registers for comparison when verifying dirty state. + /// Only normalizes fields that hypervisors may report differently even when + /// we set them explicitly: ss.db, and unusable/g for all segments. + /// Does NOT normalize type_ since we explicitly set it in dirty_sregs. + fn normalize_sregs_for_dirty( + expected: &mut CommonSpecialRegisters, + actual: &CommonSpecialRegisters, + ) { + expected.ss.db = actual.ss.db; + expected.cs.unusable = actual.cs.unusable; + expected.cs.g = actual.cs.g; + expected.ds.unusable = actual.ds.unusable; + expected.ds.g = actual.ds.g; + expected.es.unusable = actual.es.unusable; + expected.es.g = actual.es.g; + expected.fs.unusable = actual.fs.unusable; + expected.fs.g = actual.fs.g; + expected.gs.unusable = actual.gs.unusable; + expected.gs.g = actual.gs.g; + expected.ss.unusable = actual.ss.unusable; + expected.ss.g = actual.ss.g; + expected.tr.unusable = actual.tr.unusable; + expected.tr.g = actual.tr.g; + expected.ldt.unusable = actual.ldt.unusable; + expected.ldt.g = actual.ldt.g; + } + + /// Normalize special registers for comparison when verifying reset state. + /// Normalizes all hypervisor implementation-specific fields including type_, + /// since hypervisors have different default values for segment types. + fn normalize_sregs_for_reset( + expected: &mut CommonSpecialRegisters, + actual: &CommonSpecialRegisters, + ) { + normalize_sregs_for_dirty(expected, actual); + // Also normalize type_ for reset verification since hypervisors + // have different defaults (KVM returns type_=1, WHP returns type_=0) + expected.ds.type_ = actual.ds.type_; + expected.es.type_ = actual.es.type_; + expected.fs.type_ = actual.fs.type_; + expected.gs.type_ = actual.gs.type_; + expected.ss.type_ = actual.ss.type_; + } + + /// Normalize special registers for run_tests (no granularity normalization). + /// Used by tests that run actual code rather than just set vCPU state. + fn normalize_sregs_no_granularity( + expected: &mut CommonSpecialRegisters, + actual: &CommonSpecialRegisters, + ) { + expected.ss.db = actual.ss.db; + expected.cs.unusable = actual.cs.unusable; + expected.ds.unusable = actual.ds.unusable; + expected.ds.type_ = actual.ds.type_; + expected.es.unusable = actual.es.unusable; + expected.es.type_ = actual.es.type_; + expected.fs.unusable = actual.fs.unusable; + expected.fs.type_ = actual.fs.type_; + expected.gs.unusable = actual.gs.unusable; + expected.gs.type_ = actual.gs.type_; + expected.ss.unusable = actual.ss.unusable; + expected.ss.type_ = actual.ss.type_; + expected.tr.unusable = actual.tr.unusable; + expected.ldt.unusable = actual.ldt.unusable; + } + + /// Normalize FPU MXCSR for KVM quirk. + /// KVM doesn't preserve MXCSR via set_fpu/fpu(), so we need to set it manually + /// when comparing FPU state. + #[cfg_attr(not(kvm), allow(unused_variables))] + fn normalize_fpu_mxcsr_for_kvm(fpu: &mut CommonFpu, expected_mxcsr: u32) { + #[cfg(kvm)] + if *get_available_hypervisor().as_ref().unwrap() == HypervisorType::Kvm { + fpu.mxcsr = expected_mxcsr; + } + } + + /// Normalize FPU state for reset comparison. + /// When x87 FPU registers are in "empty" state (ftwx = 0), the actual register + /// contents are architecturally undefined. Some hypervisors (MSHV, WHP) correctly + /// mark them as empty but don't zero the contents. We normalize by copying fpr + /// from actual to expected since the values are irrelevant when empty. + fn normalize_fpu_for_reset(expected: &mut CommonFpu, actual: &CommonFpu) { + if actual.ftwx == 0 { + expected.fpr = actual.fpr; + } + } + + /// Assert that debug registers are in reset state. + /// Reserved bits in DR6/DR7 are read-only (set by CPU), so we only check + /// that writable bits are cleared to 0 and DR0-DR3 are zeroed. + fn assert_debug_regs_reset(vm: &dyn VirtualMachine) { + let debug_regs = vm.debug_regs().unwrap(); + let expected = CommonDebugRegs { + dr0: 0, + dr1: 0, + dr2: 0, + dr3: 0, + dr6: debug_regs.dr6 & !DR6_WRITABLE_MASK, + dr7: debug_regs.dr7 & !DR7_WRITABLE_MASK, + }; + assert_eq!(debug_regs, expected); + } + + /// Assert that general-purpose registers are in reset state. + /// After reset, all registers should be zeroed except rflags which has + /// reserved bit 1 always set. + fn assert_regs_reset(vm: &dyn VirtualMachine) { + assert_eq!( + vm.regs().unwrap(), + CommonRegisters { + rflags: 1 << 1, // Reserved bit 1 is always set + ..Default::default() + } + ); + } + + /// Assert that FPU state is in reset state. + /// Handles hypervisor-specific quirks (KVM MXCSR, empty FPU registers). + fn assert_fpu_reset(vm: &dyn VirtualMachine) { + let fpu = vm.fpu().unwrap(); + let mut expected_fpu = CommonFpu::default(); + normalize_fpu_mxcsr_for_kvm(&mut expected_fpu, fpu.mxcsr); + normalize_fpu_for_reset(&mut expected_fpu, &fpu); + assert_eq!(fpu, expected_fpu); + } + + /// Assert that special registers are in reset state. + /// Handles hypervisor-specific differences in hidden descriptor cache fields. + fn assert_sregs_reset(vm: &dyn VirtualMachine, pml4_addr: u64) { + let defaults = CommonSpecialRegisters::standard_64bit_defaults(pml4_addr); + let sregs = vm.sregs().unwrap(); + let mut expected_sregs = defaults; + // Normalize hypervisor implementation-specific fields. + // These are part of the hidden descriptor cache. While guests can write them + // indirectly (by loading segments from a crafted GDT), guests cannot read them back + // (e.g., `mov ax, ds` only returns the selector, not the hidden cache). + // KVM and MSHV reset to different default values, but both properly reset so there's + // no information leakage between tenants. + normalize_sregs_for_reset(&mut expected_sregs, &sregs); + assert_eq!(sregs, expected_sregs); + } + + /// Query CPUID.0DH.n for XSAVE component info. + /// Returns (size, offset, align_64) for the given component: + /// - size: CPUID.0DH.n:EAX - size in bytes + /// - offset: CPUID.0DH.n:EBX - offset from XSAVE base (standard format only) + /// - align_64: CPUID.0DH.n:ECX bit 1 - true if 64-byte aligned (compacted format) + fn xsave_component_info(comp_id: u32) -> (usize, usize, bool) { + let result = unsafe { std::arch::x86_64::__cpuid_count(0xD, comp_id) }; + let size = result.eax as usize; + let offset = result.ebx as usize; + let align_64 = (result.ecx & 0b10) != 0; + (size, offset, align_64) + } + + /// Query CPUID.0DH.00H for the bitmap of supported user state components. + /// EDX:EAX forms a 64-bit bitmap where bit i indicates support for component i. + fn xsave_supported_components() -> u64 { + let result = unsafe { std::arch::x86_64::__cpuid_count(0xD, 0) }; + (result.edx as u64) << 32 | (result.eax as u64) + } + + /// Dirty extended state components using compacted XSAVE format (MSHV/WHP). + /// Components are stored contiguously starting at byte 576, with alignment + /// requirements from CPUID.0DH.n:ECX[1]. + /// Returns a bitmask of components that were actually dirtied. + fn dirty_xsave_extended_compacted( + xsave: &mut [u32], + xcomp_bv: u64, + supported_components: u64, + ) -> u64 { + let mut dirtied_mask = 0u64; + let mut offset = 576usize; + + for comp_id in 2..63u32 { + // Skip if component not supported by CPU or not enabled in XCOMP_BV + if (supported_components & (1u64 << comp_id)) == 0 { + continue; + } + if (xcomp_bv & (1u64 << comp_id)) == 0 { + continue; + } + + let (size, _, align_64) = xsave_component_info(comp_id); + + // ECX[1]=1 means 64-byte aligned; ECX[1]=0 means immediately after previous + if align_64 { + offset = offset.next_multiple_of(64); + } + + // Dirty this component's data area (only if it fits in the buffer) + let start_idx = offset / 4; + let end_idx = (offset + size) / 4; + if end_idx <= xsave.len() { + for i in start_idx..end_idx { + xsave[i] = 0x12345678 ^ comp_id.wrapping_mul(0x11111111); + } + dirtied_mask |= 1u64 << comp_id; + } + + offset += size; + } + + dirtied_mask + } + + /// Dirty extended state components using standard XSAVE format (KVM). + /// Components are at fixed offsets from CPUID.0DH.n:EBX. + /// Returns a bitmask of components that were actually dirtied. + fn dirty_xsave_extended_standard(xsave: &mut [u32], supported_components: u64) -> u64 { + let mut dirtied_mask = 0u64; + + for comp_id in 2..63u32 { + // Skip if component not supported by CPU + if (supported_components & (1u64 << comp_id)) == 0 { + continue; + } + + let (size, fixed_offset, _) = xsave_component_info(comp_id); + + let start_idx = fixed_offset / 4; + let end_idx = (fixed_offset + size) / 4; + if end_idx <= xsave.len() { + for i in start_idx..end_idx { + xsave[i] = 0x12345678 ^ comp_id.wrapping_mul(0x11111111); + } + dirtied_mask |= 1u64 << comp_id; + } + } + + dirtied_mask + } + + /// Dirty the legacy XSAVE region (bytes 0-511) for testing reset_vcpu. + /// This includes FPU/x87 state, SSE state, and reserved areas. + /// + /// Layout (from Intel SDM Table 13-1): + /// Bytes 0-1: FCW, 2-3: FSW, 4: FTW, 5: reserved, 6-7: FOP + /// Bytes 8-15: FIP, 16-23: FDP + /// Bytes 24-27: MXCSR, 28-31: MXCSR_MASK (preserve - hardware defined) + /// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes) + /// Bytes 160-415: XMM0-XMM15 (16 regs × 16 bytes) + /// Bytes 416-511: Reserved + fn dirty_xsave_legacy(xsave: &mut [u32], current_xsave: &[u8]) { + // FCW (bytes 0-1) + FSW (bytes 2-3) - pack into xsave[0] + // FCW = 0x0F7F (different from default 0x037F), FSW = 0x1234 + xsave[0] = 0x0F7F | (0x1234 << 16); + // FTW (byte 4) + reserved (byte 5) + FOP (bytes 6-7) - pack into xsave[1] + // FTW = 0xAB, FOP = 0x0123 + xsave[1] = 0xAB | (0x0123 << 16); + // FIP (bytes 8-15) - xsave[2] and xsave[3] + xsave[2] = 0xDEAD0001; + xsave[3] = 0xBEEF0002; + // FDP (bytes 16-23) - xsave[4] and xsave[5] + xsave[4] = 0xCAFE0003; + xsave[5] = 0xBABE0004; + // MXCSR (bytes 24-27) - xsave[6], use valid value different from default + xsave[6] = 0x3F80; + // xsave[7] is MXCSR_MASK - preserve from current (hardware defined, read-only) + if current_xsave.len() >= 32 { + xsave[7] = u32::from_le_bytes(current_xsave[28..32].try_into().unwrap()); + } + + // ST0-ST7/MM0-MM7 (bytes 32-159, indices 8-39) + for i in 8..40 { + xsave[i] = 0xCAFEBABE; + } + // XMM0-XMM15 (bytes 160-415, indices 40-103) + for i in 40..104 { + xsave[i] = 0xDEADBEEF; + } + + // Reserved area (bytes 416-511, indices 104-127) + for i in 104..128 { + xsave[i] = 0xABCDEF12; + } + } + + /// Preserve XSAVE header (bytes 512-575) from current state. + /// This includes XSTATE_BV and XCOMP_BV which hypervisors require. + fn preserve_xsave_header(xsave: &mut [u32], current_xsave: &[u8]) { + for i in 128..144 { + let byte_offset = i * 4; + xsave[i] = u32::from_le_bytes( + current_xsave[byte_offset..byte_offset + 4] + .try_into() + .unwrap(), + ); + } + } + + fn dirty_xsave(current_xsave: &[u8]) -> Vec { + let mut xsave = vec![0u32; current_xsave.len() / 4]; + + dirty_xsave_legacy(&mut xsave, current_xsave); + preserve_xsave_header(&mut xsave, current_xsave); + + let xcomp_bv = u64::from_le_bytes(current_xsave[520..528].try_into().unwrap()); + let supported_components = xsave_supported_components(); + + // Dirty extended components and get mask of what was actually dirtied + let extended_mask = if (xcomp_bv & (1u64 << 63)) != 0 { + // Compacted format (MSHV/WHP) + dirty_xsave_extended_compacted(&mut xsave, xcomp_bv, supported_components) + } else { + // Standard format (KVM) + dirty_xsave_extended_standard(&mut xsave, supported_components) + }; + + // UPDATE XSTATE_BV to indicate dirtied components have valid data. + // WHP validates consistency between XSTATE_BV and actual data in the buffer. + // Bits 0,1 = legacy x87/SSE (always set after dirty_xsave_legacy) + // Bits 2+ = extended components that we actually dirtied + let xstate_bv = 0x3 | extended_mask; + + // Write XSTATE_BV to bytes 512-519 (u32 indices 128-129) + xsave[128] = (xstate_bv & 0xFFFFFFFF) as u32; + xsave[129] = (xstate_bv >> 32) as u32; + + xsave + } + + fn hyperlight_vm(code: &[u8]) -> HyperlightVm { + let config: SandboxConfiguration = Default::default(); + #[cfg(any(crashdump, gdb))] + let rt_cfg: SandboxRuntimeConfig = Default::default(); + + let mut layout = SandboxMemoryLayout::new(config, code.len(), 4096, 4096, 0, None).unwrap(); + + let pt_base_gpa = SandboxMemoryLayout::BASE_ADDRESS + layout.get_pt_offset(); + let pt_buf = GuestPageTableBuffer::new(pt_base_gpa); + + for rgn in layout + .get_memory_regions_::(()) + .unwrap() + .iter() + { + let readable = rgn.flags.contains(MemoryRegionFlags::READ); + let writable = rgn.flags.contains(MemoryRegionFlags::WRITE) + || rgn.flags.contains(MemoryRegionFlags::STACK_GUARD); + let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE); + let mapping = Mapping { + phys_base: rgn.guest_region.start as u64, + virt_base: rgn.guest_region.start as u64, + len: rgn.guest_region.len() as u64, + kind: MappingKind::BasicMapping(BasicMapping { + readable, + writable, + executable, + }), + }; + unsafe { vmem::map(&pt_buf, mapping) }; + } + + let mut pt_size_mapped = 0; + while pt_buf.size() > pt_size_mapped { + let mapping = Mapping { + phys_base: (pt_base_gpa + pt_size_mapped) as u64, + virt_base: (hyperlight_common::layout::SNAPSHOT_PT_GVA + pt_size_mapped) as u64, + len: (pt_buf.size() - pt_size_mapped) as u64, + kind: MappingKind::BasicMapping(BasicMapping { + readable: true, + writable: true, + executable: false, + }), + }; + unsafe { vmem::map(&pt_buf, mapping) }; + pt_size_mapped = pt_buf.size(); + } + + let pt_bytes = pt_buf.into_bytes(); + layout.set_pt_size(pt_bytes.len()); + + let mem_size = layout.get_memory_size().unwrap(); + let mut eshm = ExclusiveSharedMemory::new(mem_size).unwrap(); + eshm.copy_from_slice(&pt_bytes, layout.get_pt_offset()) + .unwrap(); + eshm.copy_from_slice(code, layout.get_guest_code_offset()) + .unwrap(); + + let load_addr = RawPtr::from(layout.get_guest_code_address() as u64); + let mut mem_mgr = + SandboxMemoryManager::new(layout, eshm, load_addr, Some(Offset::from(0)), [0u8; 16]); + mem_mgr.write_memory_layout().unwrap(); + + let (mut hshm, mut gshm) = mem_mgr.build(); + + let mut vm = set_up_hypervisor_partition( + &mut gshm, + &config, + #[cfg(any(crashdump, gdb))] + &rt_cfg, + crate::mem::exe::LoadInfo::dummy(), + ) + .unwrap(); + + let seed = rand::rng().random::(); + let peb_addr = RawPtr::from(u64::try_from(gshm.layout.peb_address).unwrap()); + let page_size = u32::try_from(page_size::get()).unwrap(); + + #[cfg(gdb)] + let dbg_mem_access_hdl = Arc::new(Mutex::new(hshm.clone())); + + let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default())); + + vm.initialise( + peb_addr, + seed, + page_size, + &mut hshm, + &host_funcs, + None, + #[cfg(gdb)] + dbg_mem_access_hdl.clone(), + ) + .unwrap(); + vm + } + + #[test] + fn reset_vcpu_simple() { + const CODE: [u8; 1] = [0xf4]; // hlt + let hyperlight_vm = hyperlight_vm(&CODE); + let available_hv = *get_available_hypervisor().as_ref().unwrap(); + + // Set all vCPU state to dirty values + let regs = dirty_regs(); + let fpu = dirty_fpu(); + let sregs = dirty_sregs(hyperlight_vm.pml4_addr); + let current_xsave = hyperlight_vm.vm.xsave().unwrap(); + let xsave = dirty_xsave(¤t_xsave); + let debug_regs = dirty_debug_regs(); + + hyperlight_vm.vm.set_xsave(&xsave).unwrap(); + hyperlight_vm.vm.set_regs(®s).unwrap(); + hyperlight_vm.vm.set_fpu(&fpu).unwrap(); + hyperlight_vm.vm.set_sregs(&sregs).unwrap(); + hyperlight_vm.vm.set_debug_regs(&debug_regs).unwrap(); + + // Verify regs were set + assert_eq!(hyperlight_vm.vm.regs().unwrap(), regs); + + // Verify fpu was set + let mut got_fpu = hyperlight_vm.vm.fpu().unwrap(); + let mut expected_fpu = fpu; + // KVM doesn't preserve mxcsr via set_fpu/fpu(), copy expected to got + normalize_fpu_mxcsr_for_kvm(&mut got_fpu, fpu.mxcsr); + // fpr only uses 80 bits per register. Normalize upper bits for comparison. + for i in 0..8 { + expected_fpu.fpr[i][10..16].copy_from_slice(&got_fpu.fpr[i][10..16]); + } + assert_eq!(got_fpu, expected_fpu); + + // Verify xsave was set by checking key dirty values in the legacy region. + // Note: set_fpu() is called after set_xsave(), so XMM registers reflect fpu state (0xCD pattern). + let got_xsave = hyperlight_vm.vm.xsave().unwrap(); + // FCW (bytes 0-1) should be 0x0F7F (set by both xsave and fpu) + let got_fcw = u16::from_le_bytes(got_xsave[0..2].try_into().unwrap()); + assert_eq!(got_fcw, 0x0F7F, "xsave FCW should be dirty"); + // MXCSR (bytes 24-27) should be 0x3F80 (set by xsave; fpu doesn't update it on KVM) + let got_mxcsr = u32::from_le_bytes(got_xsave[24..28].try_into().unwrap()); + assert_eq!(got_mxcsr, 0x3F80, "xsave MXCSR should be dirty"); + // XMM0-XMM15 (bytes 160-415): set_fpu overwrites with 0xCD pattern from dirty_fpu() + for i in 0..16 { + let offset = 160 + i * 16; + let xmm_word = u32::from_le_bytes(got_xsave[offset..offset + 4].try_into().unwrap()); + assert_eq!( + xmm_word, 0xCDCDCDCD, + "xsave XMM{i} should match fpu dirty value" + ); + } + + // Verify debug regs were set + let got_debug_regs = hyperlight_vm.vm.debug_regs().unwrap(); + let mut expected_debug_regs = debug_regs; + normalize_debug_regs(&mut expected_debug_regs, &got_debug_regs); + assert_eq!(got_debug_regs, expected_debug_regs); + + // Verify sregs were set + let got_sregs = hyperlight_vm.vm.sregs().unwrap(); + let mut expected_sregs = sregs; + normalize_sregs_for_dirty(&mut expected_sregs, &got_sregs); + assert_eq!(got_sregs, expected_sregs); + + // Reset the vCPU + hyperlight_vm.reset_vcpu().unwrap(); + + // Verify registers are reset to defaults + assert_regs_reset(hyperlight_vm.vm.as_ref()); + + // Verify FPU is reset to defaults + assert_fpu_reset(hyperlight_vm.vm.as_ref()); + + // Verify debug registers are reset to defaults + assert_debug_regs_reset(hyperlight_vm.vm.as_ref()); + + // Verify xsave is reset - should be zeroed except for hypervisor-specific fields + let reset_xsave = hyperlight_vm.vm.xsave().unwrap(); + // Build expected xsave: all zeros with fpu specific defaults. Then copy hypervisor-specific fields from actual + let mut expected_xsave = vec![0u8; reset_xsave.len()]; + #[cfg(mshv3)] + if available_hv == HypervisorType::Mshv { + // FCW (offset 0-1): When XSTATE_BV.LegacyX87 = 0 (init state), the hypervisor + // skips copying the FPU legacy region entirely, leaving zeros in the buffer. + // The actual guest FCW register is 0x037F (verified via fpu() assertion above), + // but xsave() doesn't report it because XSTATE_BV=0 means "init state, buffer + // contents undefined." We copy from actual to handle this. + expected_xsave[0..2].copy_from_slice(&reset_xsave[0..2]); + } + #[cfg(target_os = "windows")] + if available_hv == HypervisorType::Whp { + // FCW (offset 0-1): When XSTATE_BV.LegacyX87 = 0 (init state), the hypervisor + // skips copying the FPU legacy region entirely, leaving zeros in the buffer. + // The actual guest FCW register is 0x037F (verified via fpu() assertion above), + // but xsave() doesn't report it because XSTATE_BV=0 means "init state, buffer + // contents undefined." We copy from actual to handle this. + expected_xsave[0..2].copy_from_slice(&reset_xsave[0..2]); + } + #[cfg(kvm)] + if available_hv == HypervisorType::Kvm { + expected_xsave[0..2].copy_from_slice(&FP_CONTROL_WORD_DEFAULT.to_le_bytes()); + } + + // - MXCSR at offset 24-27: default FPU state set by hypervisor + expected_xsave[24..28].copy_from_slice(&MXCSR_DEFAULT.to_le_bytes()); + // - MXCSR_MASK at offset 28-31: hardware-defined, read-only + expected_xsave[28..32].copy_from_slice(&reset_xsave[28..32]); + // - Reserved bytes at offset 464-511: These are in the reserved/padding area of the legacy + // FXSAVE region (after XMM registers which end at byte 416). On KVM/Intel, these bytes + // may contain hypervisor-specific metadata that isn't cleared during vCPU reset. + // Since this is not guest-visible computational state, we copy from actual to expected. + expected_xsave[464..512].copy_from_slice(&reset_xsave[464..512]); + // - XSAVE header at offset 512-575: contains XSTATE_BV and XCOMP_BV (hypervisor-managed) + // XSTATE_BV (512-519): Bitmap indicating which state components have valid data in the + // buffer. When a bit is 0, the hypervisor uses the architectural init value for that + // component. After reset, xsave() may still return non-zero XSTATE_BV since the + // hypervisor reports which components it manages, not which have been modified. + // XCOMP_BV (520-527): Compaction bitmap. Bit 63 indicates compacted format (used by MSHV/WHP). + // When set, the XSAVE area uses a compact layout where only enabled components are stored + // contiguously. This is a format indicator, not state data, so it's preserved across reset. + // Both fields are managed by the hypervisor to describe the XSAVE area format and capabilities, + // not guest-visible computational state, so they don't need to be zeroed on reset. + if reset_xsave.len() >= 576 { + expected_xsave[512..576].copy_from_slice(&reset_xsave[512..576]); + } + assert_eq!( + reset_xsave, expected_xsave, + "xsave should be zeroed except for hypervisor-specific fields" + ); + + // Verify sregs are reset to defaults + assert_sregs_reset(hyperlight_vm.vm.as_ref(), hyperlight_vm.pml4_addr); + } + + /// Tests that actually runs code, as opposed to just setting vCPU state. + mod run_tests { + use iced_x86::code_asm::*; + + use super::*; + + #[test] + fn reset_vcpu_regs() { + let mut a = CodeAssembler::new(64).unwrap(); + a.mov(rax, 0x1111111111111111u64).unwrap(); + a.mov(rbx, 0x2222222222222222u64).unwrap(); + a.mov(rcx, 0x3333333333333333u64).unwrap(); + a.mov(rdx, 0x4444444444444444u64).unwrap(); + a.mov(rsi, 0x5555555555555555u64).unwrap(); + a.mov(rdi, 0x6666666666666666u64).unwrap(); + a.mov(rbp, 0x7777777777777777u64).unwrap(); + a.mov(r8, 0x8888888888888888u64).unwrap(); + a.mov(r9, 0x9999999999999999u64).unwrap(); + a.mov(r10, 0xAAAAAAAAAAAAAAAAu64).unwrap(); + a.mov(r11, 0xBBBBBBBBBBBBBBBBu64).unwrap(); + a.mov(r12, 0xCCCCCCCCCCCCCCCCu64).unwrap(); + a.mov(r13, 0xDDDDDDDDDDDDDDDDu64).unwrap(); + a.mov(r14, 0xEEEEEEEEEEEEEEEEu64).unwrap(); + a.mov(r15, 0xFFFFFFFFFFFFFFFFu64).unwrap(); + a.hlt().unwrap(); + let code = a.assemble(0).unwrap(); + + let hyperlight_vm = hyperlight_vm(&code); + + // After run, check registers match expected dirty state + let regs = hyperlight_vm.vm.regs().unwrap(); + let mut expected_dirty = CommonRegisters { + rax: 0x1111111111111111, + rbx: 0x2222222222222222, + rcx: 0x3333333333333333, + rdx: 0x4444444444444444, + rsi: 0x5555555555555555, + rdi: 0x6666666666666666, + rsp: 0, + rbp: 0x7777777777777777, + r8: 0x8888888888888888, + r9: 0x9999999999999999, + r10: 0xAAAAAAAAAAAAAAAA, + r11: 0xBBBBBBBBBBBBBBBB, + r12: 0xCCCCCCCCCCCCCCCC, + r13: 0xDDDDDDDDDDDDDDDD, + r14: 0xEEEEEEEEEEEEEEEE, + r15: 0xFFFFFFFFFFFFFFFF, + rip: 0, + rflags: 0, + }; + // rip, rsp, and rflags are set by the CPU, we don't expect those to match our expected values + expected_dirty.rip = regs.rip; + expected_dirty.rsp = regs.rsp; + expected_dirty.rflags = regs.rflags; + assert_eq!(regs, expected_dirty); + + // Reset vcpu + hyperlight_vm.reset_vcpu().unwrap(); + + // Check registers are reset to defaults + assert_regs_reset(hyperlight_vm.vm.as_ref()); + } + + #[test] + fn reset_vcpu_fpu() { + #[cfg(kvm)] + use crate::hypervisor::regs::MXCSR_DEFAULT; + + #[cfg(kvm)] + let available_hv = *get_available_hypervisor().as_ref().unwrap(); + + // Build code to dirty XMM registers, x87 FPU, and MXCSR + let mut a = CodeAssembler::new(64).unwrap(); + + // xmm0-xmm7: use movd + pshufd to fill with pattern + let xmm_regs_low = [xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7]; + let patterns_low: [u32; 8] = [ + 0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, + 0x88888888, + ]; + for (xmm, pat) in xmm_regs_low.iter().zip(patterns_low.iter()) { + a.mov(eax, *pat).unwrap(); + a.movd(*xmm, eax).unwrap(); + a.pshufd(*xmm, *xmm, 0).unwrap(); + } + + // xmm8-xmm15: upper XMM registers + let xmm_regs_high = [xmm8, xmm9, xmm10, xmm11, xmm12, xmm13, xmm14, xmm15]; + let patterns_high: [u32; 8] = [ + 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD, 0xEEEEEEEE, 0xFFFFFFFF, + 0x12345678, + ]; + for (xmm, pat) in xmm_regs_high.iter().zip(patterns_high.iter()) { + a.mov(eax, *pat).unwrap(); + a.movd(*xmm, eax).unwrap(); + a.pshufd(*xmm, *xmm, 0).unwrap(); + } + + // Use 7 FLDs so TOP=1 after execution, different from default TOP=0. + // This ensures reset properly clears TOP, not just register contents. + a.fldz().unwrap(); // 0.0 + a.fldl2e().unwrap(); // log2(e) + a.fldl2t().unwrap(); // log2(10) + a.fldlg2().unwrap(); // log10(2) + a.fldln2().unwrap(); // ln(2) + a.fldpi().unwrap(); // pi + // Push a memory value to also dirty last_dp + a.mov(rax, 0xDEADBEEFu64).unwrap(); + a.push(rax).unwrap(); + a.fld(qword_ptr(rsp)).unwrap(); // dirties last_dp + a.pop(rax).unwrap(); + + // Dirty FCW (0x0F7F, different from default 0x037F) + a.mov(eax, 0x0F7Fu32).unwrap(); + a.push(rax).unwrap(); + a.fldcw(word_ptr(rsp)).unwrap(); + a.pop(rax).unwrap(); + + // Dirty MXCSR (0x3F80, different from default 0x1F80) + a.mov(eax, 0x3F80u32).unwrap(); + a.push(rax).unwrap(); + a.ldmxcsr(dword_ptr(rsp)).unwrap(); + a.pop(rax).unwrap(); + + a.hlt().unwrap(); + let code = a.assemble(0).unwrap(); + + let hyperlight_vm = hyperlight_vm(&code); + + // After run, check FPU state matches expected dirty values + let fpu = hyperlight_vm.vm.fpu().unwrap(); + + #[cfg_attr(not(kvm), allow(unused_mut))] + let mut expected_dirty = CommonFpu { + fcw: 0x0F7F, + ftwx: 0xFE, // 7 registers valid (bit 0 empty after 7 pushes with TOP=1) + xmm: [ + 0x11111111111111111111111111111111_u128.to_le_bytes(), + 0x22222222222222222222222222222222_u128.to_le_bytes(), + 0x33333333333333333333333333333333_u128.to_le_bytes(), + 0x44444444444444444444444444444444_u128.to_le_bytes(), + 0x55555555555555555555555555555555_u128.to_le_bytes(), + 0x66666666666666666666666666666666_u128.to_le_bytes(), + 0x77777777777777777777777777777777_u128.to_le_bytes(), + 0x88888888888888888888888888888888_u128.to_le_bytes(), + 0x99999999999999999999999999999999_u128.to_le_bytes(), + 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_u128.to_le_bytes(), + 0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB_u128.to_le_bytes(), + 0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC_u128.to_le_bytes(), + 0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD_u128.to_le_bytes(), + 0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE_u128.to_le_bytes(), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_u128.to_le_bytes(), + 0x12345678123456781234567812345678_u128.to_le_bytes(), + ], + mxcsr: 0x3F80, + fsw: 0x0802, // TOP=1 after 7 pushes (bits 11-13), DE flag from denormal load + // fpr: 80-bit values with 6 bytes padding; may vary between CPU vendors + fpr: fpu.fpr, + // last_opcode: FPU Opcode update varies by CPU (may only update on unmasked exceptions) + last_opcode: fpu.last_opcode, + // last_ip: code is loaded at runtime-determined address + last_ip: fpu.last_ip, + // last_dp: points to stack (rsp) which is runtime-determined + last_dp: fpu.last_dp, + }; + // KVM doesn't preserve mxcsr via fpu(), copy from actual + normalize_fpu_mxcsr_for_kvm(&mut expected_dirty, fpu.mxcsr); + assert_eq!(fpu, expected_dirty); + + // Verify MXCSR via xsave on KVM (since fpu() doesn't return it) + #[cfg(kvm)] + if available_hv == HypervisorType::Kvm { + let xsave = hyperlight_vm.vm.xsave().unwrap(); + let mxcsr = u32::from_le_bytes(xsave[24..28].try_into().unwrap()); + assert_eq!(mxcsr, 0x3F80, "MXCSR in XSAVE should be dirty"); + } + + // Reset vcpu + hyperlight_vm.reset_vcpu().unwrap(); + + // Check FPU is reset to defaults + assert_fpu_reset(hyperlight_vm.vm.as_ref()); + + // Verify MXCSR via xsave on KVM + #[cfg(kvm)] + if available_hv == HypervisorType::Kvm { + let xsave = hyperlight_vm.vm.xsave().unwrap(); + let mxcsr = u32::from_le_bytes(xsave[24..28].try_into().unwrap()); + assert_eq!(mxcsr, MXCSR_DEFAULT, "MXCSR in XSAVE should be reset"); + } + } + + #[test] + fn reset_vcpu_debug_regs() { + // Build code that sets debug registers and halts + // In real mode (ring 0), we can access debug registers directly + let mut a = CodeAssembler::new(64).unwrap(); + a.mov(rax, 0xDEAD_BEEF_0000_0000u64).unwrap(); + a.mov(dr0, rax).unwrap(); + a.mov(rax, 0xDEAD_BEEF_0000_0001u64).unwrap(); + a.mov(dr1, rax).unwrap(); + a.mov(rax, 0xDEAD_BEEF_0000_0002u64).unwrap(); + a.mov(dr2, rax).unwrap(); + a.mov(rax, 0xDEAD_BEEF_0000_0003u64).unwrap(); + a.mov(dr3, rax).unwrap(); + a.mov(rax, 1u64).unwrap(); + a.mov(dr6, rax).unwrap(); + a.mov(rax, 0xFFu64).unwrap(); + a.mov(dr7, rax).unwrap(); + a.hlt().unwrap(); + let code = a.assemble(0).unwrap(); + + let hyperlight_vm = hyperlight_vm(&code); + + // Verify debug registers are dirty + let debug_regs = hyperlight_vm.vm.debug_regs().unwrap(); + let expected_dirty = CommonDebugRegs { + dr0: 0xDEAD_BEEF_0000_0000, + dr1: 0xDEAD_BEEF_0000_0001, + dr2: 0xDEAD_BEEF_0000_0002, + dr3: 0xDEAD_BEEF_0000_0003, + // dr6: guest set B0 (bit 0) = 1, reserved bits vary by CPU + dr6: (debug_regs.dr6 & !DR6_WRITABLE_MASK) | 0x1, + // dr7: guest set lower byte = 0xFF, reserved bits vary by CPU + dr7: (debug_regs.dr7 & !DR7_WRITABLE_MASK) | 0xFF, + }; + assert_eq!(debug_regs, expected_dirty); + + // Reset vcpu + hyperlight_vm.reset_vcpu().unwrap(); + + // Check debug registers are reset to default values + assert_debug_regs_reset(hyperlight_vm.vm.as_ref()); + } + + #[test] + fn reset_vcpu_sregs() { + // Build code that modifies special registers and halts + // We can modify CR0.WP, CR2, CR4.TSD, and CR8 from guest code in ring 0 + let mut a = CodeAssembler::new(64).unwrap(); + // Set CR0.WP (Write Protect, bit 16) + a.mov(rax, cr0).unwrap(); + a.or(rax, 0x10000i32).unwrap(); + a.mov(cr0, rax).unwrap(); + // Set CR2 + a.mov(rax, 0xDEADBEEFu64).unwrap(); + a.mov(cr2, rax).unwrap(); + // Set CR4.TSD (Time Stamp Disable, bit 2) + a.mov(rax, cr4).unwrap(); + a.or(rax, 0x4i32).unwrap(); + a.mov(cr4, rax).unwrap(); + // Set CR8 + a.mov(rax, 5u64).unwrap(); + a.mov(cr8, rax).unwrap(); + a.hlt().unwrap(); + let code = a.assemble(0).unwrap(); + + let hyperlight_vm = hyperlight_vm(&code); + + // Get the expected defaults + let defaults = CommonSpecialRegisters::standard_64bit_defaults(hyperlight_vm.pml4_addr); + + // Verify registers are dirty (CR0.WP, CR2, CR4.TSD and CR8 modified by our code) + let sregs = hyperlight_vm.vm.sregs().unwrap(); + let mut expected_dirty = CommonSpecialRegisters { + cr0: defaults.cr0 | 0x10000, // WP bit set + cr2: 0xDEADBEEF, + cr4: defaults.cr4 | 0x4, // TSD bit set + cr8: 0x5, + ..defaults + }; + normalize_sregs_no_granularity(&mut expected_dirty, &sregs); + assert_eq!(sregs, expected_dirty); + + // Reset vcpu + hyperlight_vm.reset_vcpu().unwrap(); + + // Check registers are reset to defaults + let sregs = hyperlight_vm.vm.sregs().unwrap(); + let mut expected_reset = defaults; + normalize_sregs_no_granularity(&mut expected_reset, &sregs); + assert_eq!(sregs, expected_reset); + } + } +} diff --git a/src/hyperlight_host/src/hypervisor/regs/fpu.rs b/src/hyperlight_host/src/hypervisor/regs/fpu.rs index 47ce8a853..93907c6a4 100644 --- a/src/hyperlight_host/src/hypervisor/regs/fpu.rs +++ b/src/hyperlight_host/src/hypervisor/regs/fpu.rs @@ -36,13 +36,11 @@ pub(crate) struct CommonFpu { 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 { @@ -52,13 +50,11 @@ impl Default for CommonFpu { 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, } } } @@ -71,13 +67,13 @@ impl From<&CommonFpu> for kvm_fpu { fcw: common_fpu.fcw, fsw: common_fpu.fsw, ftwx: common_fpu.ftwx, - pad1: common_fpu.pad1, + pad1: 0, 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, + pad2: 0, } } } @@ -90,13 +86,13 @@ impl From<&CommonFpu> for FloatingPointUnit { fcw: common_fpu.fcw, fsw: common_fpu.fsw, ftwx: common_fpu.ftwx, - pad1: common_fpu.pad1, + pad1: 0, 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, + pad2: 0, } } } @@ -109,13 +105,11 @@ impl From<&kvm_fpu> for CommonFpu { 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, } } } @@ -128,13 +122,11 @@ impl From<&FloatingPointUnit> for CommonFpu { 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, } } } @@ -174,7 +166,7 @@ impl From<&CommonFpu> for [(WHV_REGISTER_NAME, Align16); WHP FpControl: fpu.fcw, FpStatus: fpu.fsw, FpTag: fpu.ftwx, - Reserved: fpu.pad1, + Reserved: 0, LastFpOp: fpu.last_opcode, Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0_0 { LastFpRip: fpu.last_ip, @@ -293,7 +285,6 @@ impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonFpu 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 }; } @@ -355,7 +346,6 @@ mod tests { fcw: 0x1234, fsw: 0x5678, ftwx: 0x9a, - pad1: 0xbc, last_opcode: 0xdef0, last_ip: 0xdeadbeefcafebabe, last_dp: 0xabad1deaf00dbabe, @@ -365,7 +355,6 @@ mod tests { [22u8; 16], [23u8; 16], ], mxcsr: 0x1f80, - pad2: 0, } } diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs index 5bf9d0756..db47221bd 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs @@ -18,7 +18,9 @@ use std::sync::LazyLock; #[cfg(gdb)] use kvm_bindings::kvm_guest_debug; -use kvm_bindings::{kvm_debugregs, kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region}; +use kvm_bindings::{ + kvm_debugregs, kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region, kvm_xsave, +}; use kvm_ioctls::Cap::UserMemory; use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use tracing::{Span, instrument}; @@ -26,7 +28,8 @@ use tracing::{Span, instrument}; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; use crate::hypervisor::regs::{ - CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, + CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, FP_CONTROL_WORD_DEFAULT, + MXCSR_DEFAULT, }; use crate::hypervisor::virtual_machine::{VirtualMachine, VmExit}; use crate::mem::memory_region::MemoryRegion; @@ -142,12 +145,16 @@ impl VirtualMachine for KvmVm { } fn fpu(&self) -> Result { + // Note: On KVM this ignores MXCSR. + // See https://github.com/torvalds/linux/blob/d358e5254674b70f34c847715ca509e46eb81e6f/arch/x86/kvm/x86.c#L12554-L12599 let kvm_fpu = self.vcpu_fd.get_fpu()?; Ok((&kvm_fpu).into()) } fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { let kvm_fpu: kvm_fpu = fpu.into(); + // Note: On KVM this ignores MXCSR. + // See https://github.com/torvalds/linux/blob/d358e5254674b70f34c847715ca509e46eb81e6f/arch/x86/kvm/x86.c#L12554-L12599 self.vcpu_fd.set_fpu(&kvm_fpu)?; Ok(()) } @@ -174,7 +181,6 @@ impl VirtualMachine for KvmVm { Ok(()) } - #[cfg(crashdump)] fn xsave(&self) -> Result> { let xsave = self.vcpu_fd.get_xsave()?; Ok(xsave @@ -183,6 +189,51 @@ impl VirtualMachine for KvmVm { .flat_map(u32::to_le_bytes) .collect()) } + + fn reset_xsave(&self) -> Result<()> { + let mut xsave = kvm_xsave::default(); // default is zeroed 4KB buffer with no FAM + + // XSAVE area layout from Intel SDM Vol. 1 Section 13.4.1: + // - Bytes 0-1: FCW (x87 FPU Control Word) + // - Bytes 24-27: MXCSR + // - Bytes 512-519: XSTATE_BV (bitmap of valid state components) + xsave.region[0] = FP_CONTROL_WORD_DEFAULT as u32; + xsave.region[6] = MXCSR_DEFAULT; + // XSTATE_BV = 0x3: bits 0,1 = x87 + SSE valid. This tells KVM to apply + // the legacy region from this buffer. Without this, some KVM versions + // may ignore set_xsave entirely when XSTATE_BV=0. + xsave.region[128] = 0x3; + + // SAFETY: No dynamic features enabled, 4KB is sufficient + unsafe { self.vcpu_fd.set_xsave(&xsave)? }; + + Ok(()) + } + + #[cfg(test)] + #[cfg(feature = "init-paging")] + fn set_xsave(&self, xsave: &[u32]) -> Result<()> { + const KVM_XSAVE_SIZE: usize = 4096; + + if std::mem::size_of_val(xsave) != KVM_XSAVE_SIZE { + return Err(new_error!( + "Provided xsave size {} does not match KVM supported size {}", + std::mem::size_of_val(xsave), + KVM_XSAVE_SIZE + )); + } + let xsave = kvm_xsave { + region: xsave + .try_into() + .map_err(|_| new_error!("kvm xsave must be 1024 u32s"))?, + ..Default::default() + }; + // Safety: Safe because we only copy 4096 bytes + // and have not enabled any dynamic xsave features + unsafe { self.vcpu_fd.set_xsave(&xsave)? }; + + Ok(()) + } } #[cfg(gdb)] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index 5403197ce..f160d1cb9 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -85,7 +85,7 @@ pub fn is_hypervisor_present() -> bool { } /// The hypervisor types available for the current platform -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub(crate) enum HypervisorType { #[cfg(kvm)] Kvm, @@ -170,12 +170,17 @@ pub(crate) trait VirtualMachine: Debug + Send { #[allow(dead_code)] fn debug_regs(&self) -> Result; /// Set the debug registers of the vCPU - #[allow(dead_code)] fn set_debug_regs(&self, drs: &CommonDebugRegs) -> Result<()>; - /// xsave - #[cfg(crashdump)] + /// Get xsave + #[allow(dead_code)] fn xsave(&self) -> Result>; + /// Reset xsave to default state + fn reset_xsave(&self) -> Result<()>; + /// Set xsave - only used for tests + #[cfg(test)] + #[cfg(feature = "init-paging")] + fn set_xsave(&self, xsave: &[u32]) -> Result<()>; /// Get partition handle #[cfg(target_os = "windows")] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs index 2888d2fc5..80f22804a 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs @@ -21,7 +21,7 @@ use std::sync::LazyLock; #[cfg(gdb)] use mshv_bindings::{DebugRegisters, hv_message_type_HVMSG_X64_EXCEPTION_INTERCEPT}; use mshv_bindings::{ - FloatingPointUnit, SpecialRegisters, StandardRegisters, hv_message_type, + FloatingPointUnit, SpecialRegisters, StandardRegisters, XSave, 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_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, @@ -34,7 +34,8 @@ use tracing::{Span, instrument}; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; use crate::hypervisor::regs::{ - CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, + CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, FP_CONTROL_WORD_DEFAULT, + MXCSR_DEFAULT, }; use crate::hypervisor::virtual_machine::{VirtualMachine, VmExit}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; @@ -222,11 +223,65 @@ impl VirtualMachine for MshvVm { Ok(()) } - #[cfg(crashdump)] fn xsave(&self) -> Result> { let xsave = self.vcpu_fd.get_xsave()?; Ok(xsave.buffer.to_vec()) } + + fn reset_xsave(&self) -> Result<()> { + let current_xsave = self.vcpu_fd.get_xsave()?; + if current_xsave.buffer.len() < 576 { + // Minimum: 512 legacy + 64 header + return Err(new_error!( + "Unexpected xsave length {}", + current_xsave.buffer.len() + )); + } + + let mut buf = XSave::default(); // default is zeroed 4KB buffer + + // Copy XCOMP_BV (offset 520-527) - preserves feature mask + compacted bit + buf.buffer[520..528].copy_from_slice(¤t_xsave.buffer[520..528]); + + // XSAVE area layout from Intel SDM Vol. 1 Section 13.4.1: + // - Bytes 0-1: FCW (x87 FPU Control Word) + // - Bytes 24-27: MXCSR + // - Bytes 512-519: XSTATE_BV (bitmap of valid state components) + buf.buffer[0..2].copy_from_slice(&FP_CONTROL_WORD_DEFAULT.to_le_bytes()); + buf.buffer[24..28].copy_from_slice(&MXCSR_DEFAULT.to_le_bytes()); + // XSTATE_BV = 0x3: bits 0,1 = x87 + SSE valid. Explicitly tell hypervisor + // to apply the legacy region from this buffer for consistent behavior. + buf.buffer[512..520].copy_from_slice(&0x3u64.to_le_bytes()); + + self.vcpu_fd.set_xsave(&buf)?; + Ok(()) + } + + #[cfg(test)] + #[cfg(feature = "init-paging")] + fn set_xsave(&self, xsave: &[u32]) -> Result<()> { + const MSHV_XSAVE_SIZE: usize = 4096; + if std::mem::size_of_val(xsave) != MSHV_XSAVE_SIZE { + return Err(new_error!( + "Provided xsave size {} does not match MSHV supported size {}", + std::mem::size_of_val(xsave), + MSHV_XSAVE_SIZE + )); + } + + // Safety: all valid u32 values are 4 valid u8 values + let (prefix, bytes, suffix) = unsafe { xsave.align_to() }; + if !prefix.is_empty() || !suffix.is_empty() { + return Err(new_error!("Invalid xsave buffer alignment")); + } + let buf = XSave { + buffer: bytes + .try_into() + .map_err(|_| new_error!("mshv xsave must be 4096 u8s"))?, + }; + self.vcpu_fd.set_xsave(&buf)?; + Ok(()) + } } #[cfg(gdb)] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs index 4ada3714d..16fd9bc63 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs @@ -27,15 +27,16 @@ use windows_result::HRESULT; use crate::hypervisor::gdb::DebuggableVm; use crate::hypervisor::regs::{ Align16, CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, - WHP_DEBUG_REGS_NAMES, WHP_DEBUG_REGS_NAMES_LEN, WHP_FPU_NAMES, WHP_FPU_NAMES_LEN, - WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, WHP_SREGS_NAMES_LEN, + FP_CONTROL_WORD_DEFAULT, MXCSR_DEFAULT, WHP_DEBUG_REGS_NAMES, WHP_DEBUG_REGS_NAMES_LEN, + WHP_FPU_NAMES, WHP_FPU_NAMES_LEN, WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, + WHP_SREGS_NAMES_LEN, }; use crate::hypervisor::surrogate_process::SurrogateProcess; use crate::hypervisor::surrogate_process_manager::get_surrogate_process_manager; use crate::hypervisor::virtual_machine::{VirtualMachine, VmExit}; use crate::hypervisor::wrappers::HandleWrapper; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; -use crate::{Result, log_then_return, new_error}; +use crate::{HyperlightError, Result, log_then_return, new_error}; #[allow(dead_code)] // Will be used for runtime hypervisor detection pub(crate) fn is_hypervisor_present() -> bool { @@ -419,10 +420,7 @@ impl VirtualMachine for WhpVm { Ok(()) } - #[cfg(crashdump)] fn xsave(&self) -> Result> { - use crate::HyperlightError; - // Get the required buffer size by calling with NULL buffer. // If the buffer is not large enough (0 won't be), WHvGetVirtualProcessorXsaveState returns // WHV_E_INSUFFICIENT_BUFFER and sets buffer_size_needed to the required size. @@ -472,6 +470,131 @@ impl VirtualMachine for WhpVm { Ok(xsave_buffer) } + fn reset_xsave(&self) -> Result<()> { + // WHP uses compacted XSAVE format (bit 63 of XCOMP_BV set). + // We cannot just zero out the xsave area - we need to preserve the XCOMP_BV. + + // Get the required buffer size by calling with NULL buffer. + let mut buffer_size_needed: u32 = 0; + + let result = unsafe { + WHvGetVirtualProcessorXsaveState( + self.partition, + 0, + std::ptr::null_mut(), + 0, + &mut buffer_size_needed, + ) + }; + + // Expect insufficient buffer error; any other error is unexpected + if let Err(e) = result + && e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER + { + return Err(HyperlightError::WindowsAPIError(e)); + } + + // Minimum: 512 legacy + 64 header = 576 bytes + if buffer_size_needed < 576 { + return Err(new_error!("Unexpected xsave length {}", buffer_size_needed)); + } + + // Create a buffer to hold the current state (to get the correct XCOMP_BV) + let mut current_state = vec![0u8; buffer_size_needed as usize]; + let mut written_bytes = 0; + unsafe { + WHvGetVirtualProcessorXsaveState( + self.partition, + 0, + current_state.as_mut_ptr() as *mut std::ffi::c_void, + buffer_size_needed, + &mut written_bytes, + ) + }?; + + // Zero out most of the buffer, preserving only XCOMP_BV (520-528). + // Extended components with XSTATE_BV bit=0 will use their init values. + // + // - Legacy region (0-512): x87 FPU + SSE state + // - XSTATE_BV (512-520): Feature bitmap + // - XCOMP_BV (520-528): Compaction bitmap + format bit (KEEP) + // - Reserved (528-576): Header padding + // - Extended (576+): AVX, AVX-512, MPX, PKRU, AMX, etc. + current_state[0..520].fill(0); + current_state[528..].fill(0); + + // XSAVE area layout from Intel SDM Vol. 1 Section 13.4.1: + // - Bytes 0-1: FCW (x87 FPU Control Word) + // - Bytes 24-27: MXCSR + // - Bytes 512-519: XSTATE_BV (bitmap of valid state components) + current_state[0..2].copy_from_slice(&FP_CONTROL_WORD_DEFAULT.to_le_bytes()); + current_state[24..28].copy_from_slice(&MXCSR_DEFAULT.to_le_bytes()); + // XSTATE_BV = 0x3: bits 0,1 = x87 + SSE valid. Explicitly tell hypervisor + // to apply the legacy region from this buffer for consistent behavior. + current_state[512..520].copy_from_slice(&0x3u64.to_le_bytes()); + + unsafe { + WHvSetVirtualProcessorXsaveState( + self.partition, + 0, + current_state.as_ptr() as *const std::ffi::c_void, + buffer_size_needed, + ) + .map_err(|e| new_error!("Failed to reset Xsave state: {:?}", e))?; + } + + Ok(()) + } + + #[cfg(test)] + #[cfg(feature = "init-paging")] + fn set_xsave(&self, xsave: &[u32]) -> Result<()> { + use crate::HyperlightError; + + // Get the required buffer size by calling with NULL buffer. + // If the buffer is not large enough (0 won't be), WHvGetVirtualProcessorXsaveState returns + // WHV_E_INSUFFICIENT_BUFFER and sets buffer_size_needed to the required size. + let mut buffer_size_needed: u32 = 0; + + let result = unsafe { + WHvGetVirtualProcessorXsaveState( + self.partition, + 0, + std::ptr::null_mut(), + 0, + &mut buffer_size_needed, + ) + }; + + // Expect insufficient buffer error; any other error is unexpected + if let Err(e) = result + && e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER + { + return Err(HyperlightError::WindowsAPIError(e)); + } + + let provided_size = std::mem::size_of_val(xsave) as u32; + if buffer_size_needed > provided_size { + return Err(new_error!( + "Xsave buffer too small: needed {} bytes, provided {} bytes", + buffer_size_needed, + provided_size + )); + } + + unsafe { + WHvSetVirtualProcessorXsaveState( + self.partition, + 0, + xsave.as_ptr() as *const std::ffi::c_void, + buffer_size_needed, + ) + .map_err(|e| new_error!("Failed to set Xsave state: {:?}", e))?; + } + + Ok(()) + } + /// Mark that initial memory setup is complete. After this, map_memory will fail. fn complete_initial_memory_setup(&mut self) { self.initial_memory_setup_done = true; diff --git a/src/hyperlight_host/src/sandbox/hypervisor.rs b/src/hyperlight_host/src/sandbox/hypervisor.rs index bf205adaa..b4269e173 100644 --- a/src/hyperlight_host/src/sandbox/hypervisor.rs +++ b/src/hyperlight_host/src/sandbox/hypervisor.rs @@ -66,7 +66,7 @@ pub fn get_available_hypervisor() -> &'static Option { } /// The hypervisor types available for the current platform -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub(crate) enum HypervisorType { #[cfg(kvm)] Kvm, diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 349536cfc..9819de380 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -283,6 +283,8 @@ impl MultiUseSandbox { unsafe { self.vm.map_region(region)? }; } + self.vm.reset_vcpu()?; + // The restored snapshot is now our most current snapshot self.snapshot = Some(snapshot.clone()); @@ -1296,6 +1298,41 @@ mod tests { assert_ne!(sandbox3.id, sandbox_id); } + /// Test that snapshot restore properly resets vCPU debug registers. This test verifies + /// that restore() calls reset_vcpu. + #[test] + fn snapshot_restore_resets_debug_registers() { + let mut sandbox: MultiUseSandbox = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + u_sbox.evolve().unwrap() + }; + + let snapshot = sandbox.snapshot().unwrap(); + + // Verify DR0 is initially 0 (clean state) + let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap(); + assert_eq!(dr0_initial, 0, "DR0 should initially be 0"); + + // Dirty DR0 by setting it to a known non-zero value + const DIRTY_VALUE: u64 = 0xDEAD_BEEF_CAFE_BABE; + sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap(); + let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap(); + assert_eq!( + dr0_dirty, DIRTY_VALUE, + "DR0 should be dirty after SetDr0 call" + ); + + // Restore to the snapshot - this should reset vCPU state including debug registers + sandbox.restore(snapshot).unwrap(); + + let dr0_after_restore: u64 = sandbox.call("GetDr0", ()).unwrap(); + assert_eq!( + dr0_after_restore, 0, + "DR0 should be 0 after restore (reset_vcpu should have been called)" + ); + } + /// Test that sandboxes can be created and evolved with different heap sizes #[test] fn test_sandbox_creation_various_sizes() { diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 5f8df6a65..38507b5b3 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -557,6 +557,18 @@ fn use_sse2_registers() { unsafe { core::arch::asm!("movss xmm1, DWORD PTR [{0}]", in(reg) &val) }; } +#[guest_function("SetDr0")] +fn set_dr0(value: u64) { + unsafe { core::arch::asm!("mov dr0, {}", in(reg) value) }; +} + +#[guest_function("GetDr0")] +fn get_dr0() -> u64 { + let value: u64; + unsafe { core::arch::asm!("mov {}, dr0", out(reg) value) }; + value +} + #[guest_function("Add")] fn add(a: i32, b: i32) -> Result { #[host_function("HostAdd")]