Skip to content

Commit 08de4c8

Browse files
committed
Implement map_file_cow so that files can be mapped
before a VM is initialised Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 4843473 commit 08de4c8

7 files changed

Lines changed: 502 additions & 137 deletions

File tree

src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ impl HyperlightVm {
7979
_pml4_addr: u64,
8080
entrypoint: NextAction,
8181
rsp_gva: u64,
82+
page_size: usize,
8283
#[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration,
8384
#[cfg(gdb)] gdb_conn: Option<DebugCommChannel<DebugResponse, DebugMsg>>,
8485
#[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig,
@@ -145,7 +146,7 @@ impl HyperlightVm {
145146
entrypoint,
146147
rsp_gva,
147148
interrupt_handle,
148-
page_size: 0, // Will be set in `initialise`
149+
page_size,
149150

150151
next_slot: scratch_slot + 1,
151152
freed_slots: Vec::new(),
@@ -207,8 +208,6 @@ impl HyperlightVm {
207208
return Ok(());
208209
};
209210

210-
self.page_size = page_size as usize;
211-
212211
let regs = CommonRegisters {
213212
rip: initialise,
214213
// We usually keep the top of the stack 16-byte
@@ -1505,6 +1504,7 @@ mod tests {
15051504
gshm,
15061505
&config,
15071506
stack_top_gva,
1507+
page_size::get(),
15081508
#[cfg(any(crashdump, gdb))]
15091509
rt_cfg,
15101510
crate::mem::exe::LoadInfo::dummy(),

src/hyperlight_host/src/hypervisor/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ pub(crate) mod tests {
493493
gshm,
494494
&config,
495495
exn_stack_top_gva,
496+
page_size::get(),
496497
#[cfg(any(crashdump, gdb))]
497498
rt_cfg,
498499
sandbox.load_info,
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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(unix)]
75+
Unix {
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(unix)]
107+
HostFileResources::Unix {
108+
mmap_base,
109+
mmap_size,
110+
} => unsafe {
111+
libc::munmap(mmap_base, mmap_size);
112+
},
113+
}
114+
}
115+
}
116+
}
117+
118+
// SAFETY: The raw pointers in HostFileResources point to kernel-managed
119+
// mappings (Windows file mapping views / Linux mmap regions), not aliased
120+
// user-allocated heap memory. Ownership is fully contained within the
121+
// struct, and cleanup APIs (UnmapViewOfFile, CloseHandle, munmap) are
122+
// thread-safe.
123+
unsafe impl Send for PreparedFileMapping {}
124+
125+
impl PreparedFileMapping {
126+
/// Build the [`MemoryRegion`] that describes this mapping for the
127+
/// VM layer. The host resources must still be present (not yet
128+
/// consumed).
129+
pub(crate) fn to_memory_region(&self) -> Result<MemoryRegion> {
130+
let resources = self.host_resources.as_ref().ok_or_else(|| {
131+
crate::HyperlightError::Error(
132+
"PreparedFileMapping resources already consumed".to_string(),
133+
)
134+
})?;
135+
136+
match resources {
137+
#[cfg(target_os = "windows")]
138+
HostFileResources::Windows {
139+
mapping_handle,
140+
view_base,
141+
} => {
142+
let host_base = HostRegionBase {
143+
from_handle: *mapping_handle,
144+
handle_base: *view_base as usize,
145+
handle_size: self.size,
146+
offset: 0,
147+
};
148+
let host_end =
149+
<crate::mem::memory_region::HostGuestMemoryRegion as MemoryRegionKind>::add(
150+
host_base, self.size,
151+
);
152+
Ok(MemoryRegion {
153+
host_region: host_base..host_end,
154+
guest_region: self.guest_base as usize..self.guest_base as usize + self.size,
155+
flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
156+
region_type: MemoryRegionType::MappedFile,
157+
})
158+
}
159+
#[cfg(unix)]
160+
HostFileResources::Unix {
161+
mmap_base,
162+
mmap_size,
163+
} => Ok(MemoryRegion {
164+
host_region: *mmap_base as usize..(*mmap_base as usize).wrapping_add(*mmap_size),
165+
guest_region: self.guest_base as usize..self.guest_base as usize + self.size,
166+
flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
167+
region_type: MemoryRegionType::MappedFile,
168+
}),
169+
}
170+
}
171+
172+
/// Mark the host resources as consumed — ownership has been
173+
/// transferred to the VM layer. After this call, `Drop` will
174+
/// not release them.
175+
pub(crate) fn mark_consumed(&mut self) {
176+
self.host_resources = None;
177+
}
178+
}
179+
180+
/// Perform host-side file mapping preparation without requiring a VM.
181+
///
182+
/// Opens the file, creates a read-only mapping in the host process,
183+
/// and returns a [`PreparedFileMapping`] that can be applied to the
184+
/// VM later.
185+
///
186+
/// # Errors
187+
///
188+
/// Returns an error if the file cannot be opened, is empty, or the
189+
/// OS mapping calls fail.
190+
#[instrument(err(Debug), skip(file_path, guest_base), parent = Span::current())]
191+
pub(crate) fn prepare_file_cow(file_path: &Path, guest_base: u64) -> Result<PreparedFileMapping> {
192+
// Validate alignment eagerly to fail fast before allocating OS resources.
193+
let page_size = page_size::get();
194+
if guest_base as usize % page_size != 0 {
195+
log_then_return!(
196+
"map_file_cow: guest_base {:#x} is not page-aligned (page size: {:#x})",
197+
guest_base,
198+
page_size
199+
);
200+
}
201+
202+
#[cfg(target_os = "windows")]
203+
{
204+
use std::os::windows::io::AsRawHandle;
205+
206+
use windows::Win32::Foundation::HANDLE;
207+
use windows::Win32::System::Memory::{
208+
CreateFileMappingW, FILE_MAP_READ, MapViewOfFile, PAGE_READONLY,
209+
};
210+
211+
let file = std::fs::File::options().read(true).open(file_path)?;
212+
let file_size = file.metadata()?.len();
213+
if file_size == 0 {
214+
log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path);
215+
}
216+
let size = usize::try_from(file_size).map_err(|_| {
217+
HyperlightError::Error(format!(
218+
"File size {file_size} exceeds addressable range on this platform"
219+
))
220+
})?;
221+
let size = size.div_ceil(page_size) * page_size;
222+
223+
let file_handle = HANDLE(file.as_raw_handle());
224+
225+
// Create a read-only file mapping object backed by the actual file.
226+
// Pass 0,0 for size to use the file's actual size — Windows will
227+
// NOT extend a read-only file, so requesting page-aligned size
228+
// would fail for files smaller than one page.
229+
let mapping_handle =
230+
unsafe { CreateFileMappingW(file_handle, None, PAGE_READONLY, 0, 0, None) }
231+
.map_err(|e| HyperlightError::Error(format!("CreateFileMappingW failed: {e}")))?;
232+
233+
// Map a read-only view into the host process.
234+
// Passing 0 for dwNumberOfBytesToMap maps the entire file; the OS
235+
// rounds up to the next page boundary and zero-fills the remainder.
236+
let view = unsafe { MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, 0) };
237+
if view.Value.is_null() {
238+
unsafe {
239+
let _ = windows::Win32::Foundation::CloseHandle(mapping_handle);
240+
}
241+
log_then_return!(
242+
"MapViewOfFile failed: {:?}",
243+
std::io::Error::last_os_error()
244+
);
245+
}
246+
247+
Ok(PreparedFileMapping {
248+
guest_base,
249+
size,
250+
host_resources: Some(HostFileResources::Windows {
251+
mapping_handle: HandleWrapper::from(mapping_handle),
252+
view_base: view.Value,
253+
}),
254+
})
255+
}
256+
#[cfg(unix)]
257+
{
258+
use std::os::fd::AsRawFd;
259+
260+
let file = std::fs::File::options().read(true).open(file_path)?;
261+
let file_size = file.metadata()?.len();
262+
if file_size == 0 {
263+
log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path);
264+
}
265+
let size = usize::try_from(file_size).map_err(|_| {
266+
crate::HyperlightError::Error(format!(
267+
"File size {file_size} exceeds addressable range on this platform"
268+
))
269+
})?;
270+
let size = size.div_ceil(page_size) * page_size;
271+
let base = unsafe {
272+
libc::mmap(
273+
std::ptr::null_mut(),
274+
size,
275+
libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
276+
libc::MAP_PRIVATE,
277+
file.as_raw_fd(),
278+
0,
279+
)
280+
};
281+
if base == libc::MAP_FAILED {
282+
log_then_return!("mmap error: {:?}", std::io::Error::last_os_error());
283+
}
284+
285+
Ok(PreparedFileMapping {
286+
guest_base,
287+
size,
288+
host_resources: Some(HostFileResources::Unix {
289+
mmap_base: base,
290+
mmap_size: size,
291+
}),
292+
})
293+
}
294+
}

0 commit comments

Comments
 (0)