|
| 1 | +/* |
| 2 | +Copyright 2025 The Hyperlight Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +//! Host-side file mapping preparation for [`map_file_cow`]. |
| 18 | +//! |
| 19 | +//! This module splits the file mapping operation into two phases: |
| 20 | +//! - **Prepare** ([`prepare_file_cow`]): performs host-side OS calls |
| 21 | +//! (open file, create mapping) without requiring a VM. |
| 22 | +//! - **Apply**: performed by the caller (either [`MultiUseSandbox::map_file_cow`] |
| 23 | +//! or [`evolve_impl_multi_use`]) to map the prepared region into |
| 24 | +//! the guest via [`HyperlightVm::map_region`]. |
| 25 | +//! |
| 26 | +//! This separation allows [`UninitializedSandbox`] to accept |
| 27 | +//! `map_file_cow` calls before the VM exists, deferring the VM-side |
| 28 | +//! work until [`evolve()`]. |
| 29 | +
|
| 30 | +use std::ffi::c_void; |
| 31 | +use std::path::Path; |
| 32 | + |
| 33 | +use tracing::{Span, instrument}; |
| 34 | + |
| 35 | +#[cfg(target_os = "windows")] |
| 36 | +use crate::HyperlightError; |
| 37 | +#[cfg(target_os = "windows")] |
| 38 | +use crate::hypervisor::wrappers::HandleWrapper; |
| 39 | +#[cfg(target_os = "windows")] |
| 40 | +use crate::mem::memory_region::{HostRegionBase, MemoryRegionKind}; |
| 41 | +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; |
| 42 | +use crate::{Result, log_then_return}; |
| 43 | + |
| 44 | +/// A prepared (host-side) file mapping ready to be applied to a VM. |
| 45 | +/// |
| 46 | +/// Created by [`prepare_file_cow`]. The host-side OS resources (file |
| 47 | +/// mapping handle + view on Windows, mmap on Linux) are held here |
| 48 | +/// until consumed by the VM-side apply step. |
| 49 | +/// |
| 50 | +/// If dropped without being consumed, the `Drop` impl releases all |
| 51 | +/// host-side resources — preventing leaks when an |
| 52 | +/// [`UninitializedSandbox`] is dropped without evolving or when |
| 53 | +/// apply fails. |
| 54 | +#[must_use = "holds OS resources that leak if discarded — apply to a VM or let Drop clean up"] |
| 55 | +pub(crate) struct PreparedFileMapping { |
| 56 | + /// The guest address where this file should be mapped. |
| 57 | + pub(crate) guest_base: u64, |
| 58 | + /// The page-aligned size of the mapping in bytes. |
| 59 | + pub(crate) size: usize, |
| 60 | + /// Host-side OS resources. `None` after successful consumption |
| 61 | + /// by the apply step (ownership transferred to the VM layer). |
| 62 | + pub(crate) host_resources: Option<HostFileResources>, |
| 63 | +} |
| 64 | + |
| 65 | +/// Platform-specific host-side file mapping resources. |
| 66 | +pub(crate) enum HostFileResources { |
| 67 | + /// Windows: `CreateFileMappingW` handle + `MapViewOfFile` view. |
| 68 | + #[cfg(target_os = "windows")] |
| 69 | + Windows { |
| 70 | + mapping_handle: HandleWrapper, |
| 71 | + view_base: *mut c_void, |
| 72 | + }, |
| 73 | + /// Linux: `mmap` base pointer. |
| 74 | + #[cfg(target_os = "linux")] |
| 75 | + Linux { |
| 76 | + mmap_base: *mut c_void, |
| 77 | + mmap_size: usize, |
| 78 | + }, |
| 79 | +} |
| 80 | + |
| 81 | +impl Drop for PreparedFileMapping { |
| 82 | + fn drop(&mut self) { |
| 83 | + // Clean up host resources if they haven't been consumed. |
| 84 | + if let Some(resources) = self.host_resources.take() { |
| 85 | + match resources { |
| 86 | + #[cfg(target_os = "windows")] |
| 87 | + HostFileResources::Windows { |
| 88 | + mapping_handle, |
| 89 | + view_base, |
| 90 | + } => unsafe { |
| 91 | + use windows::Win32::Foundation::CloseHandle; |
| 92 | + use windows::Win32::System::Memory::{ |
| 93 | + MEMORY_MAPPED_VIEW_ADDRESS, UnmapViewOfFile, |
| 94 | + }; |
| 95 | + if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: view_base }) |
| 96 | + { |
| 97 | + tracing::error!( |
| 98 | + "PreparedFileMapping::drop: UnmapViewOfFile failed: {:?}", |
| 99 | + e |
| 100 | + ); |
| 101 | + } |
| 102 | + if let Err(e) = CloseHandle(mapping_handle.into()) { |
| 103 | + tracing::error!("PreparedFileMapping::drop: CloseHandle failed: {:?}", e); |
| 104 | + } |
| 105 | + }, |
| 106 | + #[cfg(target_os = "linux")] |
| 107 | + HostFileResources::Linux { |
| 108 | + mmap_base, |
| 109 | + mmap_size, |
| 110 | + } => unsafe { |
| 111 | + if libc::munmap(mmap_base, mmap_size) != 0 { |
| 112 | + tracing::error!( |
| 113 | + "PreparedFileMapping::drop: munmap failed: {:?}", |
| 114 | + std::io::Error::last_os_error() |
| 115 | + ); |
| 116 | + } |
| 117 | + }, |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// SAFETY: The raw pointers in HostFileResources point to kernel-managed |
| 124 | +// mappings (Windows file mapping views / Linux mmap regions), not aliased |
| 125 | +// user-allocated heap memory. Ownership is fully contained within the |
| 126 | +// struct, and cleanup APIs (UnmapViewOfFile, CloseHandle, munmap) are |
| 127 | +// thread-safe. |
| 128 | +unsafe impl Send for PreparedFileMapping {} |
| 129 | + |
| 130 | +impl PreparedFileMapping { |
| 131 | + /// Build the [`MemoryRegion`] that describes this mapping for the |
| 132 | + /// VM layer. The host resources must still be present (not yet |
| 133 | + /// consumed). |
| 134 | + pub(crate) fn to_memory_region(&self) -> Result<MemoryRegion> { |
| 135 | + let resources = self.host_resources.as_ref().ok_or_else(|| { |
| 136 | + crate::HyperlightError::Error( |
| 137 | + "PreparedFileMapping resources already consumed".to_string(), |
| 138 | + ) |
| 139 | + })?; |
| 140 | + |
| 141 | + match resources { |
| 142 | + #[cfg(target_os = "windows")] |
| 143 | + HostFileResources::Windows { |
| 144 | + mapping_handle, |
| 145 | + view_base, |
| 146 | + } => { |
| 147 | + let host_base = HostRegionBase { |
| 148 | + from_handle: *mapping_handle, |
| 149 | + handle_base: *view_base as usize, |
| 150 | + handle_size: self.size, |
| 151 | + offset: 0, |
| 152 | + }; |
| 153 | + let host_end = |
| 154 | + <crate::mem::memory_region::HostGuestMemoryRegion as MemoryRegionKind>::add( |
| 155 | + host_base, self.size, |
| 156 | + ); |
| 157 | + let guest_start = self.guest_base as usize; |
| 158 | + let guest_end = guest_start.checked_add(self.size).ok_or_else(|| { |
| 159 | + crate::HyperlightError::Error(format!( |
| 160 | + "guest_region overflow: {:#x} + {:#x}", |
| 161 | + guest_start, self.size |
| 162 | + )) |
| 163 | + })?; |
| 164 | + Ok(MemoryRegion { |
| 165 | + host_region: host_base..host_end, |
| 166 | + guest_region: guest_start..guest_end, |
| 167 | + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, |
| 168 | + region_type: MemoryRegionType::MappedFile, |
| 169 | + }) |
| 170 | + } |
| 171 | + #[cfg(target_os = "linux")] |
| 172 | + HostFileResources::Linux { |
| 173 | + mmap_base, |
| 174 | + mmap_size, |
| 175 | + } => { |
| 176 | + let guest_start = self.guest_base as usize; |
| 177 | + let guest_end = guest_start.checked_add(self.size).ok_or_else(|| { |
| 178 | + crate::HyperlightError::Error(format!( |
| 179 | + "guest_region overflow: {:#x} + {:#x}", |
| 180 | + guest_start, self.size |
| 181 | + )) |
| 182 | + })?; |
| 183 | + Ok(MemoryRegion { |
| 184 | + host_region: *mmap_base as usize |
| 185 | + ..(*mmap_base as usize).wrapping_add(*mmap_size), |
| 186 | + guest_region: guest_start..guest_end, |
| 187 | + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, |
| 188 | + region_type: MemoryRegionType::MappedFile, |
| 189 | + }) |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + /// Mark the host resources as consumed — ownership has been |
| 195 | + /// transferred to the VM layer. After this call, `Drop` will |
| 196 | + /// not release them. |
| 197 | + pub(crate) fn mark_consumed(&mut self) { |
| 198 | + self.host_resources = None; |
| 199 | + } |
| 200 | +} |
| 201 | + |
| 202 | +/// Perform host-side file mapping preparation without requiring a VM. |
| 203 | +/// |
| 204 | +/// Opens the file, creates a read-only mapping in the host process, |
| 205 | +/// and returns a [`PreparedFileMapping`] that can be applied to the |
| 206 | +/// VM later. |
| 207 | +/// |
| 208 | +/// # Errors |
| 209 | +/// |
| 210 | +/// Returns an error if the file cannot be opened, is empty, or the |
| 211 | +/// OS mapping calls fail. |
| 212 | +#[instrument(err(Debug), skip(file_path, guest_base), parent = Span::current())] |
| 213 | +pub(crate) fn prepare_file_cow(file_path: &Path, guest_base: u64) -> Result<PreparedFileMapping> { |
| 214 | + // Validate alignment eagerly to fail fast before allocating OS resources. |
| 215 | + let page_size = page_size::get(); |
| 216 | + if guest_base as usize % page_size != 0 { |
| 217 | + log_then_return!( |
| 218 | + "map_file_cow: guest_base {:#x} is not page-aligned (page size: {:#x})", |
| 219 | + guest_base, |
| 220 | + page_size |
| 221 | + ); |
| 222 | + } |
| 223 | + |
| 224 | + #[cfg(target_os = "windows")] |
| 225 | + { |
| 226 | + use std::os::windows::io::AsRawHandle; |
| 227 | + |
| 228 | + use windows::Win32::Foundation::HANDLE; |
| 229 | + use windows::Win32::System::Memory::{ |
| 230 | + CreateFileMappingW, FILE_MAP_READ, MapViewOfFile, PAGE_READONLY, |
| 231 | + }; |
| 232 | + |
| 233 | + let file = std::fs::File::options().read(true).open(file_path)?; |
| 234 | + let file_size = file.metadata()?.len(); |
| 235 | + if file_size == 0 { |
| 236 | + log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path); |
| 237 | + } |
| 238 | + let size = usize::try_from(file_size).map_err(|_| { |
| 239 | + HyperlightError::Error(format!( |
| 240 | + "File size {file_size} exceeds addressable range on this platform" |
| 241 | + )) |
| 242 | + })?; |
| 243 | + let size = size.div_ceil(page_size) * page_size; |
| 244 | + |
| 245 | + let file_handle = HANDLE(file.as_raw_handle()); |
| 246 | + |
| 247 | + // Create a read-only file mapping object backed by the actual file. |
| 248 | + // Pass 0,0 for size to use the file's actual size — Windows will |
| 249 | + // NOT extend a read-only file, so requesting page-aligned size |
| 250 | + // would fail for files smaller than one page. |
| 251 | + let mapping_handle = |
| 252 | + unsafe { CreateFileMappingW(file_handle, None, PAGE_READONLY, 0, 0, None) } |
| 253 | + .map_err(|e| HyperlightError::Error(format!("CreateFileMappingW failed: {e}")))?; |
| 254 | + |
| 255 | + // Map a read-only view into the host process. |
| 256 | + // Passing 0 for dwNumberOfBytesToMap maps the entire file; the OS |
| 257 | + // rounds up to the next page boundary and zero-fills the remainder. |
| 258 | + let view = unsafe { MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, 0) }; |
| 259 | + if view.Value.is_null() { |
| 260 | + unsafe { |
| 261 | + let _ = windows::Win32::Foundation::CloseHandle(mapping_handle); |
| 262 | + } |
| 263 | + log_then_return!( |
| 264 | + "MapViewOfFile failed: {:?}", |
| 265 | + std::io::Error::last_os_error() |
| 266 | + ); |
| 267 | + } |
| 268 | + |
| 269 | + Ok(PreparedFileMapping { |
| 270 | + guest_base, |
| 271 | + size, |
| 272 | + host_resources: Some(HostFileResources::Windows { |
| 273 | + mapping_handle: HandleWrapper::from(mapping_handle), |
| 274 | + view_base: view.Value, |
| 275 | + }), |
| 276 | + }) |
| 277 | + } |
| 278 | + #[cfg(unix)] |
| 279 | + { |
| 280 | + use std::os::fd::AsRawFd; |
| 281 | + |
| 282 | + let file = std::fs::File::options().read(true).open(file_path)?; |
| 283 | + let file_size = file.metadata()?.len(); |
| 284 | + if file_size == 0 { |
| 285 | + log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path); |
| 286 | + } |
| 287 | + let size = usize::try_from(file_size).map_err(|_| { |
| 288 | + crate::HyperlightError::Error(format!( |
| 289 | + "File size {file_size} exceeds addressable range on this platform" |
| 290 | + )) |
| 291 | + })?; |
| 292 | + let size = size.div_ceil(page_size) * page_size; |
| 293 | + let base = unsafe { |
| 294 | + // MSHV's map_user_memory requires host-writable pages (the |
| 295 | + // kernel module calls get_user_pages with write access). |
| 296 | + // KVM's KVM_MEM_READONLY slots work with read-only host pages. |
| 297 | + // PROT_EXEC is never needed — the hypervisor backs guest R+X |
| 298 | + // pages without requiring host-side execute permission. |
| 299 | + #[cfg(mshv3)] |
| 300 | + let prot = libc::PROT_READ | libc::PROT_WRITE; |
| 301 | + #[cfg(not(mshv3))] |
| 302 | + let prot = libc::PROT_READ; |
| 303 | + |
| 304 | + libc::mmap( |
| 305 | + std::ptr::null_mut(), |
| 306 | + size, |
| 307 | + prot, |
| 308 | + libc::MAP_PRIVATE, |
| 309 | + file.as_raw_fd(), |
| 310 | + 0, |
| 311 | + ) |
| 312 | + }; |
| 313 | + if base == libc::MAP_FAILED { |
| 314 | + log_then_return!("mmap error: {:?}", std::io::Error::last_os_error()); |
| 315 | + } |
| 316 | + |
| 317 | + Ok(PreparedFileMapping { |
| 318 | + guest_base, |
| 319 | + size, |
| 320 | + host_resources: Some(HostFileResources::Linux { |
| 321 | + mmap_base: base, |
| 322 | + mmap_size: size, |
| 323 | + }), |
| 324 | + }) |
| 325 | + } |
| 326 | +} |
0 commit comments