Skip to content

Commit 7f499c8

Browse files
committed
Snapshot persistence to disk
Save snapshots to disk via Snapshot::to_file(), load them back via Snapshot::from_file(). Create sandboxes directly from loaded snapshots via MultiUseSandbox::from_snapshot(), bypassing ELF parsing and guest init. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 35dae5b commit 7f499c8

File tree

9 files changed

+1560
-20
lines changed

9 files changed

+1560
-20
lines changed

src/hyperlight_host/benches/benchmarks.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,93 @@ fn shared_memory_benchmark(c: &mut Criterion) {
556556
group.finish();
557557
}
558558

559+
// ============================================================================
560+
// Benchmark Category: Snapshot Files
561+
// ============================================================================
562+
563+
fn snapshot_file_benchmark(c: &mut Criterion) {
564+
use hyperlight_host::sandbox::snapshot::Snapshot;
565+
566+
let mut group = c.benchmark_group("snapshot_files");
567+
568+
// Pre-create snapshot files for all sizes
569+
let dirs: Vec<_> = SandboxSize::all()
570+
.iter()
571+
.map(|size| {
572+
let dir = tempfile::tempdir().unwrap();
573+
let snap_path = dir.path().join(format!("{}.hls", size.name()));
574+
let snapshot = {
575+
let mut sbox = create_multiuse_sandbox_with_size(*size);
576+
sbox.snapshot().unwrap()
577+
};
578+
snapshot.to_file(&snap_path).unwrap();
579+
(dir, snapshot)
580+
})
581+
.collect();
582+
583+
// Benchmark: save_snapshot
584+
for (i, size) in SandboxSize::all().iter().enumerate() {
585+
let snap_dir = tempfile::tempdir().unwrap();
586+
let path = snap_dir.path().join("bench.hls");
587+
let snapshot = &dirs[i].1;
588+
group.bench_function(format!("save_snapshot/{}", size.name()), |b| {
589+
b.iter(|| {
590+
snapshot.to_file(&path).unwrap();
591+
});
592+
});
593+
}
594+
595+
// Benchmark: load_snapshot (mmap + header parse + hash verify)
596+
for (i, size) in SandboxSize::all().iter().enumerate() {
597+
let snap_path = dirs[i].0.path().join(format!("{}.hls", size.name()));
598+
group.bench_function(format!("load_snapshot/{}", size.name()), |b| {
599+
b.iter(|| {
600+
let _ = Snapshot::from_file(&snap_path).unwrap();
601+
});
602+
});
603+
}
604+
605+
// Benchmark: cold_start_via_evolve (new + evolve + call)
606+
for size in SandboxSize::all() {
607+
group.bench_function(format!("cold_start_via_evolve/{}", size.name()), |b| {
608+
b.iter(|| {
609+
let mut sbox = create_multiuse_sandbox_with_size(size);
610+
sbox.call::<String>("Echo", "hello\n".to_string()).unwrap();
611+
});
612+
});
613+
}
614+
615+
// Benchmark: cold_start_via_snapshot (load + from_snapshot + call)
616+
for (i, size) in SandboxSize::all().iter().enumerate() {
617+
let snap_path = dirs[i].0.path().join(format!("{}.hls", size.name()));
618+
group.bench_function(format!("cold_start_via_snapshot/{}", size.name()), |b| {
619+
b.iter(|| {
620+
let loaded = Snapshot::from_file(&snap_path).unwrap();
621+
let mut sbox = MultiUseSandbox::from_snapshot(std::sync::Arc::new(loaded)).unwrap();
622+
sbox.call::<String>("Echo", "hello\n".to_string()).unwrap();
623+
});
624+
});
625+
}
626+
627+
// Benchmark: cold_start_via_snapshot_unchecked (no hash verify)
628+
for (i, size) in SandboxSize::all().iter().enumerate() {
629+
let snap_path = dirs[i].0.path().join(format!("{}.hls", size.name()));
630+
group.bench_function(
631+
format!("cold_start_via_snapshot_unchecked/{}", size.name()),
632+
|b| {
633+
b.iter(|| {
634+
let loaded = Snapshot::from_file_unchecked(&snap_path).unwrap();
635+
let mut sbox =
636+
MultiUseSandbox::from_snapshot(std::sync::Arc::new(loaded)).unwrap();
637+
sbox.call::<String>("Echo", "hello\n".to_string()).unwrap();
638+
});
639+
},
640+
);
641+
}
642+
643+
group.finish();
644+
}
645+
559646
criterion_group! {
560647
name = benches;
561648
config = Criterion::default();
@@ -566,6 +653,7 @@ criterion_group! {
566653
guest_call_benchmark_large_param,
567654
function_call_serialization_benchmark,
568655
sample_workloads_benchmark,
569-
shared_memory_benchmark
656+
shared_memory_benchmark,
657+
snapshot_file_benchmark
570658
}
571659
criterion_main!(benches);

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,17 @@ impl HyperlightVm {
352352
self.vm.set_debug_regs(&CommonDebugRegs::default())?;
353353
self.vm.reset_xsave()?;
354354

355+
self.apply_sregs(cr3, sregs)
356+
}
357+
358+
/// Apply special registers and mark TLB for flush. Used by both
359+
/// `reset_vcpu` (which also resets GPRs/debug/FPU) and
360+
/// `from_snapshot` (which skips the redundant resets).
361+
pub(crate) fn apply_sregs(
362+
&mut self,
363+
cr3: u64,
364+
sregs: &CommonSpecialRegisters,
365+
) -> std::result::Result<(), RegisterError> {
355366
#[cfg(not(feature = "nanvix-unstable"))]
356367
{
357368
// Restore the full special registers from snapshot, but update CR3

src/hyperlight_host/src/mem/layout.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,16 @@ pub(crate) struct SandboxMemoryLayout {
225225
/// The size of the guest code section.
226226
pub(crate) code_size: usize,
227227
/// The size of the init data section (guest blob).
228-
init_data_size: usize,
228+
pub(crate) init_data_size: usize,
229229
/// Permission flags for the init data region.
230230
#[cfg_attr(feature = "nanvix-unstable", allow(unused))]
231-
init_data_permissions: Option<MemoryRegionFlags>,
231+
pub(crate) init_data_permissions: Option<MemoryRegionFlags>,
232232
/// The size of the scratch region in physical memory.
233-
scratch_size: usize,
233+
pub(crate) scratch_size: usize,
234234
/// The size of the snapshot region in physical memory.
235-
snapshot_size: usize,
235+
pub(crate) snapshot_size: usize,
236236
/// The size of the page tables (None if not yet set).
237-
pt_size: Option<usize>,
237+
pub(crate) pt_size: Option<usize>,
238238
}
239239

240240
impl SandboxMemoryLayout {
@@ -394,12 +394,6 @@ impl SandboxMemoryLayout {
394394
self.peb_file_mappings_offset()
395395
}
396396

397-
/// Get the offset in guest memory to the file_mappings pointer field.
398-
#[cfg(feature = "nanvix-unstable")]
399-
fn get_file_mappings_pointer_offset(&self) -> usize {
400-
self.get_file_mappings_size_offset() + size_of::<u64>()
401-
}
402-
403397
/// Get the offset in snapshot memory where the FileMappingInfo array starts
404398
/// (immediately after the PEB struct, within the same page).
405399
#[cfg(feature = "nanvix-unstable")]

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: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,6 +2029,221 @@ impl ReadonlySharedMemory {
20292029
})
20302030
}
20312031

2032+
/// Create a `ReadonlySharedMemory` backed by a file on disk.
2033+
///
2034+
/// The memory blob at `[offset..offset+len)` in the file is mapped
2035+
/// with copy-on-write semantics so that guest writes trigger
2036+
/// per-page CoW faults without modifying the backing file.
2037+
///
2038+
/// The mapping is surrounded by guard pages, matching the
2039+
/// `ExclusiveSharedMemory::new` layout, so that `base_ptr()` and
2040+
/// `mem_size()` return the correct values via the `SharedMemory`
2041+
/// trait.
2042+
///
2043+
/// - **Linux**: `mmap(MAP_PRIVATE)` for zero-copy file-backed CoW.
2044+
/// - **Windows**: `CreateFileMappingA(PAGE_WRITECOPY)` +
2045+
/// `MapViewOfFile(FILE_MAP_COPY)` for zero-copy file-backed CoW.
2046+
/// The returned `HostMapping` carries the file mapping handle so
2047+
/// the surrogate process can create its own view via
2048+
/// `MapViewOfFileNuma2`.
2049+
pub(crate) fn from_file(file: &std::fs::File, offset: u64, len: usize) -> Result<Self> {
2050+
if len == 0 {
2051+
return Err(new_error!(
2052+
"Cannot create file-backed shared memory with size 0"
2053+
));
2054+
}
2055+
2056+
let total_size = len.checked_add(2 * PAGE_SIZE_USIZE).ok_or_else(|| {
2057+
new_error!("Memory required for file-backed snapshot exceeded usize::MAX")
2058+
})?;
2059+
2060+
#[cfg(target_os = "linux")]
2061+
{
2062+
Self::from_file_linux(file, offset, len, total_size)
2063+
}
2064+
#[cfg(target_os = "windows")]
2065+
{
2066+
// The Windows path maps the entire file from offset 0 and uses
2067+
// the header page as the leading guard page. This requires the
2068+
// memory blob to start at exactly PAGE_SIZE.
2069+
if offset as usize != PAGE_SIZE_USIZE {
2070+
return Err(new_error!(
2071+
"Windows from_file requires offset == PAGE_SIZE, got {}",
2072+
offset
2073+
));
2074+
}
2075+
Self::from_file_windows(file, total_size)
2076+
}
2077+
}
2078+
2079+
#[cfg(target_os = "linux")]
2080+
fn from_file_linux(
2081+
file: &std::fs::File,
2082+
offset: u64,
2083+
len: usize,
2084+
total_size: usize,
2085+
) -> Result<Self> {
2086+
use std::ffi::c_void;
2087+
use std::os::unix::io::AsRawFd;
2088+
2089+
use libc::{
2090+
MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_NORESERVE, MAP_PRIVATE, PROT_NONE, PROT_READ,
2091+
PROT_WRITE, mmap, off_t, size_t,
2092+
};
2093+
2094+
let fd = file.as_raw_fd();
2095+
let offset: off_t = offset
2096+
.try_into()
2097+
.map_err(|_| new_error!("snapshot file offset {} exceeds off_t range", offset))?;
2098+
2099+
// Allocate the full region (guard + usable + guard) as anonymous
2100+
let base = unsafe {
2101+
mmap(
2102+
null_mut(),
2103+
total_size as size_t,
2104+
PROT_NONE,
2105+
MAP_ANONYMOUS | MAP_PRIVATE | MAP_NORESERVE,
2106+
-1,
2107+
0 as off_t,
2108+
)
2109+
};
2110+
if base == MAP_FAILED {
2111+
return Err(HyperlightError::MmapFailed(
2112+
std::io::Error::last_os_error().raw_os_error(),
2113+
));
2114+
}
2115+
2116+
// Map the file content over the usable portion (between guard pages).
2117+
// PROT_READ | PROT_WRITE: KVM/MSHV require writable host mappings
2118+
// to handle copy-on-write page faults from the guest.
2119+
// MAP_PRIVATE: writes go to private copies, not the file.
2120+
let usable_ptr = unsafe { (base as *mut u8).add(PAGE_SIZE_USIZE) };
2121+
let mapped = unsafe {
2122+
mmap(
2123+
usable_ptr as *mut c_void,
2124+
len as size_t,
2125+
PROT_READ | PROT_WRITE,
2126+
MAP_PRIVATE | MAP_FIXED | MAP_NORESERVE,
2127+
fd,
2128+
offset,
2129+
)
2130+
};
2131+
if mapped == MAP_FAILED {
2132+
unsafe { libc::munmap(base, total_size as size_t) };
2133+
return Err(HyperlightError::MmapFailed(
2134+
std::io::Error::last_os_error().raw_os_error(),
2135+
));
2136+
}
2137+
2138+
// Guard pages at base and base+total_size-PAGE_SIZE are already
2139+
// PROT_NONE from the anonymous mapping; MAP_FIXED only replaced
2140+
// the middle portion.
2141+
2142+
#[allow(clippy::arc_with_non_send_sync)]
2143+
Ok(ReadonlySharedMemory {
2144+
region: Arc::new(HostMapping {
2145+
ptr: base as *mut u8,
2146+
size: total_size,
2147+
}),
2148+
})
2149+
}
2150+
2151+
/// Windows implementation of file-backed read-only shared memory.
2152+
///
2153+
/// The snapshot file layout is:
2154+
/// `[header (PAGE_SIZE)][memory blob][trailing padding (PAGE_SIZE)]`.
2155+
/// We create a read-only file mapping covering the entire file and
2156+
/// map a view of `len + 2*PAGE_SIZE` bytes starting at file offset 0.
2157+
/// The header becomes the leading guard page and the trailing padding
2158+
/// becomes the trailing guard page, both via
2159+
/// `VirtualProtect(PAGE_NOACCESS)`. This gives the standard
2160+
/// `HostMapping` layout: `[guard | usable | guard]`.
2161+
#[cfg(target_os = "windows")]
2162+
fn from_file_windows(file: &std::fs::File, total_size: usize) -> Result<Self> {
2163+
use std::os::windows::io::AsRawHandle;
2164+
2165+
use windows::Win32::Foundation::HANDLE;
2166+
use windows::Win32::System::Memory::{
2167+
CreateFileMappingA, FILE_MAP_READ, MapViewOfFile, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS,
2168+
PAGE_READONLY, VirtualProtect,
2169+
};
2170+
use windows::core::PCSTR;
2171+
2172+
let file_handle = HANDLE(file.as_raw_handle());
2173+
2174+
// Create a read-only file mapping at the exact file size (pass 0,0).
2175+
// The file includes trailing PAGE_SIZE padding written by to_file(),
2176+
// so the file is at least offset + len + PAGE_SIZE = total_size bytes.
2177+
let handle =
2178+
unsafe { CreateFileMappingA(file_handle, None, PAGE_READONLY, 0, 0, PCSTR::null())? };
2179+
2180+
if handle.is_invalid() {
2181+
log_then_return!(HyperlightError::MemoryAllocationFailed(
2182+
Error::last_os_error().raw_os_error()
2183+
));
2184+
}
2185+
2186+
// Map exactly total_size (header + blob + trailing padding) bytes.
2187+
let addr = unsafe { MapViewOfFile(handle, FILE_MAP_READ, 0, 0, total_size) };
2188+
if addr.Value.is_null() {
2189+
unsafe {
2190+
let _ = windows::Win32::Foundation::CloseHandle(handle);
2191+
}
2192+
log_then_return!(HyperlightError::MemoryAllocationFailed(
2193+
Error::last_os_error().raw_os_error()
2194+
));
2195+
}
2196+
2197+
let cleanup = |ptr: *mut c_void, handle: windows::Win32::Foundation::HANDLE| unsafe {
2198+
if let Err(e) = windows::Win32::System::Memory::UnmapViewOfFile(
2199+
windows::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: ptr },
2200+
) {
2201+
tracing::error!("from_file_windows cleanup: UnmapViewOfFile failed: {:?}", e);
2202+
}
2203+
if let Err(e) = windows::Win32::Foundation::CloseHandle(handle) {
2204+
tracing::error!("from_file_windows cleanup: CloseHandle failed: {:?}", e);
2205+
}
2206+
};
2207+
2208+
// Set guard pages on both ends.
2209+
let mut unused_old_prot = PAGE_PROTECTION_FLAGS(0);
2210+
2211+
let first_guard = addr.Value;
2212+
if let Err(e) = unsafe {
2213+
VirtualProtect(
2214+
first_guard,
2215+
PAGE_SIZE_USIZE,
2216+
PAGE_NOACCESS,
2217+
&mut unused_old_prot,
2218+
)
2219+
} {
2220+
cleanup(addr.Value, handle);
2221+
log_then_return!(WindowsAPIError(e.clone()));
2222+
}
2223+
2224+
let last_guard = unsafe { first_guard.add(total_size - PAGE_SIZE_USIZE) };
2225+
if let Err(e) = unsafe {
2226+
VirtualProtect(
2227+
last_guard,
2228+
PAGE_SIZE_USIZE,
2229+
PAGE_NOACCESS,
2230+
&mut unused_old_prot,
2231+
)
2232+
} {
2233+
cleanup(addr.Value, handle);
2234+
log_then_return!(WindowsAPIError(e.clone()));
2235+
}
2236+
2237+
#[allow(clippy::arc_with_non_send_sync)]
2238+
Ok(ReadonlySharedMemory {
2239+
region: Arc::new(HostMapping {
2240+
ptr: addr.Value as *mut u8,
2241+
size: total_size,
2242+
handle,
2243+
}),
2244+
})
2245+
}
2246+
20322247
pub(crate) fn as_slice(&self) -> &[u8] {
20332248
unsafe { std::slice::from_raw_parts(self.base_ptr(), self.mem_size()) }
20342249
}

0 commit comments

Comments
 (0)