Skip to content

Commit ea6c44b

Browse files
committed
Add file backed ReadonlySharedMemory
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent b45a3f2 commit ea6c44b

2 files changed

Lines changed: 256 additions & 1 deletion

File tree

src/hyperlight_host/src/mem/memory_region.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ impl MemoryRegionType {
158158
/// shared memory mapping with guard pages.
159159
pub fn surrogate_mapping(&self) -> SurrogateMapping {
160160
match self {
161-
MemoryRegionType::MappedFile => SurrogateMapping::ReadOnlyFile,
161+
MemoryRegionType::MappedFile | MemoryRegionType::Snapshot => {
162+
SurrogateMapping::ReadOnlyFile
163+
}
162164
_ => SurrogateMapping::SandboxMemory,
163165
}
164166
}

src/hyperlight_host/src/mem/shared_mem.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,14 @@ pub struct ReadonlySharedMemory {
20122012
/// by `mapping_at`. If `None`, the full `mem_size()` is mapped.
20132013
#[cfg_attr(unshared_snapshot_mem, allow(dead_code))]
20142014
guest_mapped_size: Option<usize>,
2015+
/// Size of the leading guard region (the bytes between
2016+
/// `region.ptr` and the start of the usable memory). For most
2017+
/// constructors this is exactly `PAGE_SIZE_USIZE`. The Windows
2018+
/// `from_file` path can use a larger leading guard when the
2019+
/// snapshot file's `memory_offset` exceeds one page (which
2020+
/// happens whenever the file carries host-function metadata
2021+
/// before the memory blob).
2022+
leading_guard_size: usize,
20152023
}
20162024
// Safety: HostMapping is only non-Send/Sync (causing
20172025
// ReadonlySharedMemory to not be automatically Send/Sync) because raw
@@ -2033,6 +2041,7 @@ impl ReadonlySharedMemory {
20332041
Ok(ReadonlySharedMemory {
20342042
region: anon.region,
20352043
guest_mapped_size: None,
2044+
leading_guard_size: PAGE_SIZE_USIZE,
20362045
})
20372046
}
20382047

@@ -2045,6 +2054,7 @@ impl ReadonlySharedMemory {
20452054
Ok(ReadonlySharedMemory {
20462055
region: anon.region,
20472056
guest_mapped_size: Some(guest_mapped_size),
2057+
leading_guard_size: PAGE_SIZE_USIZE,
20482058
})
20492059
}
20502060

@@ -2055,6 +2065,223 @@ impl ReadonlySharedMemory {
20552065
self.guest_mapped_size.unwrap_or_else(|| self.mem_size())
20562066
}
20572067

2068+
/// Create a `ReadonlySharedMemory` backed by a file on disk.
2069+
///
2070+
/// Only the `len` bytes at `[offset..offset+len)` (the memory
2071+
/// blob) are exposed via `base_ptr()` and `mem_size()`.
2072+
///
2073+
/// `[offset..offset+len)` is surrounded by guard regions on the
2074+
/// host.
2075+
pub(crate) fn from_file(
2076+
file: &std::fs::File,
2077+
offset: usize,
2078+
len: usize,
2079+
guest_mapped_size: Option<usize>,
2080+
) -> Result<Self> {
2081+
if len == 0 {
2082+
return Err(new_error!(
2083+
"Cannot create file-backed shared memory with size 0"
2084+
));
2085+
}
2086+
2087+
if offset == 0 || offset % PAGE_SIZE_USIZE != 0 {
2088+
return Err(new_error!(
2089+
"snapshot file offset {} must be a non-zero multiple of PAGE_SIZE",
2090+
offset
2091+
));
2092+
}
2093+
2094+
#[cfg(target_os = "linux")]
2095+
{
2096+
Self::from_file_linux(file, offset, len, guest_mapped_size)
2097+
}
2098+
#[cfg(target_os = "windows")]
2099+
{
2100+
Self::from_file_windows(file, offset, len, guest_mapped_size)
2101+
}
2102+
}
2103+
2104+
#[cfg(target_os = "linux")]
2105+
fn from_file_linux(
2106+
file: &std::fs::File,
2107+
offset: usize,
2108+
len: usize,
2109+
guest_mapped_size: Option<usize>,
2110+
) -> Result<Self> {
2111+
use std::ffi::c_void;
2112+
use std::os::unix::io::AsRawFd;
2113+
2114+
use libc::{
2115+
MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_NORESERVE, MAP_PRIVATE, PROT_NONE, PROT_READ,
2116+
PROT_WRITE, mmap, off_t, size_t,
2117+
};
2118+
2119+
let total_size = len.checked_add(2 * PAGE_SIZE_USIZE).ok_or_else(|| {
2120+
new_error!("Memory required for file-backed snapshot exceeded usize::MAX")
2121+
})?;
2122+
2123+
let fd = file.as_raw_fd();
2124+
let offset: off_t = offset
2125+
.try_into()
2126+
.map_err(|_| new_error!("snapshot file offset {} exceeds off_t range", offset))?;
2127+
2128+
// Allocate the full region (guard + usable + guard) as anonymous
2129+
let base = unsafe {
2130+
mmap(
2131+
null_mut(),
2132+
total_size as size_t,
2133+
PROT_NONE,
2134+
MAP_ANONYMOUS | MAP_PRIVATE | MAP_NORESERVE,
2135+
-1,
2136+
0 as off_t,
2137+
)
2138+
};
2139+
if base == MAP_FAILED {
2140+
return Err(HyperlightError::MmapFailed(
2141+
std::io::Error::last_os_error().raw_os_error(),
2142+
));
2143+
}
2144+
2145+
// Map the file content over the usable portion (between guard pages).
2146+
// PROT_READ | PROT_WRITE: KVM/MSHV require writable host mappings
2147+
// to handle copy-on-write page faults from the guest.
2148+
// MAP_PRIVATE: writes go to private copies, not the file.
2149+
let usable_ptr = unsafe { (base as *mut u8).add(PAGE_SIZE_USIZE) };
2150+
let mapped = unsafe {
2151+
mmap(
2152+
usable_ptr as *mut c_void,
2153+
len as size_t,
2154+
PROT_READ | PROT_WRITE,
2155+
MAP_PRIVATE | MAP_FIXED | MAP_NORESERVE,
2156+
fd,
2157+
offset,
2158+
)
2159+
};
2160+
if mapped == MAP_FAILED {
2161+
unsafe { libc::munmap(base, total_size as size_t) };
2162+
return Err(HyperlightError::MmapFailed(
2163+
std::io::Error::last_os_error().raw_os_error(),
2164+
));
2165+
}
2166+
2167+
// Guard pages at base and base+total_size-PAGE_SIZE are already
2168+
// PROT_NONE from the anonymous mapping; MAP_FIXED only replaced
2169+
// the middle portion.
2170+
2171+
#[allow(clippy::arc_with_non_send_sync)]
2172+
Ok(ReadonlySharedMemory {
2173+
region: Arc::new(HostMapping {
2174+
ptr: base as *mut u8,
2175+
size: total_size,
2176+
}),
2177+
guest_mapped_size,
2178+
leading_guard_size: PAGE_SIZE_USIZE,
2179+
})
2180+
}
2181+
2182+
/// Windows file mappings must start at file offset 0 and cannot
2183+
/// extend beyond the file's size, so the view covers
2184+
/// `[0 .. offset + len + PAGE_SIZE)`. The leading `offset` bytes
2185+
/// (header plus any host function metadata) become the leading
2186+
/// guard, recorded in `leading_guard_size`. The trailing
2187+
/// `PAGE_SIZE` bytes (written explicitly by `to_file`) become
2188+
/// the trailing guard. Both ends are protected with
2189+
/// `VirtualProtect(PAGE_NOACCESS)`.
2190+
#[cfg(target_os = "windows")]
2191+
fn from_file_windows(
2192+
file: &std::fs::File,
2193+
offset: usize,
2194+
len: usize,
2195+
guest_mapped_size: Option<usize>,
2196+
) -> Result<Self> {
2197+
use std::os::windows::io::AsRawHandle;
2198+
2199+
use windows::Win32::Foundation::HANDLE;
2200+
use windows::Win32::System::Memory::{
2201+
CreateFileMappingA, FILE_MAP_READ, MapViewOfFile, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS,
2202+
PAGE_READONLY, VirtualProtect,
2203+
};
2204+
use windows::core::PCSTR;
2205+
2206+
let leading_guard_size = offset;
2207+
let total_size = leading_guard_size
2208+
.checked_add(len)
2209+
.and_then(|n| n.checked_add(PAGE_SIZE_USIZE))
2210+
.ok_or_else(|| {
2211+
new_error!("Memory required for file-backed snapshot exceeded usize::MAX")
2212+
})?;
2213+
debug_assert!(leading_guard_size >= PAGE_SIZE_USIZE);
2214+
debug_assert!(leading_guard_size % PAGE_SIZE_USIZE == 0);
2215+
2216+
let file_handle = HANDLE(file.as_raw_handle());
2217+
2218+
// Create a read-only file mapping at the exact file size (pass 0,0).
2219+
// The file includes trailing PAGE_SIZE padding written by to_file(),
2220+
// so the file is at least leading_guard_size + len + PAGE_SIZE bytes.
2221+
let handle =
2222+
unsafe { CreateFileMappingA(file_handle, None, PAGE_READONLY, 0, 0, PCSTR::null())? };
2223+
2224+
if handle.is_invalid() {
2225+
log_then_return!(HyperlightError::MemoryAllocationFailed(
2226+
Error::last_os_error().raw_os_error()
2227+
));
2228+
}
2229+
2230+
// Map exactly total_size (leading region + blob + trailing padding) bytes.
2231+
let addr = unsafe { MapViewOfFile(handle, FILE_MAP_READ, 0, 0, total_size) };
2232+
if addr.Value.is_null() {
2233+
unsafe {
2234+
let _ = windows::Win32::Foundation::CloseHandle(handle);
2235+
}
2236+
log_then_return!(HyperlightError::MemoryAllocationFailed(
2237+
Error::last_os_error().raw_os_error()
2238+
));
2239+
}
2240+
2241+
#[allow(clippy::arc_with_non_send_sync)]
2242+
let region = Arc::new(HostMapping {
2243+
ptr: addr.Value as *mut u8,
2244+
size: total_size,
2245+
handle,
2246+
});
2247+
2248+
// Set guard pages on both ends.
2249+
let mut unused_old_prot = PAGE_PROTECTION_FLAGS(0);
2250+
2251+
// Leading guard: covers the fixed header and any host-function
2252+
// metadata
2253+
let first_guard = addr.Value;
2254+
if let Err(e) = unsafe {
2255+
VirtualProtect(
2256+
first_guard,
2257+
leading_guard_size,
2258+
PAGE_NOACCESS,
2259+
&mut unused_old_prot,
2260+
)
2261+
} {
2262+
log_then_return!(WindowsAPIError(e.clone()));
2263+
}
2264+
2265+
// Trailing guard: the explicit PAGE_SIZE padding at the end of the file.
2266+
let last_guard = unsafe { first_guard.add(total_size - PAGE_SIZE_USIZE) };
2267+
if let Err(e) = unsafe {
2268+
VirtualProtect(
2269+
last_guard,
2270+
PAGE_SIZE_USIZE,
2271+
PAGE_NOACCESS,
2272+
&mut unused_old_prot,
2273+
)
2274+
} {
2275+
log_then_return!(WindowsAPIError(e.clone()));
2276+
}
2277+
2278+
Ok(ReadonlySharedMemory {
2279+
region,
2280+
guest_mapped_size,
2281+
leading_guard_size,
2282+
})
2283+
}
2284+
20582285
pub(crate) fn as_slice(&self) -> &[u8] {
20592286
unsafe { std::slice::from_raw_parts(self.base_ptr(), self.mem_size()) }
20602287
}
@@ -2098,6 +2325,32 @@ impl SharedMemory for ReadonlySharedMemory {
20982325
fn region(&self) -> &HostMapping {
20992326
&self.region
21002327
}
2328+
// Override the default trait accessors to use the variable-sized
2329+
// leading guard. The trailing guard is always `PAGE_SIZE_USIZE`.
2330+
fn base_addr(&self) -> usize {
2331+
self.region().ptr as usize + self.leading_guard_size
2332+
}
2333+
fn base_ptr(&self) -> *mut u8 {
2334+
self.region().ptr.wrapping_add(self.leading_guard_size)
2335+
}
2336+
fn mem_size(&self) -> usize {
2337+
self.region().size - self.leading_guard_size - PAGE_SIZE_USIZE
2338+
}
2339+
fn host_region_base(&self) -> <HostGuestMemoryRegion as MemoryRegionKind>::HostBaseType {
2340+
#[cfg(not(windows))]
2341+
{
2342+
self.base_addr()
2343+
}
2344+
#[cfg(windows)]
2345+
{
2346+
super::memory_region::HostRegionBase {
2347+
from_handle: self.region().handle.into(),
2348+
handle_base: self.region().ptr as usize,
2349+
handle_size: self.region().size,
2350+
offset: self.leading_guard_size,
2351+
}
2352+
}
2353+
}
21012354
// There's no way to get exclusive (and therefore writable) access
21022355
// to a ReadonlySharedMemory.
21032356
fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(

0 commit comments

Comments
 (0)