Skip to content

Commit 137e964

Browse files
authored
Implement map_file_cow so that files can be mapped (#1320)
before a VM is initialised Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 4843473 commit 137e964

File tree

7 files changed

+532
-137
lines changed

7 files changed

+532
-137
lines changed

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: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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

Comments
 (0)