Skip to content

Commit a7decbe

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 a7decbe

File tree

7 files changed

+517
-137
lines changed

7 files changed

+517
-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: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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+
Ok(MemoryRegion {
158+
host_region: host_base..host_end,
159+
guest_region: self.guest_base as usize..self.guest_base as usize + self.size,
160+
flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
161+
region_type: MemoryRegionType::MappedFile,
162+
})
163+
}
164+
#[cfg(target_os = "linux")]
165+
HostFileResources::Linux {
166+
mmap_base,
167+
mmap_size,
168+
} => Ok(MemoryRegion {
169+
host_region: *mmap_base as usize..(*mmap_base as usize).wrapping_add(*mmap_size),
170+
guest_region: self.guest_base as usize..self.guest_base as usize + self.size,
171+
flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
172+
region_type: MemoryRegionType::MappedFile,
173+
}),
174+
}
175+
}
176+
177+
/// Mark the host resources as consumed — ownership has been
178+
/// transferred to the VM layer. After this call, `Drop` will
179+
/// not release them.
180+
pub(crate) fn mark_consumed(&mut self) {
181+
self.host_resources = None;
182+
}
183+
}
184+
185+
/// Perform host-side file mapping preparation without requiring a VM.
186+
///
187+
/// Opens the file, creates a read-only mapping in the host process,
188+
/// and returns a [`PreparedFileMapping`] that can be applied to the
189+
/// VM later.
190+
///
191+
/// # Errors
192+
///
193+
/// Returns an error if the file cannot be opened, is empty, or the
194+
/// OS mapping calls fail.
195+
#[instrument(err(Debug), skip(file_path, guest_base), parent = Span::current())]
196+
pub(crate) fn prepare_file_cow(file_path: &Path, guest_base: u64) -> Result<PreparedFileMapping> {
197+
// Validate alignment eagerly to fail fast before allocating OS resources.
198+
let page_size = page_size::get();
199+
if guest_base as usize % page_size != 0 {
200+
log_then_return!(
201+
"map_file_cow: guest_base {:#x} is not page-aligned (page size: {:#x})",
202+
guest_base,
203+
page_size
204+
);
205+
}
206+
207+
#[cfg(target_os = "windows")]
208+
{
209+
use std::os::windows::io::AsRawHandle;
210+
211+
use windows::Win32::Foundation::HANDLE;
212+
use windows::Win32::System::Memory::{
213+
CreateFileMappingW, FILE_MAP_READ, MapViewOfFile, PAGE_READONLY,
214+
};
215+
216+
let file = std::fs::File::options().read(true).open(file_path)?;
217+
let file_size = file.metadata()?.len();
218+
if file_size == 0 {
219+
log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path);
220+
}
221+
let size = usize::try_from(file_size).map_err(|_| {
222+
HyperlightError::Error(format!(
223+
"File size {file_size} exceeds addressable range on this platform"
224+
))
225+
})?;
226+
let size = size.div_ceil(page_size) * page_size;
227+
228+
let file_handle = HANDLE(file.as_raw_handle());
229+
230+
// Create a read-only file mapping object backed by the actual file.
231+
// Pass 0,0 for size to use the file's actual size — Windows will
232+
// NOT extend a read-only file, so requesting page-aligned size
233+
// would fail for files smaller than one page.
234+
let mapping_handle =
235+
unsafe { CreateFileMappingW(file_handle, None, PAGE_READONLY, 0, 0, None) }
236+
.map_err(|e| HyperlightError::Error(format!("CreateFileMappingW failed: {e}")))?;
237+
238+
// Map a read-only view into the host process.
239+
// Passing 0 for dwNumberOfBytesToMap maps the entire file; the OS
240+
// rounds up to the next page boundary and zero-fills the remainder.
241+
let view = unsafe { MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, 0) };
242+
if view.Value.is_null() {
243+
unsafe {
244+
let _ = windows::Win32::Foundation::CloseHandle(mapping_handle);
245+
}
246+
log_then_return!(
247+
"MapViewOfFile failed: {:?}",
248+
std::io::Error::last_os_error()
249+
);
250+
}
251+
252+
Ok(PreparedFileMapping {
253+
guest_base,
254+
size,
255+
host_resources: Some(HostFileResources::Windows {
256+
mapping_handle: HandleWrapper::from(mapping_handle),
257+
view_base: view.Value,
258+
}),
259+
})
260+
}
261+
#[cfg(unix)]
262+
{
263+
use std::os::fd::AsRawFd;
264+
265+
let file = std::fs::File::options().read(true).open(file_path)?;
266+
let file_size = file.metadata()?.len();
267+
if file_size == 0 {
268+
log_then_return!("map_file_cow: cannot map an empty file: {:?}", file_path);
269+
}
270+
let size = usize::try_from(file_size).map_err(|_| {
271+
crate::HyperlightError::Error(format!(
272+
"File size {file_size} exceeds addressable range on this platform"
273+
))
274+
})?;
275+
let size = size.div_ceil(page_size) * page_size;
276+
let base = unsafe {
277+
// MSHV's map_user_memory requires host-writable pages (the
278+
// kernel module calls get_user_pages with write access).
279+
// KVM's KVM_MEM_READONLY slots work with read-only host pages.
280+
// PROT_EXEC is never needed — the hypervisor backs guest R+X
281+
// pages without requiring host-side execute permission.
282+
#[cfg(mshv3)]
283+
let prot = libc::PROT_READ | libc::PROT_WRITE;
284+
#[cfg(not(mshv3))]
285+
let prot = libc::PROT_READ;
286+
287+
libc::mmap(
288+
std::ptr::null_mut(),
289+
size,
290+
prot,
291+
libc::MAP_PRIVATE,
292+
file.as_raw_fd(),
293+
0,
294+
)
295+
};
296+
if base == libc::MAP_FAILED {
297+
log_then_return!("mmap error: {:?}", std::io::Error::last_os_error());
298+
}
299+
300+
Ok(PreparedFileMapping {
301+
guest_base,
302+
size,
303+
host_resources: Some(HostFileResources::Linux {
304+
mmap_base: base,
305+
mmap_size: size,
306+
}),
307+
})
308+
}
309+
}

0 commit comments

Comments
 (0)