Skip to content

Commit db8c56a

Browse files
committed
Retain page table root finder in memory snapshots
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 1313477 commit db8c56a

6 files changed

Lines changed: 142 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1616
* **Breaking:** Filesystem paths are now represented using `PathBuf`. `GuestBinary::FilePath` now stores a `PathBuf` instead of a `String`, and `MultiUseSandbox::generate_crashdump_to_dir` accepts `Into<PathBuf>` instead of `Into<String>`. Callers passing a `String` to `GuestBinary::FilePath` must convert it using `.into()`.
1717
* Deprecate `MultiUseSandbox::poisoned` in favor of `MultiUseSandbox::status().is_poisoned()`.
1818
* `MultiUseSandbox::restore` has been made more flexible and now accepts snapshots from any guest binary or memory layout when host functions are compatible.
19+
* **Breaking:** `PtRootFinder` now uses `Arc` and requires `Sync`.
1920

2021
Certain fixed guest addresses were changed on AArch64 to more easily
2122
accommodate 16k pages without wasting memory. Snapshots taken from

src/hyperlight_host/src/mem/mgr.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use crate::hypervisor::regs::CommonSpecialRegisters;
3434
use crate::mem::memory_region::MemoryRegion;
3535
#[cfg(crashdump)]
3636
use crate::mem::memory_region::{CrashDumpRegion, MemoryRegionFlags, MemoryRegionType};
37+
use crate::sandbox::PtRootFinder;
3738
use crate::sandbox::snapshot::{NextAction, Snapshot};
3839
use crate::{Result, new_error};
3940

