1use std::path::Path;
5#[cfg(crashdump)]
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex};
8
9use flatbuffers::FlatBufferBuilder;
10use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
11use hyperlight_common::flatbuffer_wrappers::function_types::{
12 ParameterValue, ReturnType, ReturnValue,
13};
14use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity;
15use tracing::{Span, instrument};
16
17use super::Callable;
18use super::file_mapping::prepare_file_cow;
19use super::host_funcs::FunctionRegistry;
20use super::snapshot::Snapshot;
21use crate::func::{ParameterTuple, SupportedReturnType};
22use crate::hypervisor::InterruptHandle;
23use crate::hypervisor::hyperlight_vm::{HyperlightVm, HyperlightVmError};
24use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
25use crate::mem::mgr::SandboxMemoryManager;
26use crate::mem::shared_mem::{HostSharedMemory, SharedMemory as _};
27use crate::metrics::{
28 METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
29};
30use crate::{HyperlightError, Result, log_then_return};
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum SandboxStatus {
35 Ready,
37 Poisoned,
39 Unrecoverable,
41}
42
43impl SandboxStatus {
44 pub const fn is_ready(self) -> bool {
46 matches!(self, Self::Ready)
47 }
48
49 pub const fn is_poisoned(self) -> bool {
51 matches!(self, Self::Poisoned)
52 }
53
54 pub const fn is_unrecoverable(self) -> bool {
56 matches!(self, Self::Unrecoverable)
57 }
58}
59
60pub struct MultiUseSandbox {
83 status: SandboxStatus,
84 pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
85 pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
86 vm: HyperlightVm,
87 pub(crate) snapshot: Option<Arc<Snapshot>>,
90 pt_root_finder: Option<PtRootFinder>,
94}
95
96pub type PtRootFinder = Box<dyn Fn(&[u8], &[u8], u64) -> Vec<u64> + Send>;
107
108impl MultiUseSandbox {
109 fn check_ready(&self) -> Result<()> {
110 match self.status {
111 SandboxStatus::Ready => Ok(()),
112 SandboxStatus::Poisoned => Err(HyperlightError::PoisonedSandbox),
113 SandboxStatus::Unrecoverable => Err(HyperlightError::UnrecoverableSandbox),
114 }
115 }
116
117 fn poison(&mut self) {
118 if self.status.is_ready() {
119 self.status = SandboxStatus::Poisoned;
120 }
121 }
122
123 #[instrument(skip_all, parent = Span::current(), level = "Trace")]
129 pub(super) fn from_uninit(
130 host_funcs: Arc<Mutex<FunctionRegistry>>,
131 mgr: SandboxMemoryManager<HostSharedMemory>,
132 vm: HyperlightVm,
133 ) -> MultiUseSandbox {
134 Self {
135 status: SandboxStatus::Ready,
136 host_funcs,
137 mem_mgr: mgr,
138 vm,
139 snapshot: None,
140 pt_root_finder: None,
141 }
142 }
143
144 pub fn set_pt_root_finder(&mut self, finder: PtRootFinder) {
150 self.pt_root_finder = Some(finder);
151 }
152
153 #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
213 pub fn from_snapshot(
214 snapshot: Arc<Snapshot>,
215 host_funcs: crate::HostFunctions,
216 config: Option<crate::sandbox::SandboxConfiguration>,
217 ) -> Result<Self> {
218 use rand::RngExt;
219
220 use crate::mem::ptr::RawPtr;
221 use crate::sandbox::uninitialized_evolve::set_up_hypervisor_partition;
222
223 snapshot.validate_host_functions(host_funcs.inner())?;
226
227 let host_funcs = Arc::new(Mutex::new(host_funcs.into_inner()));
228
229 let stack_top_gva = snapshot.stack_top_gva();
230 let caller_supplied_config = config.is_some();
238 let mut config = config.unwrap_or_default();
239 if caller_supplied_config {
240 warn_on_layout_override(&config, snapshot.layout());
241 }
242 config.set_input_data_size(snapshot.layout().input_data_size());
243 config.set_output_data_size(snapshot.layout().output_data_size());
244 config.set_heap_size(snapshot.layout().heap_size() as u64);
245 config.set_scratch_size(snapshot.layout().get_scratch_size());
246 let load_info = snapshot.load_info();
247
248 let mgr = crate::mem::mgr::SandboxMemoryManager::from_snapshot(&snapshot)?;
249 let (mut hshm, gshm) = mgr.build()?;
250
251 let page_size = u32::try_from(page_size::get())? as usize;
252
253 #[cfg(target_os = "linux")]
254 crate::signal_handlers::setup_signal_handlers(&config)?;
255
256 #[cfg(any(crashdump, gdb))]
262 let rt_cfg = crate::sandbox::uninitialized::SandboxRuntimeConfig {
263 #[cfg(crashdump)]
264 binary_path: None,
265 #[cfg(gdb)]
266 debug_info: config.get_guest_debug_info(),
267 #[cfg(crashdump)]
268 guest_core_dump: config.get_guest_core_dump(),
269 #[cfg(crashdump)]
270 entry_point: None,
271 };
272
273 let mut vm = set_up_hypervisor_partition(
274 gshm,
275 &config,
276 stack_top_gva,
277 page_size,
278 #[cfg(any(crashdump, gdb))]
279 rt_cfg,
280 load_info,
281 )?;
282
283 let seed = {
284 let mut rng = rand::rng();
285 rng.random::<u64>()
286 };
287 let peb_addr = RawPtr::from(u64::try_from(hshm.layout.peb_address())?);
288
289 vm.initialise(peb_addr, seed, &mut hshm, &host_funcs, None)
291 .map_err(crate::hypervisor::hyperlight_vm::HyperlightVmError::Initialize)?;
292
293 if matches!(snapshot.next_action(), super::snapshot::NextAction::Call(_)) {
294 hshm.request_libc_rng_reseed(seed as u32)?;
295 }
296
297 if matches!(snapshot.next_action(), super::snapshot::NextAction::Call(_)) {
301 let sregs = snapshot.sregs().ok_or_else(|| {
302 crate::new_error!("snapshot with NextAction::Call must have captured sregs")
303 })?;
304 #[cfg(target_arch = "x86_64")]
305 let msrs = snapshot.msrs().ok_or_else(|| {
306 crate::new_error!("snapshot with NextAction::Call must have captured MSRs")
307 })?;
308 vm.apply_sregs(hshm.layout.get_pt_base_gpa(), sregs)
309 .map_err(|e| {
310 crate::HyperlightError::HyperlightVmError(
311 crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e.into()),
312 )
313 })?;
314
315 #[cfg(target_arch = "x86_64")]
317 vm.restore_msrs(msrs).map_err(|e| {
318 crate::HyperlightError::HyperlightVmError(
319 crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e),
320 )
321 })?;
322 }
323
324 let sbox = MultiUseSandbox::from_uninit(host_funcs, hshm, vm);
325 Ok(sbox)
326 }
327
328 #[instrument(err(Debug), skip_all, parent = Span::current())]
363 pub fn snapshot(&mut self) -> Result<Arc<Snapshot>> {
364 self.check_ready()?;
365
366 if let Some(snapshot) = &self.snapshot {
367 return Ok(snapshot.clone());
368 }
369 let mapped_regions_iter = self.vm.get_mapped_regions();
370 let mapped_regions_vec: Vec<MemoryRegion> = mapped_regions_iter.cloned().collect();
371 let cr3 = self
373 .vm
374 .get_root_pt()
375 .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
376 let root_pt_gpas = if let Some(finder) = &self.pt_root_finder {
378 let roots = self.mem_mgr.shared_mem.with_contents(|snap| {
379 self.mem_mgr
380 .scratch_mem
381 .with_contents(|scratch| finder(snap, scratch, cr3))
382 })??;
383 if roots.is_empty() { vec![cr3] } else { roots }
384 } else {
385 vec![cr3]
386 };
387
388 let stack_top_gpa = self.vm.get_stack_top();
389 let sregs = self
390 .vm
391 .get_snapshot_sregs()
392 .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
393 #[cfg(target_arch = "x86_64")]
394 let msrs = self
395 .vm
396 .get_msr_reset_state()
397 .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
398 let next_action = self.vm.get_next_action();
399 let host_functions = (&*self.host_funcs.try_lock().map_err(|e| {
400 crate::new_error!("Error locking host_funcs at {}:{}: {}", file!(), line!(), e)
401 })?)
402 .into();
403
404 let memory_snapshot = self.mem_mgr.snapshot(
405 mapped_regions_vec,
406 &root_pt_gpas,
407 stack_top_gpa,
408 sregs,
409 #[cfg(target_arch = "x86_64")]
410 msrs,
411 next_action,
412 host_functions,
413 )?;
414 let snapshot = Arc::new(memory_snapshot);
415 self.snapshot = Some(snapshot.clone());
416 Ok(snapshot)
417 }
418
419 fn restore_memory_and_mappings(&mut self, snapshot: &Snapshot) -> Result<()> {
420 let (snapshot_mem, scratch_mem) = self.mem_mgr.restore_snapshot(snapshot)?;
421 if let Some(snapshot_mem) = snapshot_mem {
422 self.vm
423 .update_snapshot_mapping(snapshot_mem)
424 .map_err(HyperlightVmError::UpdateRegion)?;
425 }
426 if let Some(scratch_mem) = scratch_mem {
427 self.vm
428 .update_scratch_mapping(scratch_mem)
429 .map_err(HyperlightVmError::UpdateRegion)?;
430 }
431 Ok(())
432 }
433
434 #[instrument(err(Debug), skip_all, parent = Span::current())]
523 pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()> {
524 if self.status.is_unrecoverable() {
525 return Err(HyperlightError::UnrecoverableSandbox);
526 }
527
528 {
552 let host_funcs = self
553 .host_funcs
554 .try_lock()
555 .map_err(|e| crate::new_error!("Error locking host_funcs: {}", e))?;
556 snapshot.validate_host_functions(&host_funcs)?;
557 }
558
559 let sregs = snapshot.sregs().ok_or_else(|| {
560 HyperlightError::Error("snapshot from running sandbox should have sregs".to_string())
561 })?;
562 #[cfg(target_arch = "x86_64")]
563 let msrs = snapshot.msrs().ok_or_else(|| {
564 HyperlightError::Error("snapshot from running sandbox should have MSRs".to_string())
565 })?;
566
567 self.status = SandboxStatus::Poisoned;
569 self.snapshot = None;
570
571 let current_regions: Vec<MemoryRegion> = self.vm.get_mapped_regions().cloned().collect();
572 for region in ¤t_regions {
573 self.vm
574 .unmap_region(region)
575 .map_err(HyperlightVmError::UnmapRegion)?;
576 }
577
578 if let Err(error) = self.restore_memory_and_mappings(&snapshot) {
579 self.status = SandboxStatus::Unrecoverable;
580 return Err(error);
581 }
582
583 self.vm
585 .reset_vcpu(
586 snapshot.root_pt_gpa(),
587 sregs,
588 #[cfg(target_arch = "x86_64")]
589 msrs,
590 )
591 .map_err(HyperlightVmError::Restore)?;
592
593 self.vm.set_stack_top(snapshot.stack_top_gva());
594 self.vm.set_next_action(snapshot.next_action());
595 #[cfg(crashdump)]
598 {
599 self.vm
600 .set_crashdump_entry_point(snapshot.original_entrypoint());
601 self.vm.clear_crashdump_binary_path();
602 }
603
604 self.mem_mgr
605 .request_libc_rng_reseed(rand::random::<u32>())?;
606
607 self.snapshot = Some(snapshot.clone());
609
610 self.status = SandboxStatus::Ready;
620
621 Ok(())
622 }
623
624 #[doc(hidden)]
658 #[deprecated(
659 since = "0.8.0",
660 note = "Deprecated in favour of call and snapshot/restore."
661 )]
662 #[instrument(err(Debug), skip(self, args), parent = Span::current())]
663 pub fn call_guest_function_by_name<Output: SupportedReturnType>(
664 &mut self,
665 func_name: &str,
666 args: impl ParameterTuple,
667 ) -> Result<Output> {
668 self.check_ready()?;
669 let snapshot = self.snapshot()?;
670 let res = self.call(func_name, args);
671 self.restore(snapshot)?;
672 res
673 }
674
675 #[instrument(err(Debug), skip(self, args), parent = Span::current())]
746 pub fn call<Output: SupportedReturnType>(
747 &mut self,
748 func_name: &str,
749 args: impl ParameterTuple,
750 ) -> Result<Output> {
751 self.check_ready()?;
752 self.snapshot = None;
754 maybe_time_and_emit_guest_call(func_name, || {
755 let ret = self.call_guest_function_by_name_no_reset(
756 func_name,
757 Output::TYPE,
758 args.into_value(),
759 );
760 let ret = Output::from_value(ret?)?;
763 Ok(ret)
764 })
765 }
766
767 #[instrument(err(Debug), skip(self, rgn), parent = Span::current())]
783 pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> {
784 self.check_ready()?;
785 if rgn.flags.contains(MemoryRegionFlags::WRITE) {
786 log_then_return!("TODO: Writable mappings not yet supported");
790 }
791
792 unsafe { self.vm.map_region(rgn) }.map_err(HyperlightVmError::MapRegion)?;
794 self.snapshot = None;
795 Ok(())
796 }
797
798 #[instrument(err(Debug), skip(self, file_path, guest_base), parent = Span::current())]
807 pub fn map_file_cow(&mut self, file_path: &Path, guest_base: u64) -> Result<u64> {
808 self.check_ready()?;
809
810 let mut prepared = prepare_file_cow(file_path, guest_base)?;
812
813 let shared_size = self.mem_mgr.shared_mem.mem_size() as u64;
816 let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
817 let shared_end = base_addr.checked_add(shared_size).ok_or_else(|| {
818 crate::HyperlightError::Error("shared memory end overflow".to_string())
819 })?;
820 let mapping_end = guest_base
821 .checked_add(prepared.size as u64)
822 .ok_or_else(|| {
823 crate::HyperlightError::Error(format!(
824 "map_file_cow: guest address overflow: {:#x} + {:#x}",
825 guest_base, prepared.size
826 ))
827 })?;
828 if guest_base < shared_end && mapping_end > base_addr {
829 return Err(crate::HyperlightError::Error(format!(
830 "map_file_cow: mapping [{:#x}..{:#x}) overlaps sandbox shared memory [{:#x}..{:#x})",
831 guest_base, mapping_end, base_addr, shared_end,
832 )));
833 }
834
835 let region = prepared.to_memory_region()?;
837
838 unsafe { self.vm.map_region(®ion) }
839 .map_err(HyperlightVmError::MapRegion)
840 .map_err(crate::HyperlightError::HyperlightVmError)?;
841
842 self.snapshot = None;
843
844 let size = prepared.size as u64;
845
846 prepared.mark_consumed();
854
855 Ok(size)
856 }
857
858 #[cfg(feature = "fuzzing")]
867 #[instrument(err(Debug), skip(self, args), parent = Span::current())]
868 pub fn call_type_erased_guest_function_by_name(
869 &mut self,
870 func_name: &str,
871 ret_type: ReturnType,
872 args: Vec<ParameterValue>,
873 ) -> Result<ReturnValue> {
874 self.check_ready()?;
875 self.snapshot = None;
877 maybe_time_and_emit_guest_call(func_name, || {
878 self.call_guest_function_by_name_no_reset(func_name, ret_type, args)
879 })
880 }
881
882 fn call_guest_function_by_name_no_reset(
883 &mut self,
884 function_name: &str,
885 return_type: ReturnType,
886 args: Vec<ParameterValue>,
887 ) -> Result<ReturnValue> {
888 self.check_ready()?;
889 self.vm.clear_cancel();
893
894 let res = (|| {
895 let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args);
896
897 let fc = FunctionCall::new(
898 function_name.to_string(),
899 Some(args),
900 FunctionCallType::Guest,
901 return_type,
902 );
903
904 let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
905 let buffer = fc.encode(&mut builder);
906
907 self.mem_mgr.write_guest_function_call(buffer)?;
908
909 let dispatch_res = self
910 .vm
911 .dispatch_call_from_host(&mut self.mem_mgr, &self.host_funcs);
912
913 if let Err(e) = dispatch_res {
916 let (error, should_poison) = e.promote();
917 if should_poison {
918 self.poison();
919 }
920 return Err(error);
921 }
922
923 let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner();
924
925 match guest_result {
926 Ok(val) => Ok(val),
927 Err(guest_error) => {
928 metrics::counter!(
929 METRIC_GUEST_ERROR,
930 METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string()
931 )
932 .increment(1);
933
934 Err(HyperlightError::GuestError(
935 guest_error.code,
936 guest_error.message,
937 ))
938 }
939 }
940 })();
941
942 self.mem_mgr.abort_buffer.clear();
944
945 if let Err(e) = &res {
951 self.mem_mgr.clear_io_buffers();
952
953 if e.is_poison_error() {
955 self.poison();
956 }
957 }
958
959 res
962 }
963
964 pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
991 self.vm.interrupt_handle()
992 }
993
994 #[cfg(crashdump)]
1030 #[instrument(err(Debug), skip_all, parent = Span::current())]
1031 pub fn generate_crashdump(&mut self) -> Result<()> {
1032 crate::hypervisor::crashdump::generate_crashdump(&self.vm, &mut self.mem_mgr, None)
1033 }
1034
1035 #[cfg(crashdump)]
1042 #[instrument(err(Debug), skip_all, parent = Span::current())]
1043 pub fn generate_crashdump_to_dir(&mut self, dir: impl Into<PathBuf>) -> Result<()> {
1044 crate::hypervisor::crashdump::generate_crashdump(
1045 &self.vm,
1046 &mut self.mem_mgr,
1047 Some(dir.into()),
1048 )
1049 }
1050
1051 #[deprecated(since = "0.17.0", note = "use status().is_poisoned()")]
1083 pub fn poisoned(&self) -> bool {
1084 self.status.is_poisoned()
1085 }
1086
1087 pub fn status(&self) -> SandboxStatus {
1095 self.status
1096 }
1097}
1098
1099impl Callable for MultiUseSandbox {
1100 fn call<Output: SupportedReturnType>(
1101 &mut self,
1102 func_name: &str,
1103 args: impl ParameterTuple,
1104 ) -> Result<Output> {
1105 self.check_ready()?;
1106 self.call(func_name, args)
1107 }
1108}
1109
1110impl std::fmt::Debug for MultiUseSandbox {
1111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1112 f.debug_struct("MultiUseSandbox").finish()
1113 }
1114}
1115
1116fn warn_on_layout_override(
1121 caller: &crate::sandbox::SandboxConfiguration,
1122 snapshot: &crate::mem::layout::SandboxMemoryLayout,
1123) {
1124 let mismatches: &[(&str, u64, u64)] = &[
1125 (
1126 "input_data_size",
1127 caller.get_input_data_size() as u64,
1128 snapshot.input_data_size() as u64,
1129 ),
1130 (
1131 "output_data_size",
1132 caller.get_output_data_size() as u64,
1133 snapshot.output_data_size() as u64,
1134 ),
1135 (
1136 "heap_size",
1137 caller.get_heap_size(),
1138 snapshot.heap_size() as u64,
1139 ),
1140 (
1141 "scratch_size",
1142 caller.get_scratch_size() as u64,
1143 snapshot.get_scratch_size() as u64,
1144 ),
1145 ];
1146 for (name, supplied, snap) in mismatches {
1147 if supplied != snap {
1148 tracing::warn!(
1149 "from_snapshot ignoring caller-supplied {} ({}); using snapshot value ({})",
1150 name,
1151 supplied,
1152 snap
1153 );
1154 }
1155 }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use std::sync::{Arc, Barrier};
1161 use std::thread;
1162
1163 use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
1164 use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE};
1165 use hyperlight_testing::{c_simple_guest_as_pathbuf, simple_guest_as_pathbuf};
1166
1167 use crate::func::host_functions::Registerable;
1168 #[cfg(not(gdb))]
1169 use crate::hypervisor::hyperlight_vm::test_support::VmOperation;
1170 use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
1171 use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
1172 use crate::sandbox::SandboxConfiguration;
1173 use crate::sandbox::uninitialized::{GuestBlob, GuestEnvironment};
1174 use crate::{
1175 GuestBinary, HyperlightError, MultiUseSandbox, Result, SandboxBuilder, SandboxStatus,
1176 UninitializedSandbox,
1177 };
1178
1179 #[test]
1180 fn sandbox_status_predicates() {
1181 assert!(SandboxStatus::Ready.is_ready());
1182 assert!(!SandboxStatus::Ready.is_poisoned());
1183 assert!(!SandboxStatus::Ready.is_unrecoverable());
1184
1185 assert!(!SandboxStatus::Poisoned.is_ready());
1186 assert!(SandboxStatus::Poisoned.is_poisoned());
1187 assert!(!SandboxStatus::Poisoned.is_unrecoverable());
1188
1189 assert!(!SandboxStatus::Unrecoverable.is_ready());
1190 assert!(!SandboxStatus::Unrecoverable.is_poisoned());
1191 assert!(SandboxStatus::Unrecoverable.is_unrecoverable());
1192 }
1193
1194 #[test]
1195 fn poison() {
1196 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1197 .build()
1198 .unwrap();
1199 let snapshot = sbox.snapshot().unwrap();
1200
1201 let res = sbox
1203 .call::<()>("guest_panic", "hello".to_string())
1204 .unwrap_err();
1205 assert!(
1206 matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
1207 );
1208 assert!(sbox.status().is_poisoned());
1209
1210 let res = sbox
1212 .call::<()>("guest_panic", "hello2".to_string())
1213 .unwrap_err();
1214 assert!(matches!(res, HyperlightError::PoisonedSandbox));
1215
1216 if let Err(e) = sbox.snapshot() {
1218 assert!(sbox.status().is_poisoned());
1219 assert!(matches!(e, HyperlightError::PoisonedSandbox));
1220 } else {
1221 panic!("Snapshot should fail");
1222 }
1223
1224 {
1226 let map_mem = allocate_guest_memory();
1227 let guest_base = 0x0;
1228 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1229 let res = unsafe { sbox.map_region(®ion) }.unwrap_err();
1230 assert!(matches!(res, HyperlightError::PoisonedSandbox));
1231 }
1232
1233 {
1235 let temp_file = std::env::temp_dir().join("test_poison_map_file.bin");
1236 let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err();
1237 assert!(matches!(res, HyperlightError::PoisonedSandbox));
1238 std::fs::remove_file(&temp_file).ok(); }
1240
1241 #[allow(deprecated)]
1243 let res = sbox
1244 .call_guest_function_by_name::<String>("Echo", "test".to_string())
1245 .unwrap_err();
1246 assert!(matches!(res, HyperlightError::PoisonedSandbox));
1247
1248 sbox.restore(snapshot.clone()).unwrap();
1250 assert_eq!(sbox.status(), SandboxStatus::Ready);
1251
1252 let res = sbox.call::<String>("Echo", "hello2".to_string()).unwrap();
1254 assert_eq!(res, "hello2".to_string());
1255 assert_eq!(sbox.status(), SandboxStatus::Ready);
1256
1257 let res = sbox
1259 .call::<()>("guest_panic", "hello".to_string())
1260 .unwrap_err();
1261 assert!(
1262 matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
1263 );
1264 assert!(sbox.status().is_poisoned());
1265
1266 sbox.restore(snapshot.clone()).unwrap();
1268 assert_eq!(sbox.status(), SandboxStatus::Ready);
1269
1270 let res = sbox.call::<String>("Echo", "hello3".to_string()).unwrap();
1272 assert_eq!(res, "hello3".to_string());
1273 assert_eq!(sbox.status(), SandboxStatus::Ready);
1274
1275 let _ = sbox.snapshot().unwrap();
1277 }
1278
1279 #[test]
1281 fn host_func_error() {
1282 let path = simple_guest_as_pathbuf();
1283 let mut sandbox = SandboxBuilder::from_file(path)
1284 .host_function("HostError", || -> Result<()> {
1285 Err(HyperlightError::Error("hi".to_string()))
1286 })
1287 .build()
1288 .unwrap();
1289
1290 for _ in 0..1000 {
1292 let result = sandbox
1293 .call::<i64>(
1294 "CallGivenParamlessHostFuncThatReturnsI64",
1295 "HostError".to_string(),
1296 )
1297 .unwrap_err();
1298
1299 assert!(
1300 matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"),
1301 );
1302 }
1303 }
1304
1305 #[test]
1306 fn call_host_func_expect_error() {
1307 let path = simple_guest_as_pathbuf();
1308 let mut sandbox = SandboxBuilder::from_file(path).build().unwrap();
1309 sandbox
1310 .call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string())
1311 .unwrap();
1312 }
1313
1314 #[test]
1316 fn io_buffer_reset() {
1317 let path = simple_guest_as_pathbuf();
1318 let mut sandbox = SandboxBuilder::from_file(path)
1319 .input_data_size(4096)
1320 .output_data_size(4096)
1321 .host_function("HostAdd", |a: i32, b: i32| a + b)
1322 .build()
1323 .unwrap();
1324
1325 for _ in 0..1000 {
1327 let result = sandbox.call::<i32>("Add", (5i32, 10i32)).unwrap();
1328 assert_eq!(result, 15);
1329 let result = sandbox.call::<i32>("AddToStaticAndFail", ()).unwrap_err();
1330 assert!(
1331 matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose")
1332 );
1333 }
1334 }
1335
1336 #[test]
1338 fn test_call_guest_function_by_name() {
1339 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1340 .build()
1341 .unwrap();
1342
1343 let snapshot = sbox.snapshot().unwrap();
1344
1345 let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1346 let res: i32 = sbox.call("GetStatic", ()).unwrap();
1347 assert_eq!(res, 5);
1348
1349 sbox.restore(snapshot).unwrap();
1350 #[allow(deprecated)]
1351 let _ = sbox
1352 .call_guest_function_by_name::<i32>("AddToStatic", 5i32)
1353 .unwrap();
1354 #[allow(deprecated)]
1355 let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap();
1356 assert_eq!(res, 0);
1357 }
1358
1359 #[test]
1362 fn test_with_small_stack_and_heap() {
1363 const HEAP_SIZE: u64 = 32 * 1024;
1364 let scratch_size = {
1369 let defaults = SandboxConfiguration::default();
1370 hyperlight_common::layout::min_scratch_size(
1371 defaults.get_input_data_size(),
1372 defaults.get_output_data_size(),
1373 )
1374 } + 0x10000
1375 + 0x10000;
1376
1377 let mut sbox1 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1378 .heap_size(HEAP_SIZE)
1379 .scratch_size(scratch_size)
1380 .build()
1381 .unwrap();
1382
1383 for _ in 0..1000 {
1384 sbox1.call::<String>("Echo", "hello".to_string()).unwrap();
1385 }
1386
1387 let mut sbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1388 .heap_size(HEAP_SIZE)
1389 .scratch_size(scratch_size)
1390 .build()
1391 .unwrap();
1392
1393 for i in 0..1000 {
1394 sbox2
1395 .call::<i32>(
1396 "PrintUsingPrintf",
1397 format!("Hello World {}\n", i).to_string(),
1398 )
1399 .unwrap();
1400 }
1401 }
1402
1403 #[test]
1406 fn snapshot_evolve_restore_handles_state_correctly() {
1407 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1408 .build()
1409 .unwrap();
1410
1411 let snapshot = sbox.snapshot().unwrap();
1412
1413 let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1414
1415 let res: i32 = sbox.call("GetStatic", ()).unwrap();
1416 assert_eq!(res, 5);
1417
1418 sbox.restore(snapshot).unwrap();
1419 let res: i32 = sbox.call("GetStatic", ()).unwrap();
1420 assert_eq!(res, 0);
1421 }
1422
1423 #[test]
1424 fn test_trigger_exception_on_guest() {
1425 let mut multi_use_sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1426 .build()
1427 .unwrap();
1428
1429 let res: Result<()> = multi_use_sandbox.call("TriggerException", ());
1430
1431 assert!(res.is_err());
1432
1433 match res.unwrap_err() {
1434 HyperlightError::GuestAborted(_, msg) => {
1435 #[cfg(target_arch = "x86_64")]
1437 assert!(msg.contains("InvalidOpcode"));
1438 #[cfg(target_arch = "aarch64")]
1439 assert!(msg.contains("0x2000000"));
1440 }
1441 e => panic!("Expected HyperlightError::GuestAborted but got {:?}", e),
1442 }
1443 }
1444
1445 fn create_many_on_threads_test<const NUM_THREADS: usize, const SANDBOXES_PER_THREAD: usize>() {
1446 let start_barrier = Arc::new(Barrier::new(NUM_THREADS + 1));
1448 let mut thread_handles = vec![];
1449
1450 for _ in 0..NUM_THREADS {
1451 let barrier = start_barrier.clone();
1452
1453 let handle = thread::spawn(move || {
1454 barrier.wait();
1455
1456 for _ in 0..SANDBOXES_PER_THREAD {
1457 let guest_path = simple_guest_as_pathbuf();
1458 let mut sandbox = SandboxBuilder::from_file(guest_path).build().unwrap();
1459
1460 let result: i32 = sandbox.call("GetStatic", ()).unwrap();
1461 assert_eq!(result, 0);
1462 }
1463 });
1464
1465 thread_handles.push(handle);
1466 }
1467
1468 start_barrier.wait();
1469
1470 for handle in thread_handles {
1471 handle.join().unwrap();
1472 }
1473 }
1474
1475 #[test]
1476 fn create_200_sandboxes() {
1477 create_many_on_threads_test::<20, 10>();
1478 }
1479
1480 #[test]
1481 fn create_200_threads() {
1482 create_many_on_threads_test::<200, 1>();
1483 }
1484
1485 #[test]
1486 fn create_2000_sandboxes() {
1487 create_many_on_threads_test::<200, 10>();
1488 }
1489
1490 #[test]
1491 fn test_mmap() {
1492 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1493 .build()
1494 .unwrap();
1495
1496 let expected = b"hello world";
1497 let map_mem = page_aligned_memory(expected);
1498 let guest_base = 0x1_0000_0000; unsafe {
1501 sbox.map_region(®ion_for_memory(
1502 &map_mem,
1503 guest_base,
1504 MemoryRegionFlags::READ,
1505 ))
1506 .unwrap();
1507 }
1508
1509 let _guard = map_mem.lock.try_read().unwrap();
1510 let actual: Vec<u8> = sbox
1511 .call(
1512 "ReadMappedBuffer",
1513 (guest_base as u64, expected.len() as u64, true),
1514 )
1515 .unwrap();
1516
1517 assert_eq!(actual, expected);
1518 }
1519
1520 #[test]
1522 fn test_mmap_write_exec() {
1523 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1524 .build()
1525 .unwrap();
1526
1527 #[cfg(target_arch = "x86_64")]
1528 let expected = &[0x90, 0x90, 0x90, 0xC3]; #[cfg(target_arch = "aarch64")]
1530 let expected = &[0x1f, 0x20, 0x03, 0xd5, 0xc0, 0x03, 0x5f, 0xd6];
1531 let map_mem = page_aligned_memory(expected);
1532 let guest_base = 0x1_0000_0000; unsafe {
1535 sbox.map_region(®ion_for_memory(
1536 &map_mem,
1537 guest_base,
1538 MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
1539 ))
1540 .unwrap();
1541 }
1542
1543 let _guard = map_mem.lock.try_read().unwrap();
1544
1545 let succeed = sbox
1547 .call::<bool>(
1548 "ExecMappedBuffer",
1549 (guest_base as u64, expected.len() as u64),
1550 )
1551 .unwrap();
1552 assert!(succeed, "Expected execution of mapped buffer to succeed");
1553
1554 let err = sbox
1556 .call::<bool>(
1557 "WriteMappedBuffer",
1558 (guest_base as u64, expected.len() as u64),
1559 )
1560 .unwrap_err();
1561
1562 match err {
1563 HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {}
1564 _ => panic!("Expected MemoryAccessViolation error"),
1565 };
1566 }
1567
1568 fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory {
1569 let page_size = page_size::get();
1570 let len = src.len().div_ceil(page_size) * page_size;
1571
1572 let mut mem = ExclusiveSharedMemory::new(len).unwrap();
1573 mem.copy_from_slice(src, 0).unwrap();
1574
1575 let (_, guest_mem) = mem.build();
1576
1577 guest_mem
1578 }
1579
1580 fn region_for_memory(
1581 mem: &GuestSharedMemory,
1582 guest_base: usize,
1583 flags: MemoryRegionFlags,
1584 ) -> MemoryRegion {
1585 let len = mem.mem_size();
1586 MemoryRegion {
1587 host_region: mem.host_region_base()..mem.host_region_end(),
1588 guest_region: guest_base..(guest_base + len),
1589 flags,
1590 region_type: MemoryRegionType::Heap,
1591 }
1592 }
1593
1594 fn allocate_guest_memory() -> GuestSharedMemory {
1595 page_aligned_memory(b"test data for snapshot")
1596 }
1597
1598 #[test]
1599 fn snapshot_restore_handles_remapping_correctly() {
1600 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1601 .build()
1602 .unwrap();
1603
1604 let snapshot1 = sbox.snapshot().unwrap();
1606 assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1607
1608 let map_mem = allocate_guest_memory();
1610 let guest_base = 0x200000000_usize;
1611 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1612
1613 unsafe { sbox.map_region(®ion).unwrap() };
1614 assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1615 let orig_read = sbox
1616 .call::<Vec<u8>>(
1617 "ReadMappedBuffer",
1618 (
1619 guest_base as u64,
1620 hyperlight_common::vmem::PAGE_SIZE as u64,
1621 true,
1622 ),
1623 )
1624 .unwrap();
1625
1626 let snapshot2 = sbox.snapshot().unwrap();
1628 assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1629
1630 sbox.restore(snapshot1.clone()).unwrap();
1632 assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1633 let is_mapped = sbox
1634 .call::<bool>("CheckMapped", (guest_base as u64,))
1635 .unwrap();
1636 assert!(!is_mapped);
1637
1638 sbox.restore(snapshot2.clone()).unwrap();
1641 assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1642 let is_mapped = sbox
1643 .call::<bool>("CheckMapped", (guest_base as u64,))
1644 .unwrap();
1645 assert!(is_mapped);
1646
1647 let new_read = sbox
1649 .call::<Vec<u8>>(
1650 "ReadMappedBuffer",
1651 (
1652 guest_base as u64,
1653 hyperlight_common::vmem::PAGE_SIZE as u64,
1654 false,
1655 ),
1656 )
1657 .unwrap();
1658 assert_eq!(new_read, orig_read);
1659 }
1660
1661 #[test]
1665 fn snapshot_restore_across_sandboxes_preserves_mapped_region_contents() {
1666 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1667 .build()
1668 .unwrap();
1669
1670 let map_mem = allocate_guest_memory();
1671 let guest_base = 0x200000000_usize;
1672 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1673 unsafe { source.map_region(®ion).unwrap() };
1674
1675 let orig_read = source
1677 .call::<Vec<u8>>(
1678 "ReadMappedBuffer",
1679 (
1680 guest_base as u64,
1681 hyperlight_common::vmem::PAGE_SIZE as u64,
1682 true,
1683 ),
1684 )
1685 .unwrap();
1686
1687 let snapshot = source.snapshot().unwrap();
1688
1689 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1690 .build()
1691 .unwrap();
1692 assert_eq!(target.vm.get_mapped_regions().count(), 0);
1693
1694 target.restore(snapshot).unwrap();
1695 assert_eq!(target.vm.get_mapped_regions().count(), 0);
1696
1697 let new_read = target
1700 .call::<Vec<u8>>(
1701 "ReadMappedBuffer",
1702 (
1703 guest_base as u64,
1704 hyperlight_common::vmem::PAGE_SIZE as u64,
1705 false,
1706 ),
1707 )
1708 .unwrap();
1709 assert_eq!(new_read, orig_read);
1710 }
1711
1712 #[test]
1713 fn snapshot_restore_across_sandboxes() {
1714 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1715 .build()
1716 .unwrap();
1717
1718 let mut sandbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
1719 .build()
1720 .unwrap();
1721
1722 sandbox.call::<i32>("AddToStatic", 42i32).unwrap();
1723 assert_eq!(sandbox2.call::<i32>("GetStatic", ()).unwrap(), 0);
1724
1725 let snapshot = sandbox.snapshot().unwrap();
1726 sandbox2.restore(snapshot).unwrap();
1727 assert_eq!(sandbox2.call::<i32>("GetStatic", ()).unwrap(), 42);
1728 }
1729
1730 #[test]
1731 #[cfg(not(gdb))]
1732 fn snapshot_restore_keeps_current_base_mappings() {
1733 let path = simple_guest_as_pathbuf();
1734 let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1735 .unwrap()
1736 .evolve()
1737 .unwrap();
1738 let snapshot = sandbox.snapshot().unwrap();
1739 sandbox.restore(snapshot.clone()).unwrap();
1740 sandbox.call::<i32>("AddToStatic", 42i32).unwrap();
1741 let mappings = sandbox.vm.base_mapping_state();
1742 let fault_plan = sandbox
1743 .vm
1744 .inject_vm_faults([VmOperation::Unmap(MemoryRegionType::Snapshot)]);
1745
1746 sandbox.restore(snapshot).unwrap();
1747
1748 assert_eq!(sandbox.status(), SandboxStatus::Ready);
1749 let new_mappings = sandbox.vm.base_mapping_state();
1750 assert_eq!(new_mappings.0, mappings.0);
1752 assert_eq!(new_mappings.1.map(|m| m.1), mappings.1.map(|m| m.1));
1755 assert!(!fault_plan.is_consumed());
1756 assert_eq!(sandbox.call::<i32>("GetStatic", ()).unwrap(), 0);
1757 }
1758
1759 #[test]
1760 #[cfg(not(gdb))]
1761 fn snapshot_restore_mapping_failure_is_unrecoverable() {
1762 let path = simple_guest_as_pathbuf();
1763 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1764 .unwrap()
1765 .evolve()
1766 .unwrap();
1767 source.call::<i32>("AddToStatic", 42i32).unwrap();
1768 let snapshot = source.snapshot().unwrap();
1769
1770 let path = simple_guest_as_pathbuf();
1771 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1772 .unwrap()
1773 .evolve()
1774 .unwrap();
1775 let mappings = target.vm.base_mapping_state();
1776 let fault_plan = target
1777 .vm
1778 .inject_vm_faults([VmOperation::Map(MemoryRegionType::Snapshot)]);
1779
1780 let error = target.restore(snapshot.clone()).unwrap_err();
1781 assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
1782 assert_eq!(target.status(), SandboxStatus::Unrecoverable);
1783 assert_eq!(target.vm.base_mapping_state(), (None, mappings.1));
1784 assert!(fault_plan.is_consumed());
1785
1786 assert!(matches!(
1787 target.restore(snapshot),
1788 Err(HyperlightError::UnrecoverableSandbox)
1789 ));
1790 assert!(matches!(
1791 target.call::<i32>("GetStatic", ()),
1792 Err(HyperlightError::UnrecoverableSandbox)
1793 ));
1794 assert!(matches!(
1795 target.snapshot(),
1796 Err(HyperlightError::UnrecoverableSandbox)
1797 ));
1798
1799 let map_mem = allocate_guest_memory();
1800 let region = region_for_memory(&map_mem, 0x200000000_usize, MemoryRegionFlags::READ);
1801 assert!(matches!(
1802 unsafe { target.map_region(®ion) },
1803 Err(HyperlightError::UnrecoverableSandbox)
1804 ));
1805 }
1806
1807 #[test]
1808 #[cfg(not(gdb))]
1809 fn scratch_mapping_failure_clears_mapping_state() {
1810 let path = simple_guest_as_pathbuf();
1811 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1812 .unwrap()
1813 .evolve()
1814 .unwrap();
1815 let snapshot_mapping = target.vm.base_mapping_state().0;
1816 let scratch = ExclusiveSharedMemory::new(target.mem_mgr.scratch_mem.mem_size()).unwrap();
1817 let (_, scratch) = scratch.build();
1818 let fault_plan = target
1819 .vm
1820 .inject_vm_faults([VmOperation::Map(MemoryRegionType::Scratch)]);
1821
1822 target.vm.update_scratch_mapping(scratch).unwrap_err();
1823 assert_eq!(target.vm.base_mapping_state(), (snapshot_mapping, None));
1824 assert!(fault_plan.is_consumed());
1825 }
1826
1827 #[test]
1828 #[cfg(not(gdb))]
1829 fn snapshot_restore_unmapping_failure_is_unrecoverable() {
1830 let path = simple_guest_as_pathbuf();
1831 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1832 .unwrap()
1833 .evolve()
1834 .unwrap();
1835 source.call::<i32>("AddToStatic", 42i32).unwrap();
1836 let snapshot = source.snapshot().unwrap();
1837
1838 let path = simple_guest_as_pathbuf();
1839 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1840 .unwrap()
1841 .evolve()
1842 .unwrap();
1843 let mappings = target.vm.base_mapping_state();
1844 let fault_plan = target
1845 .vm
1846 .inject_vm_faults([VmOperation::Unmap(MemoryRegionType::Snapshot)]);
1847
1848 let error = target.restore(snapshot).unwrap_err();
1849 assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
1850 assert_eq!(target.status(), SandboxStatus::Unrecoverable);
1851 assert_eq!(target.vm.base_mapping_state(), mappings);
1852 assert!(fault_plan.is_consumed());
1853 }
1854
1855 #[test]
1856 #[cfg(not(gdb))]
1857 fn snapshot_restore_dynamic_unmapping_failure_is_recoverable() {
1858 let path = simple_guest_as_pathbuf();
1859 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1860 .unwrap()
1861 .evolve()
1862 .unwrap();
1863 source.call::<i32>("AddToStatic", 42i32).unwrap();
1864 let snapshot = source.snapshot().unwrap();
1865
1866 let path = simple_guest_as_pathbuf();
1867 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1868 .unwrap()
1869 .evolve()
1870 .unwrap();
1871 let map_mem = allocate_guest_memory();
1872 let region = region_for_memory(&map_mem, 0x200000000_usize, MemoryRegionFlags::READ);
1873 unsafe { target.map_region(®ion).unwrap() };
1874 let fault_plan = target
1875 .vm
1876 .inject_vm_faults([VmOperation::Unmap(MemoryRegionType::Heap)]);
1877
1878 let error = target.restore(snapshot.clone()).unwrap_err();
1879 assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
1880 assert!(target.status().is_poisoned());
1881 assert_eq!(target.vm.get_mapped_regions().count(), 1);
1882 assert!(fault_plan.is_consumed());
1883
1884 target.restore(snapshot).unwrap();
1885 assert_eq!(target.status(), SandboxStatus::Ready);
1886 assert_eq!(target.vm.get_mapped_regions().count(), 0);
1887 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
1888 }
1889
1890 #[test]
1891 #[cfg(not(gdb))]
1892 fn snapshot_restore_partial_dynamic_unmapping_failure_is_recoverable() {
1893 let path = simple_guest_as_pathbuf();
1894 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1895 .unwrap()
1896 .evolve()
1897 .unwrap();
1898 source.call::<i32>("AddToStatic", 42i32).unwrap();
1899 let snapshot = source.snapshot().unwrap();
1900
1901 let path = simple_guest_as_pathbuf();
1902 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1903 .unwrap()
1904 .evolve()
1905 .unwrap();
1906 let first_mem = allocate_guest_memory();
1907 let first_region =
1908 region_for_memory(&first_mem, 0x200000000_usize, MemoryRegionFlags::READ);
1909 unsafe { target.map_region(&first_region).unwrap() };
1910 let (mapped_path, _) =
1911 create_test_file("hyperlight_test_partial_dynamic_unmapping.bin", &[0; 4096]);
1912 target.map_file_cow(&mapped_path, 0x300000000).unwrap();
1913 let second_region = target.vm.get_mapped_regions().last().unwrap().clone();
1914 let fault_plan = target
1915 .vm
1916 .inject_vm_faults([VmOperation::Unmap(MemoryRegionType::MappedFile)]);
1917
1918 let error = target.restore(snapshot.clone()).unwrap_err();
1919 assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
1920 assert!(target.status().is_poisoned());
1921 assert_eq!(
1922 target.vm.get_mapped_regions().collect::<Vec<_>>(),
1923 vec![&second_region]
1924 );
1925 assert!(fault_plan.is_consumed());
1926
1927 target.restore(snapshot).unwrap();
1928 assert_eq!(target.status(), SandboxStatus::Ready);
1929 assert_eq!(target.vm.get_mapped_regions().count(), 0);
1930 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
1931 std::fs::remove_file(mapped_path).unwrap();
1932 }
1933
1934 #[test]
1935 #[cfg(not(gdb))]
1936 fn snapshot_restore_vcpu_reset_failure_is_recoverable() {
1937 let path = simple_guest_as_pathbuf();
1938 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1939 .unwrap()
1940 .evolve()
1941 .unwrap();
1942 source.call::<i32>("AddToStatic", 42i32).unwrap();
1943 let snapshot = source.snapshot().unwrap();
1944
1945 #[cfg(target_arch = "x86_64")]
1946 let reset_operations = [
1947 VmOperation::SetRegs,
1948 VmOperation::SetDebugRegs,
1949 VmOperation::ResetXsave,
1950 VmOperation::SetSregs,
1951 ];
1952 #[cfg(target_arch = "aarch64")]
1953 let reset_operations = [VmOperation::ResetVcpu];
1954
1955 for reset_operation in reset_operations {
1956 let path = simple_guest_as_pathbuf();
1957 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1958 .unwrap()
1959 .evolve()
1960 .unwrap();
1961 let fault_plan = target.vm.inject_vm_faults([reset_operation]);
1962
1963 let error = target.restore(snapshot.clone()).unwrap_err();
1964 assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
1965 assert!(target.status().is_poisoned());
1966 assert!(fault_plan.is_consumed());
1967 assert_eq!(
1968 target.vm.base_mapping_state(),
1969 (
1970 Some((
1971 target.mem_mgr.shared_mem.base_addr(),
1972 target.mem_mgr.shared_mem.mem_size(),
1973 )),
1974 Some((
1975 target.mem_mgr.scratch_mem.base_addr(),
1976 target.mem_mgr.scratch_mem.mem_size(),
1977 )),
1978 )
1979 );
1980
1981 target.restore(snapshot.clone()).unwrap();
1982 assert_eq!(target.status(), SandboxStatus::Ready);
1983 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
1984 }
1985 }
1986
1987 #[test]
1988 #[cfg(all(target_arch = "x86_64", not(gdb)))]
1989 fn snapshot_restore_msr_failure_is_recoverable() {
1990 let path = simple_guest_as_pathbuf();
1991 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1992 .unwrap()
1993 .evolve()
1994 .unwrap();
1995 source.call::<i32>("AddToStatic", 42i32).unwrap();
1996 let snapshot = source.snapshot().unwrap();
1997
1998 let path = simple_guest_as_pathbuf();
1999 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2000 .unwrap()
2001 .evolve()
2002 .unwrap();
2003 let fault_plan = target.vm.inject_vm_faults([VmOperation::SetMsrs]);
2004
2005 let error = target.restore(snapshot.clone()).unwrap_err();
2006 assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
2007 assert!(target.status().is_poisoned());
2008 assert!(fault_plan.is_consumed());
2009 assert_eq!(
2010 target.vm.base_mapping_state(),
2011 (
2012 Some((
2013 target.mem_mgr.shared_mem.base_addr(),
2014 target.mem_mgr.shared_mem.mem_size(),
2015 )),
2016 Some((
2017 target.mem_mgr.scratch_mem.base_addr(),
2018 target.mem_mgr.scratch_mem.mem_size(),
2019 )),
2020 )
2021 );
2022
2023 target.restore(snapshot).unwrap();
2024 assert_eq!(target.status(), SandboxStatus::Ready);
2025 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
2026 }
2027
2028 #[test]
2029 fn snapshot_restore_accepts_different_configured_layout() {
2030 type Configure = fn(&mut SandboxConfiguration);
2031 type LayoutValue = fn(&crate::mem::layout::SandboxMemoryLayout) -> usize;
2032 let cases: &[(&str, Configure, LayoutValue)] = &[
2033 (
2034 "input",
2035 |cfg| cfg.set_input_data_size(0x8000),
2036 |layout| layout.input_data_size(),
2037 ),
2038 (
2039 "output",
2040 |cfg| cfg.set_output_data_size(0x8000),
2041 |layout| layout.output_data_size(),
2042 ),
2043 (
2044 "heap",
2045 |cfg| cfg.set_heap_size(0x40_000),
2046 |layout| layout.heap_size(),
2047 ),
2048 (
2049 "scratch",
2050 |cfg| cfg.set_scratch_size(0x90_000),
2051 |layout| layout.get_scratch_size(),
2052 ),
2053 ];
2054
2055 for (name, configure, layout_value) in cases {
2056 for incoming_is_larger in [true, false] {
2057 let mut custom_cfg = SandboxConfiguration::default();
2058 configure(&mut custom_cfg);
2059 let (source_cfg, target_cfg) = if incoming_is_larger {
2060 (custom_cfg, SandboxConfiguration::default())
2061 } else {
2062 (SandboxConfiguration::default(), custom_cfg)
2063 };
2064
2065 let path = simple_guest_as_pathbuf();
2066 let mut source =
2067 UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
2068 .unwrap()
2069 .evolve()
2070 .unwrap();
2071
2072 let path = simple_guest_as_pathbuf();
2073 let mut target =
2074 UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
2075 .unwrap()
2076 .evolve()
2077 .unwrap();
2078
2079 let source_value = layout_value(&source.mem_mgr.layout);
2080 assert_ne!(source_value, layout_value(&target.mem_mgr.layout));
2081
2082 source.call::<i32>("AddToStatic", 42i32).unwrap();
2083 target
2084 .restore(source.snapshot().unwrap())
2085 .unwrap_or_else(|err| panic!("restore with different {name} layout: {err}"));
2086 assert_eq!(layout_value(&target.mem_mgr.layout), source_value);
2087 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
2088 }
2089 }
2090 }
2091
2092 #[test]
2093 fn snapshot_restore_recovers_oom_with_larger_heap() {
2094 let mut source_cfg = SandboxConfiguration::default();
2095 source_cfg.set_heap_size(0x20_000);
2096 let path = simple_guest_as_pathbuf();
2097 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
2098 .unwrap()
2099 .evolve()
2100 .unwrap();
2101 let snapshot = source.snapshot().unwrap();
2102
2103 let mut target_cfg = SandboxConfiguration::default();
2104 target_cfg.set_heap_size(0x8000);
2105 let path = simple_guest_as_pathbuf();
2106 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
2107 .unwrap()
2108 .evolve()
2109 .unwrap();
2110
2111 assert!(target.call::<()>("ExhaustHeap", ()).is_err());
2112 assert!(target.status().is_poisoned());
2113
2114 target.restore(snapshot).unwrap();
2115 assert!(!target.status().is_poisoned());
2116 assert_eq!(
2117 target.call::<i32>("CallMalloc", 0x10_000i32).unwrap(),
2118 0x10_000
2119 );
2120 }
2121
2122 #[test]
2123 fn snapshot_restore_applies_smaller_heap_limit() {
2124 let mut source_cfg = SandboxConfiguration::default();
2125 source_cfg.set_heap_size(0x8000);
2126 let path = simple_guest_as_pathbuf();
2127 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
2128 .unwrap()
2129 .evolve()
2130 .unwrap();
2131 let snapshot = source.snapshot().unwrap();
2132
2133 let mut target_cfg = SandboxConfiguration::default();
2134 target_cfg.set_heap_size(0x20_000);
2135 let path = simple_guest_as_pathbuf();
2136 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
2137 .unwrap()
2138 .evolve()
2139 .unwrap();
2140
2141 assert_eq!(
2142 target.call::<i32>("CallMalloc", 0x10_000i32).unwrap(),
2143 0x10_000
2144 );
2145 target.restore(snapshot).unwrap();
2146 assert_eq!(target.mem_mgr.layout.heap_size(), 0x8000);
2147 assert!(target.call::<i32>("CallMalloc", 0x10_000i32).is_err());
2148 assert!(target.status().is_poisoned());
2149 }
2150
2151 #[test]
2152 fn snapshot_restore_applies_smaller_io_limits() {
2153 let mut source_cfg = SandboxConfiguration::default();
2154 source_cfg.set_input_data_size(0x2000);
2155 source_cfg.set_output_data_size(0x2000);
2156 let path = simple_guest_as_pathbuf();
2157 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
2158 .unwrap()
2159 .evolve()
2160 .unwrap();
2161 let snapshot = source.snapshot().unwrap();
2162
2163 let mut target_cfg = SandboxConfiguration::default();
2164 target_cfg.set_input_data_size(0x8000);
2165 target_cfg.set_output_data_size(0x8000);
2166 let path = simple_guest_as_pathbuf();
2167 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
2168 .unwrap()
2169 .evolve()
2170 .unwrap();
2171 let large = "x".repeat(0x3000);
2172
2173 assert_eq!(target.call::<String>("Echo", large.clone()).unwrap(), large);
2174 target.restore(snapshot).unwrap();
2175 assert_eq!(target.mem_mgr.layout.input_data_size(), 0x2000);
2176 assert_eq!(target.mem_mgr.layout.output_data_size(), 0x2000);
2177 assert!(target.call::<String>("Echo", large).is_err());
2178 assert!(!target.status().is_poisoned());
2179 assert_eq!(
2180 target.call::<String>("Echo", "small".to_string()).unwrap(),
2181 "small"
2182 );
2183 }
2184
2185 #[test]
2186 fn snapshot_restore_alternates_different_layouts() {
2187 let mut small_cfg = SandboxConfiguration::default();
2188 small_cfg.set_input_data_size(0x2000);
2189 small_cfg.set_output_data_size(0x2000);
2190 small_cfg.set_heap_size(0x8000);
2191 let path = simple_guest_as_pathbuf();
2192 let mut small = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(small_cfg))
2193 .unwrap()
2194 .evolve()
2195 .unwrap();
2196 small.call::<i32>("AddToStatic", 11i32).unwrap();
2197 let small_snapshot = small.snapshot().unwrap();
2198
2199 let mut large_cfg = SandboxConfiguration::default();
2200 large_cfg.set_input_data_size(0x8000);
2201 large_cfg.set_output_data_size(0x8000);
2202 large_cfg.set_heap_size(0x40_000);
2203 large_cfg.set_scratch_size(0x90_000);
2204 let path = simple_guest_as_pathbuf();
2205 let mut large = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(large_cfg))
2206 .unwrap()
2207 .evolve()
2208 .unwrap();
2209 large.call::<i32>("AddToStatic", 22i32).unwrap();
2210 let large_snapshot = large.snapshot().unwrap();
2211
2212 let path = simple_guest_as_pathbuf();
2213 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2214 .unwrap()
2215 .evolve()
2216 .unwrap();
2217
2218 target.restore(small_snapshot.clone()).unwrap();
2219 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 11);
2220 assert_eq!(target.mem_mgr.layout.heap_size(), 0x8000);
2221
2222 target.restore(large_snapshot).unwrap();
2223 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 22);
2224 assert_eq!(target.mem_mgr.layout.heap_size(), 0x40_000);
2225
2226 target.restore(small_snapshot).unwrap();
2227 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 11);
2228 assert_eq!(target.mem_mgr.layout.heap_size(), 0x8000);
2229 }
2230
2231 #[test]
2232 fn snapshot_restore_replaces_rust_guest_with_c_guest() {
2233 let init_data = b"cross-layout-init-data";
2234 let source_env = GuestEnvironment {
2235 guest_binary: GuestBinary::FilePath(c_simple_guest_as_pathbuf()),
2236 init_data: Some(GuestBlob {
2237 data: init_data,
2238 permissions: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE,
2239 }),
2240 };
2241 let mut source = UninitializedSandbox::new(source_env, None)
2242 .unwrap()
2243 .evolve()
2244 .unwrap();
2245 let mut target =
2246 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
2247 .unwrap()
2248 .evolve()
2249 .unwrap();
2250
2251 assert_eq!(source.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
2252 assert_eq!(target.call::<i32>("AddToStatic", 17i32).unwrap(), 17);
2253 target.set_pt_root_finder(Box::new(|_, _, root| vec![root]));
2254 assert!(target.pt_root_finder.is_some());
2255
2256 assert_ne!(
2257 source.mem_mgr.layout.code_size(),
2258 target.mem_mgr.layout.code_size()
2259 );
2260 assert_ne!(
2261 source.mem_mgr.layout.init_data_size(),
2262 target.mem_mgr.layout.init_data_size()
2263 );
2264 assert_ne!(
2265 source.mem_mgr.layout.init_data_permissions(),
2266 target.mem_mgr.layout.init_data_permissions()
2267 );
2268
2269 let snapshot = source.snapshot().unwrap();
2270 target.restore(snapshot).unwrap();
2271 assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
2272 assert!(matches!(
2273 target.call::<i32>("GetStatic", ()),
2274 Err(HyperlightError::GuestError(
2275 ErrorCode::GuestFunctionNotFound,
2276 name
2277 )) if name == "GetStatic"
2278 ));
2279 }
2280
2281 #[test]
2282 fn snapshot_restore_replaces_c_guest_with_rust_guest() {
2283 let mut source =
2284 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
2285 .unwrap()
2286 .evolve()
2287 .unwrap();
2288 assert_eq!(source.call::<i32>("AddToStatic", 42i32).unwrap(), 42);
2289 let snapshot = source.snapshot().unwrap();
2290
2291 let mut target =
2292 UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
2293 .unwrap()
2294 .evolve()
2295 .unwrap();
2296 assert_eq!(target.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
2297
2298 target.restore(snapshot).unwrap();
2299 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
2300 assert!(matches!(
2301 target.call::<i32>("StackAllocate", 512i32),
2302 Err(HyperlightError::GuestError(
2303 ErrorCode::GuestFunctionNotFound,
2304 name
2305 )) if name == "StackAllocate"
2306 ));
2307 }
2308
2309 #[test]
2310 fn snapshot_restore_alternates_c_and_rust_guests() {
2311 let mut c_source =
2312 UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
2313 .unwrap()
2314 .evolve()
2315 .unwrap();
2316 assert_eq!(c_source.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
2317 let c_snapshot = c_source.snapshot().unwrap();
2318
2319 let mut rust_source =
2320 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
2321 .unwrap()
2322 .evolve()
2323 .unwrap();
2324 rust_source.call::<i32>("AddToStatic", 42i32).unwrap();
2325 let rust_snapshot = rust_source.snapshot().unwrap();
2326
2327 let mut target =
2328 UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
2329 .unwrap()
2330 .evolve()
2331 .unwrap();
2332 assert_eq!(target.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
2333
2334 target.restore(rust_snapshot).unwrap();
2335 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
2336 assert!(matches!(
2337 target.call::<i32>("StackAllocate", 512i32),
2338 Err(HyperlightError::GuestError(
2339 ErrorCode::GuestFunctionNotFound,
2340 name
2341 )) if name == "StackAllocate"
2342 ));
2343
2344 target.restore(c_snapshot).unwrap();
2345 assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
2346 assert!(matches!(
2347 target.call::<i32>("GetStatic", ()),
2348 Err(HyperlightError::GuestError(
2349 ErrorCode::GuestFunctionNotFound,
2350 name
2351 )) if name == "GetStatic"
2352 ));
2353 }
2354
2355 #[test]
2356 fn snapshot_restore_keeps_target_host_function_implementation() {
2357 let path = simple_guest_as_pathbuf();
2358 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2359 source
2360 .register_host_function("Echo42", || Ok(1i64))
2361 .unwrap();
2362 let mut source = source.evolve().unwrap();
2363 let snapshot = source.snapshot().unwrap();
2364
2365 let path = simple_guest_as_pathbuf();
2366 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2367 target
2368 .register_host_function("Echo42", || Ok(42i64))
2369 .unwrap();
2370 let mut target = target.evolve().unwrap();
2371
2372 target.restore(snapshot).unwrap();
2373 assert_eq!(
2374 target
2375 .call::<i64>(
2376 "CallGivenParamlessHostFuncThatReturnsI64",
2377 "Echo42".to_string(),
2378 )
2379 .unwrap(),
2380 42
2381 );
2382 }
2383
2384 #[test]
2385 fn snapshot_restore_recovers_poison_with_different_guest() {
2386 let mut source =
2387 UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
2388 .unwrap()
2389 .evolve()
2390 .unwrap();
2391 let snapshot = source.snapshot().unwrap();
2392
2393 let path = simple_guest_as_pathbuf();
2394 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2395 .unwrap()
2396 .evolve()
2397 .unwrap();
2398 assert!(target.call::<()>("ExhaustHeap", ()).is_err());
2399 assert!(target.status().is_poisoned());
2400
2401 target.restore(snapshot).unwrap();
2402 assert!(!target.status().is_poisoned());
2403 assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
2404 assert!(matches!(
2405 target.call::<i32>("GetStatic", ()),
2406 Err(HyperlightError::GuestError(
2407 ErrorCode::GuestFunctionNotFound,
2408 name
2409 )) if name == "GetStatic"
2410 ));
2411 }
2412
2413 #[test]
2416 fn snapshot_restore_failure_leaves_target_usable() {
2417 let path = simple_guest_as_pathbuf();
2418 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2419 source
2420 .register_host_function("Add", |a: i32, b: i32| Ok(a + b))
2421 .unwrap();
2422 let mut source = source.evolve().unwrap();
2423
2424 let map_mem = allocate_guest_memory();
2425 let path = simple_guest_as_pathbuf();
2426 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2427 .unwrap()
2428 .evolve()
2429 .unwrap();
2430
2431 target.call::<i32>("AddToStatic", 5i32).unwrap();
2432 let guest_base = 0x200000000_usize;
2433 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
2434 unsafe { target.map_region(®ion).unwrap() };
2436 target
2437 .call::<Vec<u8>>(
2438 "ReadMappedBuffer",
2439 (
2440 guest_base as u64,
2441 hyperlight_common::vmem::PAGE_SIZE as u64,
2442 true,
2443 ),
2444 )
2445 .unwrap();
2446 let cached_snapshot = target.snapshot().unwrap();
2447 let bad_snapshot = source.snapshot().unwrap();
2448 let err = target.restore(bad_snapshot);
2449 assert!(matches!(
2450 err,
2451 Err(HyperlightError::SnapshotHostFunctionMismatch { missing, .. })
2452 if missing.iter().any(|name| name == "Add")
2453 ));
2454
2455 assert!(Arc::ptr_eq(&target.snapshot().unwrap(), &cached_snapshot));
2456 assert_eq!(target.vm.get_mapped_regions().count(), 1);
2457 assert!(
2458 target
2459 .call::<bool>("CheckMapped", guest_base as u64)
2460 .unwrap()
2461 );
2462 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 5);
2463 target.call::<i32>("AddToStatic", 3i32).unwrap();
2464 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 8);
2465
2466 let good_snapshot = target.snapshot().unwrap();
2467 target.call::<i32>("AddToStatic", 100i32).unwrap();
2468 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 108);
2469 target.restore(good_snapshot).unwrap();
2470 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 8);
2471 }
2472
2473 #[test]
2476 fn snapshot_restore_across_sandboxes_target_has_mapped_regions() {
2477 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2478 .build()
2479 .unwrap();
2480 source.call::<i32>("AddToStatic", 23i32).unwrap();
2481 let snapshot = source.snapshot().unwrap();
2482
2483 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2484 .build()
2485 .unwrap();
2486 let map_mem = allocate_guest_memory();
2487 let guest_base = 0x200000000_usize;
2488 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
2489 unsafe { target.map_region(®ion).unwrap() };
2490 assert_eq!(target.vm.get_mapped_regions().count(), 1);
2491
2492 target.restore(snapshot).unwrap();
2493 assert_eq!(target.vm.get_mapped_regions().count(), 0);
2494 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
2495 }
2496
2497 #[test]
2498 fn snapshot_restore_unmaps_regions_overlapping_incoming_layout() {
2499 let mut source_cfg = SandboxConfiguration::default();
2500 source_cfg.set_scratch_size(0x90_000);
2501 let path = simple_guest_as_pathbuf();
2502 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
2503 .unwrap()
2504 .evolve()
2505 .unwrap();
2506 source.call::<i32>("AddToStatic", 23i32).unwrap();
2507 let snapshot = source.snapshot().unwrap();
2508
2509 let path = simple_guest_as_pathbuf();
2510 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2511 .unwrap()
2512 .evolve()
2513 .unwrap();
2514 assert!(snapshot.memory().mem_size() > target.mem_mgr.shared_mem.mem_size());
2515
2516 let map_mem = allocate_guest_memory();
2517 let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS
2518 + target.mem_mgr.shared_mem.mem_size();
2519 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
2520 unsafe { target.map_region(®ion).unwrap() };
2522
2523 target.restore(snapshot).unwrap();
2524 assert_eq!(target.vm.get_mapped_regions().count(), 0);
2525 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
2526 }
2527
2528 #[test]
2529 fn snapshot_restore_unmaps_region_overlapping_incoming_scratch() {
2530 let incoming_scratch_size = 0x90_000;
2531 let mut source_cfg = SandboxConfiguration::default();
2532 source_cfg.set_scratch_size(incoming_scratch_size);
2533 let path = simple_guest_as_pathbuf();
2534 let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
2535 .unwrap()
2536 .evolve()
2537 .unwrap();
2538 source.call::<i32>("AddToStatic", 23i32).unwrap();
2539 let snapshot = source.snapshot().unwrap();
2540
2541 let path = simple_guest_as_pathbuf();
2542 let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2543 .unwrap()
2544 .evolve()
2545 .unwrap();
2546 let guest_base =
2547 hyperlight_common::layout::scratch_base_gpa(incoming_scratch_size) as usize;
2548 let target_scratch_base =
2549 hyperlight_common::layout::scratch_base_gpa(SandboxConfiguration::DEFAULT_SCRATCH_SIZE)
2550 as usize;
2551 let map_mem = allocate_guest_memory();
2552 assert!(guest_base + map_mem.mem_size() <= target_scratch_base);
2553 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
2554 unsafe { target.map_region(®ion).unwrap() };
2556
2557 target.restore(snapshot).unwrap();
2558 assert_eq!(target.vm.get_mapped_regions().count(), 0);
2559 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
2560 }
2561
2562 #[test]
2566 fn snapshot_restore_across_sandboxes_both_have_different_mapped_regions() {
2567 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2568 .build()
2569 .unwrap();
2570 let source_mem = allocate_guest_memory();
2571 let source_base = 0x200000000_usize;
2572 let source_region = region_for_memory(&source_mem, source_base, MemoryRegionFlags::READ);
2573 unsafe { source.map_region(&source_region).unwrap() };
2574 let orig_read = source
2575 .call::<Vec<u8>>(
2576 "ReadMappedBuffer",
2577 (
2578 source_base as u64,
2579 hyperlight_common::vmem::PAGE_SIZE as u64,
2580 true,
2581 ),
2582 )
2583 .unwrap();
2584 source.call::<i32>("AddToStatic", 9i32).unwrap();
2585 let snapshot = source.snapshot().unwrap();
2586
2587 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2588 .build()
2589 .unwrap();
2590 let target_mem = allocate_guest_memory();
2591 let target_base = 0x300000000_usize;
2592 let target_region = region_for_memory(&target_mem, target_base, MemoryRegionFlags::READ);
2593 unsafe { target.map_region(&target_region).unwrap() };
2594 assert_eq!(target.vm.get_mapped_regions().count(), 1);
2595
2596 target.restore(snapshot).unwrap();
2597
2598 assert_eq!(target.vm.get_mapped_regions().count(), 0);
2599 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 9);
2600
2601 let new_read = target
2602 .call::<Vec<u8>>(
2603 "ReadMappedBuffer",
2604 (
2605 source_base as u64,
2606 hyperlight_common::vmem::PAGE_SIZE as u64,
2607 false,
2608 ),
2609 )
2610 .unwrap();
2611 assert_eq!(new_read, orig_read);
2612 }
2613
2614 #[test]
2616 fn snapshot_restore_across_sandboxes_repeated() {
2617 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2618 .build()
2619 .unwrap();
2620 source.call::<i32>("AddToStatic", 7i32).unwrap();
2621 let snapshot = source.snapshot().unwrap();
2622
2623 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2624 .build()
2625 .unwrap();
2626
2627 target.restore(snapshot.clone()).unwrap();
2628 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 7);
2629
2630 target.call::<i32>("AddToStatic", 1000i32).unwrap();
2631 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 1007);
2632
2633 target.restore(snapshot).unwrap();
2634 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 7);
2635 }
2636
2637 #[test]
2640 fn snapshot_restore_resets_debug_registers() {
2641 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2642 .build()
2643 .unwrap();
2644
2645 let snapshot = sandbox.snapshot().unwrap();
2646
2647 let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap();
2649 assert_eq!(dr0_initial, 0, "DR0 should initially be 0");
2650
2651 const DIRTY_VALUE: u64 = 0xFFFF_FEDC_7654_3210;
2654 sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap();
2655
2656 #[cfg(not(hvf))]
2658 {
2659 let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap();
2671 assert_eq!(
2672 dr0_dirty, DIRTY_VALUE,
2673 "DR0 should be dirty after SetDr0 call"
2674 );
2675 }
2676
2677 sandbox.restore(snapshot).unwrap();
2679
2680 let dr0_after_restore: u64 = sandbox.call("GetDr0", ()).unwrap();
2681 assert_eq!(
2682 dr0_after_restore, 0,
2683 "DR0 should be 0 after restore (reset_vcpu should have been called)"
2684 );
2685 }
2686
2687 #[test]
2688 #[cfg(target_arch = "x86_64")]
2689 fn snapshot_restore_resets_xcr0() {
2690 let mut sandbox: MultiUseSandbox = {
2691 let path = simple_guest_as_pathbuf();
2692 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2693 u_sbox.evolve().unwrap()
2694 };
2695
2696 assert_eq!(sandbox.call::<u64>("ReadXcr0", ()).unwrap(), 1);
2697 let snapshot = sandbox.snapshot().unwrap();
2698
2699 sandbox.call::<()>("WriteXcr0", 3u64).unwrap();
2700 assert_eq!(sandbox.call::<u64>("ReadXcr0", ()).unwrap(), 3);
2701
2702 sandbox.restore(snapshot).unwrap();
2703
2704 assert_eq!(
2705 sandbox.call::<u64>("ReadXcr0", ()).unwrap(),
2706 1,
2707 "restore must reset XCR0"
2708 );
2709 }
2710
2711 #[test]
2714 fn stale_abort_buffer_does_not_leak_across_calls() {
2715 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2716 .build()
2717 .unwrap();
2718
2719 sbox.mem_mgr.abort_buffer.extend_from_slice(&[0xAA; 1020]);
2721
2722 let res = sbox.call::<String>("Echo", "hello".to_string());
2723 assert!(
2724 res.is_ok(),
2725 "Expected Ok after stale abort buffer, got: {:?}",
2726 res.unwrap_err()
2727 );
2728
2729 assert!(
2731 sbox.mem_mgr.abort_buffer.is_empty(),
2732 "abort_buffer should be empty after a guest call"
2733 );
2734 }
2735
2736 #[test]
2738 fn test_sandbox_creation_various_sizes() {
2739 let test_cases: [(&str, u64); 3] = [
2740 ("small (8MB heap)", SMALL_HEAP_SIZE),
2741 ("medium (64MB heap)", MEDIUM_HEAP_SIZE),
2742 ("large (256MB heap)", LARGE_HEAP_SIZE),
2743 ];
2744
2745 for (name, heap_size) in test_cases {
2746 let path = simple_guest_as_pathbuf();
2747 let sbox = SandboxBuilder::from_file(path)
2748 .heap_size(heap_size)
2749 .scratch_size(0x100000)
2750 .build()
2751 .unwrap_or_else(|e| panic!("Failed to create {} sandbox: {}", name, e));
2752
2753 drop(sbox);
2754 }
2755 }
2756
2757 #[cfg(feature = "trace_guest")]
2759 fn sandbox_for_gva_tests() -> MultiUseSandbox {
2760 let path = simple_guest_as_pathbuf();
2761 SandboxBuilder::from_file(path).build().unwrap()
2762 }
2763
2764 #[cfg(feature = "trace_guest")]
2768 fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
2769 let expected: Vec<u8> = sbox
2771 .call("ReadMappedBuffer", (gva, len as u64, true))
2772 .unwrap();
2773 assert_eq!(expected.len(), len);
2774
2775 let root_pt = sbox.vm.get_root_pt().unwrap();
2777 let actual = sbox
2778 .mem_mgr
2779 .read_guest_memory_by_gva(gva, len, root_pt)
2780 .unwrap();
2781
2782 assert_eq!(
2783 actual, expected,
2784 "read_guest_memory_by_gva at GVA {:#x} (len {}) differs from guest ReadMappedBuffer",
2785 gva, len,
2786 );
2787 }
2788
2789 #[test]
2792 #[cfg(feature = "trace_guest")]
2793 fn read_guest_memory_by_gva_single_page() {
2794 let mut sbox = sandbox_for_gva_tests();
2795 let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2796 assert_gva_read_matches(&mut sbox, code_gva, 128);
2797 }
2798
2799 #[test]
2802 #[cfg(feature = "trace_guest")]
2803 fn read_guest_memory_by_gva_full_page() {
2804 let mut sbox = sandbox_for_gva_tests();
2805 let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2806 assert_gva_read_matches(&mut sbox, code_gva, 4096);
2807 }
2808
2809 #[test]
2812 #[cfg(feature = "trace_guest")]
2813 fn read_guest_memory_by_gva_unaligned_cross_page() {
2814 let mut sbox = sandbox_for_gva_tests();
2815 let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2816 let start = code_gva + 4096 - 1;
2819 println!(
2820 "Testing unaligned cross-page read starting at {:#x} spanning 4097 bytes",
2821 start
2822 );
2823 assert_gva_read_matches(&mut sbox, start, 4097);
2824 }
2825
2826 #[test]
2828 #[cfg(feature = "trace_guest")]
2829 fn read_guest_memory_by_gva_two_full_pages() {
2830 let mut sbox = sandbox_for_gva_tests();
2831 let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2832 assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
2833 }
2834
2835 #[test]
2839 #[cfg(feature = "trace_guest")]
2840 fn read_guest_memory_by_gva_cross_page_boundary() {
2841 let mut sbox = sandbox_for_gva_tests();
2842 let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2843 let start = code_gva + 4096 - 100;
2845 assert_gva_read_matches(&mut sbox, start, 200);
2846 }
2847
2848 fn create_test_file(name: &str, content: &[u8]) -> (std::path::PathBuf, Vec<u8>) {
2852 use std::io::Write;
2853
2854 let page_size = page_size::get();
2855 let padded_len = content.len().max(page_size).div_ceil(page_size) * page_size;
2856 let mut padded = vec![0u8; padded_len];
2857 padded[..content.len()].copy_from_slice(content);
2858
2859 let temp_dir = std::env::temp_dir();
2860 let path = temp_dir.join(name);
2861 let _ = std::fs::remove_file(&path); let mut f = std::fs::File::create(&path).unwrap();
2863 f.write_all(&padded).unwrap();
2864 (path, content.to_vec())
2865 }
2866
2867 #[test]
2870 fn test_map_file_cow_basic() {
2871 let expected = b"hello world from map_file_cow";
2872 let (path, expected_bytes) =
2873 create_test_file("hyperlight_test_map_file_cow_basic.bin", expected);
2874
2875 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2876 .build()
2877 .unwrap();
2878
2879 let guest_base: u64 = 0x1_0000_0000;
2880 let mapped_size = sbox.map_file_cow(&path, guest_base).unwrap();
2881 assert!(mapped_size > 0, "mapped_size should be positive");
2882 assert!(
2883 mapped_size >= expected.len() as u64,
2884 "mapped_size should be >= file content length"
2885 );
2886
2887 let actual: Vec<u8> = sbox
2889 .call(
2890 "ReadMappedBuffer",
2891 (guest_base, expected_bytes.len() as u64, true),
2892 )
2893 .unwrap();
2894
2895 assert_eq!(
2896 actual, expected_bytes,
2897 "Guest should read back the exact file content"
2898 );
2899
2900 let _ = std::fs::remove_file(&path);
2902 }
2903
2904 #[test]
2907 fn test_map_file_cow_read_only_enforcement() {
2908 let content = &[0xBB; 4096];
2909 let (path, _) = create_test_file("hyperlight_test_map_file_cow_readonly.bin", content);
2910
2911 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2912 .build()
2913 .unwrap();
2914
2915 let guest_base: u64 = 0x1_0000_0000;
2916 sbox.map_file_cow(&path, guest_base).unwrap();
2917
2918 let err = sbox
2920 .call::<bool>("WriteMappedBuffer", (guest_base, content.len() as u64))
2921 .unwrap_err();
2922
2923 match err {
2924 HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base => {}
2925 _ => panic!(
2926 "Expected MemoryAccessViolation at guest_base, got: {:?}",
2927 err
2928 ),
2929 };
2930
2931 let _ = std::fs::remove_file(&path);
2933 }
2934
2935 #[test]
2938 fn test_map_file_cow_poisoned() {
2939 let (path, _) = create_test_file("hyperlight_test_map_file_cow_poison.bin", &[0xCC; 4096]);
2940
2941 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2942 .build()
2943 .unwrap();
2944 let snapshot = sbox.snapshot().unwrap();
2945
2946 let _ = sbox
2948 .call::<()>("guest_panic", "hello".to_string())
2949 .unwrap_err();
2950 assert!(sbox.status().is_poisoned());
2951
2952 let err = sbox.map_file_cow(&path, 0x1_0000_0000).unwrap_err();
2954 assert!(matches!(err, HyperlightError::PoisonedSandbox));
2955
2956 sbox.restore(snapshot).unwrap();
2958 assert_eq!(sbox.status(), SandboxStatus::Ready);
2959 let result = sbox.map_file_cow(&path, 0x1_0000_0000);
2960 assert!(result.is_ok());
2961
2962 let _ = std::fs::remove_file(&path);
2963 }
2964
2965 #[test]
2968 fn test_map_file_cow_multi_vm_same_file() {
2969 let expected = b"shared file content across VMs";
2970 let (path, expected_bytes) =
2971 create_test_file("hyperlight_test_map_file_cow_multi_vm.bin", expected);
2972
2973 let guest_base: u64 = 0x1_0000_0000;
2974
2975 let mut sbox1 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2976 .build()
2977 .unwrap();
2978
2979 let mut sbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
2980 .build()
2981 .unwrap();
2982
2983 sbox1.map_file_cow(&path, guest_base).unwrap();
2985 sbox2.map_file_cow(&path, guest_base).unwrap();
2986
2987 let actual1: Vec<u8> = sbox1
2989 .call(
2990 "ReadMappedBuffer",
2991 (guest_base, expected_bytes.len() as u64, true),
2992 )
2993 .unwrap();
2994 let actual2: Vec<u8> = sbox2
2995 .call(
2996 "ReadMappedBuffer",
2997 (guest_base, expected_bytes.len() as u64, true),
2998 )
2999 .unwrap();
3000
3001 assert_eq!(
3002 actual1, expected_bytes,
3003 "Sandbox 1 should read correct content"
3004 );
3005 assert_eq!(
3006 actual2, expected_bytes,
3007 "Sandbox 2 should read correct content"
3008 );
3009
3010 let _ = std::fs::remove_file(&path);
3011 }
3012
3013 #[test]
3016 fn test_map_file_cow_multi_vm_threaded() {
3017 let expected = b"threaded file mapping test data";
3018 let (path, expected_bytes) =
3019 create_test_file("hyperlight_test_map_file_cow_threaded.bin", expected);
3020
3021 const NUM_THREADS: usize = 5;
3022 let path = Arc::new(path);
3023 let expected_bytes = Arc::new(expected_bytes);
3024 let barrier = Arc::new(Barrier::new(NUM_THREADS));
3025 let mut handles = vec![];
3026
3027 for _ in 0..NUM_THREADS {
3028 let path = path.clone();
3029 let expected_bytes = expected_bytes.clone();
3030 let barrier = barrier.clone();
3031
3032 handles.push(thread::spawn(move || {
3033 barrier.wait();
3034
3035 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3036 .build()
3037 .unwrap();
3038
3039 let guest_base: u64 = 0x1_0000_0000;
3040 sbox.map_file_cow(&path, guest_base).unwrap();
3041
3042 let actual: Vec<u8> = sbox
3043 .call(
3044 "ReadMappedBuffer",
3045 (guest_base, expected_bytes.len() as u64, true),
3046 )
3047 .unwrap();
3048
3049 assert_eq!(actual, *expected_bytes);
3050 }));
3051 }
3052
3053 for h in handles {
3054 h.join().unwrap();
3055 }
3056
3057 let _ = std::fs::remove_file(&*path);
3058 }
3059
3060 #[test]
3063 #[cfg(target_os = "windows")]
3064 fn test_map_file_cow_cleanup_no_handle_leak() {
3065 let (path, _) = create_test_file("hyperlight_test_map_file_cow_cleanup.bin", &[0xDD; 4096]);
3066
3067 {
3068 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3069 .build()
3070 .unwrap();
3071
3072 sbox.map_file_cow(&path, 0x1_0000_0000).unwrap();
3073 }
3075
3076 std::fs::remove_file(&path)
3077 .expect("File should be deletable after sandbox with map_file_cow is dropped");
3078 }
3079
3080 #[test]
3084 fn test_map_file_cow_snapshot_remapping_cycle() {
3085 let expected = b"snapshot remapping cycle test!";
3086 let (path, expected_bytes) =
3087 create_test_file("hyperlight_test_map_file_cow_snapshot_remap.bin", expected);
3088
3089 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3090 .build()
3091 .unwrap();
3092
3093 let guest_base: u64 = 0x1_0000_0000;
3094
3095 let snapshot1 = sbox.snapshot().unwrap();
3097
3098 sbox.map_file_cow(&path, guest_base).unwrap();
3100
3101 let actual: Vec<u8> = sbox
3103 .call(
3104 "ReadMappedBuffer",
3105 (guest_base, expected_bytes.len() as u64, true),
3106 )
3107 .unwrap();
3108 assert_eq!(actual, expected_bytes);
3109
3110 let snapshot2 = sbox.snapshot().unwrap();
3112
3113 sbox.restore(snapshot1.clone()).unwrap();
3115 let is_mapped: bool = sbox.call("CheckMapped", (guest_base,)).unwrap();
3116 assert!(
3117 !is_mapped,
3118 "Region should be unmapped after restoring to snapshot₁"
3119 );
3120
3121 sbox.restore(snapshot2).unwrap();
3124 let is_mapped: bool = sbox.call("CheckMapped", (guest_base,)).unwrap();
3125 assert!(
3126 is_mapped,
3127 "Region should be mapped after restoring to snapshot₂"
3128 );
3129 let actual2: Vec<u8> = sbox
3130 .call(
3131 "ReadMappedBuffer",
3132 (guest_base, expected_bytes.len() as u64, false),
3133 )
3134 .unwrap();
3135 assert_eq!(
3136 actual2, expected_bytes,
3137 "Data should be intact after snapshot₂ restore"
3138 );
3139
3140 let _ = std::fs::remove_file(&path);
3141 }
3142
3143 #[test]
3146 fn test_map_file_cow_snapshot_restore() {
3147 let expected = b"snapshot restore basic test!!";
3148 let (path, expected_bytes) =
3149 create_test_file("hyperlight_test_map_file_cow_snap_restore.bin", expected);
3150
3151 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3152 .build()
3153 .unwrap();
3154
3155 let guest_base: u64 = 0x1_0000_0000;
3156 sbox.map_file_cow(&path, guest_base).unwrap();
3157
3158 let actual: Vec<u8> = sbox
3160 .call(
3161 "ReadMappedBuffer",
3162 (guest_base, expected_bytes.len() as u64, true),
3163 )
3164 .unwrap();
3165 assert_eq!(actual, expected_bytes);
3166
3167 let snapshot = sbox.snapshot().unwrap();
3169
3170 sbox.restore(snapshot).unwrap();
3172
3173 let actual2: Vec<u8> = sbox
3175 .call(
3176 "ReadMappedBuffer",
3177 (guest_base, expected_bytes.len() as u64, false),
3178 )
3179 .unwrap();
3180 assert_eq!(
3181 actual2, expected_bytes,
3182 "Data should be readable after restore from snapshot"
3183 );
3184
3185 let _ = std::fs::remove_file(&path);
3186 }
3187
3188 #[test]
3192 fn test_map_file_cow_deferred_basic() {
3193 let expected = b"deferred map_file_cow test data";
3194 let (path, expected_bytes) =
3195 create_test_file("hyperlight_test_map_file_cow_deferred.bin", expected);
3196
3197 let guest_base: u64 = 0x1_0000_0000;
3198
3199 let mut u_sbox =
3200 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
3201 .unwrap();
3202
3203 let mapped_size = u_sbox.map_file_cow(&path, guest_base).unwrap();
3205 assert!(mapped_size > 0, "mapped_size should be positive");
3206 assert!(
3207 mapped_size >= expected.len() as u64,
3208 "mapped_size should be >= file content length"
3209 );
3210
3211 let mut sbox = u_sbox.evolve().unwrap();
3213
3214 let actual: Vec<u8> = sbox
3216 .call(
3217 "ReadMappedBuffer",
3218 (guest_base, expected_bytes.len() as u64, true),
3219 )
3220 .unwrap();
3221
3222 assert_eq!(
3223 actual, expected_bytes,
3224 "Guest should read back the exact file content after deferred mapping"
3225 );
3226
3227 let _ = std::fs::remove_file(&path);
3228 }
3229
3230 #[test]
3234 fn test_map_file_cow_deferred_drop_without_evolve() {
3235 let (path, _) = create_test_file(
3236 "hyperlight_test_map_file_cow_deferred_drop.bin",
3237 &[0xAA; 4096],
3238 );
3239
3240 let guest_base: u64 = 0x1_0000_0000;
3241
3242 {
3243 let mut u_sbox =
3244 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
3245 .unwrap();
3246
3247 u_sbox.map_file_cow(&path, guest_base).unwrap();
3248 }
3251
3252 #[cfg(target_os = "windows")]
3255 std::fs::remove_file(&path)
3256 .expect("File should be deletable after dropping UninitializedSandbox");
3257 #[cfg(not(target_os = "windows"))]
3258 let _ = std::fs::remove_file(&path);
3259 }
3260
3261 #[test]
3264 fn test_map_file_cow_unaligned_guest_base() {
3265 let (path, _) =
3266 create_test_file("hyperlight_test_map_file_cow_unaligned.bin", &[0xBB; 4096]);
3267
3268 let mut u_sbox =
3269 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
3270 .unwrap();
3271
3272 let unaligned_base: u64 = (page_size::get() + 1) as u64;
3274 let result = u_sbox.map_file_cow(&path, unaligned_base);
3275 assert!(
3276 result.is_err(),
3277 "map_file_cow should reject unaligned guest_base"
3278 );
3279
3280 let _ = std::fs::remove_file(&path);
3281 }
3282
3283 #[test]
3285 fn test_map_file_cow_empty_file() {
3286 let temp_dir = std::env::temp_dir();
3287 let path = temp_dir.join("hyperlight_test_map_file_cow_empty.bin");
3288 let _ = std::fs::remove_file(&path);
3289 std::fs::File::create(&path).unwrap(); let mut u_sbox =
3292 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
3293 .unwrap();
3294
3295 let guest_base: u64 = 0x1_0000_0000;
3296 let result = u_sbox.map_file_cow(&path, guest_base);
3297 assert!(result.is_err(), "map_file_cow should reject empty files");
3298
3299 let _ = std::fs::remove_file(&path);
3300 }
3301
3302 #[test]
3304 fn test_map_file_cow_overlapping_mappings() {
3305 let (path1, _) =
3306 create_test_file("hyperlight_test_map_file_cow_overlap1.bin", &[0xAA; 4096]);
3307 let (path2, _) =
3308 create_test_file("hyperlight_test_map_file_cow_overlap2.bin", &[0xBB; 4096]);
3309
3310 let guest_base: u64 = 0x1_0000_0000;
3311
3312 let mut u_sbox =
3313 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
3314 .unwrap();
3315
3316 u_sbox.map_file_cow(&path1, guest_base).unwrap();
3318
3319 let result = u_sbox.map_file_cow(&path2, guest_base);
3321 assert!(
3322 result.is_err(),
3323 "map_file_cow should reject overlapping guest address ranges"
3324 );
3325
3326 let _ = std::fs::remove_file(&path1);
3327 let _ = std::fs::remove_file(&path2);
3328 }
3329
3330 #[test]
3333 fn test_map_file_cow_shared_mem_overlap() {
3334 let (path, _) = create_test_file(
3335 "hyperlight_test_map_file_cow_overlap_shm.bin",
3336 &[0xCC; 4096],
3337 );
3338
3339 let mut u_sbox =
3340 UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
3341 .unwrap();
3342
3343 let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
3345 let result = u_sbox.map_file_cow(&path, base_addr);
3347 assert!(
3348 result.is_err(),
3349 "map_file_cow should reject guest_base inside shared memory"
3350 );
3351
3352 let _ = std::fs::remove_file(&path);
3353 }
3354
3355 #[test]
3356 fn map_region_rejects_overlapping_regions() {
3357 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3358 .build()
3359 .unwrap();
3360
3361 let mem1 = allocate_guest_memory();
3362 let mem2 = allocate_guest_memory();
3363 let guest_base: usize = 0x200000000;
3364 let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
3365
3366 unsafe { sbox.map_region(®ion1).unwrap() };
3368
3369 let region2 = region_for_memory(&mem2, guest_base, MemoryRegionFlags::READ);
3371 let err = unsafe { sbox.map_region(®ion2) }.unwrap_err();
3372 assert!(
3373 format!("{err:?}").contains("Overlapping"),
3374 "Expected Overlapping error, got: {err:?}"
3375 );
3376 }
3377
3378 #[test]
3379 fn map_region_rejects_partial_overlap() {
3380 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3381 .build()
3382 .unwrap();
3383
3384 let ps = page_size::get();
3386 let mem1 = page_aligned_memory(&vec![0xAA; ps * 2]); let mem2 = page_aligned_memory(&vec![0xBB; ps * 2]); let guest_base: usize = 0x200000000;
3389 let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
3390
3391 unsafe { sbox.map_region(®ion1).unwrap() };
3392
3393 let overlap_base = guest_base - ps;
3395 let region2 = region_for_memory(&mem2, overlap_base, MemoryRegionFlags::READ);
3396 let err = unsafe { sbox.map_region(®ion2) }.unwrap_err();
3397 assert!(
3398 format!("{err:?}").contains("verlap"),
3399 "Expected overlap error for partial overlap, got: {err:?}"
3400 );
3401 }
3402
3403 #[test]
3404 fn map_region_allows_adjacent_non_overlapping() {
3405 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3406 .build()
3407 .unwrap();
3408
3409 let mem1 = allocate_guest_memory();
3410 let mem2 = allocate_guest_memory();
3411 let guest_base: usize = 0x200000000;
3412 let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
3413 let region_size = mem1.mem_size();
3414
3415 unsafe { sbox.map_region(®ion1).unwrap() };
3416
3417 let adjacent_base = guest_base + region_size;
3419 let region2 = region_for_memory(&mem2, adjacent_base, MemoryRegionFlags::READ);
3420 unsafe { sbox.map_region(®ion2).unwrap() };
3421 }
3422
3423 #[test]
3424 fn map_region_rejects_overlap_with_snapshot() {
3425 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3426 .build()
3427 .unwrap();
3428
3429 let mem = allocate_guest_memory();
3431 let region = region_for_memory(
3432 &mem,
3433 crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS,
3434 MemoryRegionFlags::READ,
3435 );
3436 let err = unsafe { sbox.map_region(®ion) }.unwrap_err();
3437 assert!(
3438 format!("{err:?}").contains("Overlapping"),
3439 "Expected Overlapping error for snapshot overlap, got: {err:?}"
3440 );
3441 }
3442
3443 #[test]
3444 fn map_region_rejects_overlap_with_scratch() {
3445 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3446 .build()
3447 .unwrap();
3448
3449 let scratch_addr = hyperlight_common::layout::scratch_base_gpa(
3451 crate::sandbox::SandboxConfiguration::DEFAULT_SCRATCH_SIZE,
3452 ) as usize;
3453 let mem = allocate_guest_memory();
3454 let region = region_for_memory(&mem, scratch_addr, MemoryRegionFlags::READ);
3455 let err = unsafe { sbox.map_region(®ion) }.unwrap_err();
3456 assert!(
3457 format!("{err:?}").contains("verlap"),
3458 "Expected overlap error for scratch region, got: {err:?}"
3459 );
3460 }
3461
3462 #[cfg(target_arch = "x86_64")]
3463 mod msr_tests {
3464 use super::*;
3465 use crate::hypervisor::hyperlight_vm::{CreateHyperlightVmError, HyperlightVmError};
3466 use crate::hypervisor::regs::{
3467 MSR_APERF, MSR_BNDCFGS, MSR_CSTAR, MSR_DEBUGCTL, MSR_IA32_SSP,
3468 MSR_INTERRUPT_SSP_TABLE_ADDR, MSR_KERNEL_GS_BASE as KERNEL_GS_BASE, MSR_LSTAR,
3469 MSR_MPERF, MSR_MTRR_DEF_TYPE, MSR_MTRR_FIX64K_00000, MSR_PAT, MSR_PL0_SSP, MSR_PL1_SSP,
3470 MSR_PL2_SSP, MSR_PL3_SSP, MSR_S_CET, MSR_SFMASK, MSR_SPEC_CTRL, MSR_STAR,
3471 MSR_SYSENTER_CS as SYSENTER_CS, MSR_SYSENTER_EIP, MSR_SYSENTER_ESP, MSR_TSC,
3472 MSR_TSC_ADJUST, MSR_TSC_AUX, MSR_TSC_DEADLINE, MSR_TSX_CTRL, MSR_U_CET,
3473 MSR_UMWAIT_CONTROL, MSR_VIRT_SPEC_CTRL, MSR_XFD, MSR_XFD_ERR, MSR_XSS,
3474 };
3475 use crate::hypervisor::virtual_machine::{
3476 CreateVmError, RegisterError, ResetVcpuError, VmError,
3477 };
3478 use crate::sandbox::snapshot::Snapshot;
3479
3480 fn assert_msr_not_declarable(error: &HyperlightError, expected: u32) {
3481 assert!(
3482 matches!(
3483 error,
3484 HyperlightError::HyperlightVmError(HyperlightVmError::Create(
3485 CreateHyperlightVmError::Vm(VmError::CreateVm(
3486 CreateVmError::MsrNotDeclarable { msr, .. }
3487 ))
3488 )) if *msr == expected
3489 ),
3490 "expected MsrNotAllowable for {expected:#x}, got: {error:?}"
3491 );
3492 }
3493
3494 fn assert_snapshot_msr_index_invalid(error: &HyperlightError) {
3495 assert!(
3496 matches!(
3497 error,
3498 HyperlightError::HyperlightVmError(HyperlightVmError::Restore(
3499 ResetVcpuError::Register(RegisterError::InvalidSnapshotMsrIndex { .. })
3500 ))
3501 ),
3502 "expected InvalidSnapshotMsrIndex, got: {error:?}"
3503 );
3504 }
3505
3506 #[test]
3507 fn kernel_gs_base_does_not_leak_through_swapgs() {
3508 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3509 .build()
3510 .unwrap();
3511
3512 let original: u64 = sandbox.call("ReadKernelGsBaseViaSwapgs", ()).unwrap();
3513 let sentinel = if original == 0x0000_7AAA_5555_AAAA {
3514 0x0000_6BBB_4444_BBBB
3515 } else {
3516 0x0000_7AAA_5555_AAAA
3517 };
3518 let snapshot = sandbox.snapshot().unwrap();
3519
3520 sandbox
3521 .call::<()>("WriteKernelGsBaseViaSwapgs", sentinel)
3522 .unwrap();
3523 assert_eq!(
3524 sandbox
3525 .call::<u64>("ReadKernelGsBaseViaSwapgs", ())
3526 .unwrap(),
3527 sentinel
3528 );
3529
3530 sandbox.restore(snapshot).unwrap();
3531 assert_eq!(
3532 sandbox
3533 .call::<u64>("ReadKernelGsBaseViaSwapgs", ())
3534 .unwrap(),
3535 original,
3536 "KERNEL_GS_BASE leaked across restore"
3537 );
3538 }
3539
3540 #[test]
3541 fn snapshot_msr_values_survive_full_in_memory_lifecycle() {
3542 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3543 .guest_msrs(&[KERNEL_GS_BASE])
3544 .unwrap()
3545 .build()
3546 .unwrap();
3547 let first = 0x1111;
3548 let second = 0x2222;
3549 let third = 0x3333;
3550
3551 source
3552 .call::<()>("WriteMSR", (KERNEL_GS_BASE, first))
3553 .unwrap();
3554 assert_eq!(
3555 source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3556 first
3557 );
3558 let first_snapshot = source.snapshot().unwrap();
3559
3560 source
3561 .call::<()>("WriteMSR", (KERNEL_GS_BASE, second))
3562 .unwrap();
3563 assert_eq!(
3564 source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3565 second
3566 );
3567 source.restore(first_snapshot.clone()).unwrap();
3568 assert_eq!(
3569 source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3570 first
3571 );
3572
3573 let mut clone = SandboxBuilder::from_snapshot(first_snapshot.clone())
3574 .guest_msrs(&[KERNEL_GS_BASE])
3575 .unwrap()
3576 .build()
3577 .unwrap();
3578 assert_eq!(clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(), first);
3579
3580 clone
3581 .call::<()>("WriteMSR", (KERNEL_GS_BASE, third))
3582 .unwrap();
3583 assert_eq!(clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(), third);
3584 let third_snapshot = clone.snapshot().unwrap();
3585 source.restore(third_snapshot.clone()).unwrap();
3586 assert_eq!(
3587 source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3588 third
3589 );
3590
3591 let mut second_clone = SandboxBuilder::from_snapshot(third_snapshot)
3592 .guest_msrs(&[KERNEL_GS_BASE])
3593 .unwrap()
3594 .build()
3595 .unwrap();
3596 assert_eq!(
3597 second_clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3598 third
3599 );
3600 second_clone.restore(first_snapshot).unwrap();
3601 assert_eq!(
3602 second_clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3603 first
3604 );
3605 }
3606
3607 #[test]
3608 fn equivalent_msr_configs_are_order_independent_across_sandboxes() {
3609 let source_order = [KERNEL_GS_BASE, SYSENTER_CS];
3610 let target_order = [SYSENTER_CS, KERNEL_GS_BASE];
3611 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3612 .guest_msrs(&source_order)
3613 .unwrap()
3614 .build()
3615 .unwrap();
3616 source
3617 .call::<()>("WriteMSR", (KERNEL_GS_BASE, 0x4444u64))
3618 .unwrap();
3619 assert_eq!(
3620 source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3621 0x4444
3622 );
3623 source
3624 .call::<()>("WriteMSR", (SYSENTER_CS, 0x5555u64))
3625 .unwrap();
3626 assert_eq!(source.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0x5555);
3627 let snapshot = source.snapshot().unwrap();
3628
3629 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3630 .guest_msrs(&target_order)
3631 .unwrap()
3632 .build()
3633 .unwrap();
3634 target
3635 .call::<()>("WriteMSR", (KERNEL_GS_BASE, 0xAAAAu64))
3636 .unwrap();
3637 assert_eq!(
3638 target.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3639 0xAAAA
3640 );
3641 target
3642 .call::<()>("WriteMSR", (SYSENTER_CS, 0xBBBBu64))
3643 .unwrap();
3644 assert_eq!(target.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0xBBBB);
3645 target.restore(snapshot.clone()).unwrap();
3646 assert_eq!(
3647 target.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3648 0x4444
3649 );
3650 assert_eq!(target.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0x5555);
3651
3652 let mut clone = SandboxBuilder::from_snapshot(snapshot)
3653 .guest_msrs(&target_order)
3654 .unwrap()
3655 .build()
3656 .unwrap();
3657 assert_eq!(
3658 clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3659 0x4444
3660 );
3661 assert_eq!(clone.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0x5555);
3662 }
3663
3664 #[test]
3668 fn snapshot_restores_into_superset_guest_msrs() {
3669 const SYSENTER_ESP: u32 = 0x175;
3670 let sentinel: u64 = 0x1234;
3671 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3672 .guest_msrs(&[SYSENTER_CS])
3673 .unwrap()
3674 .build()
3675 .unwrap();
3676 source
3677 .call::<()>("WriteMSR", (SYSENTER_CS, sentinel))
3678 .unwrap();
3679 let snapshot = source.snapshot().unwrap();
3680
3681 let mut clone = SandboxBuilder::from_snapshot(snapshot.clone())
3682 .guest_msrs(&[SYSENTER_CS, SYSENTER_ESP])
3683 .unwrap()
3684 .build()
3685 .unwrap();
3686 assert_eq!(clone.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), sentinel);
3687 let baseline: u64 = clone.call("ReadMSR", SYSENTER_ESP).unwrap();
3688
3689 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3690 .guest_msrs(&[SYSENTER_CS, SYSENTER_ESP])
3691 .unwrap()
3692 .build()
3693 .unwrap();
3694 target
3695 .call::<()>("WriteMSR", (SYSENTER_ESP, baseline ^ 0x55))
3696 .unwrap();
3697 target.restore(snapshot).unwrap();
3698 assert_eq!(
3699 target.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(),
3700 sentinel
3701 );
3702 assert_eq!(
3704 target.call::<u64>("ReadMSR", SYSENTER_ESP).unwrap(),
3705 baseline
3706 );
3707 }
3708
3709 #[test]
3714 fn snapshot_rejects_non_superset_guest_msrs() {
3715 const SYSENTER_ESP: u32 = 0x175;
3716 let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3717 .guest_msrs(&[SYSENTER_CS])
3718 .unwrap()
3719 .build()
3720 .unwrap();
3721 source
3722 .call::<()>("WriteMSR", (SYSENTER_CS, 0x1234u64))
3723 .unwrap();
3724 let snapshot = source.snapshot().unwrap();
3725
3726 for dest in [&[][..], &[SYSENTER_ESP][..]] {
3730 let err = SandboxBuilder::from_snapshot(snapshot.clone())
3731 .guest_msrs(dest)
3732 .unwrap()
3733 .build()
3734 .expect_err("from_snapshot must reject an unrestorable snapshot MSR");
3735 assert_snapshot_msr_index_invalid(&err);
3736
3737 let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3738 .guest_msrs(dest)
3739 .unwrap()
3740 .build()
3741 .unwrap();
3742 let err = target
3743 .restore(snapshot.clone())
3744 .expect_err("restore must reject an unrestorable snapshot MSR");
3745 assert_snapshot_msr_index_invalid(&err);
3746 assert!(target.status().is_poisoned());
3747 assert!(matches!(
3748 target.call::<String>("Echo", "hi".to_string()),
3749 Err(HyperlightError::PoisonedSandbox)
3750 ));
3751 }
3752 }
3753
3754 #[test]
3755 fn from_pre_init_snapshot_uses_local_msr_reset_set() {
3756 let mut config = SandboxConfiguration::default();
3757 config.guest_msrs(&[KERNEL_GS_BASE]).unwrap();
3758 let snapshot = Arc::new(
3759 Snapshot::from_env(GuestBinary::FilePath(simple_guest_as_pathbuf()), config)
3760 .unwrap(),
3761 );
3762 assert!(snapshot.msrs().is_none());
3763
3764 let mut sandbox = SandboxBuilder::from_snapshot(snapshot.clone())
3765 .guest_msrs(&[KERNEL_GS_BASE])
3766 .unwrap()
3767 .build()
3768 .unwrap();
3769 let baseline: u64 = sandbox.call("ReadMSR", KERNEL_GS_BASE).unwrap();
3770 sandbox
3771 .call::<()>("WriteMSR", (KERNEL_GS_BASE, baseline ^ 0x55))
3772 .unwrap();
3773 assert_eq!(
3774 sandbox.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
3775 baseline ^ 0x55
3776 );
3777 }
3778
3779 #[test]
3780 #[cfg(kvm)]
3781 fn guest_cannot_enable_x2apic_through_apic_base() {
3782 use crate::hypervisor::regs::APIC_BASE_X2APIC_ENABLE;
3783 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
3784
3785 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
3786 return;
3787 }
3788
3789 const MSR_IA32_APIC_BASE: u32 = 0x1B;
3790 const MSR_X2APIC_BASE: u32 = 0x800;
3791 const APIC_BASE_DEFAULT: u64 = 0xFEE0_0900;
3792
3793 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3794 .build()
3795 .unwrap();
3796 let snapshot = sandbox.snapshot().unwrap();
3797
3798 let x2apic_base = APIC_BASE_DEFAULT | APIC_BASE_X2APIC_ENABLE;
3799 let result = sandbox.call::<()>("WriteMSR", (MSR_IA32_APIC_BASE, x2apic_base));
3800 assert!(
3801 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
3802 "guest enabled x2APIC through APIC_BASE: {result:?}"
3803 );
3804 assert!(sandbox.status().is_poisoned());
3805 sandbox.restore(snapshot).unwrap();
3806 assert!(!sandbox.status().is_poisoned());
3807
3808 let result = sandbox.call::<()>("WriteMSR", (MSR_X2APIC_BASE, 1u64));
3809 assert!(
3810 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
3811 "x2APIC MSR access succeeded after restore: {result:?}"
3812 );
3813 assert!(sandbox.status().is_poisoned());
3814 }
3815
3816 #[test]
3817 #[cfg(kvm)]
3818 fn denied_msr_access_poisons_sandbox() {
3819 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
3820
3821 match get_available_hypervisor() {
3822 Some(HypervisorType::Kvm) => {}
3823 _ => {
3824 return;
3825 }
3826 }
3827
3828 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3829 .build()
3830 .unwrap();
3831
3832 let snapshot = sbox.snapshot().unwrap();
3833 let msr_index: u32 = 0xC000_0102; let result = sbox.call::<u64>("ReadMSR", msr_index);
3836 assert!(
3837 matches!(&result, Err(HyperlightError::GuestAborted(_, _))),
3838 "RDMSR 0x{:X}: expected direct #GP, got: {:?}",
3839 msr_index,
3840 result
3841 );
3842 assert!(sbox.status().is_poisoned());
3843
3844 sbox.restore(snapshot.clone()).unwrap();
3845
3846 let result = sbox.call::<()>("WriteMSR", (msr_index, 0x5u64));
3847 assert!(
3848 matches!(&result, Err(HyperlightError::GuestAborted(_, _))),
3849 "WRMSR 0x{:X}: expected direct #GP, got: {:?}",
3850 msr_index,
3851 result
3852 );
3853 assert!(sbox.status().is_poisoned());
3854 }
3855
3856 #[test]
3857 #[cfg(target_arch = "x86_64")]
3858 fn nested_virtualization_is_hidden_from_guest() {
3859 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3860 .build()
3861 .unwrap();
3862
3863 let features: u32 = sandbox.call("NestedVirtualizationCpuid", ()).unwrap();
3864 assert_eq!(features & 0b11, 0, "guest CPUID exposes VMX or SVM");
3865 }
3866
3867 #[test]
3868 #[cfg(kvm)]
3869 fn nested_vmx_setup_msrs_are_denied() {
3870 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
3871
3872 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
3873 return;
3874 }
3875
3876 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3877 .build()
3878 .unwrap();
3879
3880 let snapshot = sandbox.snapshot().unwrap();
3881 let vmx_basic: u32 = 0x480;
3882 let result = sandbox.call::<u64>("ReadMSR", vmx_basic);
3883 assert!(
3884 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
3885 "RDMSR 0x{vmx_basic:X}: expected direct #GP, got: {result:?}"
3886 );
3887
3888 sandbox.restore(snapshot).unwrap();
3889 let feature_control: u32 = 0x3A;
3890 let result = sandbox.call::<()>("WriteMSR", (feature_control, 0x5u64));
3891 assert!(
3892 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
3893 "WRMSR 0x{feature_control:X}: expected direct #GP, got: {result:?}"
3894 );
3895 }
3896
3897 #[test]
3902 #[cfg(target_arch = "x86_64")]
3903 fn guest_cannot_enter_vmx_operation() {
3904 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3905 .build()
3906 .unwrap();
3907
3908 let result = sandbox.call::<()>("EnableVmxOperation", ());
3909 assert!(
3910 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
3911 "guest entered VMX operation via CR4.VMXE: {result:?}"
3912 );
3913 assert!(sandbox.status().is_poisoned());
3914 }
3915
3916 #[test]
3921 #[cfg(target_arch = "x86_64")]
3922 fn guest_vmlaunch_faults() {
3923 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3924 .build()
3925 .unwrap();
3926
3927 let result = sandbox.call::<()>("ExecuteVmlaunch", ());
3928 assert!(
3929 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
3930 "guest executed VMLAUNCH without faulting: {result:?}"
3931 );
3932 assert!(sandbox.status().is_poisoned());
3933 }
3934
3935 #[test]
3938 #[cfg(kvm)]
3939 fn x2apic_is_hidden_from_guest_cpuid() {
3940 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
3941
3942 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
3943 return;
3944 }
3945
3946 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3947 .build()
3948 .unwrap();
3949
3950 assert!(
3951 !sandbox.call::<bool>("X2apicSupported", ()).unwrap(),
3952 "guest CPUID advertises x2APIC"
3953 );
3954 }
3955
3956 #[test]
3958 #[cfg(target_arch = "x86_64")]
3959 fn test_allow_non_resettable_msr_fails_creation() {
3960 let err = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3962 .guest_msrs(&[0x49])
3963 .unwrap()
3964 .build()
3965 .unwrap_err();
3966
3967 assert_msr_not_declarable(&err, 0x49);
3968 }
3969
3970 #[test]
3972 #[cfg(kvm)]
3973 fn unclassified_declared_msr_rejected_at_creation() {
3974 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
3975
3976 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
3977 return;
3978 }
3979
3980 let err = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3982 .guest_msrs(&[0x1A0])
3983 .unwrap()
3984 .build()
3985 .expect_err("an unclassified declared MSR must be rejected at creation");
3986
3987 assert_msr_not_declarable(&err, 0x1A0);
3988 }
3989
3990 #[test]
3991 #[cfg(target_arch = "x86_64")]
3992 fn test_multiple_guest_msrs_reset_across_restore() {
3993 let msrs: [u32; 4] = [0x174, 0x175, 0x176, 0xC000_0102];
3995
3996 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
3997 .guest_msrs(&msrs)
3998 .unwrap()
3999 .build()
4000 .unwrap();
4001
4002 let baseline_snapshot = sbox.snapshot().unwrap();
4003
4004 let value: u64 = 0x1000;
4005 for &msr in &msrs {
4006 sbox.call::<()>("WriteMSR", (msr, value)).unwrap();
4007 let read_value: u64 = sbox.call("ReadMSR", msr).unwrap();
4008 assert_eq!(read_value, value, "MSR 0x{msr:X} should be writable");
4009 }
4010
4011 sbox.restore(baseline_snapshot).unwrap();
4012 for &msr in &msrs {
4013 let read_value: u64 = sbox.call("ReadMSR", msr).unwrap();
4014 assert_ne!(
4015 read_value, value,
4016 "MSR 0x{msr:X} should be reset to baseline across restore"
4017 );
4018 }
4019 }
4020
4021 #[test]
4023 #[cfg(target_arch = "x86_64")]
4024 fn test_declared_msr_does_not_leak_across_restore() {
4025 let msr_index: u32 = 0xC000_0102; let sentinel: u64 = 0xCAFE_F00D;
4027
4028 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4029 .guest_msrs(&[msr_index])
4030 .unwrap()
4031 .build()
4032 .unwrap();
4033
4034 let baseline = sbox.snapshot().unwrap();
4035 let original: u64 = sbox.call("ReadMSR", msr_index).unwrap();
4036 assert_ne!(
4037 original, sentinel,
4038 "test sentinel must differ from the baseline value"
4039 );
4040
4041 sbox.call::<()>("WriteMSR", (msr_index, sentinel)).unwrap();
4042 assert_eq!(
4043 sbox.call::<u64>("ReadMSR", msr_index).unwrap(),
4044 sentinel,
4045 "sentinel should be observable before restore"
4046 );
4047 sbox.restore(baseline).unwrap();
4048
4049 let after: u64 = sbox.call("ReadMSR", msr_index).unwrap();
4050 assert_ne!(after, sentinel, "sentinel leaked across restore");
4051 assert_eq!(after, original, "MSR not reset to its baseline value");
4052 }
4053
4054 #[test]
4056 #[cfg(all(kvm, target_arch = "x86_64"))]
4057 fn test_debugctl_and_x2apic_msr_denied_by_default() {
4058 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
4059
4060 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
4061 return;
4062 }
4063
4064 for msr_index in [0x1D9_u32, 0x800] {
4065 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4066 .build()
4067 .unwrap();
4068
4069 let result = sbox.call::<()>("WriteMSR", (msr_index, 0x1u64));
4070 assert!(
4071 matches!(&result, Err(HyperlightError::GuestAborted(_, _))),
4072 "WRMSR 0x{msr_index:X}: expected direct #GP, got: {result:?}"
4073 );
4074 assert!(
4075 sbox.status().is_poisoned(),
4076 "sandbox should be poisoned after a denied WRMSR to 0x{msr_index:X}"
4077 );
4078 }
4079 }
4080
4081 #[test]
4082 #[cfg(all(kvm, target_arch = "x86_64"))]
4083 fn all_kvm_custom_msrs_are_denied() {
4084 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
4085
4086 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
4087 return;
4088 }
4089
4090 const KVM_CUSTOM_MSR_START: u32 = 0x4B56_4D00;
4091 const KVM_CUSTOM_MSR_END: u32 = 0x4B56_4DFF;
4092
4093 let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4094 .build()
4095 .unwrap();
4096 let snapshot = sandbox.snapshot().unwrap();
4097
4098 for index in KVM_CUSTOM_MSR_START..=KVM_CUSTOM_MSR_END {
4099 let result = sandbox.call::<u64>("ReadMSR", index);
4100 assert!(
4101 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
4102 "RDMSR {index:#x} was not denied: {result:?}"
4103 );
4104 sandbox.restore(snapshot.clone()).unwrap();
4105
4106 let result = sandbox.call::<()>("WriteMSR", (index, 1u64));
4107 assert!(
4108 matches!(result, Err(HyperlightError::GuestAborted(_, _))),
4109 "WRMSR {index:#x} was not denied: {result:?}"
4110 );
4111 sandbox.restore(snapshot.clone()).unwrap();
4112 }
4113 }
4114
4115 #[test]
4119 #[cfg(target_arch = "x86_64")]
4120 fn unresettable_msr_classes_do_not_leak() {
4121 let cases: &[(u32, &str)] = &[
4122 (0xC1, "PMU IA32_PMC0"),
4123 (0x186, "PMU IA32_PERFEVTSEL0"),
4124 (0x38F, "PMU IA32_PERF_GLOBAL_CTRL"),
4125 (0x1C8, "LBR_SELECT"),
4126 (0x14CE, "arch-LBR IA32_LBR_CTL"),
4127 (0x1D4, "FRED IA32_FRED_CONFIG"),
4128 (0xC001_0114, "AMD VM_CR"),
4129 (0xC001_0117, "AMD VM_HSAVE_PA"),
4130 ];
4131
4132 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4133 .build()
4134 .unwrap();
4135
4136 for &(msr, _name) in cases {
4137 assert_msr_write_does_not_survive_restore(&mut sbox, msr, 0x1);
4138 }
4139 }
4140
4141 #[test]
4144 #[cfg(target_arch = "x86_64")]
4145 fn misc_enable_guest_write_does_not_survive_restore() {
4146 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4147 .build()
4148 .unwrap();
4149 assert_msr_write_does_not_survive_restore(&mut sbox, 0x1A0, 1u64 << 40);
4150 }
4151
4152 #[test]
4154 #[cfg(target_arch = "x86_64")]
4155 fn runtime_msr_table_entries_are_justified() {
4156 use crate::hypervisor::regs::resettable_msr_indices;
4157
4158 #[cfg(kvm)]
4159 let is_kvm = matches!(
4160 crate::hypervisor::virtual_machine::get_available_hypervisor(),
4161 Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm)
4162 );
4163 #[cfg(not(kvm))]
4164 let is_kvm = false;
4165
4166 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4167 .build()
4168 .unwrap();
4169
4170 let reset_indices: Vec<u32> = sbox.vm.reset_set_indices();
4171
4172 for index in resettable_msr_indices() {
4173 if !reset_indices.contains(&index) {
4174 assert_omitted_msr_does_not_retain(&mut sbox, index);
4175 } else if is_kvm && index == KERNEL_GS_BASE {
4176 } else if (index == MSR_TSC && !is_kvm) || matches!(index, MSR_MPERF | MSR_APERF) {
4179 assert_guest_counter_is_writable_and_restored(&mut sbox, index);
4180 } else if let Some(test_value) = guest_write_test_value(index) {
4181 assert_guest_msr_is_writable_and_restored(&mut sbox, index, test_value);
4182 } else {
4183 assert!(
4184 reset_exception_reason(index).is_some(),
4185 "MSR 0x{index:X} is in the reset set without positive guest-write coverage or an explicit reason"
4186 );
4187 }
4188 }
4189 }
4190
4191 fn assert_omitted_msr_does_not_retain(sbox: &mut MultiUseSandbox, index: u32) {
4192 let baseline = sbox.snapshot().unwrap();
4193 let original: u64 = match sbox.call("ReadMSR", index) {
4194 Ok(value) => value,
4195 Err(_) => {
4196 assert!(
4197 sbox.status().is_poisoned(),
4198 "0x{index:X}: fault did not poison sandbox"
4199 );
4200 sbox.restore(baseline).unwrap();
4201 return;
4202 }
4203 };
4204 let preferred = guest_write_test_value(index).unwrap_or(original ^ 1);
4205 let candidates = [preferred, original ^ 1, original ^ 2, 0, 1, 0x1000];
4206
4207 for candidate in candidates {
4208 if candidate == original {
4209 continue;
4210 }
4211 if sbox.call::<()>("WriteMSR", (index, candidate)).is_err() {
4212 assert!(
4213 sbox.status().is_poisoned(),
4214 "0x{index:X}: fault did not poison sandbox"
4215 );
4216 sbox.restore(baseline.clone()).unwrap();
4217 continue;
4218 }
4219 let written: u64 = sbox.call("ReadMSR", index).unwrap_or_else(|error| {
4220 panic!("0x{index:X}: read after successful write failed: {error:?}")
4221 });
4222 if written != original {
4223 sbox.restore(baseline).unwrap();
4224 let after: u64 = sbox.call("ReadMSR", index).unwrap();
4225 assert_eq!(
4226 after, original,
4227 "0x{index:X}: guest retained a write but the MSR is absent from the reset set"
4228 );
4229 return;
4230 }
4231 sbox.restore(baseline.clone()).unwrap();
4232 }
4233 }
4234
4235 fn guest_write_test_value(index: u32) -> Option<u64> {
4236 match index {
4237 SYSENTER_CS => Some(0x10),
4238 MSR_SYSENTER_ESP | MSR_SYSENTER_EIP => Some(0x1000),
4239 MSR_PAT => Some(0x0007_0406_0007_0406),
4240 MSR_STAR => Some(0x001B_0008_0000_0000),
4241 MSR_LSTAR | MSR_CSTAR => Some(0x1000),
4242 MSR_SFMASK => Some(0x200),
4243 KERNEL_GS_BASE => Some(0x1000),
4244 MSR_TSC_ADJUST => Some(0x1000),
4245 MSR_TSC_AUX => Some(0x5),
4246 MSR_MTRR_DEF_TYPE => Some(0xC00),
4247 0x200..=0x21F if index & 1 == 0 => Some(0x6), 0x200..=0x21F => Some(0x800), MSR_MTRR_FIX64K_00000 | 0x258 | 0x259 | 0x268..=0x26F => {
4250 Some(0x0606_0606_0606_0606)
4251 }
4252 _ => None,
4253 }
4254 }
4255
4256 fn reset_exception_reason(index: u32) -> Option<&'static str> {
4257 match index {
4258 MSR_TSC => Some("KVM denies direct guest TSC MSR access"),
4259 MSR_IA32_SSP => Some(
4260 "active SSP has no architectural RDMSR/WRMSR; covered by active_ssp_does_not_leak_across_restore",
4261 ),
4262 MSR_DEBUGCTL => Some("DEBUGCTL support depends on exposed debug features"),
4263 MSR_SPEC_CTRL => Some("SPEC_CTRL writable bits depend on mitigation features"),
4264 MSR_U_CET
4265 | MSR_S_CET
4266 | MSR_PL0_SSP
4267 | MSR_PL1_SSP
4268 | MSR_PL2_SSP
4269 | MSR_PL3_SSP
4270 | MSR_INTERRUPT_SSP_TABLE_ADDR => {
4271 Some("CET writable state depends on exposed CET features")
4272 }
4273 MSR_TSX_CTRL => Some("TSX_CTRL writable bits depend on exposed TSX features"),
4274 MSR_XFD | MSR_XFD_ERR => Some("XFD writable bits depend on exposed XSAVE features"),
4275 MSR_UMWAIT_CONTROL => {
4276 Some("UMWAIT_CONTROL writable bits depend on exposed WAITPKG features")
4277 }
4278 MSR_TSC_DEADLINE => {
4279 Some("TSC_DEADLINE writable bits depend on exposed APIC-timer features")
4280 }
4281 MSR_BNDCFGS => Some("BNDCFGS writable bits depend on exposed MPX features"),
4282 MSR_XSS => Some("XSS writable bits depend on exposed XSAVE features"),
4283 MSR_VIRT_SPEC_CTRL => {
4284 Some("VIRT_SPEC_CTRL writable bits depend on exposed AMD SSBD virtualization")
4285 }
4286 _ => None,
4287 }
4288 }
4289
4290 fn assert_guest_msr_is_writable_and_restored(
4291 sbox: &mut MultiUseSandbox,
4292 index: u32,
4293 sentinel: u64,
4294 ) {
4295 let baseline = sbox.snapshot().unwrap();
4296 let original: u64 = sbox
4297 .call("ReadMSR", index)
4298 .unwrap_or_else(|error| panic!("0x{index:X}: guest RDMSR failed: {error:?}"));
4299 let value = if original == sentinel { 0 } else { sentinel };
4300
4301 sbox.call::<()>("WriteMSR", (index, value))
4302 .unwrap_or_else(|error| panic!("0x{index:X}: guest WRMSR failed: {error:?}"));
4303 let written: u64 = sbox
4304 .call("ReadMSR", index)
4305 .unwrap_or_else(|error| panic!("0x{index:X}: guest read-back failed: {error:?}"));
4306 assert_eq!(written, value, "0x{index:X}: guest write did not stick");
4307
4308 sbox.restore(baseline).unwrap();
4309 let restored: u64 = sbox.call("ReadMSR", index).unwrap();
4310 assert_eq!(
4311 restored, original,
4312 "0x{index:X}: restore did not recover the baseline"
4313 );
4314 }
4315
4316 fn assert_guest_counter_is_writable_and_restored(sbox: &mut MultiUseSandbox, index: u32) {
4317 let baseline = sbox.snapshot().unwrap();
4318 let original: u64 = sbox.call("ReadMSR", index).unwrap();
4319 let jump = original.wrapping_add(1 << 60);
4320
4321 sbox.call::<()>("WriteMSR", (index, jump)).unwrap();
4322 let written: u64 = sbox.call("ReadMSR", index).unwrap();
4323 assert!(
4324 written >= jump / 2,
4325 "0x{index:X}: guest write did not stick"
4326 );
4327
4328 sbox.restore(baseline).unwrap();
4329 let restored: u64 = sbox.call("ReadMSR", index).unwrap();
4330 assert!(
4331 restored < jump / 2,
4332 "0x{index:X}: restore did not pull the counter below the guest-written jump"
4333 );
4334 }
4335
4336 #[cfg(target_arch = "x86_64")]
4338 fn assert_msr_write_does_not_survive_restore(
4339 sbox: &mut MultiUseSandbox,
4340 msr: u32,
4341 sentinel: u64,
4342 ) {
4343 let baseline = sbox.snapshot().unwrap();
4344 let original: u64 = match sbox.call("ReadMSR", msr) {
4345 Ok(v) => v,
4346 Err(_) => {
4347 assert!(
4348 sbox.status().is_poisoned(),
4349 "0x{msr:X}: a faulting RDMSR should poison the sandbox"
4350 );
4351 sbox.restore(baseline).unwrap();
4352 return;
4353 }
4354 };
4355 assert_ne!(
4356 original, sentinel,
4357 "0x{msr:X}: sentinel must differ from baseline"
4358 );
4359
4360 if sbox.call::<()>("WriteMSR", (msr, sentinel)).is_err() {
4361 assert!(
4362 sbox.status().is_poisoned(),
4363 "0x{msr:X}: a faulting WRMSR should poison the sandbox"
4364 );
4365 sbox.restore(baseline).unwrap();
4366 return;
4367 }
4368
4369 sbox.restore(baseline).unwrap();
4370 let after: u64 = sbox.call("ReadMSR", msr).unwrap();
4371 assert_eq!(
4372 after, original,
4373 "0x{msr:X}: MSR leaked across restore (expected 0x{original:X}, got 0x{after:X})"
4374 );
4375 }
4376
4377 #[test]
4379 #[ignore = "slow host-dependent hardware MSR audit"]
4380 #[cfg(target_arch = "x86_64")]
4381 fn test_no_msr_leaks_across_restore_full_window_sweep() {
4382 const FREE_RUNNING: &[u32] = &[
4384 0x10, 0xE7, 0xE8, ];
4388
4389 #[cfg(kvm)]
4390 if matches!(
4391 crate::hypervisor::virtual_machine::get_available_hypervisor(),
4392 Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm)
4393 ) {
4394 return;
4395 }
4396
4397 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4398 .build()
4399 .unwrap();
4400
4401 let baseline = sbox.snapshot().unwrap();
4402
4403 let mut readable = 0usize;
4405 let mut exercised: Vec<u32> = Vec::new();
4406 let mut read_only: Vec<u32> = Vec::new();
4407 let mut masked_only: Vec<u32> = Vec::new();
4408 let mut free_running_leaked: Vec<u32> = Vec::new();
4410
4411 let low = 0x0000_0000u32..=0x0000_1FFF;
4413 let hyperv_synthetic = 0x4000_0000u32..=0x4000_1FFF;
4415 let extended = 0xC000_0000u32..=0xC001_FFFF;
4417 let windows = low.chain(hyperv_synthetic).chain(extended);
4418 for msr in windows {
4419 let original: u64 = match sbox.call("ReadMSR", msr) {
4420 Ok(v) => v,
4421 Err(_) => {
4422 sbox.restore(baseline.clone()).unwrap();
4423 continue;
4424 }
4425 };
4426 readable += 1;
4427
4428 if FREE_RUNNING.contains(&msr) {
4430 let jump = original.wrapping_add(1 << 60);
4431 if sbox.call::<()>("WriteMSR", (msr, jump)).is_err() {
4432 sbox.restore(baseline.clone()).unwrap();
4433 read_only.push(msr);
4434 continue;
4435 }
4436 let planted = match sbox.call::<u64>("ReadMSR", msr) {
4437 Ok(v) => v,
4438 Err(_) => {
4439 sbox.restore(baseline.clone()).unwrap();
4440 masked_only.push(msr);
4441 continue;
4442 }
4443 };
4444 if planted < jump / 2 {
4445 sbox.restore(baseline.clone()).unwrap();
4446 masked_only.push(msr);
4447 continue;
4448 }
4449 sbox.restore(baseline.clone()).unwrap();
4450 let after: u64 = sbox.call("ReadMSR", msr).unwrap();
4451 if after < jump / 2 {
4452 exercised.push(msr);
4453 } else {
4454 free_running_leaked.push(msr);
4455 }
4456 continue;
4457 }
4458
4459 let candidates = [
4461 original ^ 0x55,
4462 original ^ 0x1,
4463 original ^ (1 << 12),
4464 original ^ (1 << 20),
4465 original ^ (1 << 32),
4466 original.wrapping_add(1),
4467 0,
4468 ];
4469 let mut planted = false;
4470 let mut saw_write = false;
4471 for cand in candidates {
4472 if cand == original {
4473 continue;
4474 }
4475 if sbox.call::<()>("WriteMSR", (msr, cand)).is_err() {
4476 sbox.restore(baseline.clone()).unwrap();
4477 continue;
4478 }
4479 saw_write = true;
4480 match sbox.call::<u64>("ReadMSR", msr) {
4481 Ok(v) if v != original => {
4482 planted = true;
4483 break;
4484 }
4485 _ => {
4486 sbox.restore(baseline.clone()).unwrap();
4487 }
4488 }
4489 }
4490
4491 if planted {
4492 sbox.restore(baseline.clone()).unwrap();
4493 match sbox.call::<u64>("ReadMSR", msr) {
4494 Ok(after) => assert_eq!(
4495 after, original,
4496 "0x{msr:X}: a guest MSR write leaked across restore \
4497 (expected 0x{original:X}, got 0x{after:X})"
4498 ),
4499 Err(e) => panic!("0x{msr:X}: read-back after restore failed: {e:?}"),
4500 }
4501 exercised.push(msr);
4502 } else if saw_write {
4503 masked_only.push(msr);
4504 } else {
4505 read_only.push(msr);
4506 }
4507 }
4508
4509 let fmt = |v: &[u32]| {
4510 v.iter()
4511 .map(|m| format!("0x{m:X}"))
4512 .collect::<Vec<_>>()
4513 .join(", ")
4514 };
4515 eprintln!(
4516 "full-window MSR sweep: readable={readable} exercised={} masked_only={} read_only={}",
4517 exercised.len(),
4518 masked_only.len(),
4519 read_only.len()
4520 );
4521 eprintln!(" exercised: [{}]", fmt(&exercised));
4522 eprintln!(" masked_only: [{}]", fmt(&masked_only));
4523 eprintln!(" read_only: [{}]", fmt(&read_only));
4524 eprintln!(" free_running_leaked: [{}]", fmt(&free_running_leaked));
4525 assert!(
4526 free_running_leaked.is_empty(),
4527 "free-running MSRs not reset across restore on this backend: [{}]",
4528 fmt(&free_running_leaked)
4529 );
4530 assert!(
4531 !exercised.is_empty(),
4532 "sweep was vacuous: no guest MSR write ever retained a value that restore \
4533 then rolled back, so the rollback path was never exercised"
4534 );
4535 }
4536
4537 #[test]
4541 #[cfg(all(any(mshv3, target_os = "windows"), target_arch = "x86_64"))]
4542 fn active_ssp_does_not_leak_across_restore() {
4543 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4544 .build()
4545 .unwrap();
4546
4547 if !sbox.call::<bool>("CetShadowStackSupported", ()).unwrap() {
4548 return;
4549 }
4550
4551 let baseline = sbox.snapshot().unwrap();
4552 let original: u64 = sbox.call("ReadActiveSsp", ()).unwrap();
4553
4554 let mutated: u64 = sbox.call("IncrementActiveSsp", ()).unwrap();
4555 assert_ne!(mutated, original, "guest did not change active SSP");
4556 let seen: u64 = sbox.call("ReadActiveSsp", ()).unwrap();
4557 assert_eq!(seen, mutated, "guest did not observe its own SSP mutation");
4558
4559 sbox.restore(baseline).unwrap();
4560
4561 let after: u64 = sbox.call("ReadActiveSsp", ()).unwrap();
4562 assert_eq!(
4563 after, original,
4564 "active SSP leaked across restore (original=0x{original:X}, mutated=0x{mutated:X}, after=0x{after:X})"
4565 );
4566 }
4567
4568 #[test]
4573 #[cfg(all(kvm, target_arch = "x86_64"))]
4574 fn kvm_does_not_expose_cet_to_guest() {
4575 use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
4576
4577 if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
4578 return;
4579 }
4580
4581 let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4582 .build()
4583 .unwrap();
4584 assert!(
4585 !sbox.call::<bool>("CetShadowStackSupported", ()).unwrap(),
4586 "KVM guest CPUID exposes CET shadow stacks"
4587 );
4588
4589 let err = SandboxBuilder::from_file(simple_guest_as_pathbuf())
4592 .guest_msrs(&[MSR_S_CET])
4593 .unwrap()
4594 .build()
4595 .expect_err("allowing IA32_S_CET must be rejected when CET is hidden");
4596 assert_msr_not_declarable(&err, MSR_S_CET);
4597 }
4598 }
4599
4600 mod from_snapshot {
4602 use std::sync::Arc;
4603
4604 use hyperlight_testing::simple_guest_as_pathbuf;
4605
4606 use crate::func::Registerable;
4607 use crate::sandbox::SandboxConfiguration;
4608 use crate::sandbox::snapshot::Snapshot;
4609 use crate::{GuestBinary, HostFunctions, HyperlightError, MultiUseSandbox, SandboxBuilder};
4610
4611 fn make_sandbox() -> MultiUseSandbox {
4612 let path = simple_guest_as_pathbuf();
4613 SandboxBuilder::from_file(path).build().unwrap()
4614 }
4615
4616 fn make_sandbox_with_add() -> MultiUseSandbox {
4618 let path = simple_guest_as_pathbuf();
4619 SandboxBuilder::from_file(path)
4620 .host_function("Add", |a: i32, b: i32| a + b)
4621 .build()
4622 .unwrap()
4623 }
4624
4625 fn host_funcs_with_matching_add() -> HostFunctions {
4626 let mut hf = HostFunctions::default();
4627 hf.register_host_function("Add", |a: i32, b: i32| Ok(a + b))
4628 .unwrap();
4629 hf
4630 }
4631
4632 #[test]
4633 fn round_trip_running_sandbox() {
4634 let mut sbox = make_sandbox();
4635 sbox.call::<i32>("AddToStatic", 11i32).unwrap();
4636 let snapshot = sbox.snapshot().unwrap();
4637 let mut sbox2 = SandboxBuilder::from_snapshot(snapshot).build().unwrap();
4638 assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 11);
4639 let echoed: String = sbox2.call("Echo", "hi".to_string()).unwrap();
4640 assert_eq!(echoed, "hi");
4641 }
4642
4643 #[test]
4644 fn round_trip_pre_init_snapshot() {
4645 let path = simple_guest_as_pathbuf();
4646 let snap =
4647 Snapshot::from_env(GuestBinary::FilePath(path), SandboxConfiguration::default())
4648 .unwrap();
4649 let mut sbox = SandboxBuilder::from_snapshot(Arc::new(snap))
4650 .build()
4651 .unwrap();
4652 assert_eq!(sbox.call::<i32>("GetStatic", ()).unwrap(), 0);
4653 }
4654
4655 #[test]
4659 fn arc_clone_isolation_and_restore_compat() {
4660 let mut sbox = make_sandbox();
4661 sbox.call::<i32>("AddToStatic", 3i32).unwrap();
4662 let snapshot = sbox.snapshot().unwrap();
4663
4664 let mut a = SandboxBuilder::from_snapshot(snapshot.clone())
4665 .build()
4666 .unwrap();
4667 let mut b = SandboxBuilder::from_snapshot(snapshot.clone())
4668 .build()
4669 .unwrap();
4670 assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 3);
4671 assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
4672
4673 a.call::<i32>("AddToStatic", 7i32).unwrap();
4674 assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 10);
4675 assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
4676
4677 a.restore(snapshot.clone()).unwrap();
4678 b.restore(snapshot).unwrap();
4679 assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 3);
4680 assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
4681 }
4682
4683 #[test]
4684 fn accepts_matching_host_functions() {
4685 let mut sbox = make_sandbox_with_add();
4686 sbox.call::<i32>("AddToStatic", 5i32).unwrap();
4687 let snap = sbox.snapshot().unwrap();
4688 let mut sbox2 = SandboxBuilder::from_snapshot(snap)
4689 .host_functions(host_funcs_with_matching_add())
4690 .build()
4691 .unwrap();
4692 assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 5);
4693 }
4694
4695 #[test]
4696 fn rejects_missing_host_function() {
4697 let mut sbox = make_sandbox_with_add();
4698 let snap = sbox.snapshot().unwrap();
4699 let err = SandboxBuilder::from_snapshot(snap)
4700 .build()
4701 .expect_err("missing `Add` must be rejected");
4702 assert!(
4703 matches!(
4704 &err,
4705 HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
4706 if missing.iter().any(|n| n == "Add") && signature_mismatches.is_empty()
4707 ),
4708 "got: {:?}",
4709 err
4710 );
4711 }
4712
4713 #[test]
4719 fn restore_rejects_missing_host_function() {
4720 let mut sbox_with_add = make_sandbox_with_add();
4721 let snap = sbox_with_add.snapshot().unwrap();
4722 let mut sbox_without_add = make_sandbox();
4723 let err = sbox_without_add
4724 .restore(snap)
4725 .expect_err("missing `Add` must be rejected on restore");
4726 assert!(
4727 matches!(
4728 &err,
4729 HyperlightError::SnapshotHostFunctionMismatch { missing, .. }
4730 if missing.iter().any(|n| n == "Add")
4731 ),
4732 "got: {:?}",
4733 err
4734 );
4735 }
4736
4737 #[test]
4740 fn restore_rejects_signature_mismatch() {
4741 let mut sbox_with_add = make_sandbox_with_add();
4742 let snap = sbox_with_add.snapshot().unwrap();
4743 let path = simple_guest_as_pathbuf();
4744 let mut sbox_wrong_add = SandboxBuilder::from_file(path)
4745 .host_function("Add", |a: String, b: String| format!("{a}{b}"))
4746 .build()
4747 .unwrap();
4748 let err = sbox_wrong_add
4749 .restore(snap)
4750 .expect_err("signature mismatch on `Add` must be rejected on restore");
4751 assert!(
4752 matches!(
4753 &err,
4754 HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
4755 if missing.is_empty() && signature_mismatches.iter().any(|s| s.contains("Add"))
4756 ),
4757 "got: {:?}",
4758 err
4759 );
4760 }
4761
4762 #[test]
4765 fn restore_across_sandboxes_with_superset_host_funcs() {
4766 let mut source = make_sandbox_with_add();
4767 source.call::<i32>("AddToStatic", 17i32).unwrap();
4768 let snap = source.snapshot().unwrap();
4769
4770 let path = simple_guest_as_pathbuf();
4771 let mut target = SandboxBuilder::from_file(path)
4772 .host_function("Add", |a: i32, b: i32| a + b)
4773 .host_function("Mul", |a: i32, b: i32| a * b)
4774 .build()
4775 .unwrap();
4776
4777 target.restore(snap).unwrap();
4778 assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 17);
4779 }
4780
4781 #[test]
4782 fn rejects_signature_mismatch() {
4783 let mut sbox = make_sandbox_with_add();
4784 let snap = sbox.snapshot().unwrap();
4785 let mut hf = HostFunctions::default();
4786 hf.register_host_function("Add", |a: String, b: String| Ok(format!("{a}{b}")))
4787 .unwrap();
4788 let err = SandboxBuilder::from_snapshot(snap)
4789 .host_functions(hf)
4790 .build()
4791 .expect_err("signature mismatch on `Add` must be rejected");
4792 assert!(
4793 matches!(
4794 &err,
4795 HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
4796 if missing.is_empty() && signature_mismatches.iter().any(|s| s.contains("Add"))
4797 ),
4798 "got: {:?}",
4799 err
4800 );
4801 }
4802
4803 #[test]
4806 fn accepts_extra_host_functions() {
4807 let mut sbox = make_sandbox_with_add();
4808 sbox.call::<i32>("AddToStatic", 9i32).unwrap();
4809 let snap = sbox.snapshot().unwrap();
4810 let mut hf = host_funcs_with_matching_add();
4811 hf.register_host_function("Mul", |a: i32, b: i32| Ok(a * b))
4812 .unwrap();
4813 let mut sbox2 = SandboxBuilder::from_snapshot(snap)
4814 .host_functions(hf)
4815 .build()
4816 .unwrap();
4817 assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 9);
4818 }
4819
4820 #[test]
4823 fn re_snapshot_after_from_snapshot() {
4824 let mut sbox = make_sandbox();
4825 sbox.call::<i32>("AddToStatic", 4i32).unwrap();
4826 let snap1 = sbox.snapshot().unwrap();
4827
4828 let mut sbox2 = SandboxBuilder::from_snapshot(snap1).build().unwrap();
4829 sbox2.call::<i32>("AddToStatic", 6i32).unwrap();
4830 let snap2 = sbox2.snapshot().unwrap();
4831
4832 sbox2.call::<i32>("AddToStatic", 100i32).unwrap();
4833 assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 110);
4834
4835 sbox2.restore(snap2.clone()).unwrap();
4836 assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 10);
4837
4838 let mut sbox3 = SandboxBuilder::from_snapshot(snap2).build().unwrap();
4839 assert_eq!(sbox3.call::<i32>("GetStatic", ()).unwrap(), 10);
4840 }
4841
4842 #[test]
4845 fn supplied_host_function_is_callable() {
4846 let path = simple_guest_as_pathbuf();
4847 let mut sbox = SandboxBuilder::from_file(path)
4848 .host_function("Echo42", || 1i64)
4849 .build()
4850 .unwrap();
4851 let snap = sbox.snapshot().unwrap();
4852
4853 let mut hf = HostFunctions::default();
4854 hf.register_host_function("Echo42", || Ok(42i64)).unwrap();
4855 let mut sbox2 = SandboxBuilder::from_snapshot(snap)
4856 .host_functions(hf)
4857 .build()
4858 .unwrap();
4859
4860 let got: i64 = sbox2
4861 .call(
4862 "CallGivenParamlessHostFuncThatReturnsI64",
4863 "Echo42".to_string(),
4864 )
4865 .unwrap();
4866 assert_eq!(got, 42);
4867 }
4868
4869 #[test]
4872 fn pre_init_snapshot_accepts_arbitrary_host_functions() {
4873 let path = simple_guest_as_pathbuf();
4874 let snap =
4875 Snapshot::from_env(GuestBinary::FilePath(path), SandboxConfiguration::default())
4876 .unwrap();
4877 let mut hf = HostFunctions::default();
4878 hf.register_host_function("Unrelated", |a: i32| Ok(a + 1))
4879 .unwrap();
4880 let mut sbox = SandboxBuilder::from_snapshot(Arc::new(snap))
4881 .host_functions(hf)
4882 .build()
4883 .unwrap();
4884 assert_eq!(sbox.call::<i32>("GetStatic", ()).unwrap(), 0);
4885 }
4886
4887 #[test]
4891 fn snapshot_generation_propagates() {
4892 let mut sbox = make_sandbox();
4893 sbox.call::<i32>("AddToStatic", 1i32).unwrap();
4894 let snap1 = sbox.snapshot().unwrap();
4895 let gen1 = snap1.snapshot_generation();
4896 sbox.call::<i32>("AddToStatic", 1i32).unwrap();
4897 let snap2 = sbox.snapshot().unwrap();
4898 let gen2 = snap2.snapshot_generation();
4899 assert_eq!(gen2, gen1 + 1);
4900
4901 let mut sbox2 = SandboxBuilder::from_snapshot(snap2).build().unwrap();
4902 sbox2.call::<i32>("AddToStatic", 1i32).unwrap();
4903 let snap3 = sbox2.snapshot().unwrap();
4904 assert_eq!(snap3.snapshot_generation(), gen2 + 1);
4905 }
4906
4907 #[test]
4912 fn late_register_invalidates_snapshot_cache() {
4913 let mut sbox = make_sandbox();
4914 let _ = sbox.snapshot().unwrap();
4916
4917 sbox.register_host_function("Echo42", || Ok(42i64)).unwrap();
4918
4919 let snap = sbox.snapshot().unwrap();
4923 let err = SandboxBuilder::from_snapshot(snap)
4924 .build()
4925 .expect_err("late-registered `Echo42` must be required by the new snapshot");
4926 let msg = format!("{}", err);
4927 assert!(msg.contains("Echo42"), "got: {}", msg);
4928 }
4929 }
4930}