From d67f8e69dc8d798d55528a8979d52f92c4a7648f Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Wed, 18 Mar 2026 23:52:28 +0000 Subject: [PATCH] feat: pass file mapping info via PEB with label support Add FileMappingInfo struct (guest_addr, size, label) and file_mappings field to HyperlightPEB so file mapping metadata is communicated to the guest through the PEB. Space for MAX_FILE_MAPPINGS (32) entries is statically reserved after the PEB struct to avoid dynamic layout changes. - Add label parameter to map_file_cow (optional, defaults to filename) - Add shared memory overlap validation (full mapped range) - Add inter-mapping overlap detection - Add write_file_mapping_entry on SandboxMemoryManager - Add MAX_FILE_MAPPINGS limit enforcement at registration time - Update PEB region sizing in get_memory_regions and layout tests Signed-off-by: Simon Davies --- src/hyperlight_common/src/mem.rs | 43 ++ .../examples/crashdump/main.rs | 6 +- src/hyperlight_host/src/mem/layout.rs | 73 ++- src/hyperlight_host/src/mem/mgr.rs | 62 ++ .../src/sandbox/file_mapping.rs | 80 ++- .../src/sandbox/initialized_multi_use.rs | 550 +++++++++++++++++- .../src/sandbox/uninitialized.rs | 78 ++- .../src/sandbox/uninitialized_evolve.rs | 9 + 8 files changed, 860 insertions(+), 41 deletions(-) diff --git a/src/hyperlight_common/src/mem.rs b/src/hyperlight_common/src/mem.rs index 3c86f8ecf..ad0c4882d 100644 --- a/src/hyperlight_common/src/mem.rs +++ b/src/hyperlight_common/src/mem.rs @@ -28,6 +28,43 @@ pub struct GuestMemoryRegion { pub ptr: u64, } +/// Maximum length of a file mapping label (excluding null terminator). +pub const FILE_MAPPING_LABEL_MAX_LEN: usize = 63; + +/// Maximum number of file mappings that can be registered in the PEB. +/// +/// Space for this many [`FileMappingInfo`] entries is statically +/// reserved immediately after the [`HyperlightPEB`] struct within the +/// same memory region. The reservation happens at layout time +/// (see `SandboxMemoryLayout::new`) so the guest heap never overlaps +/// the array, regardless of how many entries are actually used. +pub const MAX_FILE_MAPPINGS: usize = 32; + +/// Describes a single file mapping in the guest address space. +/// +/// Stored in the PEB's file mappings array so the guest can discover +/// which files have been mapped, at what address, and with what label. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct FileMappingInfo { + /// The guest address where the file is mapped. + pub guest_addr: u64, + /// The page-aligned size of the mapping in bytes. + pub size: u64, + /// Null-terminated C-style label (max 63 chars + null). + pub label: [u8; FILE_MAPPING_LABEL_MAX_LEN + 1], +} + +impl Default for FileMappingInfo { + fn default() -> Self { + Self { + guest_addr: 0, + size: 0, + label: [0u8; FILE_MAPPING_LABEL_MAX_LEN + 1], + } + } +} + #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct HyperlightPEB { @@ -35,4 +72,10 @@ pub struct HyperlightPEB { pub output_stack: GuestMemoryRegion, pub init_data: GuestMemoryRegion, pub guest_heap: GuestMemoryRegion, + /// File mappings array descriptor. + /// **Note:** `size` holds the **entry count** (number of valid + /// [`FileMappingInfo`] entries), NOT a byte size. `ptr` holds the + /// guest address of the preallocated array (immediately after the + /// PEB struct). + pub file_mappings: GuestMemoryRegion, } diff --git a/src/hyperlight_host/examples/crashdump/main.rs b/src/hyperlight_host/examples/crashdump/main.rs index f4575ab5b..ef0868e57 100644 --- a/src/hyperlight_host/examples/crashdump/main.rs +++ b/src/hyperlight_host/examples/crashdump/main.rs @@ -152,7 +152,7 @@ fn guest_crash_auto_dump(guest_path: &str) -> hyperlight_host::Result<()> { // Map a file as read-only into the guest at a known address. let mapping_file = create_mapping_file(); let guest_base: u64 = 0x200000000; - let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base)?; + let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base, None)?; println!("Mapped {len} bytes at guest address {guest_base:#x} (read-only)."); // Call WriteMappedBuffer — the guest maps the address in its page tables @@ -259,7 +259,7 @@ fn guest_crash_with_dump_disabled(guest_path: &str) -> hyperlight_host::Result<( let mapping_file = create_mapping_file(); let guest_base: u64 = 0x200000000; - let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base)?; + let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base, None)?; println!("Calling guest function 'WriteMappedBuffer' on read-only region..."); let result = sandbox.call::("WriteMappedBuffer", (guest_base, len)); @@ -401,7 +401,7 @@ mod tests { // automatically. This mapping lets us verify that GDB can read // a specific sentinel string from a known address. let len = sbox - .map_file_cow(&data_file, MAP_GUEST_BASE) + .map_file_cow(&data_file, MAP_GUEST_BASE, None) .expect("map_file_cow"); // Read the mapped region back through the guest and verify it diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index a6380dbbf..829c61234 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -63,7 +63,7 @@ limitations under the License. use std::fmt::Debug; use std::mem::{offset_of, size_of}; -use hyperlight_common::mem::{GuestMemoryRegion, HyperlightPEB, PAGE_SIZE_USIZE}; +use hyperlight_common::mem::{HyperlightPEB, PAGE_SIZE_USIZE}; use tracing::{Span, instrument}; use super::memory_region::MemoryRegionType::{Code, Heap, InitData, Peb}; @@ -92,6 +92,7 @@ pub(crate) struct SandboxMemoryLayout { peb_output_data_offset: usize, peb_init_data_offset: usize, peb_heap_data_offset: usize, + peb_file_mappings_offset: usize, guest_heap_buffer_offset: usize, init_data_offset: usize, @@ -141,6 +142,10 @@ impl Debug for SandboxMemoryLayout { "Guest Heap Offset", &format_args!("{:#x}", self.peb_heap_data_offset), ) + .field( + "File Mappings Offset", + &format_args!("{:#x}", self.peb_file_mappings_offset), + ) .field( "Guest Heap Buffer Offset", &format_args!("{:#x}", self.guest_heap_buffer_offset), @@ -211,13 +216,25 @@ impl SandboxMemoryLayout { let peb_output_data_offset = peb_offset + offset_of!(HyperlightPEB, output_stack); let peb_init_data_offset = peb_offset + offset_of!(HyperlightPEB, init_data); let peb_heap_data_offset = peb_offset + offset_of!(HyperlightPEB, guest_heap); + let peb_file_mappings_offset = peb_offset + offset_of!(HyperlightPEB, file_mappings); // The following offsets are the actual values that relate to memory layout, // which are written to PEB struct let peb_address = Self::BASE_ADDRESS + peb_offset; - // make sure heap buffer starts at 4K boundary - let guest_heap_buffer_offset = (peb_heap_data_offset + size_of::()) - .next_multiple_of(PAGE_SIZE_USIZE); + // make sure heap buffer starts at 4K boundary. + // The FileMappingInfo array is stored immediately after the PEB struct. + // We statically reserve space for MAX_FILE_MAPPINGS entries so that + // the heap never overlaps the array, even when all slots are used. + // The host writes file mapping metadata here via write_file_mapping_entry; + // the guest only reads the entries. We don't know at layout time how + // many file mappings the host will register, so we reserve space for + // the maximum number. + // The heap starts at the next page boundary after this reserved area. + let file_mappings_array_end = peb_offset + + size_of::() + + hyperlight_common::mem::MAX_FILE_MAPPINGS + * size_of::(); + let guest_heap_buffer_offset = file_mappings_array_end.next_multiple_of(PAGE_SIZE_USIZE); // make sure init data starts at 4K boundary let init_data_offset = @@ -230,6 +247,7 @@ impl SandboxMemoryLayout { peb_output_data_offset, peb_init_data_offset, peb_heap_data_offset, + peb_file_mappings_offset, sandbox_memory_config: cfg, code_size, guest_heap_buffer_offset, @@ -350,6 +368,28 @@ impl SandboxMemoryLayout { self.peb_heap_data_offset } + /// Get the offset in guest memory to the file_mappings count field + /// (the `size` field of the `GuestMemoryRegion` in the PEB). + pub(crate) fn get_file_mappings_size_offset(&self) -> usize { + self.peb_file_mappings_offset + } + + /// Get the offset in guest memory to the file_mappings pointer field. + fn get_file_mappings_pointer_offset(&self) -> usize { + self.get_file_mappings_size_offset() + size_of::() + } + + /// Get the offset in snapshot memory where the FileMappingInfo array starts + /// (immediately after the PEB struct, within the same page). + pub(crate) fn get_file_mappings_array_offset(&self) -> usize { + self.peb_offset + size_of::() + } + + /// Get the guest address of the FileMappingInfo array. + fn get_file_mappings_array_gva(&self) -> u64 { + (Self::BASE_ADDRESS + self.get_file_mappings_array_offset()) as u64 + } + /// Get the offset of the heap pointer in guest memory, #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_heap_pointer_offset(&self) -> usize { @@ -446,9 +486,12 @@ impl SandboxMemoryLayout { )); } - // PEB + // PEB + preallocated FileMappingInfo array + let peb_and_array_size = size_of::() + + hyperlight_common::mem::MAX_FILE_MAPPINGS + * size_of::(); let heap_offset = builder.push_page_aligned( - size_of::(), + peb_and_array_size, MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, Peb, ); @@ -588,6 +631,18 @@ impl SandboxMemoryLayout { shared_mem.write_u64(self.get_heap_size_offset(), self.heap_size.try_into()?)?; shared_mem.write_u64(self.get_heap_pointer_offset(), addr)?; + // Set up the file_mappings descriptor in the PEB. + // - The `size` field holds the number of valid FileMappingInfo + // entries currently written (initially 0 — entries are added + // later by map_file_cow / evolve). + // - The `ptr` field holds the guest address of the preallocated + // FileMappingInfo array + shared_mem.write_u64(self.get_file_mappings_size_offset(), 0)?; + shared_mem.write_u64( + self.get_file_mappings_pointer_offset(), + self.get_file_mappings_array_gva(), + )?; + // End of setting up the PEB // The input and output data regions do not have their layout @@ -611,7 +666,11 @@ mod tests { // in order of layout expected_size += layout.code_size; - expected_size += size_of::().next_multiple_of(PAGE_SIZE_USIZE); + // PEB + preallocated FileMappingInfo array + let peb_and_array = size_of::() + + hyperlight_common::mem::MAX_FILE_MAPPINGS + * size_of::(); + expected_size += peb_and_array.next_multiple_of(PAGE_SIZE_USIZE); expected_size += layout.heap_size.next_multiple_of(PAGE_SIZE_USIZE); diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 9f167f216..b3bd1177f 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +use std::mem::offset_of; + use flatbuffers::FlatBufferBuilder; use hyperlight_common::flatbuffer_wrappers::function_call::{ FunctionCall, validate_guest_function_call_buffer, @@ -351,6 +353,66 @@ impl SandboxMemoryManager { } impl SandboxMemoryManager { + /// Write a [`FileMappingInfo`] entry into the PEB's preallocated array. + /// + /// Reads the current entry count from the PEB, validates that the + /// array isn't full ([`MAX_FILE_MAPPINGS`]), writes the entry at the + /// next available slot, and increments the count. + /// + /// This is the **only** place that writes to the PEB file mappings + /// array — both `MultiUseSandbox::map_file_cow` and the evolve loop + /// call through here so the logic is not duplicated. + /// + /// # Errors + /// + /// Returns an error if [`MAX_FILE_MAPPINGS`] has been reached. + /// + /// [`FileMappingInfo`]: hyperlight_common::mem::FileMappingInfo + /// [`MAX_FILE_MAPPINGS`]: hyperlight_common::mem::MAX_FILE_MAPPINGS + pub(crate) fn write_file_mapping_entry( + &mut self, + guest_addr: u64, + size: u64, + label: &[u8; hyperlight_common::mem::FILE_MAPPING_LABEL_MAX_LEN + 1], + ) -> Result<()> { + use hyperlight_common::mem::{FileMappingInfo, MAX_FILE_MAPPINGS}; + + // Read the current entry count from the PEB. This is the source + // of truth — it survives snapshot/restore because the PEB is + // part of shared memory that gets snapshotted. + let current_count = + self.shared_mem + .read::(self.layout.get_file_mappings_size_offset())? as usize; + + if current_count >= MAX_FILE_MAPPINGS { + return Err(crate::new_error!( + "file mapping limit reached ({} of {})", + current_count, + MAX_FILE_MAPPINGS, + )); + } + + // Write the entry into the next available slot. + let entry_offset = self.layout.get_file_mappings_array_offset() + + current_count * std::mem::size_of::(); + let guest_addr_offset = offset_of!(FileMappingInfo, guest_addr); + let size_offset = offset_of!(FileMappingInfo, size); + let label_offset = offset_of!(FileMappingInfo, label); + self.shared_mem + .write::(entry_offset + guest_addr_offset, guest_addr)?; + self.shared_mem + .write::(entry_offset + size_offset, size)?; + self.shared_mem + .copy_from_slice(label, entry_offset + label_offset)?; + + // Increment the entry count. + let new_count = (current_count + 1) as u64; + self.shared_mem + .write::(self.layout.get_file_mappings_size_offset(), new_count)?; + + Ok(()) + } + /// Reads a host function call from memory #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn get_host_function_call(&mut self) -> Result { diff --git a/src/hyperlight_host/src/sandbox/file_mapping.rs b/src/hyperlight_host/src/sandbox/file_mapping.rs index 42cc0796e..28f95136b 100644 --- a/src/hyperlight_host/src/sandbox/file_mapping.rs +++ b/src/hyperlight_host/src/sandbox/file_mapping.rs @@ -57,6 +57,8 @@ pub(crate) struct PreparedFileMapping { pub(crate) guest_base: u64, /// The page-aligned size of the mapping in bytes. pub(crate) size: usize, + /// Null-terminated C-style label for this mapping (max 63 chars + null). + pub(crate) label: [u8; hyperlight_common::mem::FILE_MAPPING_LABEL_MAX_LEN + 1], /// Host-side OS resources. `None` after successful consumption /// by the apply step (ownership transferred to the VM layer). pub(crate) host_resources: Option, @@ -199,18 +201,84 @@ impl PreparedFileMapping { } } +/// Build a null-terminated C-style label from the provided string. +/// +/// When `truncate_ok` is true (used for auto-derived labels from +/// filenames), labels longer than [`FILE_MAPPING_LABEL_MAX_LEN`] are +/// silently truncated with a warning. When false (explicit user +/// labels), overlength labels are rejected with an error. +/// +/// # Errors +/// +/// Returns an error if the label exceeds the max length (and truncation +/// is not allowed) or contains null bytes. +fn build_label( + label: &str, + truncate_ok: bool, +) -> Result<[u8; hyperlight_common::mem::FILE_MAPPING_LABEL_MAX_LEN + 1]> { + use hyperlight_common::mem::FILE_MAPPING_LABEL_MAX_LEN; + let bytes = label.as_bytes(); + if bytes.contains(&0) { + log_then_return!("map_file_cow: label must not contain null bytes"); + } + let effective = if bytes.len() > FILE_MAPPING_LABEL_MAX_LEN { + if truncate_ok { + tracing::warn!( + "map_file_cow: auto-derived label truncated from {} to {} bytes: {:?}", + bytes.len(), + FILE_MAPPING_LABEL_MAX_LEN, + label, + ); + &bytes[..FILE_MAPPING_LABEL_MAX_LEN] + } else { + log_then_return!( + "map_file_cow: label length {} exceeds maximum of {} bytes", + bytes.len(), + FILE_MAPPING_LABEL_MAX_LEN + ); + } + } else { + bytes + }; + let mut buf = [0u8; FILE_MAPPING_LABEL_MAX_LEN + 1]; + buf[..effective.len()].copy_from_slice(effective); + // Remaining bytes are already zero (null terminator). + Ok(buf) +} + /// 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. +/// VM later. An optional `label` identifies this mapping in the PEB +/// (defaults to the file name if `None`). /// /// # 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 { +/// Returns an error if the file cannot be opened, is empty, the label +/// is too long, or the OS mapping calls fail. +#[instrument(err(Debug), skip(file_path, guest_base, label), parent = Span::current())] +pub(crate) fn prepare_file_cow( + file_path: &Path, + guest_base: u64, + label: Option<&str>, +) -> Result { + // Build the label — default to the file name if not provided. + // Long default labels (from filenames) are truncated with a warning; + // explicitly provided labels that are too long are rejected. + let default_label; + let (label_str, truncate_ok) = match label { + Some(l) => (l, false), + None => { + default_label = file_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + (default_label as &str, true) + } + }; + let label_bytes = build_label(label_str, truncate_ok)?; + // Validate alignment eagerly to fail fast before allocating OS resources. let page_size = page_size::get(); if guest_base as usize % page_size != 0 { @@ -269,6 +337,7 @@ pub(crate) fn prepare_file_cow(file_path: &Path, guest_base: u64) -> Result Result Result { + #[instrument(err(Debug), skip(self, file_path, guest_base, label), parent = Span::current())] + pub fn map_file_cow( + &mut self, + file_path: &Path, + guest_base: u64, + label: Option<&str>, + ) -> Result { if self.poisoned { return Err(crate::HyperlightError::PoisonedSandbox); } + + // Pre-check the file mapping limit before doing any expensive + // OS or VM work. The PEB count is the source of truth. + let current_count = self + .mem_mgr + .shared_mem + .read::(self.mem_mgr.layout.get_file_mappings_size_offset())? + as usize; + if current_count >= hyperlight_common::mem::MAX_FILE_MAPPINGS { + return Err(crate::HyperlightError::Error(format!( + "map_file_cow: file mapping limit reached ({} of {})", + current_count, + hyperlight_common::mem::MAX_FILE_MAPPINGS, + ))); + } + // Phase 1: host-side OS work (open file, create mapping) - let mut prepared = prepare_file_cow(file_path, guest_base)?; + let mut prepared = prepare_file_cow(file_path, guest_base, label)?; + + // Validate that the full mapped range doesn't overlap the + // sandbox's primary shared memory region. + let shared_size = self.mem_mgr.shared_mem.mem_size() as u64; + let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; + let shared_end = base_addr.checked_add(shared_size).ok_or_else(|| { + crate::HyperlightError::Error("shared memory end overflow".to_string()) + })?; + let mapping_end = guest_base + .checked_add(prepared.size as u64) + .ok_or_else(|| { + crate::HyperlightError::Error(format!( + "map_file_cow: guest address overflow: {:#x} + {:#x}", + guest_base, prepared.size + )) + })?; + if guest_base < shared_end && mapping_end > base_addr { + return Err(crate::HyperlightError::Error(format!( + "map_file_cow: mapping [{:#x}..{:#x}) overlaps sandbox shared memory [{:#x}..{:#x})", + guest_base, mapping_end, base_addr, shared_end, + ))); + } // Phase 2: VM-side work (map into guest address space) let region = prepared.to_memory_region()?; + // Check for overlaps with existing file mappings in the VM. + for existing_region in self.vm.get_mapped_regions() { + let ex_start = existing_region.guest_region.start as u64; + let ex_end = existing_region.guest_region.end as u64; + if guest_base < ex_end && mapping_end > ex_start { + return Err(crate::HyperlightError::Error(format!( + "map_file_cow: mapping [{:#x}..{:#x}) overlaps existing mapping [{:#x}..{:#x})", + guest_base, mapping_end, ex_start, ex_end, + ))); + } + } + // Reset snapshot since we are mutating the sandbox state self.snapshot = None; @@ -569,12 +627,25 @@ impl MultiUseSandbox { .map_err(HyperlightVmError::MapRegion) .map_err(crate::HyperlightError::HyperlightVmError)?; - // Successfully mapped — transfer host resource ownership to - // the VM layer. let size = prepared.size as u64; + + // Mark consumed immediately after map_region succeeds. + // On Windows, WhpVm::map_memory copies the file mapping handle + // into its own `file_mappings` vec for cleanup on drop. If we + // deferred mark_consumed(), both PreparedFileMapping::drop and + // WhpVm::drop would release the same handle — a double-close. + // On Linux the hypervisor holds a reference to the host mmap; + // freeing it here would leave a dangling backing. prepared.mark_consumed(); self.mem_mgr.mapped_rgns += 1; + // Record the mapping metadata in the PEB. If this fails the VM + // still holds a valid mapping but the PEB won't list it — the + // limit was already pre-checked above so this should not fail + // in practice. + self.mem_mgr + .write_file_mapping_entry(prepared.guest_base, size, &prepared.label)?; + Ok(size) } @@ -897,7 +968,7 @@ mod tests { #[cfg(target_os = "linux")] { let temp_file = std::env::temp_dir().join("test_poison_map_file.bin"); - let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err(); + let res = sbox.map_file_cow(&temp_file, 0x0, None).unwrap_err(); assert!(matches!(res, HyperlightError::PoisonedSandbox)); std::fs::remove_file(&temp_file).ok(); // Clean up } @@ -1563,7 +1634,7 @@ mod tests { .unwrap(); let guest_base: u64 = 0x1_0000_0000; - let mapped_size = sbox.map_file_cow(&path, guest_base).unwrap(); + let mapped_size = sbox.map_file_cow(&path, guest_base, None).unwrap(); assert!(mapped_size > 0, "mapped_size should be positive"); assert!( mapped_size >= expected.len() as u64, @@ -1603,7 +1674,7 @@ mod tests { .unwrap(); let guest_base: u64 = 0x1_0000_0000; - sbox.map_file_cow(&path, guest_base).unwrap(); + sbox.map_file_cow(&path, guest_base, None).unwrap(); // Writing to the mapped region should fail with MemoryAccessViolation let err = sbox @@ -1643,13 +1714,13 @@ mod tests { assert!(sbox.poisoned()); // map_file_cow should fail with PoisonedSandbox - let err = sbox.map_file_cow(&path, 0x1_0000_0000).unwrap_err(); + let err = sbox.map_file_cow(&path, 0x1_0000_0000, None).unwrap_err(); assert!(matches!(err, HyperlightError::PoisonedSandbox)); // Restore and verify map_file_cow works again sbox.restore(snapshot).unwrap(); assert!(!sbox.poisoned()); - let result = sbox.map_file_cow(&path, 0x1_0000_0000); + let result = sbox.map_file_cow(&path, 0x1_0000_0000, None); assert!(result.is_ok()); let _ = std::fs::remove_file(&path); @@ -1682,8 +1753,8 @@ mod tests { .unwrap(); // Map the same file into both sandboxes - sbox1.map_file_cow(&path, guest_base).unwrap(); - sbox2.map_file_cow(&path, guest_base).unwrap(); + sbox1.map_file_cow(&path, guest_base, None).unwrap(); + sbox2.map_file_cow(&path, guest_base, None).unwrap(); // Both should read the correct content let actual1: Vec = sbox1 @@ -1742,7 +1813,7 @@ mod tests { .unwrap(); let guest_base: u64 = 0x1_0000_0000; - sbox.map_file_cow(&path, guest_base).unwrap(); + sbox.map_file_cow(&path, guest_base, None).unwrap(); let actual: Vec = sbox .call( @@ -1778,7 +1849,7 @@ mod tests { .evolve() .unwrap(); - sbox.map_file_cow(&path, 0x1_0000_0000).unwrap(); + sbox.map_file_cow(&path, 0x1_0000_0000, None).unwrap(); // sandbox dropped here } @@ -1809,7 +1880,7 @@ mod tests { let snapshot1 = sbox.snapshot().unwrap(); // 2. Map the file - sbox.map_file_cow(&path, guest_base).unwrap(); + sbox.map_file_cow(&path, guest_base, None).unwrap(); // Verify we can read it let actual: Vec = sbox @@ -1870,7 +1941,7 @@ mod tests { .unwrap(); let guest_base: u64 = 0x1_0000_0000; - sbox.map_file_cow(&path, guest_base).unwrap(); + sbox.map_file_cow(&path, guest_base, None).unwrap(); // Read the content to verify mapping works let actual: Vec = sbox @@ -1920,7 +1991,7 @@ mod tests { .unwrap(); // Map the file before evolving — this defers the VM-side work. - let mapped_size = u_sbox.map_file_cow(&path, guest_base).unwrap(); + let mapped_size = u_sbox.map_file_cow(&path, guest_base, None).unwrap(); assert!(mapped_size > 0, "mapped_size should be positive"); assert!( mapped_size >= expected.len() as u64, @@ -1965,7 +2036,7 @@ mod tests { ) .unwrap(); - u_sbox.map_file_cow(&path, guest_base).unwrap(); + u_sbox.map_file_cow(&path, guest_base, None).unwrap(); // u_sbox dropped here without evolving — PreparedFileMapping::drop // should clean up host-side OS resources. } @@ -1994,7 +2065,7 @@ mod tests { // 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); + let result = u_sbox.map_file_cow(&path, unaligned_base, None); assert!( result.is_err(), "map_file_cow should reject unaligned guest_base" @@ -2018,9 +2089,446 @@ mod tests { .unwrap(); let guest_base: u64 = 0x1_0000_0000; - let result = u_sbox.map_file_cow(&path, guest_base); + let result = u_sbox.map_file_cow(&path, guest_base, None); assert!(result.is_err(), "map_file_cow should reject empty files"); let _ = std::fs::remove_file(&path); } + + /// Tests that `map_file_cow` with a custom label succeeds. + #[test] + fn test_map_file_cow_custom_label() { + let (path, _) = create_test_file("hyperlight_test_map_file_cow_label.bin", &[0xDD; 4096]); + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let result = sbox.map_file_cow(&path, 0x1_0000_0000, Some("my_ramfs")); + assert!( + result.is_ok(), + "map_file_cow with custom label should succeed" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that `map_file_cow` on a MultiUseSandbox correctly writes + /// the FileMappingInfo entry (count, guest_addr, size, label) into + /// the PEB. + #[test] + fn test_map_file_cow_peb_entry_multiuse() { + use std::mem::offset_of; + + use hyperlight_common::mem::{FILE_MAPPING_LABEL_MAX_LEN, FileMappingInfo}; + + let (path, _) = create_test_file("hyperlight_test_peb_entry_multiuse.bin", &[0xDD; 4096]); + + let guest_base: u64 = 0x1_0000_0000; + let label = "my_ramfs"; + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + // Map with an explicit label. + let mapped_size = sbox.map_file_cow(&path, guest_base, Some(label)).unwrap(); + + // Read back the PEB file_mappings count. + let count = sbox + .mem_mgr + .shared_mem + .read::(sbox.mem_mgr.layout.get_file_mappings_size_offset()) + .unwrap(); + assert_eq!( + count, 1, + "PEB file_mappings count should be 1 after one mapping" + ); + + // Read back the first FileMappingInfo entry. + let entry_offset = sbox.mem_mgr.layout.get_file_mappings_array_offset(); + + let stored_addr = sbox + .mem_mgr + .shared_mem + .read::(entry_offset + offset_of!(FileMappingInfo, guest_addr)) + .unwrap(); + assert_eq!(stored_addr, guest_base, "PEB entry guest_addr should match"); + + let stored_size = sbox + .mem_mgr + .shared_mem + .read::(entry_offset + offset_of!(FileMappingInfo, size)) + .unwrap(); + assert_eq!( + stored_size, mapped_size, + "PEB entry size should match mapped_size" + ); + + // Read back the label bytes and verify. + let label_offset = entry_offset + offset_of!(FileMappingInfo, label); + let mut label_buf = [0u8; FILE_MAPPING_LABEL_MAX_LEN + 1]; + for (i, byte) in label_buf.iter_mut().enumerate() { + *byte = sbox + .mem_mgr + .shared_mem + .read::(label_offset + i) + .unwrap(); + } + let label_len = label_buf + .iter() + .position(|&b| b == 0) + .unwrap_or(label_buf.len()); + let stored_label = std::str::from_utf8(&label_buf[..label_len]).unwrap(); + assert_eq!(stored_label, label, "PEB entry label should match"); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that deferred `map_file_cow` (before evolve) correctly + /// writes FileMappingInfo entries into the PEB during evolve. + #[test] + fn test_map_file_cow_peb_entry_deferred() { + use std::mem::offset_of; + + use hyperlight_common::mem::{FILE_MAPPING_LABEL_MAX_LEN, FileMappingInfo}; + + let (path, _) = create_test_file("hyperlight_test_peb_entry_deferred.bin", &[0xEE; 4096]); + + let guest_base: u64 = 0x1_0000_0000; + let label = "deferred_fs"; + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + let mapped_size = u_sbox.map_file_cow(&path, guest_base, Some(label)).unwrap(); + + // Evolve — PEB entries should be written during this step. + let sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); + + // Read back count. + let count = sbox + .mem_mgr + .shared_mem + .read::(sbox.mem_mgr.layout.get_file_mappings_size_offset()) + .unwrap(); + assert_eq!(count, 1, "PEB file_mappings count should be 1 after evolve"); + + // Read back the entry. + let entry_offset = sbox.mem_mgr.layout.get_file_mappings_array_offset(); + + let stored_addr = sbox + .mem_mgr + .shared_mem + .read::(entry_offset + offset_of!(FileMappingInfo, guest_addr)) + .unwrap(); + assert_eq!(stored_addr, guest_base); + + let stored_size = sbox + .mem_mgr + .shared_mem + .read::(entry_offset + offset_of!(FileMappingInfo, size)) + .unwrap(); + assert_eq!(stored_size, mapped_size); + + // Verify the label. + let label_offset = entry_offset + offset_of!(FileMappingInfo, label); + let mut label_buf = [0u8; FILE_MAPPING_LABEL_MAX_LEN + 1]; + for (i, byte) in label_buf.iter_mut().enumerate() { + *byte = sbox + .mem_mgr + .shared_mem + .read::(label_offset + i) + .unwrap(); + } + let label_len = label_buf + .iter() + .position(|&b| b == 0) + .unwrap_or(label_buf.len()); + let stored_label = std::str::from_utf8(&label_buf[..label_len]).unwrap(); + assert_eq!( + stored_label, label, + "PEB entry label should match after evolve" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that mapping 5 files (3 deferred + 2 post-evolve) correctly + /// populates all PEB FileMappingInfo slots with the right guest_addr, + /// size, and label for each entry. + #[test] + fn test_map_file_cow_peb_multiple_entries() { + use std::mem::{offset_of, size_of}; + + use hyperlight_common::mem::{FILE_MAPPING_LABEL_MAX_LEN, FileMappingInfo}; + + const NUM_FILES: usize = 5; + const DEFERRED_COUNT: usize = 3; + + // Create 5 test files with distinct content. + let mut paths = Vec::new(); + let mut labels: Vec = Vec::new(); + for i in 0..NUM_FILES { + let name = format!("hyperlight_test_peb_multi_{}.bin", i); + let content = vec![i as u8 + 0xA0; 4096]; + let (path, _) = create_test_file(&name, &content); + paths.push(path); + labels.push(format!("file_{}", i)); + } + + // Each file gets a unique guest base, spaced 1 page apart + // (well outside the shared memory region). + let page_size = page_size::get() as u64; + let base: u64 = 0x1_0000_0000; + let guest_bases: Vec = (0..NUM_FILES as u64) + .map(|i| base + i * page_size) + .collect(); + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + // Map 3 files before evolve (deferred path). + let mut mapped_sizes = Vec::new(); + for i in 0..DEFERRED_COUNT { + let size = u_sbox + .map_file_cow(&paths[i], guest_bases[i], Some(&labels[i])) + .unwrap(); + mapped_sizes.push(size); + } + + // Evolve — deferred mappings applied + PEB entries written. + let mut sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); + + // Map 2 more files post-evolve (MultiUseSandbox path). + for i in DEFERRED_COUNT..NUM_FILES { + let size = sbox + .map_file_cow(&paths[i], guest_bases[i], Some(&labels[i])) + .unwrap(); + mapped_sizes.push(size); + } + + // Verify PEB count equals 5. + let count = sbox + .mem_mgr + .shared_mem + .read::(sbox.mem_mgr.layout.get_file_mappings_size_offset()) + .unwrap(); + assert_eq!( + count, NUM_FILES as u64, + "PEB should have {NUM_FILES} entries" + ); + + // Verify each entry's guest_addr, size, and label. + let array_base = sbox.mem_mgr.layout.get_file_mappings_array_offset(); + for i in 0..NUM_FILES { + let entry_offset = array_base + i * size_of::(); + + let stored_addr = sbox + .mem_mgr + .shared_mem + .read::(entry_offset + offset_of!(FileMappingInfo, guest_addr)) + .unwrap(); + assert_eq!( + stored_addr, guest_bases[i], + "Entry {i}: guest_addr mismatch" + ); + + let stored_size = sbox + .mem_mgr + .shared_mem + .read::(entry_offset + offset_of!(FileMappingInfo, size)) + .unwrap(); + assert_eq!(stored_size, mapped_sizes[i], "Entry {i}: size mismatch"); + + // Read and verify the label. + let label_base = entry_offset + offset_of!(FileMappingInfo, label); + let mut label_buf = [0u8; FILE_MAPPING_LABEL_MAX_LEN + 1]; + for (j, byte) in label_buf.iter_mut().enumerate() { + *byte = sbox.mem_mgr.shared_mem.read::(label_base + j).unwrap(); + } + let label_len = label_buf + .iter() + .position(|&b| b == 0) + .unwrap_or(label_buf.len()); + let stored_label = std::str::from_utf8(&label_buf[..label_len]).unwrap(); + assert_eq!(stored_label, labels[i], "Entry {i}: label mismatch"); + } + + // Clean up. + for path in &paths { + let _ = std::fs::remove_file(path); + } + } + + /// Tests that an explicitly provided label exceeding 63 bytes is rejected. + #[test] + fn test_map_file_cow_label_too_long() { + let (path, _) = + create_test_file("hyperlight_test_map_file_cow_long_label.bin", &[0xEE; 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(); + + // A label of exactly 64 bytes exceeds the 63-byte max. + let long_label = "A".repeat(64); + let result = u_sbox.map_file_cow(&path, guest_base, Some(&long_label)); + assert!( + result.is_err(), + "map_file_cow should reject labels longer than 63 bytes" + ); + + // Labels at exactly 63 bytes should be fine. + let ok_label = "B".repeat(63); + let result = u_sbox.map_file_cow(&path, guest_base, Some(&ok_label)); + assert!( + result.is_ok(), + "map_file_cow should accept labels of exactly 63 bytes" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that labels containing null bytes are rejected. + #[test] + fn test_map_file_cow_label_null_byte() { + let (path, _) = + create_test_file("hyperlight_test_map_file_cow_null_label.bin", &[0xFF; 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(); + + let result = u_sbox.map_file_cow(&path, guest_base, Some("has\0null")); + assert!( + result.is_err(), + "map_file_cow should reject labels containing null bytes" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that mapping two files to overlapping GPA ranges is rejected. + #[test] + fn test_map_file_cow_overlapping_mappings() { + let (path1, _) = + create_test_file("hyperlight_test_map_file_cow_overlap1.bin", &[0xAA; 4096]); + let (path2, _) = + create_test_file("hyperlight_test_map_file_cow_overlap2.bin", &[0xBB; 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(); + + // First mapping should succeed. + u_sbox.map_file_cow(&path1, guest_base, None).unwrap(); + + // Second mapping at the same address should fail (overlap). + let result = u_sbox.map_file_cow(&path2, guest_base, None); + assert!( + result.is_err(), + "map_file_cow should reject overlapping guest address ranges" + ); + + let _ = std::fs::remove_file(&path1); + let _ = std::fs::remove_file(&path2); + } + + /// Tests that `map_file_cow` rejects a guest_base that overlaps + /// the sandbox's shared memory region. + #[test] + fn test_map_file_cow_shared_mem_overlap() { + let (path, _) = create_test_file( + "hyperlight_test_map_file_cow_overlap_shm.bin", + &[0xCC; 4096], + ); + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + // Use BASE_ADDRESS itself — smack in the middle of shared memory. + let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; + // page-align it (BASE_ADDRESS is 0x1000, already page-aligned) + let result = u_sbox.map_file_cow(&path, base_addr, None); + assert!( + result.is_err(), + "map_file_cow should reject guest_base inside shared memory" + ); + + let _ = std::fs::remove_file(&path); + } + + /// Tests that exceeding MAX_FILE_MAPPINGS on UninitializedSandbox + /// is rejected at registration time. + #[test] + fn test_map_file_cow_max_limit() { + use hyperlight_common::mem::MAX_FILE_MAPPINGS; + + let mut u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap(); + + let page_size = page_size::get() as u64; + // Base well outside shared memory. + let base: u64 = 0x1_0000_0000; + + // Register MAX_FILE_MAPPINGS files — each needs a distinct file + // and a non-overlapping GPA. + let mut paths = Vec::new(); + for i in 0..MAX_FILE_MAPPINGS { + let name = format!("hyperlight_test_max_limit_{}.bin", i); + let (path, _) = create_test_file(&name, &[0xAA; 4096]); + let guest_base = base + (i as u64) * page_size; + u_sbox.map_file_cow(&path, guest_base, None).unwrap(); + paths.push(path); + } + + // The (MAX_FILE_MAPPINGS + 1)th should fail. + let name = format!("hyperlight_test_max_limit_{}.bin", MAX_FILE_MAPPINGS); + let (path, _) = create_test_file(&name, &[0xBB; 4096]); + let guest_base = base + (MAX_FILE_MAPPINGS as u64) * page_size; + let result = u_sbox.map_file_cow(&path, guest_base, None); + assert!( + result.is_err(), + "map_file_cow should reject after MAX_FILE_MAPPINGS registrations" + ); + + // Clean up. + for p in &paths { + let _ = std::fs::remove_file(p); + } + let _ = std::fs::remove_file(&path); + } } diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index 0d0e045f4..6cbe7921b 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -31,11 +31,9 @@ use crate::func::{ParameterTuple, SupportedReturnType}; use crate::log_build_details; use crate::mem::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionFlags}; use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::shared_mem::ExclusiveSharedMemory; #[cfg(feature = "nanvix-unstable")] use crate::mem::shared_mem::HostSharedMemory; -#[cfg(feature = "nanvix-unstable")] -use crate::mem::shared_mem::SharedMemory; +use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory}; use crate::sandbox::SandboxConfiguration; use crate::{MultiUseSandbox, Result, new_error}; @@ -441,19 +439,89 @@ impl UninitializedSandbox { /// The file mapping is prepared immediately (host-side OS work) but /// the actual VM-side mapping is deferred until [`evolve()`](Self::evolve). /// + /// An optional `label` identifies this mapping in the PEB's + /// `FileMappingInfo` array (max 63 bytes, defaults to the file name). + /// + /// The `guest_base` must be page-aligned and must lie **outside** + /// the sandbox's primary shared memory region (`BASE_ADDRESS` to + /// `BASE_ADDRESS + shared_mem_size`). + /// /// Returns the length of the mapping in bytes. - #[instrument(err(Debug), skip(self, file_path, guest_base), parent = Span::current())] + #[instrument(err(Debug), skip(self, file_path, guest_base, label), parent = Span::current())] pub fn map_file_cow( &mut self, file_path: &std::path::Path, guest_base: u64, + label: Option<&str>, ) -> crate::Result { - let prepared = super::file_mapping::prepare_file_cow(file_path, guest_base)?; + // Fail fast if the preallocated PEB array is already full. + if self.pending_file_mappings.len() >= hyperlight_common::mem::MAX_FILE_MAPPINGS { + return Err(crate::HyperlightError::Error(format!( + "map_file_cow: file mapping limit reached ({} of {})", + self.pending_file_mappings.len(), + hyperlight_common::mem::MAX_FILE_MAPPINGS, + ))); + } + + // Validate that guest_base is outside the sandbox's primary memory slot. + // (Full range check happens after prepare_file_cow when we know the mapped size.) + let shared_size = self.mgr.shared_mem.mem_size() as u64; + let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; + + let prepared = super::file_mapping::prepare_file_cow(file_path, guest_base, label)?; + + // Validate full mapped range doesn't overlap shared memory. + let mapping_end = guest_base + .checked_add(prepared.size as u64) + .ok_or_else(|| { + crate::HyperlightError::Error(format!( + "map_file_cow: guest address overflow: {:#x} + {:#x}", + guest_base, prepared.size + )) + })?; + let shared_end = base_addr.checked_add(shared_size).ok_or_else(|| { + crate::HyperlightError::Error("shared memory end overflow".to_string()) + })?; + if guest_base < shared_end && mapping_end > base_addr { + return Err(crate::HyperlightError::Error(format!( + "map_file_cow: mapping [{:#x}..{:#x}) overlaps sandbox shared memory [{:#x}..{:#x})", + guest_base, mapping_end, base_addr, shared_end, + ))); + } + let size = prepared.size as u64; + + // Check for overlaps with existing pending file mappings. + let new_start = guest_base; + let new_end = mapping_end; + for existing in &self.pending_file_mappings { + let ex_start = existing.guest_base; + let ex_end = ex_start.checked_add(existing.size as u64).ok_or_else(|| { + crate::HyperlightError::Error(format!( + "map_file_cow: existing mapping address overflow: {:#x} + {:#x}", + ex_start, existing.size + )) + })?; + if new_start < ex_end && new_end > ex_start { + return Err(crate::HyperlightError::Error(format!( + "map_file_cow: mapping [{:#x}..{:#x}) overlaps existing mapping [{:#x}..{:#x})", + new_start, new_end, ex_start, ex_end, + ))); + } + } + self.pending_file_mappings.push(prepared); Ok(size) } + /// Returns the total size of the sandbox shared memory region in bytes. + /// + /// This is useful for placing file mappings at guest physical addresses + /// that don't overlap the primary shared memory slot. + pub fn shared_mem_size(&self) -> usize { + self.mgr.shared_mem.mem_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 449f45935..bbb759944 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -99,6 +99,7 @@ pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result Result