@@ -308,6 +309,7 @@ where
308309
#[cfg(target_arch = "x86_64")] msrs: Vec<crate::hypervisor::regs::MsrEntry>,
309310
next_action: NextAction,
310311
host_functions: HostFunctionDetails,
312+
pt_root_finder: Option<PtRootFinder>,
311313
) -> Result<Snapshot> {
312314
self.snapshot_count += 1;
313315
Snapshot::new(
@@ -325,6 +327,7 @@ where
325327
self.original_entrypoint,
326328
self.snapshot_count,
327329
host_functions,
330+
pt_root_finder,
328331
)
329332
}
330333
}

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub struct MultiUseSandbox {
116116
///
117117
/// Returns a list of root page table GPAs to walk. If the list is
118118
/// empty, only `root_pt_gpa` is used.
119-
pub type PtRootFinder = Box<dyn Fn(&[u8], &[u8], u64) -> Vec<u64> + Send>;
119+
pub type PtRootFinder = Arc<dyn Fn(&[u8], &[u8], u64) -> Vec<u64> + Send + Sync>;
120120

121121
impl MultiUseSandbox {
122122
fn ensure_usable(&self) -> Result<()> {
@@ -157,8 +157,12 @@ impl MultiUseSandbox {
157157
/// Set a callback that discovers page table roots from guest memory.
158158
/// The callback receives (snapshot_mem, scratch_mem, cr3) and returns
159159
/// the list of root GPAs to walk during snapshot creation.
160+
///
161+
/// In-memory snapshots retain the finder across restore. The finder is not
162+
/// serialized.
160163
pub fn set_pt_root_finder(&mut self, finder: PtRootFinder) {
161164
self.pt_root_finder = Some(finder);
165+
self.snapshot = None;
162166
}
163167

164168
/// Create a `MultiUseSandbox` directly from a [`Snapshot`],
@@ -328,7 +332,8 @@ impl MultiUseSandbox {
328332
})?;
329333
}
330334

331-
let sbox = MultiUseSandbox::from_uninit(host_funcs, hshm, vm);
335+
let mut sbox = MultiUseSandbox::from_uninit(host_funcs, hshm, vm);
336+
sbox.pt_root_finder = snapshot.pt_root_finder().cloned();
332337
Ok(sbox)
333338
}
334339

@@ -420,6 +425,7 @@ impl MultiUseSandbox {
420425
msrs,
421426
next_action,
422427
host_functions,
428+
self.pt_root_finder.clone(),
423429
)?;
424430
let snapshot = Arc::new(memory_snapshot);
425431
self.snapshot = Some(snapshot.clone());
@@ -613,7 +619,7 @@ impl MultiUseSandbox {
613619
self.vm.clear_crashdump_binary_path();
614620
}
615621

616-
self.pt_root_finder = None;
622+
self.pt_root_finder = snapshot.pt_root_finder().cloned();
617623

618624
// The restored snapshot is now our most current snapshot
619625
self.snapshot = Some(snapshot.clone());
@@ -1183,6 +1189,7 @@ fn warn_on_layout_override(
11831189

11841190
#[cfg(test)]
11851191
mod tests {
1192+
use std::sync::atomic::{AtomicUsize, Ordering};
11861193
use std::sync::{Arc, Barrier};
11871194
use std::thread;
11881195

@@ -1196,6 +1203,7 @@ mod tests {
11961203
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
11971204
use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
11981205
use crate::sandbox::SandboxConfiguration;
1206+
use crate::sandbox::snapshot::Snapshot;
11991207
use crate::sandbox::uninitialized::{GuestBlob, GuestEnvironment};
12001208
use crate::{
12011209
GuestBinary, HyperlightError, MultiUseSandbox, Result, SandboxStatus, UninitializedSandbox,
@@ -1216,6 +1224,23 @@ mod tests {
12161224
assert!(SandboxStatus::Unrecoverable.is_unrecoverable());
12171225
}
12181226

1227+
trait AmbiguousIfSync<Marker> {
1228+
fn assert_not_sync() {}
1229+
}
1230+
1231+
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
1232+
impl<T: ?Sized + Sync> AmbiguousIfSync<u8> for T {}
1233+
1234+
#[test]
1235+
fn snapshot_and_sandbox_thread_safety() {
1236+
fn assert_send<T: Send>() {}
1237+
fn assert_send_sync<T: Send + Sync>() {}
1238+
1239+
assert_send::<MultiUseSandbox>();
1240+
let _ = <MultiUseSandbox as AmbiguousIfSync<_>>::assert_not_sync;
1241+
assert_send_sync::<Snapshot>();
1242+
}
1243+
12191244
#[test]
12201245
fn poison() {
12211246
let mut sbox: MultiUseSandbox = {
@@ -2275,6 +2300,8 @@ mod tests {
22752300
.unwrap()
22762301
.evolve()
22772302
.unwrap();
2303+
let source_finder: crate::sandbox::PtRootFinder = Arc::new(|_, _, root| vec![root]);
2304+
source.set_pt_root_finder(source_finder.clone());
22782305
let mut target =
22792306
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
22802307
.unwrap()
@@ -2283,8 +2310,7 @@ mod tests {
22832310

22842311
assert_eq!(source.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
22852312
assert_eq!(target.call::<i32>("AddToStatic", 17i32).unwrap(), 17);
2286-
target.set_pt_root_finder(Box::new(|_, _, root| vec![root]));
2287-
assert!(target.pt_root_finder.is_some());
2313+
target.set_pt_root_finder(Arc::new(|_, _, _| Vec::new()));
22882314

22892315
assert_ne!(
22902316
source.mem_mgr.layout.code_size(),
@@ -2301,7 +2327,10 @@ mod tests {
23012327

23022328
let snapshot = source.snapshot().unwrap();
23032329
target.restore(snapshot).unwrap();
2304-
assert!(target.pt_root_finder.is_none());
2330+
assert!(Arc::ptr_eq(
2331+
target.pt_root_finder.as_ref().unwrap(),
2332+
&source_finder
2333+
));
23052334
assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
23062335
assert!(matches!(
23072336
target.call::<i32>("GetStatic", ()),
@@ -2312,6 +2341,68 @@ mod tests {
23122341
));
23132342
}
23142343

2344+
#[test]
2345+
fn snapshot_restore_clears_absent_pt_root_finder() {
2346+
let path = simple_guest_as_pathbuf();
2347+
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2348+
.unwrap()
2349+
.evolve()
2350+
.unwrap();
2351+
let snapshot = source.snapshot().unwrap();
2352+
assert!(snapshot.pt_root_finder().is_none());
2353+
2354+
let path = simple_guest_as_pathbuf();
2355+
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2356+
.unwrap()
2357+
.evolve()
2358+
.unwrap();
2359+
target.set_pt_root_finder(Arc::new(|_, _, root| vec![root]));
2360+
2361+
target.restore(snapshot).unwrap();
2362+
assert!(target.pt_root_finder.is_none());
2363+
}
2364+
2365+
#[test]
2366+
fn snapshot_restore_uses_retained_pt_root_finder() {
2367+
let source_calls = Arc::new(AtomicUsize::new(0));
2368+
let source_calls_in_finder = source_calls.clone();
2369+
let source_finder: crate::sandbox::PtRootFinder = Arc::new(move |_, _, _| {
2370+
source_calls_in_finder.fetch_add(1, Ordering::Relaxed);
2371+
Vec::new()
2372+
});
2373+
let path = simple_guest_as_pathbuf();
2374+
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2375+
.unwrap()
2376+
.evolve()
2377+
.unwrap();
2378+
source.set_pt_root_finder(source_finder);
2379+
let snapshot = source.snapshot().unwrap();
2380+
2381+
let target_calls = Arc::new(AtomicUsize::new(0));
2382+
let target_calls_in_finder = target_calls.clone();
2383+
let target_finder: crate::sandbox::PtRootFinder = Arc::new(move |_, _, root| {
2384+
target_calls_in_finder.fetch_add(1, Ordering::Relaxed);
2385+
vec![root]
2386+
});
2387+
let path = simple_guest_as_pathbuf();
2388+
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2389+
.unwrap()
2390+
.evolve()
2391+
.unwrap();
2392+
target.set_pt_root_finder(target_finder);
2393+
target.restore(snapshot).unwrap();
2394+
2395+
let source_calls_before = source_calls.load(Ordering::Relaxed);
2396+
target.call::<i32>("GetStatic", ()).unwrap();
2397+
target.snapshot().unwrap();
2398+
2399+
assert_eq!(
2400+
source_calls.load(Ordering::Relaxed),
2401+
source_calls_before + 1
2402+
);
2403+
assert_eq!(target_calls.load(Ordering::Relaxed), 0);
2404+
}
2405+
23152406
#[test]
23162407
fn snapshot_restore_replaces_c_guest_with_rust_guest() {
23172408
let mut source =

src/hyperlight_host/src/sandbox/snapshot/file/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ impl Snapshot {
334334
/// guest is running. Any release that breaks the format is called
335335
/// out in the Hyperlight changelog.
336336
///
337+
/// A [`PtRootFinder`](crate::sandbox::PtRootFinder) configured with
338+
/// [`set_pt_root_finder`](crate::MultiUseSandbox::set_pt_root_finder) is not
339+
/// serialized. Set it again on any sandbox created from the loaded snapshot.
340+
///
337341
/// # Examples
338342
///
339343
/// ```no_run
@@ -668,6 +672,11 @@ impl Snapshot {
668672
/// guest is running. Any release that breaks the format is called
669673
/// out in the Hyperlight changelog.
670674
///
675+
/// If the source sandbox used
676+
/// [`MultiUseSandbox::set_pt_root_finder`](crate::MultiUseSandbox::set_pt_root_finder),
677+
/// set the finder again on the sandbox created from this snapshot. The finder
678+
/// is not serialized.
679+
///
671680
/// # Verification
672681
///
673682
/// This method does not check the manifest, config, or snapshot
@@ -909,6 +918,7 @@ impl Snapshot {
909918
original_entrypoint: cfg.original_entrypoint_addr,
910919
snapshot_generation,
911920
host_functions,
921+
pt_root_finder: None,
912922
})
913923
}
914924
}

src/hyperlight_host/src/sandbox/snapshot/file_tests.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use sha2::{Digest as _, Sha256};
2626

2727
use crate::func::Registerable;
2828
use crate::mem::layout::SandboxMemoryLayout;
29+
use crate::sandbox::PtRootFinder;
2930
use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot};
3031
use crate::{GuestBinary, HostFunctions, MultiUseSandbox, UninitializedSandbox};
3132

@@ -95,9 +96,21 @@ fn find_snapshot_blob(oci_dir: &std::path::Path) -> std::path::PathBuf {
9596

9697
#[test]
9798
fn from_snapshot_already_initialized_in_memory() {
98-
let snapshot = create_snapshot();
99+
let mut source = create_test_sandbox();
100+
let initial_snapshot = source.snapshot().unwrap();
101+
let finder: PtRootFinder = Arc::new(|_, _, root| vec![root]);
102+
source.set_pt_root_finder(finder.clone());
103+
let snapshot = source.snapshot().unwrap();
104+
assert!(!Arc::ptr_eq(&initial_snapshot, &snapshot));
105+
assert!(Arc::ptr_eq(snapshot.pt_root_finder().unwrap(), &finder));
106+
99107
let mut sbox2 =
100108
MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None).unwrap();
109+
let restored_snapshot = sbox2.snapshot().unwrap();
110+
assert!(Arc::ptr_eq(
111+
restored_snapshot.pt_root_finder().unwrap(),
112+
&finder
113+
));
101114
let result: i32 = sbox2.call("GetStatic", ()).unwrap();
102115
assert_eq!(result, 0);
103116
}
@@ -119,7 +132,9 @@ fn from_snapshot_in_memory_pre_init() {
119132

120133
#[test]
121134
fn round_trip_save_load_call() {
122-
let snapshot = create_snapshot();
135+
let mut source = create_test_sandbox();
136+
source.set_pt_root_finder(Arc::new(|_, _, root| vec![root]));
137+
let snapshot = source.snapshot().unwrap();
123138

124139
let dir = tempfile::tempdir().unwrap();
125140
let oci = dir.path().join("snap");
@@ -128,6 +143,7 @@ fn round_trip_save_load_call() {
128143
.unwrap();
129144

130145
let loaded = Snapshot::checked_load(&oci, OciTag::new("latest").unwrap()).unwrap();
146+
assert!(loaded.pt_root_finder().is_none());
131147
let mut sbox2 =
132148
MultiUseSandbox::from_snapshot(Arc::new(loaded), HostFunctions::default(), None).unwrap();
133149

src/hyperlight_host/src/sandbox/snapshot/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ use crate::mem::layout::SandboxMemoryLayout;
3939
use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegion, MemoryRegionFlags};
4040
use crate::mem::mgr::{GuestPageTableBuffer, SnapshotSharedMemory};
4141
use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory};
42-
use crate::sandbox::SandboxConfiguration;
4342
use crate::sandbox::uninitialized::{GuestBinary, GuestEnvironment};
43+
use crate::sandbox::{PtRootFinder, SandboxConfiguration};
4444

4545
const PTE_SIZE: usize = size_of::<vmem::PageTableEntry>();
4646

@@ -123,6 +123,9 @@ pub struct Snapshot {
123123
/// `HostFunctions` set that is missing required functions or
124124
/// has mismatched signatures.
125125
host_functions: HostFunctionDetails,
126+
127+
/// Runtime-only page-table root finder retained by in-memory snapshots.
128+
pt_root_finder: Option<PtRootFinder>,
126129
}
127130
impl core::convert::AsRef<Snapshot> for Snapshot {
128131
fn as_ref(&self) -> &Self {
@@ -406,6 +409,7 @@ impl Snapshot {
406409
host_functions: HostFunctionDetails {
407410
host_functions: None,
408411
},
412+
pt_root_finder: None,
409413
})
410414
}
411415

@@ -432,6 +436,7 @@ impl Snapshot {
432436
original_entrypoint: u64,
433437
snapshot_generation: u64,
434438
host_functions: HostFunctionDetails,
439+
pt_root_finder: Option<PtRootFinder>,
435440
) -> Result<Self> {
436441
let mut phys_seen = HashMap::<u64, usize>::new();
437442
let scratch_gva = scratch_base_gva(layout.get_scratch_size());
@@ -593,6 +598,7 @@ impl Snapshot {
593598
original_entrypoint,
594599
snapshot_generation,
595600
host_functions,
601+
pt_root_finder,
596602
})
597603
}
598604

@@ -601,6 +607,10 @@ impl Snapshot {
601607
self.snapshot_generation
602608
}
603609

610+
pub(crate) fn pt_root_finder(&self) -> Option<&PtRootFinder> {
611+
self.pt_root_finder.as_ref()
612+
}
613+
604614
/// Return the main memory contents of the snapshot
605615
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
606616
pub(crate) fn memory(&self) -> &ReadonlySharedMemory {
@@ -792,6 +802,7 @@ mod tests {
792802
0,
793803
1,
794804
HostFunctionDetails::default(),
805+
None,
795806
)
796807
.unwrap();
797808

@@ -812,6 +823,7 @@ mod tests {
812823
0,
813824
2,
814825
HostFunctionDetails::default(),
826+
None,
815827
)
816828
.unwrap();
817829

0 commit comments

Comments
 (0)