Skip to main content

hyperlight_host/sandbox/
initialized_multi_use.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use 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/// The lifecycle state of a [`MultiUseSandbox`].
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum SandboxStatus {
35    /// The sandbox can execute guest operations.
36    Ready,
37    /// The sandbox requires a successful restore before further use.
38    Poisoned,
39    /// The sandbox cannot be used and must be discarded.
40    Unrecoverable,
41}
42
43impl SandboxStatus {
44    /// Returns whether the sandbox can execute guest operations.
45    pub const fn is_ready(self) -> bool {
46        matches!(self, Self::Ready)
47    }
48
49    /// Returns whether the sandbox requires a successful restore.
50    pub const fn is_poisoned(self) -> bool {
51        matches!(self, Self::Poisoned)
52    }
53
54    /// Returns whether the sandbox must be discarded.
55    pub const fn is_unrecoverable(self) -> bool {
56        matches!(self, Self::Unrecoverable)
57    }
58}
59
60/// A fully initialized sandbox that can execute guest function calls.
61///
62/// Guest functions can be called repeatedly while maintaining state between calls.
63/// The sandbox supports creating snapshots and restoring to previous states.
64///
65/// ## Sandbox status
66///
67/// The sandbox becomes [`Poisoned`](SandboxStatus::Poisoned) when guest
68/// execution does not complete normally. Causes include guest panics or aborts,
69/// invalid memory access, stack overflow, heap exhaustion, and cancellation
70/// through [`InterruptHandle::kill()`]. Interrupted execution can leak
71/// allocations, corrupt allocator metadata, leave resources locked, or partially
72/// update state.
73///
74/// Use [`restore()`](Self::restore) with a snapshot taken before the interrupted
75/// execution to make a poisoned sandbox ready again. Restore reinstates the
76/// captured memory and vCPU state and removes dynamic mappings.
77///
78/// A restore failure that prevents Hyperlight from establishing valid base
79/// memory mappings can leave the sandbox
80/// [`Unrecoverable`](SandboxStatus::Unrecoverable). Further restore attempts and
81/// guest operations are rejected. The sandbox must be discarded.
82pub struct MultiUseSandbox {
83    status: SandboxStatus,
84    pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
85    pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
86    vm: HyperlightVm,
87    /// If the current state of the sandbox has been captured in a snapshot,
88    /// that snapshot is stored here.
89    pub(crate) snapshot: Option<Arc<Snapshot>>,
90    /// Optional callback to discover page table roots from guest memory.
91    /// Given (snapshot_mem, scratch_mem, cr3), returns a list of root GPAs.
92    /// If not set, only CR3 is used as the single root.
93    pt_root_finder: Option<PtRootFinder>,
94}
95
96/// Callback for discovering page table roots from guest memory.
97///
98/// Called during [`MultiUseSandbox::snapshot`] with:
99/// - `snapshot_mem` - the sandbox's snapshot (shared) memory as a byte slice
100/// - `scratch_mem` - the sandbox's scratch memory as a byte slice
101/// - `root_pt_gpa` - the root page table GPA of the currently-executing
102///   address space
103///
104/// Returns a list of root page table GPAs to walk. If the list is
105/// empty, only `root_pt_gpa` is used.
106pub 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    /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance.
124    ///
125    /// This function is not equivalent to doing an `evolve` from uninitialized
126    /// to initialized, and is purposely not exposed publicly outside the crate
127    /// (as a `From` implementation would be)
128    #[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    /// Set a callback that discovers page table roots from guest memory.
145    /// The callback receives (snapshot_mem, scratch_mem, cr3) and returns
146    /// the list of root GPAs to walk during snapshot creation.
147    ///
148    /// The callback must support every guest restored into this sandbox.
149    pub fn set_pt_root_finder(&mut self, finder: PtRootFinder) {
150        self.pt_root_finder = Some(finder);
151    }
152
153    /// Create a `MultiUseSandbox` directly from a [`Snapshot`],
154    /// bypassing guest binary loading and initialization.
155    ///
156    /// This is useful for fast sandbox creation when a snapshot of
157    /// an already-initialized guest is available, either saved to disk
158    /// or captured in memory from another sandbox.
159    ///
160    /// The provided [`HostFunctions`] must include every host function
161    /// that was registered on the sandbox at the time the snapshot was
162    /// taken (matched by name and signature). Additional host functions
163    /// not present in the snapshot are allowed. A mismatch returns
164    /// [`SnapshotHostFunctionMismatch`](crate::HyperlightError::SnapshotHostFunctionMismatch)
165    /// carrying the missing names and signature differences.
166    ///
167    /// An optional [`SandboxConfiguration`](crate::sandbox::SandboxConfiguration)
168    /// can be supplied to override runtime settings such as timeouts and
169    /// interrupt behavior. Memory layout fields
170    /// (`input_data_size`, `output_data_size`, `heap_size`, `scratch_size`)
171    /// are always taken from the snapshot. Any values supplied in
172    /// `config` for those fields are ignored. On x86_64 the `config` must
173    /// declare every guest MSR the snapshot was taken with (see
174    /// [`SandboxConfiguration::guest_msrs`](crate::sandbox::SandboxConfiguration::guest_msrs)),
175    /// or the load fails with an MSR mismatch.
176    ///
177    /// # Examples
178    ///
179    /// From a snapshot taken on another sandbox:
180    ///
181    /// ```no_run
182    /// # use std::sync::Arc;
183    /// # use hyperlight_host::{HostFunctions, MultiUseSandbox, SandboxBuilder};
184    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
185    /// // Create and initialize a sandbox the normal way
186    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
187    ///
188    /// // Capture a snapshot of the initialized state
189    /// let snapshot = sandbox.snapshot()?;
190    ///
191    /// // Create a new sandbox directly from the snapshot
192    /// let mut sandbox2 = MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None)?;
193    /// let result: i32 = sandbox2.call("GetValue", ())?;
194    /// # Ok(())
195    /// # }
196    /// ```
197    ///
198    /// From a snapshot loaded from disk:
199    ///
200    /// ```no_run
201    /// # use std::sync::Arc;
202    /// # use hyperlight_host::{HostFunctions, MultiUseSandbox};
203    /// # use hyperlight_host::sandbox::snapshot::{OciTag, Snapshot};
204    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
205    /// let tag = OciTag::new("latest")?;
206    /// let snapshot = Arc::new(Snapshot::load("./guest_snapshot", tag)?);
207    /// let mut sandbox = MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None)?;
208    /// let result: String = sandbox.call("Echo", "hello".to_string())?;
209    /// # Ok(())
210    /// # }
211    /// ```
212    #[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        // Validate that the provided host functions are a superset of
224        // those required by the snapshot.
225        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        // Start from the caller's config (if any) so runtime fields
231        // such as timeouts and interrupt knobs are honored, then
232        // overwrite the layout fields from the snapshot. The on-disk
233        // layout is fixed, so any layout values supplied by the
234        // caller are silently ignored. Warn if the caller passed a
235        // config whose layout fields disagree with the snapshot, so
236        // the override is at least visible.
237        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        // Runtime config for the restored sandbox. `guest_core_dump`
257        // (crashdump) and `guest_debug_info` (gdb) come from the caller's
258        // config. `binary_path` stays `None`. `set_up_hypervisor_partition`
259        // fills `entry_point` from the manager's entry point so crashdumps
260        // carry the correct `AT_ENTRY`.
261        #[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        // noop for NextAction::Call
290        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 the snapshot was taken from an already-initialized guest
298        // (NextAction::Call), apply the captured special registers so
299        // the guest resumes in the correct CPU state.
300        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            // Restore captured MSR state.
316            #[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    /// Creates a snapshot of the sandbox's current memory state.
329    ///
330    /// The returned snapshot can be applied to any
331    /// [`MultiUseSandbox`] whose registered host functions are a
332    /// superset of those registered here at the time of capture. See
333    /// [`MultiUseSandbox::restore`] and
334    /// [`MultiUseSandbox::from_snapshot`] for the exact compatibility
335    /// rules and the error variants returned on mismatch.
336    ///
337    /// On x86_64, the snapshot saves a small core of essential CPU state plus
338    /// each MSR declared with
339    /// [`SandboxBuilder::guest_msrs`](crate::SandboxBuilder::guest_msrs).
340    ///
341    /// ## Sandbox status
342    ///
343    /// This method returns [`crate::HyperlightError::PoisonedSandbox`] when the
344    /// sandbox is poisoned and [`crate::HyperlightError::UnrecoverableSandbox`]
345    /// when it is unrecoverable.
346    ///
347    /// # Examples
348    ///
349    /// ```no_run
350    /// # use hyperlight_host::SandboxBuilder;
351    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
352    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
353    ///
354    /// // Modify sandbox state
355    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 42)?;
356    ///
357    /// // Capture a snapshot of the current memory state
358    /// let snapshot = sandbox.snapshot()?;
359    /// # Ok(())
360    /// # }
361    /// ```
362    #[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        // Get CR3 from the vCPU
372        let cr3 = self
373            .vm
374            .get_root_pt()
375            .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
376        // Use the callback if set, otherwise just CR3
377        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    /// Restores the sandbox's memory to a previously captured snapshot state.
435    ///
436    /// The sandbox's registered host functions must be a superset of
437    /// those required by the snapshot (matched by name and
438    /// signature). Extras on the sandbox are allowed. The registry
439    /// itself is left unchanged. A mismatch returns
440    /// [`SnapshotHostFunctionMismatch`](crate::HyperlightError::SnapshotHostFunctionMismatch)
441    /// carrying the missing names and signature differences.
442    ///
443    /// On x86_64, this restores the MSR state captured by
444    /// [`MultiUseSandbox::snapshot`]:
445    /// [`SandboxBuilder::guest_msrs`](crate::SandboxBuilder::guest_msrs)
446    /// selects which MSRs are saved and restored.
447    ///
448    /// Restore writes the snapshot's saved MSRs. On KVM the destination must
449    /// declare every MSR the snapshot saved. An MSR restore failure leaves the
450    /// sandbox poisoned.
451    ///
452    /// ## Status after restore
453    ///
454    /// A successful restore sets the status to [`Ready`](SandboxStatus::Ready).
455    /// The restored state includes snapshot and scratch memory, vCPU state,
456    /// stack state, the next VM action, captured MSRs on x86_64, and the removal
457    /// of dynamic memory mappings. This discards leaked allocations, restores
458    /// allocator and lock state, and rolls back partial updates.
459    ///
460    /// Restore failures have three status outcomes:
461    ///
462    /// * Snapshot compatibility failures happen before mutation and leave the
463    ///   current status unchanged.
464    /// * A failure while restoring base memory or its VM mappings sets the
465    ///   status to [`Unrecoverable`](SandboxStatus::Unrecoverable). The sandbox
466    ///   must be discarded.
467    /// * A later failure while restoring vCPU state, MSRs, or dynamic mappings
468    ///   leaves the sandbox [`Poisoned`](SandboxStatus::Poisoned). Restore can be
469    ///   retried with a compatible snapshot.
470    ///
471    /// Calling this method on an unrecoverable sandbox returns
472    /// [`crate::HyperlightError::UnrecoverableSandbox`].
473    ///
474    /// # Examples
475    ///
476    /// ```no_run
477    /// # use hyperlight_host::SandboxBuilder;
478    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
479    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
480    ///
481    /// // Take initial snapshot from this sandbox
482    /// let snapshot = sandbox.snapshot()?;
483    ///
484    /// // Modify sandbox state
485    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 100)?;
486    /// let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
487    /// assert_eq!(value, 100);
488    ///
489    /// // Restore to previous state (same sandbox)
490    /// sandbox.restore(snapshot)?;
491    /// let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
492    /// assert_eq!(restored_value, 0); // Back to initial state
493    /// # Ok(())
494    /// # }
495    /// ```
496    ///
497    /// ## Recovering from Poison
498    ///
499    /// ```no_run
500    /// # use hyperlight_host::SandboxBuilder;
501    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
502    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
503    ///
504    /// // Take snapshot before potentially poisoning operation
505    /// let snapshot = sandbox.snapshot()?;
506    ///
507    /// // This might poison the sandbox (guest not run to completion)
508    /// let result = sandbox.call::<()>("guest_panic", ());
509    /// if result.is_err() {
510    ///     if sandbox.status().is_poisoned() {
511    ///         // Restore from snapshot to clear poison
512    ///         sandbox.restore(snapshot.clone())?;
513    ///         assert!(sandbox.status().is_ready());
514    ///         
515    ///         // Sandbox is now usable again
516    ///         sandbox.call::<String>("Echo", "hello".to_string())?;
517    ///     }
518    /// }
519    /// # Ok(())
520    /// # }
521    /// ```
522    #[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        // Currently, we do not try to optimise restore to the
529        // most-current snapshot. This is because the most-current
530        // snapshot, while it must have identical virtual memory
531        // layout to the current sandbox, does not necessarily have
532        // the exact same /physical/ memory contents. It is not
533        // entirely inconceivable that this could lead to breakage of
534        // cross-request isolation in some way, although it would
535        // require some /very/ odd code.  For example, suppose that a
536        // service uses Hyperlight to sandbox native code from
537        // clients, and promises cross-request isolation. A tenant
538        // provides a binary that can process two forms of request,
539        // either writing a secret into physical memory, or reading
540        // from arbitrary physical memory, assuming that the two kinds
541        // of requests can never (dangerously) meet in the same
542        // sandbox.
543        //
544        // It is presently unclear whether this is a sensible threat
545        // model, especially since Hyperlight is often used with
546        // managed-code runtimes which do not allow even arbitrary
547        // access to virtual memory, much less physical memory.
548        // However, out of an abundance of caution, the optimisation
549        // is presently disabled.
550
551        {
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        // Errors below leave the sandbox poisoned unless base mapping updates make it unrecoverable.
568        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 &current_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        // Restore captured MSR state as part of the x86_64 vCPU reset.
584        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        // Carry the guest ELF entry point across restore so a later
596        // crashdump fills `AT_ENTRY` from the restored image.
597        #[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        // The restored snapshot is now our most current snapshot
608        self.snapshot = Some(snapshot.clone());
609
610        // Clear poison state when successfully restoring from snapshot.
611        //
612        // # Safety:
613        // This is safe because:
614        // 1. Snapshots can only be taken from non-poisoned sandboxes (verified at snapshot creation)
615        // 2. Restoration completely replaces all memory state, eliminating:
616        //    - All leaked heap allocations (memory is restored to snapshot state)
617        //    - All corrupted data structures (overwritten with consistent snapshot data)
618        //    - All inconsistent global state (reset to snapshot values)
619        self.status = SandboxStatus::Ready;
620
621        Ok(())
622    }
623
624    /// Calls a guest function by name with the specified arguments.
625    ///
626    /// Changes made to the sandbox during execution are *not* persisted.
627    ///
628    /// ## Poisoned Sandbox
629    ///
630    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
631    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
632    ///
633    /// # Examples
634    ///
635    /// ```no_run
636    /// # use hyperlight_host::SandboxBuilder;
637    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
638    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
639    ///
640    /// // Call function with no arguments
641    /// let result: i32 = sandbox.call_guest_function_by_name("GetCounter", ())?;
642    ///
643    /// // Call function with single argument
644    /// let doubled: i32 = sandbox.call_guest_function_by_name("Double", 21)?;
645    /// assert_eq!(doubled, 42);
646    ///
647    /// // Call function with multiple arguments
648    /// let sum: i32 = sandbox.call_guest_function_by_name("Add", (10, 32))?;
649    /// assert_eq!(sum, 42);
650    ///
651    /// // Call function returning string
652    /// let message: String = sandbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?;
653    /// assert_eq!(message, "Hello, World!");
654    /// # Ok(())
655    /// # }
656    /// ```
657    #[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    /// Calls a guest function by name with the specified arguments.
676    ///
677    /// Changes made to the sandbox during execution are persisted.
678    ///
679    /// ## Poisoned Sandbox
680    ///
681    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
682    /// is already poisoned before the call. Use [`restore()`](Self::restore) to recover from
683    /// a poisoned state.
684    ///
685    /// ## Sandbox Poisoning
686    ///
687    /// If this method returns an error, the sandbox may be poisoned if the guest was not run
688    /// to completion (due to panic, abort, memory violation, stack/heap exhaustion, or forced
689    /// termination). Use [`status()`](Self::status) to check the sandbox state and
690    /// [`restore()`](Self::restore) to recover if needed.
691    ///
692    /// If this method returns `Ok`, the sandbox is guaranteed to **not** be poisoned - the guest
693    /// function completed successfully and the sandbox state is consistent.
694    ///
695    /// # Examples
696    ///
697    /// ```no_run
698    /// # use hyperlight_host::SandboxBuilder;
699    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
700    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
701    ///
702    /// // Call function with no arguments
703    /// let result: i32 = sandbox.call("GetCounter", ())?;
704    ///
705    /// // Call function with single argument
706    /// let doubled: i32 = sandbox.call("Double", 21)?;
707    /// assert_eq!(doubled, 42);
708    ///
709    /// // Call function with multiple arguments
710    /// let sum: i32 = sandbox.call("Add", (10, 32))?;
711    /// assert_eq!(sum, 42);
712    ///
713    /// // Call function returning string
714    /// let message: String = sandbox.call("Echo", "Hello, World!".to_string())?;
715    /// assert_eq!(message, "Hello, World!");
716    /// # Ok(())
717    /// # }
718    /// ```
719    ///
720    /// ## Handling Potential Poisoning
721    ///
722    /// ```no_run
723    /// # use hyperlight_host::SandboxBuilder;
724    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
725    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
726    ///
727    /// // Take snapshot before risky operation
728    /// let snapshot = sandbox.snapshot()?;
729    ///
730    /// // Call potentially unsafe guest function
731    /// let result = sandbox.call::<String>("RiskyOperation", "input".to_string());
732    ///
733    /// // Check if the call failed and poisoned the sandbox
734    /// if let Err(e) = result {
735    ///     eprintln!("Guest function failed: {}", e);
736    ///     
737    ///     if sandbox.status().is_poisoned() {
738    ///         eprintln!("Sandbox was poisoned, restoring from snapshot");
739    ///         sandbox.restore(snapshot.clone())?;
740    ///     }
741    /// }
742    /// # Ok(())
743    /// # }
744    /// ```
745    #[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        // Reset snapshot since we are mutating the sandbox state
753        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            // Use the ? operator to allow converting any hyperlight_common::func::Error
761            // returned by from_value into a HyperlightError
762            let ret = Output::from_value(ret?)?;
763            Ok(ret)
764        })
765    }
766
767    /// Maps a region of host memory into the sandbox address space.
768    ///
769    /// The base address and length must meet platform alignment requirements
770    /// (typically page-aligned). The `region_type` field is ignored as guest
771    /// page table entries are not created.
772    ///
773    /// ## Poisoned Sandbox
774    ///
775    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
776    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
777    ///
778    /// # Safety
779    ///
780    /// The caller must ensure the host memory region remains valid and unmodified
781    /// for the lifetime of `self`.
782    #[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            // TODO: Implement support for writable mappings, which
787            // need to be registered with the memory manager so that
788            // writes can be rolled back when necessary.
789            log_then_return!("TODO: Writable mappings not yet supported");
790        }
791
792        // Map first so overlaps are rejected before resetting the snapshot
793        unsafe { self.vm.map_region(rgn) }.map_err(HyperlightVmError::MapRegion)?;
794        self.snapshot = None;
795        Ok(())
796    }
797
798    /// Map the contents of a file into the guest at a particular address
799    ///
800    /// Returns the length of the mapping in bytes.
801    ///
802    /// ## Poisoned Sandbox
803    ///
804    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
805    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
806    #[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        // Phase 1: host-side OS work (open file, create mapping)
811        let mut prepared = prepare_file_cow(file_path, guest_base)?;
812
813        // Validate that the full mapped range doesn't overlap the
814        // sandbox's primary shared memory region.
815        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        // Phase 2: VM-side work (map into guest address space)
836        let region = prepared.to_memory_region()?;
837
838        unsafe { self.vm.map_region(&region) }
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        // Mark consumed immediately after map_region succeeds.
847        // On Windows, WhpVm::map_memory copies the file mapping handle
848        // into its own `file_mappings` vec for cleanup on drop. If we
849        // deferred mark_consumed(), both PreparedFileMapping::drop and
850        // WhpVm::drop would release the same handle — a double-close.
851        // On Linux the hypervisor holds a reference to the host mmap;
852        // freeing it here would leave a dangling backing.
853        prepared.mark_consumed();
854
855        Ok(size)
856    }
857
858    /// Calls a guest function with type-erased parameters and return values.
859    ///
860    /// This function is used for fuzz testing parameter and return type handling.
861    ///
862    /// ## Poisoned Sandbox
863    ///
864    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
865    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
866    #[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        // Reset snapshot since we are mutating the sandbox state
876        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        // ===== KILL() TIMING POINT 1 =====
890        // Clear any stale cancellation from a previous guest function call or if kill() was called too early.
891        // Any kill() that completed (even partially) BEFORE this line has NO effect on this call.
892        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            // Convert dispatch errors to HyperlightErrors to maintain backwards compatibility
914            // but first determine if sandbox should be poisoned
915            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        // Clear partial abort bytes so they don't leak across calls.
943        self.mem_mgr.abort_buffer.clear();
944
945        // In the happy path we do not need to clear io-buffers from the host because:
946        // - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::<FunctionCall>()`
947        // - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result`
948        // - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call`
949        // - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value`
950        if let Err(e) = &res {
951            self.mem_mgr.clear_io_buffers();
952
953            // Determine if we should poison the sandbox.
954            if e.is_poison_error() {
955                self.poison();
956            }
957        }
958
959        // Note: clear_call_active() is automatically called when _guard is dropped here
960
961        res
962    }
963
964    /// Returns a handle for interrupting guest execution.
965    ///
966    /// # Examples
967    ///
968    /// ```no_run
969    /// # use hyperlight_host::SandboxBuilder;
970    /// # use std::thread;
971    /// # use std::time::Duration;
972    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
973    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
974    ///
975    /// // Get interrupt handle before starting long-running operation
976    /// let interrupt_handle = sandbox.interrupt_handle();
977    ///
978    /// // Spawn thread to interrupt after timeout
979    /// let handle_clone = interrupt_handle.clone();
980    /// thread::spawn(move || {
981    ///     thread::sleep(Duration::from_secs(5));
982    ///     handle_clone.kill();
983    /// });
984    ///
985    /// // This call may be interrupted by the spawned thread
986    /// let result = sandbox.call_guest_function_by_name::<i32>("LongRunningFunction", ());
987    /// # Ok(())
988    /// # }
989    /// ```
990    pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
991        self.vm.interrupt_handle()
992    }
993
994    /// Generate a crash dump of the current state of the VM underlying this sandbox.
995    ///
996    /// Creates an ELF core dump file that can be used for debugging. The dump
997    /// captures the current state of the sandbox including registers, memory regions,
998    /// and other execution context.
999    ///
1000    /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
1001    /// environment variable. If not set, it defaults to the system's temporary directory.
1002    ///
1003    /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
1004    /// is also configured to allow core dumps (which is the default behavior).
1005    ///
1006    /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
1007    /// guest that dont cause crashes (e.g. a guest function that does not return)
1008    ///
1009    /// # Examples
1010    ///
1011    /// Attach to your running process with gdb and call this function:
1012    ///
1013    /// ```shell
1014    /// sudo gdb -p <pid_of_your_process>
1015    /// (gdb) info threads
1016    /// # find the thread that is running the guest function you want to debug
1017    /// (gdb) thread <thread_number>
1018    /// # switch to the frame where you have access to your MultiUseSandbox instance
1019    /// (gdb) backtrace
1020    /// (gdb) frame <frame_number>
1021    /// # get the pointer to your MultiUseSandbox instance
1022    /// # Get the sandbox pointer
1023    /// (gdb) print sandbox
1024    /// # Call the crashdump function
1025    /// call sandbox.generate_crashdump()
1026    /// ```
1027    /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
1028    ///
1029    #[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    /// Generate a crash dump of the current state of the VM, writing to `dir`.
1036    ///
1037    /// Like [`generate_crashdump`](Self::generate_crashdump), but the core dump
1038    /// file is placed in `dir` instead of consulting the `HYPERLIGHT_CORE_DUMP_DIR`
1039    /// environment variable.  This avoids the need for callers to use
1040    /// `unsafe { std::env::set_var(...) }`.
1041    #[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    /// Returns whether the sandbox is poisoned.
1052    ///
1053    /// Use [`status()`](Self::status) to distinguish every lifecycle state.
1054    ///
1055    /// ## Causes of Poisoning
1056    ///
1057    /// The sandbox becomes poisoned when guest execution is interrupted:
1058    /// - **Panics/Aborts** - Guest code panics or calls `abort()`
1059    /// - **Invalid Memory Access** - Read/write/execute violations  
1060    /// - **Stack Overflow** - Guest exhausts stack space
1061    /// - **Heap Exhaustion** - Guest runs out of heap memory
1062    /// - **Forced Termination** - [`InterruptHandle::kill()`] called during execution
1063    ///
1064    /// ## Recovery
1065    ///
1066    /// To clear the poison state, use [`restore()`](Self::restore) with a snapshot
1067    /// that was taken before the sandbox became poisoned.
1068    ///
1069    /// # Examples
1070    ///
1071    /// ```no_run
1072    /// # use hyperlight_host::SandboxBuilder;
1073    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1074    /// let mut sandbox = SandboxBuilder::from_file("guest.bin").build()?;
1075    ///
1076    /// if sandbox.status().is_poisoned() {
1077    ///     println!("Sandbox is poisoned");
1078    /// }
1079    /// # Ok(())
1080    /// # }
1081    /// ```
1082    #[deprecated(since = "0.17.0", note = "use status().is_poisoned()")]
1083    pub fn poisoned(&self) -> bool {
1084        self.status.is_poisoned()
1085    }
1086
1087    /// Returns the sandbox lifecycle status.
1088    ///
1089    /// * [`Ready`](SandboxStatus::Ready) permits guest operations and snapshots.
1090    /// * [`Poisoned`](SandboxStatus::Poisoned) rejects guest operations and
1091    ///   snapshots. A successful [`restore()`](Self::restore) makes it ready.
1092    /// * [`Unrecoverable`](SandboxStatus::Unrecoverable) rejects all further
1093    ///   operations, including restore. The sandbox must be discarded.
1094    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
1116/// Emit a warning for each memory-layout field in `caller` that
1117/// disagrees with `snapshot`. Used by [`MultiUseSandbox::from_snapshot`]
1118/// to surface ignored caller-supplied layout values, since those
1119/// fields are always taken from the snapshot.
1120fn 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        // poison on purpose
1202        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        // guest calls should fail when poisoned
1211        let res = sbox
1212            .call::<()>("guest_panic", "hello2".to_string())
1213            .unwrap_err();
1214        assert!(matches!(res, HyperlightError::PoisonedSandbox));
1215
1216        // snapshot should fail when poisoned
1217        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        // map_region should fail when poisoned
1225        {
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(&region) }.unwrap_err();
1230            assert!(matches!(res, HyperlightError::PoisonedSandbox));
1231        }
1232
1233        // map_file_cow should fail when poisoned
1234        {
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(); // Clean up
1239        }
1240
1241        // call_guest_function_by_name (deprecated) should fail when poisoned
1242        #[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        // restore to non-poisoned snapshot should work and clear poison
1249        sbox.restore(snapshot.clone()).unwrap();
1250        assert_eq!(sbox.status(), SandboxStatus::Ready);
1251
1252        // guest calls should work again after restore
1253        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        // re-poison on purpose
1258        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        // restore to non-poisoned snapshot should work again
1267        sbox.restore(snapshot.clone()).unwrap();
1268        assert_eq!(sbox.status(), SandboxStatus::Ready);
1269
1270        // guest calls should work again
1271        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        // snapshot should work again
1276        let _ = sbox.snapshot().unwrap();
1277    }
1278
1279    /// Make sure input/output buffers are properly reset after guest call (with host call)
1280    #[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        // will exhaust io if leaky
1291        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    /// Make sure input/output buffers are properly reset after guest call (with host call)
1315    #[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        // will exhaust io if leaky. Tests both success and error paths
1326        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    /// Tests that call_guest_function_by_name restores the state correctly
1337    #[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    // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (24K) and heap(32K).
1360    // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
1361    #[test]
1362    fn test_with_small_stack_and_heap() {
1363        const HEAP_SIZE: u64 = 32 * 1024;
1364        // min_scratch_size already includes 1 page (4k on most
1365        // platforms) of guest stack, so add 20k more to get 24k
1366        // total, and then add some more for the eagerly-copied page
1367        // tables on amd64
1368        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    /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
1404    /// and restoring a snapshot from before evolving restores the previous state
1405    #[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                // msg should indicate we got an invalid opcode exception
1436                #[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        // barrier to make sure all threads start their work simultaneously
1447        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; // Arbitrary guest base address
1499
1500        unsafe {
1501            sbox.map_region(&region_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    // Makes sure MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE executable but not writable
1521    #[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]; // NOOP slide to RET
1529        #[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; // Arbitrary guest base address
1533
1534        unsafe {
1535            sbox.map_region(&region_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        // Execute should pass since memory is executable
1546        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        // write should fail because the memory is mapped as read-only
1555        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        // 1. Take snapshot 1 with no additional regions mapped
1605        let snapshot1 = sbox.snapshot().unwrap();
1606        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1607
1608        // 2. Map a memory region
1609        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(&region).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        // 3. Take snapshot 2 with 1 region mapped
1627        let snapshot2 = sbox.snapshot().unwrap();
1628        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1629
1630        // 4. Re(store to snapshot 1 (should unmap the region)
1631        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        // 5. Restore forward to snapshot 2 (should have folded the
1639        //    region into the snapshot)
1640        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        // Verify the region is the same
1648        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    /// Compaction copies mapped-region pages into the snapshot blob,
1662    /// so cross-instance restore preserves their contents without the
1663    /// target ever mapping the region.
1664    #[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(&region).unwrap() };
1674
1675        // do_map=true installs the guest PTE for the region.
1676        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        // Snapshot PTEs resolve to GPAs in the snapshot blob, so the
1698        // data is readable without re-mapping.
1699        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        // Snapshot mapping must be identical (no remap).
1751        assert_eq!(new_mappings.0, mappings.0);
1752        // On Windows, scratch is freshly allocated each restore so the
1753        // base address may change, but the size must stay the same.
1754        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(&region) },
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(&region).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    /// Validation runs before any memory or vCPU mutation, so a
2414    /// rejected `restore` leaves the target usable.
2415    #[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        // SAFETY: `map_mem` is page-aligned and outlives every use of `target`.
2435        unsafe { target.map_region(&region).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    /// `snapshot.regions()` is empty post-compaction, so restore
2474    /// unmaps anything the target had mapped.
2475    #[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(&region).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        // SAFETY: `map_mem` is page-aligned and outlives every use of `target`.
2521        unsafe { target.map_region(&region).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        // SAFETY: `map_mem` is page-aligned and outlives every use of `target`.
2555        unsafe { target.map_region(&region).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    /// Compacted snapshot data is reachable at the source's GVA even
2563    /// when the target had a different region mapped at a different
2564    /// GVA.
2565    #[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    /// Repeated restore of the same snapshot is idempotent.
2615    #[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 that snapshot restore properly resets vCPU debug registers. This test verifies
2638    /// that restore() calls reset_vcpu().
2639    #[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        // Verify DR0 is initially 0 (clean state)
2648        let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap();
2649        assert_eq!(dr0_initial, 0, "DR0 should initially be 0");
2650
2651        // Dirty DR0 by setting it to a known non-zero value, avoiding
2652        // bits that are reserved in aarch64 DBGBVR0_EL1
2653        const DIRTY_VALUE: u64 = 0xFFFF_FEDC_7654_3210;
2654        sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap();
2655
2656        // Validate that DR0 was in fact dirtied
2657        #[cfg(not(hvf))]
2658        {
2659            // This check does not work on hvf, because it relies on
2660            // state being persisted across sandbox calls in a system
2661            // register that is not usually supported by Hyperlight
2662            // (DBGBVR0), whereas hvf may (if there is a lot of
2663            // contention on the system) destroy and re-create its
2664            // vcpu, preserving only the "supported" hyperlight state
2665            // msrs.
2666            //
2667            // We could disable this test entirely on hvf, but a test
2668            // that occasionally checks for what it is meant to is
2669            // probably better than one that never does.
2670            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        // Restore to the snapshot - this should reset vCPU state including debug registers
2678        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 that stale abort buffer bytes from a previous call don't
2712    /// leak into the next call.
2713    #[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        // Simulate a partial abort
2720        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        // The buffer should be empty after the call.
2730        assert!(
2731            sbox.mem_mgr.abort_buffer.is_empty(),
2732            "abort_buffer should be empty after a guest call"
2733        );
2734    }
2735
2736    /// Test that sandboxes can be created and evolved with different heap sizes
2737    #[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    /// Helper: create a MultiUseSandbox from the simple guest with default config.
2758    #[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    /// Helper: read memory at `gva` of length `len` from the guest side via
2765    /// `ReadMappedBuffer(gva, len, false)` and from the host side via
2766    /// `read_guest_memory_by_gva`, then assert both views are identical.
2767    #[cfg(feature = "trace_guest")]
2768    fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
2769        // Guest reads via its own page tables
2770        let expected: Vec<u8> = sbox
2771            .call("ReadMappedBuffer", (gva, len as u64, true))
2772            .unwrap();
2773        assert_eq!(expected.len(), len);
2774
2775        // Host reads by walking the same page tables
2776        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 reading a small buffer (< 1 page) from guest memory via GVA.
2790    /// Uses the guest code section which is already identity-mapped.
2791    #[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 reading exactly one full page (4096 bytes) from guest memory.
2800    /// Uses the guest code section
2801    #[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 that a read starting at an odd (non-page-aligned) address and
2810    /// spanning two page boundaries returns correct data.
2811    #[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        // Start 1 byte before the second page boundary and read 4097 bytes
2817        // (spans 2 full page boundaries).
2818        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 reading exactly two full pages (8192 bytes) from guest memory.
2827    #[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 reading a region that spans across a page boundary: starts
2836    /// 100 bytes before the end of the first page and reads 200 bytes
2837    /// into the second page.
2838    #[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        // Start 100 bytes before the first page boundary, read across it.
2844        let start = code_gva + 4096 - 100;
2845        assert_gva_read_matches(&mut sbox, start, 200);
2846    }
2847
2848    /// Helper: create a temp file with known content, padded to be
2849    /// at least page-aligned (4096 bytes). Returns the path and the
2850    /// *original* content bytes (before padding).
2851    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); // clean up from previous runs
2862        let mut f = std::fs::File::create(&path).unwrap();
2863        f.write_all(&padded).unwrap();
2864        (path, content.to_vec())
2865    }
2866
2867    /// Tests the basic `map_file_cow` flow: map a file, read its content
2868    /// from the guest, and verify it matches.
2869    #[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        // Read the content back from the guest
2888        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        // Clean up
2901        let _ = std::fs::remove_file(&path);
2902    }
2903
2904    /// Tests that `map_file_cow` enforces read-only access: writing to
2905    /// the mapped region from the guest should cause a MemoryAccessViolation.
2906    #[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        // Writing to the mapped region should fail with MemoryAccessViolation
2919        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        // Clean up
2932        let _ = std::fs::remove_file(&path);
2933    }
2934
2935    /// Tests that `map_file_cow` returns `PoisonedSandbox` when the
2936    /// sandbox is poisoned.
2937    #[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        // Poison the sandbox
2947        let _ = sbox
2948            .call::<()>("guest_panic", "hello".to_string())
2949            .unwrap_err();
2950        assert!(sbox.status().is_poisoned());
2951
2952        // map_file_cow should fail with PoisonedSandbox
2953        let err = sbox.map_file_cow(&path, 0x1_0000_0000).unwrap_err();
2954        assert!(matches!(err, HyperlightError::PoisonedSandbox));
2955
2956        // Restore and verify map_file_cow works again
2957        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    /// Tests that two separate sandboxes can map the same file
2966    /// simultaneously and both read it correctly.
2967    #[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        // Map the same file into both sandboxes
2984        sbox1.map_file_cow(&path, guest_base).unwrap();
2985        sbox2.map_file_cow(&path, guest_base).unwrap();
2986
2987        // Both should read the correct content
2988        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    /// Tests that multiple threads can each create a sandbox, map the
3014    /// same file, read it, and drop without errors.
3015    #[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    /// Tests that file cleanup works after dropping a sandbox that used
3061    /// `map_file_cow` — the file should be deletable (no leaked handles).
3062    #[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            // sandbox dropped here
3074        }
3075
3076        std::fs::remove_file(&path)
3077            .expect("File should be deletable after sandbox with map_file_cow is dropped");
3078    }
3079
3080    /// Tests snapshot/restore cycle with map_file_cow:
3081    /// snapshot₁ (no file) → map file → snapshot₂ → restore₁ (unmapped)
3082    /// → restore₂ (data folded into snapshot).
3083    #[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        // 1. snapshot₁ — no file mapped
3096        let snapshot1 = sbox.snapshot().unwrap();
3097
3098        // 2. Map the file
3099        sbox.map_file_cow(&path, guest_base).unwrap();
3100
3101        // Verify we can read it
3102        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        // 3. snapshot₂ — file mapped (data folded into snapshot)
3111        let snapshot2 = sbox.snapshot().unwrap();
3112
3113        // 4. Restore to snapshot₁ — file should be unmapped
3114        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        // 5. Restore to snapshot₂ — data should still be readable
3122        //    (folded into snapshot memory, not the original file mapping)
3123        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    /// Tests that snapshot correctly captures map_file_cow data and
3144    /// restore brings it back.
3145    #[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        // Read the content to verify mapping works
3159        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        // Take snapshot — folds file data into snapshot memory
3168        let snapshot = sbox.snapshot().unwrap();
3169
3170        // Restore — the file-backed region is unmapped but data is in snapshot
3171        sbox.restore(snapshot).unwrap();
3172
3173        // Data should still be readable from snapshot memory
3174        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    /// Tests the deferred `map_file_cow` flow: map a file on
3189    /// `UninitializedSandbox` (before evolve), then evolve and verify
3190    /// the guest can read the mapped content.
3191    #[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        // Map the file before evolving — this defers the VM-side work.
3204        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        // Evolve — deferred mappings are applied during this step.
3212        let mut sbox = u_sbox.evolve().unwrap();
3213
3214        // Verify the guest can read the mapped content.
3215        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    /// Tests that dropping an `UninitializedSandbox` with pending
3231    /// deferred file mappings does not leak or crash — the
3232    /// `PreparedFileMapping::Drop` should clean up host resources.
3233    #[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            // u_sbox dropped here without evolving — PreparedFileMapping::drop
3249            // should clean up host-side OS resources.
3250        }
3251
3252        // If we get here without a crash/hang, cleanup worked.
3253        // On Windows, also verify the file handle was released.
3254        #[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    /// Tests that `prepare_file_cow` rejects unaligned `guest_base`
3262    /// addresses eagerly, before allocating any OS resources.
3263    #[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        // Use an intentionally unaligned address (page_size + 1).
3273        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    /// Tests that `prepare_file_cow` rejects empty files.
3284    #[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(); // create empty file
3290
3291        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    /// Tests that mapping two files to overlapping GPA ranges is rejected.
3303    #[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        // First mapping should succeed.
3317        u_sbox.map_file_cow(&path1, guest_base).unwrap();
3318
3319        // Second mapping at the same address should fail (overlap).
3320        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    /// Tests that `map_file_cow` rejects a guest_base that overlaps
3331    /// the sandbox's shared memory region.
3332    #[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        // Use BASE_ADDRESS itself — smack in the middle of shared memory.
3344        let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
3345        // page-align it (BASE_ADDRESS is 0x1000, already page-aligned)
3346        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        // First mapping should succeed
3367        unsafe { sbox.map_region(&region1).unwrap() };
3368
3369        // Exact same range should fail
3370        let region2 = region_for_memory(&mem2, guest_base, MemoryRegionFlags::READ);
3371        let err = unsafe { sbox.map_region(&region2) }.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        // Use multi-page regions so partial overlap is geometrically possible
3385        let ps = page_size::get();
3386        let mem1 = page_aligned_memory(&vec![0xAA; ps * 2]); // 2 pages
3387        let mem2 = page_aligned_memory(&vec![0xBB; ps * 2]); // 2 pages
3388        let guest_base: usize = 0x200000000;
3389        let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
3390
3391        unsafe { sbox.map_region(&region1).unwrap() };
3392
3393        // region2 starts one page before region1, overlapping by one page
3394        let overlap_base = guest_base - ps;
3395        let region2 = region_for_memory(&mem2, overlap_base, MemoryRegionFlags::READ);
3396        let err = unsafe { sbox.map_region(&region2) }.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(&region1).unwrap() };
3416
3417        // Adjacent region (starts right after the first one ends) should succeed
3418        let adjacent_base = guest_base + region_size;
3419        let region2 = region_for_memory(&mem2, adjacent_base, MemoryRegionFlags::READ);
3420        unsafe { sbox.map_region(&region2).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        // Try to map at BASE_ADDRESS (0x1000) which overlaps the snapshot region
3430        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(&region) }.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        // The scratch region occupies the top of the GPA space
3450        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(&region) }.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        /// A restore succeeds when the destination declares a superset of the
3665        /// snapshot's guest MSRs. The snapshot's declared MSR keeps its saved
3666        /// value. An MSR the destination adds resets to the baseline.
3667        #[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            // An MSR the destination adds resets to its baseline.
3703            assert_eq!(
3704                target.call::<u64>("ReadMSR", SYSENTER_ESP).unwrap(),
3705                baseline
3706            );
3707        }
3708
3709        /// A restore is rejected when the snapshot captured an MSR the
3710        /// destination neither declares nor covers as a core MSR. The contract
3711        /// is the same on every backend: both restore paths fail and poison
3712        /// the sandbox.
3713        #[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            // A destination that declares nothing, and one that declares a
3727            // disjoint MSR, both reject because the snapshot's SYSENTER_CS is
3728            // neither declared by the destination nor a core MSR.
3729            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; // IA32_KERNEL_GS_BASE
3834
3835            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        /// The guest cannot enter VMX operation, so the nested VM-enter/exit
3898        /// path that loads and stores MSRs through dedicated VMCS fields is
3899        /// unreachable. VMX is hidden from guest CPUID on every backend, so
3900        /// `CR4.VMXE` is a reserved bit and the guest faults.
3901        #[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        /// Executing a VM-enter (`VMLAUNCH`) in the guest faults. The guest is
3917        /// never in VMX operation, so the instruction raises `#UD` before any
3918        /// VMCS-field MSR load or store can run. This exercises the instruction
3919        /// path directly, not just the `CR4.VMXE` prerequisite.
3920        #[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        /// x2APIC is denied at the MSR level and Hyperlight keeps the APIC in
3936        /// xAPIC mode, so the guest CPUID does not advertise x2APIC.
3937        #[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        /// A write-only command cannot enter the reset set.
3957        #[test]
3958        #[cfg(target_arch = "x86_64")]
3959        fn test_allow_non_resettable_msr_fails_creation() {
3960            // IA32_PRED_CMD, a write-only command MSR
3961            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        /// Host support cannot authorize an unclassified MSR.
3971        #[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            // IA32_MISC_ENABLE: host-probeable, not in MSR_TABLE
3981            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            // Resettable MSRs the guest may write once declared.
3994            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        /// A declared guest write must not survive restore.
4022        #[test]
4023        #[cfg(target_arch = "x86_64")]
4024        fn test_declared_msr_does_not_leak_across_restore() {
4025            let msr_index: u32 = 0xC000_0102; // IA32_KERNEL_GS_BASE
4026            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        /// KVM denies DEBUGCTL through its filter and x2APIC through xAPIC mode.
4055        #[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        /// Unresettable feature-class MSRs must not retain guest writes. PMU,
4116        /// LBR, and FRED are perfmon or feature gated. The AMD virtualization
4117        /// MSRs are gated on nested-virt capability the sandbox never requests.
4118        #[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        /// A guest write to IA32_MISC_ENABLE leaves no retained state. Hyper-V
4142        /// drops the write on Intel and faults it on AMD.
4143        #[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        /// Every stateful table entry needs runtime reset coverage.
4153        #[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                    // Direct WRMSR is denied. The dedicated SWAPGS test proves
4177                    // the instruction-side mutation is restored.
4178                } 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), // MTRR_PHYSBASEn
4248                0x200..=0x21F => Some(0x800),                 // MTRR_PHYSMASKn
4249                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        /// Verifies that a guest MSR write faults or resets to its baseline.
4337        #[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        /// Audits Hyper-V MSR bitmap ranges for guest state retained by restore.
4378        #[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            // Free-running counters use a magnitude check after restore.
4383            const FREE_RUNNING: &[u32] = &[
4384                0x10, // IA32_TIME_STAMP_COUNTER
4385                0xE7, // IA32_MPERF
4386                0xE8, // IA32_APERF
4387            ];
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            // At least one retained write must exercise restore.
4404            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            // Collect all free-running leaks for one diagnostic.
4409            let mut free_running_leaked: Vec<u32> = Vec::new();
4410
4411            // Architectural and low model-specific indices.
4412            let low = 0x0000_0000u32..=0x0000_1FFF;
4413            // Hyper-V synthetic indices.
4414            let hyperv_synthetic = 0x4000_0000u32..=0x4000_1FFF;
4415            // Extended and AMD model-specific indices.
4416            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                // A large jump distinguishes reset from normal counter progress.
4429                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                // Multiple candidates cover MSRs with restricted writable bits.
4460                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        /// Active SSP is guest-writable state the Hyper-V backends reset
4538        /// across restore. Skips where the guest cannot use CET shadow
4539        /// stacks, which includes every KVM host.
4540        #[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        /// Hyperlight hides CET from KVM guests, so shadow stacks cannot be
4569        /// enabled and active SSP cannot be moved. Active SSP has no
4570        /// architectural MSR, so it is absent from the KVM reset set and the
4571        /// backend never restores it. Hiding CET keeps that gap unreachable.
4572        #[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            // With CET hidden the host cannot read or write IA32_S_CET, so
4590            // allowing it is rejected at VM creation.
4591            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    /// Tests for [`MultiUseSandbox::from_snapshot`] in-memory.
4601    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        /// Sandbox with an extra `Add(i32, i32) -> i32` host function.
4617        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        /// Two sandboxes built from clones of one `Arc<Snapshot>` can
4656        /// each `restore` back to it, and stay memory-isolated from
4657        /// each other in between.
4658        #[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        /// `restore` must also reject a snapshot whose required host
4714        /// functions are not a subset of the target sandbox's. This
4715        /// matters across sandboxes: a snapshot taken from a sandbox
4716        /// with `Add` registered cannot be restored into a layout
4717        /// compatible sandbox that lacks `Add`.
4718        #[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        /// `restore` rejects a snapshot whose required host function
4738        /// shares a name with the target's but disagrees on signature.
4739        #[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        /// Cross-instance `restore` succeeds when the target registers
4763        /// a strict superset of the snapshot's host functions.
4764        #[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        /// Supplied host-function set may be a strict superset of the
4804        /// snapshot's required set.
4805        #[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        /// A sandbox built via `from_snapshot` can itself be snapshotted
4821        /// and restored, and its snapshots are restore-compatible with it.
4822        #[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        /// The host function closure supplied to `from_snapshot` (not the
4843        /// original sandbox's closure) is the one invoked at runtime.
4844        #[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        /// Pre-init snapshots record no required host functions, so any
4870        /// `HostFunctions` set is accepted.
4871        #[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        /// Snapshots taken from a sandbox built via `from_snapshot`
4888        /// must continue the generation counter of the snapshot they
4889        /// were constructed from, matching `restore`.
4890        #[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        /// Registering a host function on an already-evolved
4908        /// `MultiUseSandbox` must invalidate its cached snapshot, so
4909        /// that the next `snapshot()` reflects the new required
4910        /// host-function set.
4911        #[test]
4912        fn late_register_invalidates_snapshot_cache() {
4913            let mut sbox = make_sandbox();
4914            // Force a cached snapshot to exist.
4915            let _ = sbox.snapshot().unwrap();
4916
4917            sbox.register_host_function("Echo42", || Ok(42i64)).unwrap();
4918
4919            // The next snapshot must include `Echo42` as a required
4920            // host function, so building a sandbox from it without
4921            // `Echo42` must fail.
4922            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}