From 2f3efedfb5af47045d229785e884820da495717a Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 27 Feb 2026 16:36:48 +0000 Subject: [PATCH 1/5] feat: add configurable scratch region base GPA Add `set_scratch_base_gpa()` / `get_scratch_base_gpa()` to `SandboxConfiguration` so that callers can override where the scratch region is placed in guest physical address space. By default the scratch region sits at the top of the maximum GPA range (36-bit on KVM). 32-bit guests cannot address those GPAs, so this option lets them place the scratch region within their addressable range (e.g. just below 4 GB). When `None`, the existing default behaviour is preserved. Signed-off-by: danbugs --- src/hyperlight_host/src/hypervisor/gdb/mod.rs | 8 +- .../src/hypervisor/hyperlight_vm.rs | 35 ++++++--- src/hyperlight_host/src/mem/layout.rs | 54 ++++++++++++-- src/hyperlight_host/src/sandbox/config.rs | 31 ++++++++ src/hyperlight_host/src/sandbox/snapshot.rs | 74 ++++++++++++------- 5 files changed, 156 insertions(+), 46 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 6ef5a3322..d18484d3d 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -39,7 +39,7 @@ use crate::hypervisor::virtual_machine::{HypervisorError, RegisterError, Virtual use crate::mem::layout::SandboxMemoryLayout; use crate::mem::memory_region::MemoryRegion; use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::shared_mem::{HostSharedMemory, SharedMemory}; +use crate::mem::shared_mem::HostSharedMemory; #[derive(Debug, Error)] pub enum GdbTargetError { @@ -166,8 +166,7 @@ impl DebugMemoryAccess { .dbg_mem_access_fn .try_lock() .map_err(|e| DebugMemoryAccessError::LockFailed(file!(), line!(), e.to_string()))?; - let scratch_base = - hyperlight_common::layout::scratch_base_gpa(mgr.scratch_mem.mem_size()); + let scratch_base = mgr.layout.get_scratch_base_gpa(); let (mem, offset, name): (&mut HostSharedMemory, _, _) = if gpa >= scratch_base { ( &mut mgr.scratch_mem, @@ -249,8 +248,7 @@ impl DebugMemoryAccess { .dbg_mem_access_fn .try_lock() .map_err(|e| DebugMemoryAccessError::LockFailed(file!(), line!(), e.to_string()))?; - let scratch_base = - hyperlight_common::layout::scratch_base_gpa(mgr.scratch_mem.mem_size()); + let scratch_base = mgr.layout.get_scratch_base_gpa(); let (mem, offset, name): (&mut HostSharedMemory, _, _) = if gpa >= scratch_base { ( &mut mgr.scratch_mem, diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index 93cdd1216..bc46ec9be 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -101,6 +101,8 @@ pub(crate) struct HyperlightVm { // The current scratch region, used to keep it alive as long as it // is used & when unmapping scratch_memory: Option, + // The resolved base GPA of the scratch region + scratch_base_gpa: u64, mmap_regions: Vec<(u32, MemoryRegion)>, // Later mapped regions (slot number, region) @@ -407,6 +409,9 @@ impl HyperlightVm { let snapshot_slot = 0u32; let scratch_slot = 1u32; + let scratch_base_gpa = config + .get_scratch_base_gpa() + .unwrap_or_else(|| hyperlight_common::layout::scratch_base_gpa(scratch_mem.mem_size())); #[cfg_attr(not(gdb), allow(unused_mut))] let mut ret = Self { vm, @@ -422,6 +427,7 @@ impl HyperlightVm { snapshot_memory: None, scratch_slot, scratch_memory: None, + scratch_base_gpa, mmap_regions: Vec::new(), @@ -605,11 +611,11 @@ impl HyperlightVm { &mut self, scratch: GuestSharedMemory, ) -> Result<(), UpdateRegionError> { - let guest_base = hyperlight_common::layout::scratch_base_gpa(scratch.mem_size()); + let guest_base = self.scratch_base_gpa; let rgn = scratch.mapping_at(guest_base, MemoryRegionType::Scratch); if let Some(old_scratch) = self.scratch_memory.replace(scratch) { - let old_base = hyperlight_common::layout::scratch_base_gpa(old_scratch.mem_size()); + let old_base = self.scratch_base_gpa; let old_rgn = old_scratch.mapping_at(old_base, MemoryRegionType::Scratch); self.vm.unmap_memory((self.scratch_slot, &old_rgn))?; } @@ -2093,8 +2099,14 @@ mod tests { // Map the scratch region at the top of the address space let scratch_size = config.get_scratch_size(); - let scratch_gpa = hyperlight_common::layout::scratch_base_gpa(scratch_size); - let scratch_gva = hyperlight_common::layout::scratch_base_gva(scratch_size); + let scratch_gpa = config + .get_scratch_base_gpa() + .unwrap_or_else(|| hyperlight_common::layout::scratch_base_gpa(scratch_size)); + let scratch_gva = if config.get_scratch_base_gpa().is_some() { + scratch_gpa // Custom override: GVA = GPA (32-bit / non-init-paging guest) + } else { + hyperlight_common::layout::scratch_base_gva(scratch_size) + }; let scratch_mapping = Mapping { phys_base: scratch_gpa, virt_base: scratch_gva, @@ -2129,9 +2141,13 @@ mod tests { let (mut hshm, gshm) = mem_mgr.build().unwrap(); let peb_address = gshm.layout.peb_address; - let stack_top_gva = hyperlight_common::layout::MAX_GVA as u64 - - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET - + 1; + let scratch_base_gva = if config.get_scratch_base_gpa().is_some() { + config.get_scratch_base_gpa().unwrap() + } else { + hyperlight_common::layout::scratch_base_gva(config.get_scratch_size()) + }; + let stack_top_gva = scratch_base_gva + config.get_scratch_size() as u64 + - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET; let mut vm = set_up_hypervisor_partition( gshm, &config, @@ -2735,9 +2751,10 @@ mod tests { /// Get the stack top GVA, same as the regular codepath. fn stack_top_gva(&self) -> u64 { - hyperlight_common::layout::MAX_GVA as u64 + let scratch_size = crate::sandbox::SandboxConfiguration::DEFAULT_SCRATCH_SIZE; + let scratch_base_gva = hyperlight_common::layout::scratch_base_gva(scratch_size); + scratch_base_gva + scratch_size as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET - + 1 } } diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 4146be864..97ff50392 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -108,6 +108,15 @@ pub(crate) struct SandboxMemoryLayout { // The size of the scratch region in physical memory; note that // this will appear under the top of physical memory. scratch_size: usize, + // The resolved base GPA of the scratch region. Computed from the + // optional override in SandboxConfiguration, or defaults to + // MAX_GPA - scratch_size + 1. + scratch_base_gpa: u64, + // The resolved base GVA of the scratch region. For non-init-paging + // (no page tables), GVA = GPA (identity mapped). For init-paging + // (64-bit page tables), GVA = MAX_GVA - scratch_size + 1 (high + // canonical address). + scratch_base_gva: u64, } impl Debug for SandboxMemoryLayout { @@ -158,6 +167,14 @@ impl Debug for SandboxMemoryLayout { "Scratch region size", &format_args!("{:#x}", self.scratch_size), ) + .field( + "Scratch base GPA", + &format_args!("{:#x}", self.scratch_base_gpa), + ) + .field( + "Scratch base GVA", + &format_args!("{:#x}", self.scratch_base_gva), + ) .finish() } } @@ -204,6 +221,18 @@ impl SandboxMemoryLayout { return Err(MemoryRequestTooSmall(scratch_size, min_scratch_size)); } + let scratch_base_gpa = cfg + .get_scratch_base_gpa() + .unwrap_or_else(|| hyperlight_common::layout::scratch_base_gpa(scratch_size)); + + // When a custom GPA is set, GVA = GPA (non-init-paging / 32-bit guest). + // When using the default, GVA comes from the separate MAX_GVA-based computation. + let scratch_base_gva = if cfg.get_scratch_base_gpa().is_some() { + scratch_base_gpa + } else { + hyperlight_common::layout::scratch_base_gva(scratch_size) + }; + let guest_code_offset = 0; // The following offsets are to the fields of the PEB struct itself! let peb_offset = code_size.next_multiple_of(PAGE_SIZE_USIZE); @@ -240,6 +269,8 @@ impl SandboxMemoryLayout { init_data_permissions, pt_size: None, scratch_size, + scratch_base_gpa, + scratch_base_gva, }) } @@ -262,6 +293,21 @@ impl SandboxMemoryLayout { self.scratch_size } + /// Get the resolved base GPA of the scratch region. + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_scratch_base_gpa(&self) -> u64 { + self.scratch_base_gpa + } + + /// Get the base GVA of the scratch region. + /// For non-init-paging guests, GVA = GPA (identity mapped). + /// For init-paging guests, defaults to the high canonical address + /// (MAX_GVA - scratch_size + 1), or equals the custom GPA when overridden. + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_scratch_base_gva(&self) -> u64 { + self.scratch_base_gva + } + /// Get the offset in guest memory to the output data pointer. #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_output_data_pointer_offset(&self) -> usize { @@ -281,8 +327,7 @@ impl SandboxMemoryLayout { /// Get the guest virtual address of the start of output data. #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn get_output_data_buffer_gva(&self) -> u64 { - hyperlight_common::layout::scratch_base_gva(self.scratch_size) - + self.sandbox_memory_config.get_input_data_size() as u64 + self.get_scratch_base_gva() + self.sandbox_memory_config.get_input_data_size() as u64 } /// Get the offset into the host scratch buffer of the start of @@ -310,7 +355,7 @@ impl SandboxMemoryLayout { /// Get the guest virtual address of the start of input data #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_input_data_buffer_gva(&self) -> u64 { - hyperlight_common::layout::scratch_base_gva(self.scratch_size) + self.get_scratch_base_gva() } /// Get the offset into the host scratch buffer of the start of @@ -333,8 +378,7 @@ impl SandboxMemoryLayout { /// copied on restore #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn get_pt_base_gpa(&self) -> u64 { - hyperlight_common::layout::scratch_base_gpa(self.scratch_size) - + self.get_pt_base_scratch_offset() as u64 + self.scratch_base_gpa + self.get_pt_base_scratch_offset() as u64 } /// Get the first GPA of the scratch region that the host hasn't diff --git a/src/hyperlight_host/src/sandbox/config.rs b/src/hyperlight_host/src/sandbox/config.rs index f12387a0b..b773d0af0 100644 --- a/src/hyperlight_host/src/sandbox/config.rs +++ b/src/hyperlight_host/src/sandbox/config.rs @@ -74,6 +74,15 @@ pub struct SandboxConfiguration { interrupt_vcpu_sigrtmin_offset: u8, /// How much writable memory to offer the guest scratch_size: usize, + /// Override for the scratch region's base guest physical address. + /// When 0, the scratch region is placed at `MAX_GPA - scratch_size + 1`. + /// Set this to place the scratch region at a custom GPA (e.g., for 32-bit guests + /// that cannot address the default 36-bit location). + /// + /// Note: this is a C-compatible struct, so even though this optional + /// field should be represented as an `Option`, that type is not + /// FFI-safe, so it cannot be. 0 is used as the sentinel for "no override". + scratch_base_gpa: u64, } impl SandboxConfiguration { @@ -102,6 +111,7 @@ impl SandboxConfiguration { output_data_size: usize, heap_size_override: Option, scratch_size: usize, + scratch_base_gpa: Option, interrupt_retry_delay: Duration, interrupt_vcpu_sigrtmin_offset: u8, #[cfg(gdb)] guest_debug_info: Option, @@ -112,6 +122,7 @@ impl SandboxConfiguration { output_data_size: max(output_data_size, Self::MIN_OUTPUT_SIZE), heap_size_override: heap_size_override.unwrap_or(0), scratch_size, + scratch_base_gpa: scratch_base_gpa.unwrap_or(0), interrupt_retry_delay, interrupt_vcpu_sigrtmin_offset, #[cfg(gdb)] @@ -215,6 +226,23 @@ impl SandboxConfiguration { self.scratch_size = scratch_size; } + /// Override the base guest physical address of the scratch region. + /// By default, the scratch region is placed at `MAX_GPA - scratch_size + 1`. + /// Use this to place it at a custom GPA (e.g., within 32-bit address space). + pub fn set_scratch_base_gpa(&mut self, gpa: u64) { + self.scratch_base_gpa = gpa; + } + + /// Get the optional scratch base GPA override. + /// Returns `None` when the default placement should be used (value is 0). + pub(crate) fn get_scratch_base_gpa(&self) -> Option { + if self.scratch_base_gpa == 0 { + None + } else { + Some(self.scratch_base_gpa) + } + } + #[cfg(crashdump)] #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn get_guest_core_dump(&self) -> bool { @@ -249,6 +277,7 @@ impl Default for SandboxConfiguration { Self::DEFAULT_OUTPUT_SIZE, None, Self::DEFAULT_SCRATCH_SIZE, + None, Self::DEFAULT_INTERRUPT_RETRY_DELAY, Self::INTERRUPT_VCPU_SIGRTMIN_OFFSET, #[cfg(gdb)] @@ -274,6 +303,7 @@ mod tests { OUTPUT_DATA_SIZE_OVERRIDE, Some(HEAP_SIZE_OVERRIDE), SCRATCH_SIZE_OVERRIDE, + None, SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY, SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET, #[cfg(gdb)] @@ -302,6 +332,7 @@ mod tests { SandboxConfiguration::MIN_OUTPUT_SIZE - 1, None, SandboxConfiguration::DEFAULT_SCRATCH_SIZE, + None, SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY, SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET, #[cfg(gdb)] diff --git a/src/hyperlight_host/src/sandbox/snapshot.rs b/src/hyperlight_host/src/sandbox/snapshot.rs index bdf823423..6d719481e 100644 --- a/src/hyperlight_host/src/sandbox/snapshot.rs +++ b/src/hyperlight_host/src/sandbox/snapshot.rs @@ -16,7 +16,6 @@ limitations under the License. use std::sync::atomic::{AtomicU64, Ordering}; -use hyperlight_common::layout::{scratch_base_gpa, scratch_base_gva}; use hyperlight_common::vmem::{self, BasicMapping, CowMapping, Mapping, MappingKind, PAGE_SIZE}; use tracing::{Span, instrument}; @@ -177,10 +176,9 @@ fn hash(memory: &[u8], regions: &[MemoryRegion]) -> Result<[u8; 32]> { pub(crate) fn access_gpa<'a>( snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, - scratch_size: usize, + scratch_base: u64, gpa: u64, ) -> Option<(&'a ExclusiveSharedMemory, usize)> { - let scratch_base = scratch_base_gpa(scratch_size); if gpa >= scratch_base { Some((scratch, (gpa - scratch_base) as usize)) } else if gpa >= SandboxMemoryLayout::BASE_ADDRESS as u64 { @@ -193,20 +191,20 @@ pub(crate) fn access_gpa<'a>( pub(crate) struct SharedMemoryPageTableBuffer<'a> { snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, - scratch_size: usize, + scratch_base_gpa: u64, root: u64, } impl<'a> SharedMemoryPageTableBuffer<'a> { pub(crate) fn new( snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, - scratch_size: usize, + scratch_base_gpa: u64, root: u64, ) -> Self { Self { snap, scratch, - scratch_size, + scratch_base_gpa, root, } } @@ -217,7 +215,7 @@ impl<'a> hyperlight_common::vmem::TableReadOps for SharedMemoryPageTableBuffer<' addr + offset } unsafe fn read_entry(&self, addr: u64) -> u64 { - let memoff = access_gpa(self.snap, self.scratch, self.scratch_size, addr); + let memoff = access_gpa(self.snap, self.scratch, self.scratch_base_gpa, addr); let Some(pte_bytes) = memoff.and_then(|(mem, off)| mem.as_slice().get(off..off + 8)) else { // Attacker-controlled data pointed out-of-bounds. We'll // default to returning 0 in this case, which, for most @@ -250,16 +248,17 @@ fn filtered_mappings<'a>( snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, regions: &[MemoryRegion], - scratch_size: usize, + scratch_base_gpa: u64, + scratch_base_gva: u64, root_pt: u64, ) -> Vec<(Mapping, &'a [u8])> { - let op = SharedMemoryPageTableBuffer::new(snap, scratch, scratch_size, root_pt); + let op = SharedMemoryPageTableBuffer::new(snap, scratch, scratch_base_gpa, root_pt); unsafe { hyperlight_common::vmem::virt_to_phys(&op, 0, hyperlight_common::layout::MAX_GVA as u64) } .filter_map(move |mapping| { // the scratch map doesn't count - if mapping.virt_base >= scratch_base_gva(scratch_size) { + if mapping.virt_base >= scratch_base_gva { return None; } // neither does the mapping of the snapshot's own page tables @@ -270,7 +269,7 @@ fn filtered_mappings<'a>( } // todo: is it useful to warn if we can't resolve this? let contents = - unsafe { guest_page(snap, scratch, regions, scratch_size, mapping.phys_base) }?; + unsafe { guest_page(snap, scratch, regions, scratch_base_gpa, mapping.phys_base) }?; Some((mapping, contents)) }) .collect() @@ -287,7 +286,7 @@ unsafe fn guest_page<'a>( snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, regions: &[MemoryRegion], - scratch_size: usize, + scratch_base: u64, gpa: u64, ) -> Option<&'a [u8]> { if !gpa.is_multiple_of(PAGE_SIZE as u64) { @@ -304,7 +303,7 @@ unsafe fn guest_page<'a>( }); } } - let (mem, off) = access_gpa(snap, scratch, scratch_size, gpa)?; + let (mem, off) = access_gpa(snap, scratch, scratch_base, gpa)?; if off + PAGE_SIZE <= mem.as_slice().len() { Some(&mem.as_slice()[off..off + PAGE_SIZE]) } else { @@ -312,11 +311,16 @@ unsafe fn guest_page<'a>( } } -fn map_specials(pt_buf: &GuestPageTableBuffer, scratch_size: usize) { +fn map_specials( + pt_buf: &GuestPageTableBuffer, + scratch_base_gpa: u64, + scratch_base_gva: u64, + scratch_size: usize, +) { // Map the scratch region let mapping = Mapping { - phys_base: scratch_base_gpa(scratch_size), - virt_base: scratch_base_gva(scratch_size), + phys_base: scratch_base_gpa, + virt_base: scratch_base_gva, len: scratch_size as u64, kind: MappingKind::Basic(BasicMapping { readable: true, @@ -405,16 +409,20 @@ impl Snapshot { } // 2. Map the special mappings - map_specials(&pt_buf, layout.get_scratch_size()); + map_specials( + &pt_buf, + layout.get_scratch_base_gpa(), + layout.get_scratch_base_gva(), + layout.get_scratch_size(), + ); let pt_bytes = pt_buf.into_bytes(); layout.set_pt_size(pt_bytes.len())?; memory.extend(&pt_bytes); }; - let exn_stack_top_gva = hyperlight_common::layout::MAX_GVA as u64 - - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET - + 1; + let exn_stack_top_gva = layout.get_scratch_base_gva() + layout.get_scratch_size() as u64 + - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET; let extra_regions = Vec::new(); let hash = hash(&memory, &extra_regions)?; @@ -455,11 +463,15 @@ impl Snapshot { ) -> Result { let memory = shared_mem.with_exclusivity(|snap_e| { scratch_mem.with_exclusivity(|scratch_e| { - let scratch_size = layout.get_scratch_size(); - // Pass 1: count how many pages need to live - let live_pages = - filtered_mappings(snap_e, scratch_e, ®ions, scratch_size, root_pt_gpa); + let live_pages = filtered_mappings( + snap_e, + scratch_e, + ®ions, + layout.get_scratch_base_gpa(), + layout.get_scratch_base_gva(), + root_pt_gpa, + ); // Pass 2: copy them, and map them // TODO: Look for opportunities to hugepage map @@ -490,7 +502,12 @@ impl Snapshot { unsafe { vmem::map(&pt_buf, mapping) }; } // Phase 3: Map the special mappings - map_specials(&pt_buf, layout.get_scratch_size()); + map_specials( + &pt_buf, + layout.get_scratch_base_gpa(), + layout.get_scratch_base_gva(), + layout.get_scratch_size(), + ); let pt_bytes = pt_buf.into_bytes(); layout.set_pt_size(pt_bytes.len())?; snapshot_memory.extend(&pt_bytes); @@ -592,7 +609,8 @@ mod tests { fn make_simple_pt_mems() -> (SandboxMemoryManager, u64) { let cfg = crate::sandbox::SandboxConfiguration::default(); - let scratch_mem = ExclusiveSharedMemory::new(cfg.get_scratch_size()).unwrap(); + let scratch_size = cfg.get_scratch_size(); + let scratch_mem = ExclusiveSharedMemory::new(scratch_size).unwrap(); let pt_base = PAGE_SIZE + SandboxMemoryLayout::BASE_ADDRESS; let pt_buf = GuestPageTableBuffer::new(pt_base); let mapping = Mapping { @@ -606,7 +624,9 @@ mod tests { }), }; unsafe { vmem::map(&pt_buf, mapping) }; - super::map_specials(&pt_buf, PAGE_SIZE); + let scratch_base_gpa = hyperlight_common::layout::scratch_base_gpa(scratch_size); + let scratch_base_gva = hyperlight_common::layout::scratch_base_gva(scratch_size); + super::map_specials(&pt_buf, scratch_base_gpa, scratch_base_gva, scratch_size); let pt_bytes = pt_buf.into_bytes(); let mut snapshot_mem = ExclusiveSharedMemory::new(PAGE_SIZE + pt_bytes.len()).unwrap(); From 61b5aedc4f3d11565f5445375467646e42100158 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 27 Feb 2026 16:40:44 +0000 Subject: [PATCH 2/5] fix: make snapshot region writable for non-init-paging guests Signed-off-by: danbugs --- src/hyperlight_host/src/mem/shared_mem.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index 2cd3b8acc..8cd2d25a3 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -679,7 +679,22 @@ impl GuestSharedMemory { MemoryRegionType::Scratch => { MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE } - MemoryRegionType::Snapshot => MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + // For init-paging, the snapshot is read-only because guest page + // tables provide CoW semantics for writable pages. For + // non-init-paging there are no guest page tables, so the snapshot + // must be writable — otherwise writes (including the CPU setting + // the "Accessed" bit in GDT descriptors during segment loads) + // cause EPT violations that KVM retries forever. + MemoryRegionType::Snapshot => { + #[cfg(feature = "init-paging")] + { + MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE + } + #[cfg(not(feature = "init-paging"))] + { + MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE + } + } #[allow(clippy::panic)] // In the future, all the host side knowledge about memory // region types should collapse down to Snapshot vs From b961eb1812cffba27a1009b8ebd865da3f98be26 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 27 Feb 2026 21:52:08 +0000 Subject: [PATCH 3/5] fix: use init-paging feature gate for scratch GVA computation The GVA=GPA decision should be based on the init-paging feature flag, not on whether scratch_base_gpa is customized. A user could set a custom GPA while still using init-paging (page tables map default high-canonical GVA to the custom GPA). Signed-off-by: danbugs --- .../src/hypervisor/hyperlight_vm.rs | 13 +++---------- src/hyperlight_host/src/mem/layout.rs | 19 +++++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index bc46ec9be..ea730bde7 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -2102,11 +2102,7 @@ mod tests { let scratch_gpa = config .get_scratch_base_gpa() .unwrap_or_else(|| hyperlight_common::layout::scratch_base_gpa(scratch_size)); - let scratch_gva = if config.get_scratch_base_gpa().is_some() { - scratch_gpa // Custom override: GVA = GPA (32-bit / non-init-paging guest) - } else { - hyperlight_common::layout::scratch_base_gva(scratch_size) - }; + let scratch_gva = hyperlight_common::layout::scratch_base_gva(scratch_size); let scratch_mapping = Mapping { phys_base: scratch_gpa, virt_base: scratch_gva, @@ -2141,11 +2137,8 @@ mod tests { let (mut hshm, gshm) = mem_mgr.build().unwrap(); let peb_address = gshm.layout.peb_address; - let scratch_base_gva = if config.get_scratch_base_gpa().is_some() { - config.get_scratch_base_gpa().unwrap() - } else { - hyperlight_common::layout::scratch_base_gva(config.get_scratch_size()) - }; + let scratch_base_gva = + hyperlight_common::layout::scratch_base_gva(config.get_scratch_size()); let stack_top_gva = scratch_base_gva + config.get_scratch_size() as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET; let mut vm = set_up_hypervisor_partition( diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 97ff50392..38d803236 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -225,13 +225,13 @@ impl SandboxMemoryLayout { .get_scratch_base_gpa() .unwrap_or_else(|| hyperlight_common::layout::scratch_base_gpa(scratch_size)); - // When a custom GPA is set, GVA = GPA (non-init-paging / 32-bit guest). - // When using the default, GVA comes from the separate MAX_GVA-based computation. - let scratch_base_gva = if cfg.get_scratch_base_gpa().is_some() { - scratch_base_gpa - } else { - hyperlight_common::layout::scratch_base_gva(scratch_size) - }; + // For init-paging guests, GVA comes from the high canonical address + // (MAX_GVA - scratch_size + 1). For non-init-paging guests, GVA = GPA + // (identity mapped, no hypervisor-managed page tables). + #[cfg(feature = "init-paging")] + let scratch_base_gva = hyperlight_common::layout::scratch_base_gva(scratch_size); + #[cfg(not(feature = "init-paging"))] + let scratch_base_gva = scratch_base_gpa; let guest_code_offset = 0; // The following offsets are to the fields of the PEB struct itself! @@ -300,9 +300,8 @@ impl SandboxMemoryLayout { } /// Get the base GVA of the scratch region. - /// For non-init-paging guests, GVA = GPA (identity mapped). - /// For init-paging guests, defaults to the high canonical address - /// (MAX_GVA - scratch_size + 1), or equals the custom GPA when overridden. + /// For init-paging guests (feature = "init-paging"), this is the high canonical address + /// (MAX_GVA - scratch_size + 1). For non-init-paging guests, GVA = GPA (identity mapped). #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn get_scratch_base_gva(&self) -> u64 { self.scratch_base_gva From 4b579d3cc4546b128ebb34ff9bb75cc107924168 Mon Sep 17 00:00:00 2001 From: danbugs Date: Tue, 3 Mar 2026 23:17:05 +0000 Subject: [PATCH 4/5] fix: use scratch base GPA instead of scratch size in page table walker The access_gpa and SharedMemoryPageTableBuffer::new functions now take a scratch_base_gpa (u64) instead of scratch_size (usize). Update the read_guest_memory_by_gva caller to pass the correct value from layout.get_scratch_base_gpa(). Signed-off-by: danbugs --- src/hyperlight_host/src/mem/mgr.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 6c9433dab..7b7110412 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -435,11 +435,11 @@ impl SandboxMemoryManager { use crate::sandbox::snapshot::{SharedMemoryPageTableBuffer, access_gpa}; - let scratch_size = self.scratch_mem.mem_size(); + let scratch_base_gpa = self.layout.get_scratch_base_gpa(); self.shared_mem.with_exclusivity(|snap| { self.scratch_mem.with_exclusivity(|scratch| { - let pt_buf = SharedMemoryPageTableBuffer::new(snap, scratch, scratch_size, root_pt); + let pt_buf = SharedMemoryPageTableBuffer::new(snap, scratch, scratch_base_gpa, root_pt); // Walk page tables to get all mappings that cover the GVA range let mappings: Vec<_> = unsafe { @@ -479,7 +479,7 @@ impl SandboxMemoryManager { // Translate the GPA to host memory let gpa = mapping.phys_base + page_offset as u64; - let (mem, offset) = access_gpa(snap, scratch, scratch_size, gpa) + let (mem, offset) = access_gpa(snap, scratch, scratch_base_gpa, gpa) .ok_or_else(|| { new_error!( "Failed to resolve GPA {:#x} to host memory (GVA {:#x})", From f5e61161dc75374d91a510c70079b7c3a7fcb380 Mon Sep 17 00:00:00 2001 From: danbugs Date: Wed, 4 Mar 2026 01:14:13 +0000 Subject: [PATCH 5/5] fix: initialize all segment registers and XSAVE state for MSHV/WHP compatibility MSHV and WHP reject vCPU state with zeroed segment registers (ES, SS, FS, GS, LDT) and uninitialized XSAVE areas. Properly initialize all segment registers in standard_real_mode_defaults() and add reset_xsave() call after set_sregs() to ensure FPU state (FCW, MXCSR) is valid. Signed-off-by: danbugs --- .../src/hypervisor/hyperlight_vm.rs | 6 +++ .../src/hypervisor/regs/special_regs.rs | 37 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index ea730bde7..6450b5d20 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -376,6 +376,12 @@ impl HyperlightVm { vm.set_sregs(&CommonSpecialRegisters::standard_real_mode_defaults()) .map_err(VmError::Register)?; + // Initialize XSAVE state with proper FPU defaults (FCW, MXCSR). + // MSHV (unlike KVM) does not provide sane defaults for the XSAVE + // area on a freshly created vCPU, and will reject the VP state + // on the first run if it is left uninitialized. + vm.reset_xsave().map_err(VmError::Register)?; + #[cfg(any(kvm, mshv3))] let interrupt_handle: Arc = Arc::new(LinuxInterruptHandle { state: AtomicU8::new(0), diff --git a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs index 37c0b94da..5b4784fad 100644 --- a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs @@ -106,34 +106,55 @@ impl CommonSpecialRegisters { #[cfg(not(feature = "init-paging"))] pub(crate) fn standard_real_mode_defaults() -> Self { + // In real mode, all data/code segment registers must have valid + // limit, present, type_, and s fields. MSHV (unlike KVM) rejects + // the vCPU state if any segment has limit=0 / present=0. + let data_seg = CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 3, // data, read/write, accessed + present: 1, + s: 1, // non-system + ..Default::default() + }; + CommonSpecialRegisters { cs: CommonSegmentRegister { base: 0, selector: 0, limit: 0xFFFF, - type_: 11, + type_: 11, // code, readable, accessed present: 1, - s: 1, + s: 1, // non-system ..Default::default() }, - ds: CommonSegmentRegister { + ds: data_seg, + es: data_seg, + ss: data_seg, + fs: data_seg, + gs: data_seg, + tr: CommonSegmentRegister { base: 0, selector: 0, limit: 0xFFFF, - type_: 3, + type_: 11, // 32-bit busy TSS present: 1, - s: 1, + s: 0, // system segment ..Default::default() }, - tr: CommonSegmentRegister { + ldt: CommonSegmentRegister { base: 0, selector: 0, limit: 0xFFFF, - type_: 11, + type_: 2, // LDT descriptor present: 1, - s: 0, + s: 0, // system segment ..Default::default() }, + // CR0.ET (bit 4) is hardwired to 1 on modern x86 CPUs. + // MSHV rejects the vCPU state if ET is not set. + cr0: 1 << 4, ..Default::default() } }