-
Notifications
You must be signed in to change notification settings - Fork 163
Implement map_file_cow before a sandbox is initialised #1320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
simongdavies
merged 1 commit into
hyperlight-dev:main
from
simongdavies:prepare-map-file-cow
Mar 18, 2026
+532
−137
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HostFileResources>, | ||
| } | ||
|
|
||
| /// 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<MemoryRegion> { | ||
| 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 = | ||
| <crate::mem::memory_region::HostGuestMemoryRegion as MemoryRegionKind>::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<PreparedFileMapping> { | ||
| // 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, | ||
| ) | ||
simongdavies marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
| 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, | ||
| }), | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.