From 7804836ab6df51bf12d8282570b1bc3622ffa059 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Wed, 11 Mar 2026 23:58:08 +0000 Subject: [PATCH] Implement map_file_cow so that files can be mapped before a VM is initialised Signed-off-by: Simon Davies --- .../src/hypervisor/hyperlight_vm/x86_64.rs | 6 +- src/hyperlight_host/src/hypervisor/mod.rs | 1 + .../src/sandbox/file_mapping.rs | 326 ++++++++++++++++++ .../src/sandbox/initialized_multi_use.rs | 276 ++++++++------- src/hyperlight_host/src/sandbox/mod.rs | 2 + .../src/sandbox/uninitialized.rs | 22 ++ .../src/sandbox/uninitialized_evolve.rs | 36 +- 7 files changed, 532 insertions(+), 137 deletions(-) create mode 100644 src/hyperlight_host/src/sandbox/file_mapping.rs diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 57b09fc20..ce6116c71 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -79,6 +79,7 @@ impl HyperlightVm { _pml4_addr: u64, entrypoint: NextAction, rsp_gva: u64, + page_size: usize, #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, #[cfg(gdb)] gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, @@ -145,7 +146,7 @@ impl HyperlightVm { entrypoint, rsp_gva, interrupt_handle, - page_size: 0, // Will be set in `initialise` + page_size, next_slot: scratch_slot + 1, freed_slots: Vec::new(), @@ -207,8 +208,6 @@ impl HyperlightVm { return Ok(()); }; - self.page_size = page_size as usize; - let regs = CommonRegisters { rip: initialise, // We usually keep the top of the stack 16-byte @@ -1505,6 +1504,7 @@ mod tests { gshm, &config, stack_top_gva, + page_size::get(), #[cfg(any(crashdump, gdb))] rt_cfg, crate::mem::exe::LoadInfo::dummy(), diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index 18c13c242..72a9d85d4 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -493,6 +493,7 @@ pub(crate) mod tests { gshm, &config, exn_stack_top_gva, + page_size::get(), #[cfg(any(crashdump, gdb))] rt_cfg, sandbox.load_info, diff --git a/src/hyperlight_host/src/sandbox/file_mapping.rs b/src/hyperlight_host/src/sandbox/file_mapping.rs new file mode 100644 index 000000000..42cc0796e --- /dev/null +++ b/src/hyperlight_host/src/sandbox/file_mapping.rs @@ -0,0 +1,326 @@ +/* +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. +*/ + +//! Host-side file mapping preparation for [`map_file_cow`]. +//! +//! This module splits the file mapping operation into two phases: +//! - **Prepare** ([`prepare_file_cow`]): performs host-side OS calls +//! (open file, create mapping) without requiring a VM. +//! - **Apply**: performed by the caller (either [`MultiUseSandbox::map_file_cow`] +//! or [`evolve_impl_multi_use`]) to map the prepared region into +//! the guest via [`HyperlightVm::map_region`]. +//! +//! This separation allows [`UninitializedSandbox`] to accept +//! `map_file_cow` calls before the VM exists, deferring the VM-side +//! work until [`evolve()`]. + +use std::ffi::c_void; +use std::path::Path; + +use tracing::{Span, instrument}; + +#[cfg(target_os = "windows")] +use crate::HyperlightError; +#[cfg(target_os = "windows")] +use crate::hypervisor::wrappers::HandleWrapper; +#[cfg(target_os = "windows")] +use crate::mem::memory_region::{HostRegionBase, MemoryRegionKind}; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +use crate::{Result, log_then_return}; + +/// A prepared (host-side) file mapping ready to be applied to a VM. +/// +/// Created by [`prepare_file_cow`]. The host-side OS resources (file +/// mapping handle + view on Windows, mmap on Linux) are held here +/// until consumed by the VM-side apply step. +/// +/// If dropped without being consumed, the `Drop` impl releases all +/// host-side resources — preventing leaks when an +/// [`UninitializedSandbox`] is dropped without evolving or when +/// apply fails. +#[must_use = "holds OS resources that leak if discarded — apply to a VM or let Drop clean up"] +pub(crate) struct PreparedFileMapping { + /// The guest address where this file should be mapped. + pub(crate) guest_base: u64, + /// The page-aligned size of the mapping in bytes. + pub(crate) size: usize, + /// Host-side OS resources. `None` after successful consumption + /// by the apply step (ownership transferred to the VM layer). + pub(crate) host_resources: Option, +} + +/// Platform-specific host-side file mapping resources. +pub(crate) enum HostFileResources { + /// Windows: `CreateFileMappingW` handle + `MapViewOfFile` view. + #[cfg(target_os = "windows")] + Windows { + mapping_handle: HandleWrapper, + view_base: *mut c_void, + }, + /// Linux: `mmap` base pointer. + #[cfg(target_os = "linux")] + Linux { + mmap_base: *mut c_void, + mmap_size: usize, + }, +} + +impl Drop for PreparedFileMapping { + fn drop(&mut self) { + // Clean up host resources if they haven't been consumed. + if let Some(resources) = self.host_resources.take() { + match resources { + #[cfg(target_os = "windows")] + HostFileResources::Windows { + mapping_handle, + view_base, + } => unsafe { + use windows::Win32::Foundation::CloseHandle; + use windows::Win32::System::Memory::{ + MEMORY_MAPPED_VIEW_ADDRESS, UnmapViewOfFile, + }; + if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: view_base }) + { + tracing::error!( + "PreparedFileMapping::drop: UnmapViewOfFile failed: {:?}", + e + ); + } + if let Err(e) = CloseHandle(mapping_handle.into()) { + tracing::error!("PreparedFileMapping::drop: CloseHandle failed: {:?}", e); + } + }, + #[cfg(target_os = "linux")] + HostFileResources::Linux { + mmap_base, + mmap_size, + } => unsafe { + if libc::munmap(mmap_base, mmap_size) != 0 { + tracing::error!( + "PreparedFileMapping::drop: munmap failed: {:?}", + std::io::Error::last_os_error() + ); + } + }, + } + } + } +} + +// SAFETY: The raw pointers in HostFileResources point to kernel-managed +// mappings (Windows file mapping views / Linux mmap regions), not aliased +// user-allocated heap memory. Ownership is fully contained within the +// struct, and cleanup APIs (UnmapViewOfFile, CloseHandle, munmap) are +// thread-safe. +unsafe impl Send for PreparedFileMapping {} + +impl PreparedFileMapping { + /// Build the [`MemoryRegion`] that describes this mapping for the + /// VM layer. The host resources must still be present (not yet + /// consumed). + pub(crate) fn to_memory_region(&self) -> Result { + let resources = self.host_resources.as_ref().ok_or_else(|| { + crate::HyperlightError::Error( + "PreparedFileMapping resources already consumed".to_string(), + ) + })?; + + match resources { + #[cfg(target_os = "windows")] + HostFileResources::Windows { + mapping_handle, + view_base, + } => { + let host_base = HostRegionBase { + from_handle: *mapping_handle, + handle_base: *view_base as usize, + handle_size: self.size, + offset: 0, + }; + let host_end = + ::add( + host_base, self.size, + ); + let guest_start = self.guest_base as usize; + let guest_end = guest_start.checked_add(self.size).ok_or_else(|| { + crate::HyperlightError::Error(format!( + "guest_region overflow: {:#x} + {:#x}", + guest_start, self.size + )) + })?; + Ok(MemoryRegion { + host_region: host_base..host_end, + guest_region: guest_start..guest_end, + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::MappedFile, + }) + } + #[cfg(target_os = "linux")] + HostFileResources::Linux { + mmap_base, + mmap_size, + } => { + let guest_start = self.guest_base as usize; + let guest_end = guest_start.checked_add(self.size).ok_or_else(|| { + crate::HyperlightError::Error(format!( + "guest_region overflow: {:#x} + {:#x}", + guest_start, self.size + )) + })?; + Ok(MemoryRegion { + host_region: *mmap_base as usize + ..(*mmap_base as usize).wrapping_add(*mmap_size), + guest_region: guest_start..guest_end, + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::MappedFile, + }) + } + } + } + + /// Mark the host resources as consumed — ownership has been + /// transferred to the VM layer. After this call, `Drop` will + /// not release them. + pub(crate) fn mark_consumed(&mut self) { + self.host_resources = None; + } +} + +/// Perform host-side file mapping preparation without requiring a VM. +/// +/// Opens the file, creates a read-only mapping in the host process, +/// and returns a [`PreparedFileMapping`] that can be applied to the +/// VM later. +/// +/// # Errors +/// +/// Returns an error if the file cannot be opened, is empty, or the +/// OS mapping calls fail. +#[instrument(err(Debug), skip(file_path, guest_base), parent = Span::current())] +pub(crate) fn prepare_file_cow(file_path: &Path, guest_base: u64) -> Result { + // Validate alignment eagerly to fail fast before allocating OS resources. + let page_size = page_size::get(); + if guest_base as usize % page_size != 0 { + log_then_return!( + "map_file_cow: guest_base {:#x} is not page-aligned (page size: {:#x})", + guest_base, + page_size + ); + } + + #[cfg(target_os = "windows")] + { + use std::os::windows::io::AsRawHandle; + + use windows::Win32::Foundation::HANDLE; + use windows::Win32::System::Memory::{ + CreateFileMappingW, FILE_MAP_READ, MapViewOfFile, PAGE_READONLY, + }; + + let file = std::fs::File::options().read(true).open(file_path)?; + let file_size = file.metadata()?.len(); + if file_size == 0 { + log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path); + } + let size = usize::try_from(file_size).map_err(|_| { + HyperlightError::Error(format!( + "File size {file_size} exceeds addressable range on this platform" + )) + })?; + let size = size.div_ceil(page_size) * page_size; + + let file_handle = HANDLE(file.as_raw_handle()); + + // Create a read-only file mapping object backed by the actual file. + // Pass 0,0 for size to use the file's actual size — Windows will + // NOT extend a read-only file, so requesting page-aligned size + // would fail for files smaller than one page. + let mapping_handle = + unsafe { CreateFileMappingW(file_handle, None, PAGE_READONLY, 0, 0, None) } + .map_err(|e| HyperlightError::Error(format!("CreateFileMappingW failed: {e}")))?; + + // Map a read-only view into the host process. + // Passing 0 for dwNumberOfBytesToMap maps the entire file; the OS + // rounds up to the next page boundary and zero-fills the remainder. + let view = unsafe { MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, 0) }; + if view.Value.is_null() { + unsafe { + let _ = windows::Win32::Foundation::CloseHandle(mapping_handle); + } + log_then_return!( + "MapViewOfFile failed: {:?}", + std::io::Error::last_os_error() + ); + } + + Ok(PreparedFileMapping { + guest_base, + size, + host_resources: Some(HostFileResources::Windows { + mapping_handle: HandleWrapper::from(mapping_handle), + view_base: view.Value, + }), + }) + } + #[cfg(unix)] + { + use std::os::fd::AsRawFd; + + let file = std::fs::File::options().read(true).open(file_path)?; + let file_size = file.metadata()?.len(); + if file_size == 0 { + log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path); + } + let size = usize::try_from(file_size).map_err(|_| { + crate::HyperlightError::Error(format!( + "File size {file_size} exceeds addressable range on this platform" + )) + })?; + let size = size.div_ceil(page_size) * page_size; + let base = unsafe { + // MSHV's map_user_memory requires host-writable pages (the + // kernel module calls get_user_pages with write access). + // KVM's KVM_MEM_READONLY slots work with read-only host pages. + // PROT_EXEC is never needed — the hypervisor backs guest R+X + // pages without requiring host-side execute permission. + #[cfg(mshv3)] + let prot = libc::PROT_READ | libc::PROT_WRITE; + #[cfg(not(mshv3))] + let prot = libc::PROT_READ; + + libc::mmap( + std::ptr::null_mut(), + size, + prot, + libc::MAP_PRIVATE, + file.as_raw_fd(), + 0, + ) + }; + if base == libc::MAP_FAILED { + log_then_return!("mmap error: {:?}", std::io::Error::last_os_error()); + } + + Ok(PreparedFileMapping { + guest_base, + size, + host_resources: Some(HostFileResources::Linux { + mmap_base: base, + mmap_size: size, + }), + }) + } +} diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 97044c521..a2a7f08a3 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -15,10 +15,6 @@ limitations under the License. */ use std::collections::HashSet; -#[cfg(unix)] -use std::os::fd::AsRawFd; -#[cfg(unix)] -use std::os::linux::fs::MetadataExt; use std::path::Path; use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; @@ -30,31 +26,26 @@ use hyperlight_common::flatbuffer_wrappers::function_types::{ }; use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity; use tracing::{Span, instrument}; -#[cfg(target_os = "windows")] -use windows::Win32::Foundation::CloseHandle; -#[cfg(target_os = "windows")] -use windows::Win32::System::Memory::{ - CreateFileMappingW, FILE_MAP_READ, MapViewOfFile, PAGE_READONLY, UnmapViewOfFile, -}; use super::Callable; +use super::file_mapping::prepare_file_cow; use super::host_funcs::FunctionRegistry; use super::snapshot::Snapshot; use crate::HyperlightError::{self, SnapshotSandboxMismatch}; +use crate::Result; use crate::func::{ParameterTuple, SupportedReturnType}; use crate::hypervisor::InterruptHandle; use crate::hypervisor::hyperlight_vm::{HyperlightVm, HyperlightVmError}; -#[cfg(target_os = "windows")] -use crate::hypervisor::wrappers::HandleWrapper; -#[cfg(target_os = "windows")] -use crate::mem::memory_region::{HostRegionBase, MemoryRegionKind}; -use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +#[cfg(target_os = "linux")] +use crate::log_then_return; +use crate::mem::memory_region::MemoryRegion; +#[cfg(target_os = "linux")] +use crate::mem::memory_region::MemoryRegionFlags; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::HostSharedMemory; use crate::metrics::{ METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call, }; -use crate::{Result, log_then_return}; /// A fully initialized sandbox that can execute guest functions multiple times. /// @@ -565,127 +556,26 @@ impl MultiUseSandbox { if self.poisoned { return Err(crate::HyperlightError::PoisonedSandbox); } - #[cfg(windows)] - { - use std::os::windows::io::AsRawHandle; - - use windows::Win32::Foundation::HANDLE; + // Phase 1: host-side OS work (open file, create mapping) + let mut prepared = prepare_file_cow(file_path, guest_base)?; - let file = std::fs::File::options().read(true).open(file_path)?; - let file_size = file.metadata()?.len(); - if file_size == 0 { - log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path); - } - let page_size = page_size::get(); - let size = usize::try_from(file_size).map_err(|_| { - HyperlightError::Error(format!( - "File size {file_size} exceeds addressable range on this platform" - )) - })?; - let size = size.div_ceil(page_size) * page_size; - - let file_handle = HANDLE(file.as_raw_handle()); - - // Create a read-only file mapping object backed by the actual file. - // Pass 0,0 for size to use the file's actual size — Windows will - // NOT extend a read-only file, so requesting page-aligned size - // would fail for files smaller than one page. MapViewOfFile and - // the surrogate process will round up to page boundaries internally. - let mapping_handle = - unsafe { CreateFileMappingW(file_handle, None, PAGE_READONLY, 0, 0, None) } - .map_err(|e| { - HyperlightError::Error(format!("CreateFileMappingW failed: {e}")) - })?; - - // Map a read-only view into the host process. - // Passing 0 for dwNumberOfBytesToMap maps the entire file; the OS - // rounds up to the next page boundary and zero-fills the remainder. - let view = unsafe { MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, 0) }; - if view.Value.is_null() { - // Clean up the mapping handle before returning - unsafe { - let _ = CloseHandle(mapping_handle); - } - log_then_return!( - "MapViewOfFile failed: {:?}", - std::io::Error::last_os_error() - ); - } + // Phase 2: VM-side work (map into guest address space) + let region = prepared.to_memory_region()?; - let host_base = HostRegionBase { - from_handle: HandleWrapper::from(mapping_handle), - handle_base: view.Value as usize, - handle_size: size, - offset: 0, - }; - - let host_end = - ::add( - host_base, size, - ); - - let region = MemoryRegion { - host_region: host_base..host_end, - guest_region: guest_base as usize..guest_base as usize + size, - flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, - region_type: MemoryRegionType::MappedFile, - }; - - // Reset snapshot since we are mutating the sandbox state - self.snapshot = None; - - if let Err(err) = unsafe { self.vm.map_region(®ion) } - .map_err(HyperlightVmError::MapRegion) - .map_err(HyperlightError::HyperlightVmError) - { - // Clean up host-side resources on failure - unsafe { - let _ = UnmapViewOfFile(view); - let _ = CloseHandle(mapping_handle); - } - return Err(err); - } + // Reset snapshot since we are mutating the sandbox state + self.snapshot = None; - self.mem_mgr.mapped_rgns += 1; + unsafe { self.vm.map_region(®ion) } + .map_err(HyperlightVmError::MapRegion) + .map_err(crate::HyperlightError::HyperlightVmError)?; - Ok(size as u64) - } - #[cfg(unix)] - unsafe { - let file = std::fs::File::options() - .read(true) - .write(true) - .open(file_path)?; - let file_size = file.metadata()?.st_size(); - if file_size == 0 { - log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path); - } - let page_size = page_size::get(); - let size = (file_size as usize).div_ceil(page_size) * page_size; - let base = libc::mmap( - std::ptr::null_mut(), - size, - libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC, - libc::MAP_PRIVATE, - file.as_raw_fd(), - 0, - ); - if base == libc::MAP_FAILED { - log_then_return!("mmap error: {:?}", std::io::Error::last_os_error()); - } + // Successfully mapped — transfer host resource ownership to + // the VM layer. + let size = prepared.size as u64; + prepared.mark_consumed(); + self.mem_mgr.mapped_rgns += 1; - if let Err(err) = self.map_region(&MemoryRegion { - host_region: base as usize..base.wrapping_add(size) as usize, - guest_region: guest_base as usize..guest_base as usize + size, - flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, - region_type: MemoryRegionType::MappedFile, - }) { - libc::munmap(base, size); - return Err(err); - }; - - Ok(size as u64) - } + Ok(size) } /// Calls a guest function with type-erased parameters and return values. @@ -2011,4 +1901,126 @@ mod tests { let _ = std::fs::remove_file(&path); } + + /// Tests the deferred `map_file_cow` flow: map a file on + /// `UninitializedSandbox` (before evolve), then evolve and verify + /// the guest can read the mapped content. + #[test] + fn test_map_file_cow_deferred_basic() { + let expected = b"deferred map_file_cow test data"; + let (path, expected_bytes) = + create_test_file("hyperlight_test_map_file_cow_deferred.bin", expected); + + let guest_base: u64 = 0x1_0000_0000; + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + // Map the file before evolving — this defers the VM-side work. + let mapped_size = u_sbox.map_file_cow(&path, guest_base).unwrap(); + assert!(mapped_size > 0, "mapped_size should be positive"); + assert!( + mapped_size >= expected.len() as u64, + "mapped_size should be >= file content length" + ); + + // Evolve — deferred mappings are applied during this step. + let mut sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); + + // Verify the guest can read the mapped content. + let actual: Vec = sbox + .call( + "ReadMappedBuffer", + (guest_base, expected_bytes.len() as u64, true), + ) + .unwrap(); + + assert_eq!( + actual, expected_bytes, + "Guest should read back the exact file content after deferred mapping" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that dropping an `UninitializedSandbox` with pending + /// deferred file mappings does not leak or crash — the + /// `PreparedFileMapping::Drop` should clean up host resources. + #[test] + fn test_map_file_cow_deferred_drop_without_evolve() { + let (path, _) = create_test_file( + "hyperlight_test_map_file_cow_deferred_drop.bin", + &[0xAA; 4096], + ); + + let guest_base: u64 = 0x1_0000_0000; + + { + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + u_sbox.map_file_cow(&path, guest_base).unwrap(); + // u_sbox dropped here without evolving — PreparedFileMapping::drop + // should clean up host-side OS resources. + } + + // If we get here without a crash/hang, cleanup worked. + // On Windows, also verify the file handle was released. + #[cfg(target_os = "windows")] + std::fs::remove_file(&path) + .expect("File should be deletable after dropping UninitializedSandbox"); + #[cfg(not(target_os = "windows"))] + let _ = std::fs::remove_file(&path); + } + + /// Tests that `prepare_file_cow` rejects unaligned `guest_base` + /// addresses eagerly, before allocating any OS resources. + #[test] + fn test_map_file_cow_unaligned_guest_base() { + let (path, _) = + create_test_file("hyperlight_test_map_file_cow_unaligned.bin", &[0xBB; 4096]); + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + // Use an intentionally unaligned address (page_size + 1). + let unaligned_base: u64 = (page_size::get() + 1) as u64; + let result = u_sbox.map_file_cow(&path, unaligned_base); + assert!( + result.is_err(), + "map_file_cow should reject unaligned guest_base" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that `prepare_file_cow` rejects empty files. + #[test] + fn test_map_file_cow_empty_file() { + let temp_dir = std::env::temp_dir(); + let path = temp_dir.join("hyperlight_test_map_file_cow_empty.bin"); + let _ = std::fs::remove_file(&path); + std::fs::File::create(&path).unwrap(); // create empty file + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + let guest_base: u64 = 0x1_0000_0000; + let result = u_sbox.map_file_cow(&path, guest_base); + assert!(result.is_err(), "map_file_cow should reject empty files"); + + let _ = std::fs::remove_file(&path); + } } diff --git a/src/hyperlight_host/src/sandbox/mod.rs b/src/hyperlight_host/src/sandbox/mod.rs index ccb15e3fb..6d0550be7 100644 --- a/src/hyperlight_host/src/sandbox/mod.rs +++ b/src/hyperlight_host/src/sandbox/mod.rs @@ -16,6 +16,8 @@ limitations under the License. /// Configuration needed to establish a sandbox. pub mod config; +/// Host-side file mapping preparation for `map_file_cow`. +pub(crate) mod file_mapping; /// Functionality for reading, but not modifying host functions pub(crate) mod host_funcs; /// Functionality for dealing with initialized sandboxes that can diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index 66c2c875b..0d0e045f4 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -183,6 +183,9 @@ pub struct UninitializedSandbox { /// multiple counters that would have divergent cached values. #[cfg(feature = "nanvix-unstable")] counter_taken: std::sync::atomic::AtomicBool, + /// File mappings prepared by [`Self::map_file_cow`] that will be + /// applied to the VM during [`Self::evolve`]. + pub(crate) pending_file_mappings: Vec, } impl Debug for UninitializedSandbox { @@ -381,6 +384,7 @@ impl UninitializedSandbox { deferred_hshm: Arc::new(Mutex::new(None)), #[cfg(feature = "nanvix-unstable")] counter_taken: std::sync::atomic::AtomicBool::new(false), + pending_file_mappings: Vec::new(), }; // If we were passed a writer for host print register it otherwise use the default. @@ -432,6 +436,24 @@ impl UninitializedSandbox { evolve_impl_multi_use(self) } + /// Map the contents of a file into the guest at a particular address. + /// + /// The file mapping is prepared immediately (host-side OS work) but + /// the actual VM-side mapping is deferred until [`evolve()`](Self::evolve). + /// + /// Returns the length of the mapping in bytes. + #[instrument(err(Debug), skip(self, file_path, guest_base), parent = Span::current())] + pub fn map_file_cow( + &mut self, + file_path: &std::path::Path, + guest_base: u64, + ) -> crate::Result { + let prepared = super::file_mapping::prepare_file_cow(file_path, guest_base)?; + let size = prepared.size as u64; + self.pending_file_mappings.push(prepared); + Ok(size) + } + /// Sets the maximum log level for guest code execution. /// /// If not set, the log level is determined by the `RUST_LOG` environment variable, diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index b4c95013b..449f45935 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -53,10 +53,16 @@ pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result Result, #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, stack_top_gva: u64, + page_size: usize, #[cfg(any(crashdump, gdb))] rt_cfg: SandboxRuntimeConfig, _load_info: LoadInfo, ) -> Result { @@ -153,6 +184,7 @@ pub(crate) fn set_up_hypervisor_partition( mgr.layout.get_pt_base_gpa(), mgr.entrypoint, stack_top_gva, + page_size, config, #[cfg(gdb)] gdb_conn,