Skip to main content

hyperlight_host/sandbox/
initialized_multi_use.rs

1/*
2Copyright 2025  The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17use std::path::Path;
18use std::sync::{Arc, Mutex};
19
20use flatbuffers::FlatBufferBuilder;
21use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
22use hyperlight_common::flatbuffer_wrappers::function_types::{
23    ParameterValue, ReturnType, ReturnValue,
24};
25use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity;
26use tracing::{Span, instrument};
27
28use super::Callable;
29use super::file_mapping::prepare_file_cow;
30use super::host_funcs::FunctionRegistry;
31use super::snapshot::Snapshot;
32use crate::func::{ParameterTuple, SupportedReturnType};
33use crate::hypervisor::InterruptHandle;
34use crate::hypervisor::hyperlight_vm::{HyperlightVm, HyperlightVmError};
35use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
36use crate::mem::mgr::SandboxMemoryManager;
37use crate::mem::shared_mem::{HostSharedMemory, SharedMemory as _};
38use crate::metrics::{
39    METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
40};
41use crate::{HyperlightError, Result, log_then_return};
42
43/// A fully initialized sandbox that can execute guest functions multiple times.
44///
45/// Guest functions can be called repeatedly while maintaining state between calls.
46/// The sandbox supports creating snapshots and restoring to previous states.
47///
48/// ## Sandbox Poisoning
49///
50/// The sandbox becomes **poisoned** when the guest is not run to completion, leaving it in
51/// an inconsistent state that could compromise memory safety, data integrity, or security.
52///
53/// ### When Does Poisoning Occur?
54///
55/// Poisoning happens when guest execution is interrupted before normal completion:
56///
57/// - **Guest panics or aborts** - When a guest function panics, crashes, or calls `abort()`,
58///   the normal cleanup and unwinding process is interrupted
59/// - **Invalid memory access** - Attempts to read/write/execute memory outside allowed regions
60/// - **Stack overflow** - Guest exhausts its stack space during execution
61/// - **Heap exhaustion** - Guest runs out of heap memory
62/// - **Host-initiated cancellation** - Calling [`InterruptHandle::kill()`] to forcefully
63///   terminate an in-progress guest function
64///
65/// ### Why This Is Unsafe
66///
67/// When guest execution doesn't complete normally, critical cleanup operations are skipped:
68///
69/// - **Memory leaks** - Heap allocations remain unreachable as the call stack is unwound
70/// - **Corrupted allocator state** - Memory allocator metadata (free lists, heap headers)
71///   left inconsistent
72/// - **Locked resources** - Mutexes or other synchronization primitives remain locked
73/// - **Partial state updates** - Data structures left half-modified (corrupted linked lists,
74///   inconsistent hash tables, etc.)
75///
76/// ### Recovery
77///
78/// Use [`restore()`](Self::restore) with a snapshot taken before poisoning occurred.
79/// This is the **only safe way** to recover - it completely replaces all memory state,
80/// eliminating any inconsistencies. See [`restore()`](Self::restore) for details.
81pub struct MultiUseSandbox {
82    /// Whether this sandbox is poisoned
83    poisoned: bool,
84    pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
85    pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
86    vm: HyperlightVm,
87    #[cfg(gdb)]
88    dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
89    /// If the current state of the sandbox has been captured in a snapshot,
90    /// that snapshot is stored here.
91    pub(crate) snapshot: Option<Arc<Snapshot>>,
92    /// Optional callback to discover page table roots from guest memory.
93    /// Given (snapshot_mem, scratch_mem, cr3), returns a list of root GPAs.
94    /// If not set, only CR3 is used as the single root.
95    pt_root_finder: Option<PtRootFinder>,
96}
97
98/// Callback for discovering page table roots from guest memory.
99///
100/// Called during [`MultiUseSandbox::snapshot`] with:
101/// - `snapshot_mem` - the sandbox's snapshot (shared) memory as a byte slice
102/// - `scratch_mem` - the sandbox's scratch memory as a byte slice
103/// - `root_pt_gpa` - the root page table GPA of the currently-executing
104///   address space
105///
106/// Returns a list of root page table GPAs to walk. If the list is
107/// empty, only `root_pt_gpa` is used.
108pub type PtRootFinder = Box<dyn Fn(&[u8], &[u8], u64) -> Vec<u64> + Send>;
109
110impl MultiUseSandbox {
111    /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance.
112    ///
113    /// This function is not equivalent to doing an `evolve` from uninitialized
114    /// to initialized, and is purposely not exposed publicly outside the crate
115    /// (as a `From` implementation would be)
116    #[instrument(skip_all, parent = Span::current(), level = "Trace")]
117    pub(super) fn from_uninit(
118        host_funcs: Arc<Mutex<FunctionRegistry>>,
119        mgr: SandboxMemoryManager<HostSharedMemory>,
120        vm: HyperlightVm,
121        #[cfg(gdb)] dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
122    ) -> MultiUseSandbox {
123        Self {
124            poisoned: false,
125            host_funcs,
126            mem_mgr: mgr,
127            vm,
128            #[cfg(gdb)]
129            dbg_mem_access_fn,
130            snapshot: None,
131            pt_root_finder: None,
132        }
133    }
134
135    /// Set a callback that discovers page table roots from guest memory.
136    /// The callback receives (snapshot_mem, scratch_mem, cr3) and returns
137    /// the list of root GPAs to walk during snapshot creation.
138    pub fn set_pt_root_finder(&mut self, finder: PtRootFinder) {
139        self.pt_root_finder = Some(finder);
140    }
141
142    /// Create a `MultiUseSandbox` directly from a [`Snapshot`],
143    /// bypassing [`UninitializedSandbox`](crate::UninitializedSandbox)
144    /// and [`evolve()`](crate::UninitializedSandbox::evolve).
145    ///
146    /// This is useful for fast sandbox creation when a snapshot of
147    /// an already-initialized guest is available, either saved to disk
148    /// or captured in memory from another sandbox.
149    ///
150    /// The provided [`HostFunctions`] must include every host function
151    /// that was registered on the sandbox at the time the snapshot was
152    /// taken (matched by name and signature). Additional host functions
153    /// not present in the snapshot are allowed. A mismatch returns
154    /// [`SnapshotHostFunctionMismatch`](crate::HyperlightError::SnapshotHostFunctionMismatch)
155    /// carrying the missing names and signature differences.
156    ///
157    /// An optional [`SandboxConfiguration`](crate::sandbox::SandboxConfiguration)
158    /// can be supplied to override runtime settings such as timeouts and
159    /// interrupt behavior. Memory layout fields
160    /// (`input_data_size`, `output_data_size`, `heap_size`, `scratch_size`)
161    /// are always taken from the snapshot. Any values supplied in
162    /// `config` for those fields are ignored.
163    ///
164    /// # Examples
165    ///
166    /// From a snapshot taken on another sandbox:
167    ///
168    /// ```no_run
169    /// # use std::sync::Arc;
170    /// # use hyperlight_host::{HostFunctions, MultiUseSandbox, UninitializedSandbox, GuestBinary};
171    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
172    /// // Create and initialize a sandbox the normal way
173    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
174    ///     GuestBinary::FilePath("guest.bin".into()),
175    ///     None,
176    /// )?.evolve()?;
177    ///
178    /// // Capture a snapshot of the initialized state
179    /// let snapshot = sandbox.snapshot()?;
180    ///
181    /// // Create a new sandbox directly from the snapshot
182    /// let mut sandbox2 = MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None)?;
183    /// let result: i32 = sandbox2.call("GetValue", ())?;
184    /// # Ok(())
185    /// # }
186    /// ```
187    ///
188    /// From a snapshot loaded from disk:
189    ///
190    /// ```no_run
191    /// # use std::sync::Arc;
192    /// # use hyperlight_host::{HostFunctions, MultiUseSandbox};
193    /// # use hyperlight_host::sandbox::snapshot::{OciTag, Snapshot};
194    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
195    /// let tag = OciTag::new("latest")?;
196    /// let snapshot = Arc::new(Snapshot::load("./guest_snapshot", tag)?);
197    /// let mut sandbox = MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None)?;
198    /// let result: String = sandbox.call("Echo", "hello".to_string())?;
199    /// # Ok(())
200    /// # }
201    /// ```
202    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
203    pub fn from_snapshot(
204        snapshot: Arc<Snapshot>,
205        host_funcs: crate::HostFunctions,
206        config: Option<crate::sandbox::SandboxConfiguration>,
207    ) -> Result<Self> {
208        use rand::RngExt;
209
210        use crate::mem::ptr::RawPtr;
211        use crate::sandbox::uninitialized_evolve::set_up_hypervisor_partition;
212
213        // Validate that the provided host functions are a superset of
214        // those required by the snapshot.
215        snapshot.validate_host_functions(host_funcs.inner())?;
216
217        let host_funcs = Arc::new(Mutex::new(host_funcs.into_inner()));
218
219        let stack_top_gva = snapshot.stack_top_gva();
220        // Start from the caller's config (if any) so runtime fields
221        // such as timeouts and interrupt knobs are honored, then
222        // overwrite the layout fields from the snapshot. The on-disk
223        // layout is fixed, so any layout values supplied by the
224        // caller are silently ignored. Warn if the caller passed a
225        // config whose layout fields disagree with the snapshot, so
226        // the override is at least visible.
227        let caller_supplied_config = config.is_some();
228        let mut config = config.unwrap_or_default();
229        if caller_supplied_config {
230            warn_on_layout_override(&config, snapshot.layout());
231        }
232        config.set_input_data_size(snapshot.layout().input_data_size);
233        config.set_output_data_size(snapshot.layout().output_data_size);
234        config.set_heap_size(snapshot.layout().heap_size as u64);
235        config.set_scratch_size(snapshot.layout().get_scratch_size());
236        let load_info = snapshot.load_info();
237
238        let mgr = crate::mem::mgr::SandboxMemoryManager::from_snapshot(&snapshot)?;
239        let (mut hshm, gshm) = mgr.build()?;
240
241        let page_size = u32::try_from(page_size::get())? as usize;
242
243        #[cfg(target_os = "linux")]
244        crate::signal_handlers::setup_signal_handlers(&config)?;
245
246        // Build the runtime config from the caller's `SandboxConfiguration`
247        // so that `guest_core_dump` (crashdump) and `guest_debug_info` (gdb)
248        // take effect just like they do in the normal evolve path.
249        // `binary_path` and `entry_point` are not available from a snapshot
250        // and are left unset. This only affects metadata in core dumps.
251        #[cfg(any(crashdump, gdb))]
252        let rt_cfg = crate::sandbox::uninitialized::SandboxRuntimeConfig {
253            #[cfg(crashdump)]
254            binary_path: None,
255            #[cfg(gdb)]
256            debug_info: config.get_guest_debug_info(),
257            #[cfg(crashdump)]
258            guest_core_dump: config.get_guest_core_dump(),
259            #[cfg(crashdump)]
260            entry_point: None,
261        };
262
263        let mut vm = set_up_hypervisor_partition(
264            gshm,
265            &config,
266            stack_top_gva,
267            page_size,
268            #[cfg(any(crashdump, gdb))]
269            rt_cfg,
270            load_info,
271        )?;
272
273        let seed = {
274            let mut rng = rand::rng();
275            rng.random::<u64>()
276        };
277        let peb_addr = RawPtr::from(u64::try_from(hshm.layout.peb_address())?);
278
279        #[cfg(gdb)]
280        let dbg_mem_access_hdl = Arc::new(Mutex::new(hshm.clone()));
281
282        // noop for NextAction::Call
283        vm.initialise(
284            peb_addr,
285            seed,
286            &mut hshm,
287            &host_funcs,
288            None,
289            #[cfg(gdb)]
290            dbg_mem_access_hdl,
291        )
292        .map_err(crate::hypervisor::hyperlight_vm::HyperlightVmError::Initialize)?;
293
294        // If the snapshot was taken from an already-initialized guest
295        // (NextAction::Call), apply the captured special registers so
296        // the guest resumes in the correct CPU state.
297        if matches!(snapshot.entrypoint(), super::snapshot::NextAction::Call(_)) {
298            let sregs = snapshot.sregs().ok_or_else(|| {
299                crate::new_error!("snapshot with NextAction::Call must have captured sregs")
300            })?;
301            vm.apply_sregs(hshm.layout.get_pt_base_gpa(), sregs)
302                .map_err(|e| {
303                    crate::HyperlightError::HyperlightVmError(
304                        crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e.into()),
305                    )
306                })?;
307        }
308
309        #[cfg(gdb)]
310        let dbg_mem_wrapper = Arc::new(Mutex::new(hshm.clone()));
311
312        let sbox = MultiUseSandbox::from_uninit(
313            host_funcs,
314            hshm,
315            vm,
316            #[cfg(gdb)]
317            dbg_mem_wrapper,
318        );
319        Ok(sbox)
320    }
321
322    /// Creates a snapshot of the sandbox's current memory state.
323    ///
324    /// The returned snapshot can be applied to any
325    /// [`MultiUseSandbox`] whose memory layout is structurally
326    /// compatible with this sandbox's layout and whose registered
327    /// host functions are a superset of those registered here at the
328    /// time of capture. See [`MultiUseSandbox::restore`] and
329    /// [`MultiUseSandbox::from_snapshot`] for the exact compatibility
330    /// rules and the error variants returned on mismatch.
331    ///
332    /// ## Poisoned Sandbox
333    ///
334    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
335    /// is currently poisoned. Snapshots can only be taken from non-poisoned sandboxes.
336    ///
337    /// # Examples
338    ///
339    /// ```no_run
340    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
341    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
342    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
343    ///     GuestBinary::FilePath("guest.bin".into()),
344    ///     None
345    /// )?.evolve()?;
346    ///
347    /// // Modify sandbox state
348    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 42)?;
349    ///
350    /// // Capture a snapshot of the current memory state
351    /// let snapshot = sandbox.snapshot()?;
352    /// # Ok(())
353    /// # }
354    /// ```
355    #[instrument(err(Debug), skip_all, parent = Span::current())]
356    pub fn snapshot(&mut self) -> Result<Arc<Snapshot>> {
357        if self.poisoned {
358            return Err(crate::HyperlightError::PoisonedSandbox);
359        }
360
361        if let Some(snapshot) = &self.snapshot {
362            return Ok(snapshot.clone());
363        }
364        let mapped_regions_iter = self.vm.get_mapped_regions();
365        let mapped_regions_vec: Vec<MemoryRegion> = mapped_regions_iter.cloned().collect();
366        // Get CR3 from the vCPU
367        let cr3 = self
368            .vm
369            .get_root_pt()
370            .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
371        // Use the callback if set, otherwise just CR3
372        let root_pt_gpas = if let Some(finder) = &self.pt_root_finder {
373            let roots = self.mem_mgr.shared_mem.with_contents(|snap| {
374                self.mem_mgr
375                    .scratch_mem
376                    .with_contents(|scratch| finder(snap, scratch, cr3))
377            })??;
378            if roots.is_empty() { vec![cr3] } else { roots }
379        } else {
380            vec![cr3]
381        };
382
383        let stack_top_gpa = self.vm.get_stack_top();
384        let sregs = self
385            .vm
386            .get_snapshot_sregs()
387            .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
388        let entrypoint = self.vm.get_entrypoint();
389        let host_functions = (&*self.host_funcs.try_lock().map_err(|e| {
390            crate::new_error!("Error locking host_funcs at {}:{}: {}", file!(), line!(), e)
391        })?)
392            .into();
393
394        let memory_snapshot = self.mem_mgr.snapshot(
395            mapped_regions_vec,
396            &root_pt_gpas,
397            stack_top_gpa,
398            sregs,
399            entrypoint,
400            host_functions,
401        )?;
402        let snapshot = Arc::new(memory_snapshot);
403        self.snapshot = Some(snapshot.clone());
404        Ok(snapshot)
405    }
406
407    /// Restores the sandbox's memory to a previously captured snapshot state.
408    ///
409    /// The snapshot's memory layout must be structurally compatible
410    /// with this sandbox's layout, otherwise this returns
411    /// [`SnapshotLayoutMismatch`](crate::HyperlightError::SnapshotLayoutMismatch).
412    ///
413    /// The sandbox's registered host functions must be a superset of
414    /// those required by the snapshot (matched by name and
415    /// signature). Extras on the sandbox are allowed. The registry
416    /// itself is left unchanged. A mismatch returns
417    /// [`SnapshotHostFunctionMismatch`](crate::HyperlightError::SnapshotHostFunctionMismatch)
418    /// carrying the missing names and signature differences.
419    ///
420    /// ## Poison State Recovery
421    ///
422    /// This method automatically clears any poison state when successful. This is safe because:
423    /// - Snapshots can only be taken from non-poisoned sandboxes
424    /// - Restoration completely replaces all memory state, eliminating any inconsistencies
425    ///   caused by incomplete guest execution
426    ///
427    /// ### What Gets Fixed During Restore
428    ///
429    /// When a poisoned sandbox is restored, the memory state is completely reset:
430    /// - **Leaked heap memory** - All allocations from interrupted execution are discarded
431    /// - **Corrupted allocator metadata** - Free lists and heap headers restored to consistent state
432    /// - **Locked mutexes** - All lock state is reset
433    /// - **Partial updates** - Data structures restored to their pre-execution state
434    ///
435    /// # Examples
436    ///
437    /// ```no_run
438    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
439    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
440    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
441    ///     GuestBinary::FilePath("guest.bin".into()),
442    ///     None
443    /// )?.evolve()?;
444    ///
445    /// // Take initial snapshot from this sandbox
446    /// let snapshot = sandbox.snapshot()?;
447    ///
448    /// // Modify sandbox state
449    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 100)?;
450    /// let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
451    /// assert_eq!(value, 100);
452    ///
453    /// // Restore to previous state (same sandbox)
454    /// sandbox.restore(snapshot)?;
455    /// let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
456    /// assert_eq!(restored_value, 0); // Back to initial state
457    /// # Ok(())
458    /// # }
459    /// ```
460    ///
461    /// ## Recovering from Poison
462    ///
463    /// ```no_run
464    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary, HyperlightError};
465    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
466    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
467    ///     GuestBinary::FilePath("guest.bin".into()),
468    ///     None
469    /// )?.evolve()?;
470    ///
471    /// // Take snapshot before potentially poisoning operation
472    /// let snapshot = sandbox.snapshot()?;
473    ///
474    /// // This might poison the sandbox (guest not run to completion)
475    /// let result = sandbox.call::<()>("guest_panic", ());
476    /// if result.is_err() {
477    ///     if sandbox.poisoned() {
478    ///         // Restore from snapshot to clear poison
479    ///         sandbox.restore(snapshot.clone())?;
480    ///         assert!(!sandbox.poisoned());
481    ///         
482    ///         // Sandbox is now usable again
483    ///         sandbox.call::<String>("Echo", "hello".to_string())?;
484    ///     }
485    /// }
486    /// # Ok(())
487    /// # }
488    /// ```
489    #[instrument(err(Debug), skip_all, parent = Span::current())]
490    pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()> {
491        // Currently, we do not try to optimise restore to the
492        // most-current snapshot. This is because the most-current
493        // snapshot, while it must have identical virtual memory
494        // layout to the current sandbox, does not necessarily have
495        // the exact same /physical/ memory contents. It is not
496        // entirely inconceivable that this could lead to breakage of
497        // cross-request isolation in some way, although it would
498        // require some /very/ odd code.  For example, suppose that a
499        // service uses Hyperlight to sandbox native code from
500        // clients, and promises cross-request isolation. A tenant
501        // provides a binary that can process two forms of request,
502        // either writing a secret into physical memory, or reading
503        // from arbitrary physical memory, assuming that the two kinds
504        // of requests can never (dangerously) meet in the same
505        // sandbox.
506        //
507        // It is presently unclear whether this is a sensible threat
508        // model, especially since Hyperlight is often used with
509        // managed-code runtimes which do not allow even arbitrary
510        // access to virtual memory, much less physical memory.
511        // However, out of an abundance of caution, the optimisation
512        // is presently disabled.
513
514        {
515            let host_funcs = self
516                .host_funcs
517                .try_lock()
518                .map_err(|e| crate::new_error!("Error locking host_funcs: {}", e))?;
519            snapshot.validate_compatibility(&self.mem_mgr.layout, &host_funcs)?;
520        }
521
522        let (gsnapshot, gscratch) = self.mem_mgr.restore_snapshot(&snapshot)?;
523        if let Some(gsnapshot) = gsnapshot {
524            self.vm
525                .update_snapshot_mapping(gsnapshot)
526                .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
527        }
528        if let Some(gscratch) = gscratch {
529            self.vm
530                .update_scratch_mapping(gscratch)
531                .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
532        }
533
534        let sregs = snapshot.sregs().ok_or_else(|| {
535            HyperlightError::Error("snapshot from running sandbox should have sregs".to_string())
536        })?;
537        // TODO (ludfjig): Go through the rest of possible errors in this `MultiUseSandbox::restore` function
538        // and determine if they should also poison the sandbox.
539        self.vm
540            .reset_vcpu(snapshot.root_pt_gpa(), sregs)
541            .map_err(|e| {
542                self.poisoned = true;
543                HyperlightVmError::Restore(e)
544            })?;
545
546        self.vm.set_stack_top(snapshot.stack_top_gva());
547        self.vm.set_entrypoint(snapshot.entrypoint());
548
549        let current_regions: Vec<MemoryRegion> = self.vm.get_mapped_regions().cloned().collect();
550        for region in &current_regions {
551            self.vm
552                .unmap_region(region)
553                .map_err(HyperlightVmError::UnmapRegion)?;
554        }
555
556        // The restored snapshot is now our most current snapshot
557        self.snapshot = Some(snapshot.clone());
558
559        // Clear poison state when successfully restoring from snapshot.
560        //
561        // # Safety:
562        // This is safe because:
563        // 1. Snapshots can only be taken from non-poisoned sandboxes (verified at snapshot creation)
564        // 2. Restoration completely replaces all memory state, eliminating:
565        //    - All leaked heap allocations (memory is restored to snapshot state)
566        //    - All corrupted data structures (overwritten with consistent snapshot data)
567        //    - All inconsistent global state (reset to snapshot values)
568        self.poisoned = false;
569
570        Ok(())
571    }
572
573    /// Calls a guest function by name with the specified arguments.
574    ///
575    /// Changes made to the sandbox during execution are *not* persisted.
576    ///
577    /// ## Poisoned Sandbox
578    ///
579    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
580    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
581    ///
582    /// # Examples
583    ///
584    /// ```no_run
585    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
586    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
587    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
588    ///     GuestBinary::FilePath("guest.bin".into()),
589    ///     None
590    /// )?.evolve()?;
591    ///
592    /// // Call function with no arguments
593    /// let result: i32 = sandbox.call_guest_function_by_name("GetCounter", ())?;
594    ///
595    /// // Call function with single argument
596    /// let doubled: i32 = sandbox.call_guest_function_by_name("Double", 21)?;
597    /// assert_eq!(doubled, 42);
598    ///
599    /// // Call function with multiple arguments
600    /// let sum: i32 = sandbox.call_guest_function_by_name("Add", (10, 32))?;
601    /// assert_eq!(sum, 42);
602    ///
603    /// // Call function returning string
604    /// let message: String = sandbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?;
605    /// assert_eq!(message, "Hello, World!");
606    /// # Ok(())
607    /// # }
608    /// ```
609    #[doc(hidden)]
610    #[deprecated(
611        since = "0.8.0",
612        note = "Deprecated in favour of call and snapshot/restore."
613    )]
614    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
615    pub fn call_guest_function_by_name<Output: SupportedReturnType>(
616        &mut self,
617        func_name: &str,
618        args: impl ParameterTuple,
619    ) -> Result<Output> {
620        if self.poisoned {
621            return Err(crate::HyperlightError::PoisonedSandbox);
622        }
623        let snapshot = self.snapshot()?;
624        let res = self.call(func_name, args);
625        self.restore(snapshot)?;
626        res
627    }
628
629    /// Calls a guest function by name with the specified arguments.
630    ///
631    /// Changes made to the sandbox during execution are persisted.
632    ///
633    /// ## Poisoned Sandbox
634    ///
635    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
636    /// is already poisoned before the call. Use [`restore()`](Self::restore) to recover from
637    /// a poisoned state.
638    ///
639    /// ## Sandbox Poisoning
640    ///
641    /// If this method returns an error, the sandbox may be poisoned if the guest was not run
642    /// to completion (due to panic, abort, memory violation, stack/heap exhaustion, or forced
643    /// termination). Use [`poisoned()`](Self::poisoned) to check the poison state and
644    /// [`restore()`](Self::restore) to recover if needed.
645    ///
646    /// If this method returns `Ok`, the sandbox is guaranteed to **not** be poisoned - the guest
647    /// function completed successfully and the sandbox state is consistent.
648    ///
649    /// # Examples
650    ///
651    /// ```no_run
652    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
653    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
654    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
655    ///     GuestBinary::FilePath("guest.bin".into()),
656    ///     None
657    /// )?.evolve()?;
658    ///
659    /// // Call function with no arguments
660    /// let result: i32 = sandbox.call("GetCounter", ())?;
661    ///
662    /// // Call function with single argument
663    /// let doubled: i32 = sandbox.call("Double", 21)?;
664    /// assert_eq!(doubled, 42);
665    ///
666    /// // Call function with multiple arguments
667    /// let sum: i32 = sandbox.call("Add", (10, 32))?;
668    /// assert_eq!(sum, 42);
669    ///
670    /// // Call function returning string
671    /// let message: String = sandbox.call("Echo", "Hello, World!".to_string())?;
672    /// assert_eq!(message, "Hello, World!");
673    /// # Ok(())
674    /// # }
675    /// ```
676    ///
677    /// ## Handling Potential Poisoning
678    ///
679    /// ```no_run
680    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
681    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
682    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
683    ///     GuestBinary::FilePath("guest.bin".into()),
684    ///     None
685    /// )?.evolve()?;
686    ///
687    /// // Take snapshot before risky operation
688    /// let snapshot = sandbox.snapshot()?;
689    ///
690    /// // Call potentially unsafe guest function
691    /// let result = sandbox.call::<String>("RiskyOperation", "input".to_string());
692    ///
693    /// // Check if the call failed and poisoned the sandbox
694    /// if let Err(e) = result {
695    ///     eprintln!("Guest function failed: {}", e);
696    ///     
697    ///     if sandbox.poisoned() {
698    ///         eprintln!("Sandbox was poisoned, restoring from snapshot");
699    ///         sandbox.restore(snapshot.clone())?;
700    ///     }
701    /// }
702    /// # Ok(())
703    /// # }
704    /// ```
705    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
706    pub fn call<Output: SupportedReturnType>(
707        &mut self,
708        func_name: &str,
709        args: impl ParameterTuple,
710    ) -> Result<Output> {
711        if self.poisoned {
712            return Err(crate::HyperlightError::PoisonedSandbox);
713        }
714        // Reset snapshot since we are mutating the sandbox state
715        self.snapshot = None;
716        maybe_time_and_emit_guest_call(func_name, || {
717            let ret = self.call_guest_function_by_name_no_reset(
718                func_name,
719                Output::TYPE,
720                args.into_value(),
721            );
722            // Use the ? operator to allow converting any hyperlight_common::func::Error
723            // returned by from_value into a HyperlightError
724            let ret = Output::from_value(ret?)?;
725            Ok(ret)
726        })
727    }
728
729    /// Maps a region of host memory into the sandbox address space.
730    ///
731    /// The base address and length must meet platform alignment requirements
732    /// (typically page-aligned). The `region_type` field is ignored as guest
733    /// page table entries are not created.
734    ///
735    /// ## Poisoned Sandbox
736    ///
737    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
738    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
739    ///
740    /// # Safety
741    ///
742    /// The caller must ensure the host memory region remains valid and unmodified
743    /// for the lifetime of `self`.
744    #[instrument(err(Debug), skip(self, rgn), parent = Span::current())]
745    pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> {
746        if self.poisoned {
747            return Err(crate::HyperlightError::PoisonedSandbox);
748        }
749        if rgn.flags.contains(MemoryRegionFlags::WRITE) {
750            // TODO: Implement support for writable mappings, which
751            // need to be registered with the memory manager so that
752            // writes can be rolled back when necessary.
753            log_then_return!("TODO: Writable mappings not yet supported");
754        }
755
756        // Map first so overlaps are rejected before resetting the snapshot
757        unsafe { self.vm.map_region(rgn) }.map_err(HyperlightVmError::MapRegion)?;
758        self.snapshot = None;
759        Ok(())
760    }
761
762    /// Map the contents of a file into the guest at a particular address
763    ///
764    /// Returns the length of the mapping in bytes.
765    ///
766    /// ## Poisoned Sandbox
767    ///
768    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
769    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
770    #[instrument(err(Debug), skip(self, file_path, guest_base), parent = Span::current())]
771    pub fn map_file_cow(&mut self, file_path: &Path, guest_base: u64) -> Result<u64> {
772        if self.poisoned {
773            return Err(crate::HyperlightError::PoisonedSandbox);
774        }
775
776        // Phase 1: host-side OS work (open file, create mapping)
777        let mut prepared = prepare_file_cow(file_path, guest_base)?;
778
779        // Validate that the full mapped range doesn't overlap the
780        // sandbox's primary shared memory region.
781        let shared_size = self.mem_mgr.shared_mem.mem_size() as u64;
782        let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
783        let shared_end = base_addr.checked_add(shared_size).ok_or_else(|| {
784            crate::HyperlightError::Error("shared memory end overflow".to_string())
785        })?;
786        let mapping_end = guest_base
787            .checked_add(prepared.size as u64)
788            .ok_or_else(|| {
789                crate::HyperlightError::Error(format!(
790                    "map_file_cow: guest address overflow: {:#x} + {:#x}",
791                    guest_base, prepared.size
792                ))
793            })?;
794        if guest_base < shared_end && mapping_end > base_addr {
795            return Err(crate::HyperlightError::Error(format!(
796                "map_file_cow: mapping [{:#x}..{:#x}) overlaps sandbox shared memory [{:#x}..{:#x})",
797                guest_base, mapping_end, base_addr, shared_end,
798            )));
799        }
800
801        // Phase 2: VM-side work (map into guest address space)
802        let region = prepared.to_memory_region()?;
803
804        unsafe { self.vm.map_region(&region) }
805            .map_err(HyperlightVmError::MapRegion)
806            .map_err(crate::HyperlightError::HyperlightVmError)?;
807
808        self.snapshot = None;
809
810        let size = prepared.size as u64;
811
812        // Mark consumed immediately after map_region succeeds.
813        // On Windows, WhpVm::map_memory copies the file mapping handle
814        // into its own `file_mappings` vec for cleanup on drop. If we
815        // deferred mark_consumed(), both PreparedFileMapping::drop and
816        // WhpVm::drop would release the same handle — a double-close.
817        // On Linux the hypervisor holds a reference to the host mmap;
818        // freeing it here would leave a dangling backing.
819        prepared.mark_consumed();
820
821        Ok(size)
822    }
823
824    /// Calls a guest function with type-erased parameters and return values.
825    ///
826    /// This function is used for fuzz testing parameter and return type handling.
827    ///
828    /// ## Poisoned Sandbox
829    ///
830    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
831    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
832    #[cfg(feature = "fuzzing")]
833    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
834    pub fn call_type_erased_guest_function_by_name(
835        &mut self,
836        func_name: &str,
837        ret_type: ReturnType,
838        args: Vec<ParameterValue>,
839    ) -> Result<ReturnValue> {
840        if self.poisoned {
841            return Err(crate::HyperlightError::PoisonedSandbox);
842        }
843        // Reset snapshot since we are mutating the sandbox state
844        self.snapshot = None;
845        maybe_time_and_emit_guest_call(func_name, || {
846            self.call_guest_function_by_name_no_reset(func_name, ret_type, args)
847        })
848    }
849
850    fn call_guest_function_by_name_no_reset(
851        &mut self,
852        function_name: &str,
853        return_type: ReturnType,
854        args: Vec<ParameterValue>,
855    ) -> Result<ReturnValue> {
856        if self.poisoned {
857            return Err(crate::HyperlightError::PoisonedSandbox);
858        }
859        // ===== KILL() TIMING POINT 1 =====
860        // Clear any stale cancellation from a previous guest function call or if kill() was called too early.
861        // Any kill() that completed (even partially) BEFORE this line has NO effect on this call.
862        self.vm.clear_cancel();
863
864        let res = (|| {
865            let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args);
866
867            let fc = FunctionCall::new(
868                function_name.to_string(),
869                Some(args),
870                FunctionCallType::Guest,
871                return_type,
872            );
873
874            let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
875            let buffer = fc.encode(&mut builder);
876
877            self.mem_mgr.write_guest_function_call(buffer)?;
878
879            let dispatch_res = self.vm.dispatch_call_from_host(
880                &mut self.mem_mgr,
881                &self.host_funcs,
882                #[cfg(gdb)]
883                self.dbg_mem_access_fn.clone(),
884            );
885
886            // Convert dispatch errors to HyperlightErrors to maintain backwards compatibility
887            // but first determine if sandbox should be poisoned
888            if let Err(e) = dispatch_res {
889                let (error, should_poison) = e.promote();
890                self.poisoned |= should_poison;
891                return Err(error);
892            }
893
894            let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner();
895
896            match guest_result {
897                Ok(val) => Ok(val),
898                Err(guest_error) => {
899                    metrics::counter!(
900                        METRIC_GUEST_ERROR,
901                        METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string()
902                    )
903                    .increment(1);
904
905                    Err(HyperlightError::GuestError(
906                        guest_error.code,
907                        guest_error.message,
908                    ))
909                }
910            }
911        })();
912
913        // Clear partial abort bytes so they don't leak across calls.
914        self.mem_mgr.abort_buffer.clear();
915
916        // In the happy path we do not need to clear io-buffers from the host because:
917        // - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::<FunctionCall>()`
918        // - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result`
919        // - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call`
920        // - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value`
921        if let Err(e) = &res {
922            self.mem_mgr.clear_io_buffers();
923
924            // Determine if we should poison the sandbox.
925            self.poisoned |= e.is_poison_error();
926        }
927
928        // Note: clear_call_active() is automatically called when _guard is dropped here
929
930        res
931    }
932
933    /// Returns a handle for interrupting guest execution.
934    ///
935    /// # Examples
936    ///
937    /// ```no_run
938    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
939    /// # use std::thread;
940    /// # use std::time::Duration;
941    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
942    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
943    ///     GuestBinary::FilePath("guest.bin".into()),
944    ///     None
945    /// )?.evolve()?;
946    ///
947    /// // Get interrupt handle before starting long-running operation
948    /// let interrupt_handle = sandbox.interrupt_handle();
949    ///
950    /// // Spawn thread to interrupt after timeout
951    /// let handle_clone = interrupt_handle.clone();
952    /// thread::spawn(move || {
953    ///     thread::sleep(Duration::from_secs(5));
954    ///     handle_clone.kill();
955    /// });
956    ///
957    /// // This call may be interrupted by the spawned thread
958    /// let result = sandbox.call_guest_function_by_name::<i32>("LongRunningFunction", ());
959    /// # Ok(())
960    /// # }
961    /// ```
962    pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
963        self.vm.interrupt_handle()
964    }
965
966    /// Generate a crash dump of the current state of the VM underlying this sandbox.
967    ///
968    /// Creates an ELF core dump file that can be used for debugging. The dump
969    /// captures the current state of the sandbox including registers, memory regions,
970    /// and other execution context.
971    ///
972    /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
973    /// environment variable. If not set, it defaults to the system's temporary directory.
974    ///
975    /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
976    /// is also configured to allow core dumps (which is the default behavior).
977    ///
978    /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
979    /// guest that dont cause crashes (e.g. a guest function that does not return)
980    ///
981    /// # Examples
982    ///
983    /// Attach to your running process with gdb and call this function:
984    ///
985    /// ```shell
986    /// sudo gdb -p <pid_of_your_process>
987    /// (gdb) info threads
988    /// # find the thread that is running the guest function you want to debug
989    /// (gdb) thread <thread_number>
990    /// # switch to the frame where you have access to your MultiUseSandbox instance
991    /// (gdb) backtrace
992    /// (gdb) frame <frame_number>
993    /// # get the pointer to your MultiUseSandbox instance
994    /// # Get the sandbox pointer
995    /// (gdb) print sandbox
996    /// # Call the crashdump function
997    /// call sandbox.generate_crashdump()
998    /// ```
999    /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
1000    ///
1001    #[cfg(crashdump)]
1002    #[instrument(err(Debug), skip_all, parent = Span::current())]
1003    pub fn generate_crashdump(&mut self) -> Result<()> {
1004        crate::hypervisor::crashdump::generate_crashdump(&self.vm, &mut self.mem_mgr, None)
1005    }
1006
1007    /// Generate a crash dump of the current state of the VM, writing to `dir`.
1008    ///
1009    /// Like [`generate_crashdump`](Self::generate_crashdump), but the core dump
1010    /// file is placed in `dir` instead of consulting the `HYPERLIGHT_CORE_DUMP_DIR`
1011    /// environment variable.  This avoids the need for callers to use
1012    /// `unsafe { std::env::set_var(...) }`.
1013    #[cfg(crashdump)]
1014    #[instrument(err(Debug), skip_all, parent = Span::current())]
1015    pub fn generate_crashdump_to_dir(&mut self, dir: impl Into<String>) -> Result<()> {
1016        crate::hypervisor::crashdump::generate_crashdump(
1017            &self.vm,
1018            &mut self.mem_mgr,
1019            Some(dir.into()),
1020        )
1021    }
1022
1023    /// Returns whether the sandbox is currently poisoned.
1024    ///
1025    /// A poisoned sandbox is in an inconsistent state due to the guest not running to completion.
1026    /// All operations will be rejected until the sandbox is restored from a non-poisoned snapshot.
1027    ///
1028    /// ## Causes of Poisoning
1029    ///
1030    /// The sandbox becomes poisoned when guest execution is interrupted:
1031    /// - **Panics/Aborts** - Guest code panics or calls `abort()`
1032    /// - **Invalid Memory Access** - Read/write/execute violations  
1033    /// - **Stack Overflow** - Guest exhausts stack space
1034    /// - **Heap Exhaustion** - Guest runs out of heap memory
1035    /// - **Forced Termination** - [`InterruptHandle::kill()`] called during execution
1036    ///
1037    /// ## Recovery
1038    ///
1039    /// To clear the poison state, use [`restore()`](Self::restore) with a snapshot
1040    /// that was taken before the sandbox became poisoned.
1041    ///
1042    /// # Examples
1043    ///
1044    /// ```no_run
1045    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
1046    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1047    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
1048    ///     GuestBinary::FilePath("guest.bin".into()),
1049    ///     None
1050    /// )?.evolve()?;
1051    ///
1052    /// // Check if sandbox is poisoned
1053    /// if sandbox.poisoned() {
1054    ///     println!("Sandbox is poisoned and needs attention");
1055    /// }
1056    /// # Ok(())
1057    /// # }
1058    /// ```
1059    pub fn poisoned(&self) -> bool {
1060        self.poisoned
1061    }
1062}
1063
1064impl Callable for MultiUseSandbox {
1065    fn call<Output: SupportedReturnType>(
1066        &mut self,
1067        func_name: &str,
1068        args: impl ParameterTuple,
1069    ) -> Result<Output> {
1070        if self.poisoned {
1071            return Err(crate::HyperlightError::PoisonedSandbox);
1072        }
1073        self.call(func_name, args)
1074    }
1075}
1076
1077impl std::fmt::Debug for MultiUseSandbox {
1078    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1079        f.debug_struct("MultiUseSandbox").finish()
1080    }
1081}
1082
1083/// Emit a warning for each memory-layout field in `caller` that
1084/// disagrees with `snapshot`. Used by [`MultiUseSandbox::from_snapshot`]
1085/// to surface ignored caller-supplied layout values, since those
1086/// fields are always taken from the snapshot.
1087fn warn_on_layout_override(
1088    caller: &crate::sandbox::SandboxConfiguration,
1089    snapshot: &crate::mem::layout::SandboxMemoryLayout,
1090) {
1091    let mismatches: &[(&str, u64, u64)] = &[
1092        (
1093            "input_data_size",
1094            caller.get_input_data_size() as u64,
1095            snapshot.input_data_size as u64,
1096        ),
1097        (
1098            "output_data_size",
1099            caller.get_output_data_size() as u64,
1100            snapshot.output_data_size as u64,
1101        ),
1102        (
1103            "heap_size",
1104            caller.get_heap_size(),
1105            snapshot.heap_size as u64,
1106        ),
1107        (
1108            "scratch_size",
1109            caller.get_scratch_size() as u64,
1110            snapshot.get_scratch_size() as u64,
1111        ),
1112    ];
1113    for (name, supplied, snap) in mismatches {
1114        if supplied != snap {
1115            tracing::warn!(
1116                "from_snapshot ignoring caller-supplied {} ({}); using snapshot value ({})",
1117                name,
1118                supplied,
1119                snap
1120            );
1121        }
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use std::sync::{Arc, Barrier};
1128    use std::thread;
1129
1130    use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
1131    use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE};
1132    use hyperlight_testing::simple_guest_as_string;
1133
1134    use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
1135    use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
1136    use crate::sandbox::SandboxConfiguration;
1137    use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox};
1138
1139    #[test]
1140    fn poison() {
1141        let mut sbox: MultiUseSandbox = {
1142            let path = simple_guest_as_string().unwrap();
1143            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1144            u_sbox.evolve()
1145        }
1146        .unwrap();
1147        let snapshot = sbox.snapshot().unwrap();
1148
1149        // poison on purpose
1150        let res = sbox
1151            .call::<()>("guest_panic", "hello".to_string())
1152            .unwrap_err();
1153        assert!(
1154            matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
1155        );
1156        assert!(sbox.poisoned());
1157
1158        // guest calls should fail when poisoned
1159        let res = sbox
1160            .call::<()>("guest_panic", "hello2".to_string())
1161            .unwrap_err();
1162        assert!(matches!(res, HyperlightError::PoisonedSandbox));
1163
1164        // snapshot should fail when poisoned
1165        if let Err(e) = sbox.snapshot() {
1166            assert!(sbox.poisoned());
1167            assert!(matches!(e, HyperlightError::PoisonedSandbox));
1168        } else {
1169            panic!("Snapshot should fail");
1170        }
1171
1172        // map_region should fail when poisoned
1173        {
1174            let map_mem = allocate_guest_memory();
1175            let guest_base = 0x0;
1176            let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1177            let res = unsafe { sbox.map_region(&region) }.unwrap_err();
1178            assert!(matches!(res, HyperlightError::PoisonedSandbox));
1179        }
1180
1181        // map_file_cow should fail when poisoned
1182        {
1183            let temp_file = std::env::temp_dir().join("test_poison_map_file.bin");
1184            let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err();
1185            assert!(matches!(res, HyperlightError::PoisonedSandbox));
1186            std::fs::remove_file(&temp_file).ok(); // Clean up
1187        }
1188
1189        // call_guest_function_by_name (deprecated) should fail when poisoned
1190        #[allow(deprecated)]
1191        let res = sbox
1192            .call_guest_function_by_name::<String>("Echo", "test".to_string())
1193            .unwrap_err();
1194        assert!(matches!(res, HyperlightError::PoisonedSandbox));
1195
1196        // restore to non-poisoned snapshot should work and clear poison
1197        sbox.restore(snapshot.clone()).unwrap();
1198        assert!(!sbox.poisoned());
1199
1200        // guest calls should work again after restore
1201        let res = sbox.call::<String>("Echo", "hello2".to_string()).unwrap();
1202        assert_eq!(res, "hello2".to_string());
1203        assert!(!sbox.poisoned());
1204
1205        // re-poison on purpose
1206        let res = sbox
1207            .call::<()>("guest_panic", "hello".to_string())
1208            .unwrap_err();
1209        assert!(
1210            matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
1211        );
1212        assert!(sbox.poisoned());
1213
1214        // restore to non-poisoned snapshot should work again
1215        sbox.restore(snapshot.clone()).unwrap();
1216        assert!(!sbox.poisoned());
1217
1218        // guest calls should work again
1219        let res = sbox.call::<String>("Echo", "hello3".to_string()).unwrap();
1220        assert_eq!(res, "hello3".to_string());
1221        assert!(!sbox.poisoned());
1222
1223        // snapshot should work again
1224        let _ = sbox.snapshot().unwrap();
1225    }
1226
1227    /// Make sure input/output buffers are properly reset after guest call (with host call)
1228    #[test]
1229    fn host_func_error() {
1230        let path = simple_guest_as_string().unwrap();
1231        let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1232        sandbox
1233            .register("HostError", || -> Result<()> {
1234                Err(HyperlightError::Error("hi".to_string()))
1235            })
1236            .unwrap();
1237        let mut sandbox = sandbox.evolve().unwrap();
1238
1239        // will exhaust io if leaky
1240        for _ in 0..1000 {
1241            let result = sandbox
1242                .call::<i64>(
1243                    "CallGivenParamlessHostFuncThatReturnsI64",
1244                    "HostError".to_string(),
1245                )
1246                .unwrap_err();
1247
1248            assert!(
1249                matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"),
1250            );
1251        }
1252    }
1253
1254    #[test]
1255    fn call_host_func_expect_error() {
1256        let path = simple_guest_as_string().unwrap();
1257        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1258        let mut sandbox = sandbox.evolve().unwrap();
1259        sandbox
1260            .call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string())
1261            .unwrap();
1262    }
1263
1264    /// Make sure input/output buffers are properly reset after guest call (with host call)
1265    #[test]
1266    fn io_buffer_reset() {
1267        let mut cfg = SandboxConfiguration::default();
1268        cfg.set_input_data_size(4096);
1269        cfg.set_output_data_size(4096);
1270        let path = simple_guest_as_string().unwrap();
1271        let mut sandbox =
1272            UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1273        sandbox.register("HostAdd", |a: i32, b: i32| a + b).unwrap();
1274        let mut sandbox = sandbox.evolve().unwrap();
1275
1276        // will exhaust io if leaky. Tests both success and error paths
1277        for _ in 0..1000 {
1278            let result = sandbox.call::<i32>("Add", (5i32, 10i32)).unwrap();
1279            assert_eq!(result, 15);
1280            let result = sandbox.call::<i32>("AddToStaticAndFail", ()).unwrap_err();
1281            assert!(
1282                matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose")
1283            );
1284        }
1285    }
1286
1287    /// Tests that call_guest_function_by_name restores the state correctly
1288    #[test]
1289    fn test_call_guest_function_by_name() {
1290        let mut sbox: MultiUseSandbox = {
1291            let path = simple_guest_as_string().unwrap();
1292            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1293            u_sbox.evolve()
1294        }
1295        .unwrap();
1296
1297        let snapshot = sbox.snapshot().unwrap();
1298
1299        let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1300        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1301        assert_eq!(res, 5);
1302
1303        sbox.restore(snapshot).unwrap();
1304        #[allow(deprecated)]
1305        let _ = sbox
1306            .call_guest_function_by_name::<i32>("AddToStatic", 5i32)
1307            .unwrap();
1308        #[allow(deprecated)]
1309        let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap();
1310        assert_eq!(res, 0);
1311    }
1312
1313    // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (24K) and heap(20K).
1314    // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
1315    #[test]
1316    fn test_with_small_stack_and_heap() {
1317        let mut cfg = SandboxConfiguration::default();
1318        cfg.set_heap_size(20 * 1024);
1319        // min_scratch_size already includes 1 page (4k on most
1320        // platforms) of guest stack, so add 20k more to get 24k
1321        // total, and then add some more for the eagerly-copied page
1322        // tables on amd64
1323        let min_scratch = hyperlight_common::layout::min_scratch_size(
1324            cfg.get_input_data_size(),
1325            cfg.get_output_data_size(),
1326        );
1327        cfg.set_scratch_size(min_scratch + 0x10000 + 0x10000);
1328
1329        let mut sbox1: MultiUseSandbox = {
1330            let path = simple_guest_as_string().unwrap();
1331            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1332            u_sbox.evolve()
1333        }
1334        .unwrap();
1335
1336        for _ in 0..1000 {
1337            sbox1.call::<String>("Echo", "hello".to_string()).unwrap();
1338        }
1339
1340        let mut sbox2: MultiUseSandbox = {
1341            let path = simple_guest_as_string().unwrap();
1342            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1343            u_sbox.evolve()
1344        }
1345        .unwrap();
1346
1347        for i in 0..1000 {
1348            sbox2
1349                .call::<i32>(
1350                    "PrintUsingPrintf",
1351                    format!("Hello World {}\n", i).to_string(),
1352                )
1353                .unwrap();
1354        }
1355    }
1356
1357    /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
1358    /// and restoring a snapshot from before evolving restores the previous state
1359    #[test]
1360    fn snapshot_evolve_restore_handles_state_correctly() {
1361        let mut sbox: MultiUseSandbox = {
1362            let path = simple_guest_as_string().unwrap();
1363            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1364            u_sbox.evolve()
1365        }
1366        .unwrap();
1367
1368        let snapshot = sbox.snapshot().unwrap();
1369
1370        let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1371
1372        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1373        assert_eq!(res, 5);
1374
1375        sbox.restore(snapshot).unwrap();
1376        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1377        assert_eq!(res, 0);
1378    }
1379
1380    #[test]
1381    fn test_trigger_exception_on_guest() {
1382        let usbox = UninitializedSandbox::new(
1383            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1384            None,
1385        )
1386        .unwrap();
1387
1388        let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap();
1389
1390        let res: Result<()> = multi_use_sandbox.call("TriggerException", ());
1391
1392        assert!(res.is_err());
1393
1394        match res.unwrap_err() {
1395            HyperlightError::GuestAborted(_, msg) => {
1396                // msg should indicate we got an invalid opcode exception
1397                #[cfg(target_arch = "x86_64")]
1398                assert!(msg.contains("InvalidOpcode"));
1399                #[cfg(target_arch = "aarch64")]
1400                assert!(msg.contains("0x2000000"));
1401            }
1402            e => panic!("Expected HyperlightError::GuestAborted but got {:?}", e),
1403        }
1404    }
1405
1406    #[test]
1407    fn create_200_sandboxes() {
1408        const NUM_THREADS: usize = 10;
1409        const SANDBOXES_PER_THREAD: usize = 20;
1410
1411        // barrier to make sure all threads start their work simultaneously
1412        let start_barrier = Arc::new(Barrier::new(NUM_THREADS + 1));
1413        let mut thread_handles = vec![];
1414
1415        for _ in 0..NUM_THREADS {
1416            let barrier = start_barrier.clone();
1417
1418            let handle = thread::spawn(move || {
1419                barrier.wait();
1420
1421                for _ in 0..SANDBOXES_PER_THREAD {
1422                    let guest_path = simple_guest_as_string().expect("Guest Binary Missing");
1423                    let uninit =
1424                        UninitializedSandbox::new(GuestBinary::FilePath(guest_path), None).unwrap();
1425
1426                    let mut sandbox: MultiUseSandbox = uninit.evolve().unwrap();
1427
1428                    let result: i32 = sandbox.call("GetStatic", ()).unwrap();
1429                    assert_eq!(result, 0);
1430                }
1431            });
1432
1433            thread_handles.push(handle);
1434        }
1435
1436        start_barrier.wait();
1437
1438        for handle in thread_handles {
1439            handle.join().unwrap();
1440        }
1441    }
1442
1443    #[test]
1444    fn test_mmap() {
1445        let mut sbox = UninitializedSandbox::new(
1446            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1447            None,
1448        )
1449        .unwrap()
1450        .evolve()
1451        .unwrap();
1452
1453        let expected = b"hello world";
1454        let map_mem = page_aligned_memory(expected);
1455        let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1456
1457        unsafe {
1458            sbox.map_region(&region_for_memory(
1459                &map_mem,
1460                guest_base,
1461                MemoryRegionFlags::READ,
1462            ))
1463            .unwrap();
1464        }
1465
1466        let _guard = map_mem.lock.try_read().unwrap();
1467        let actual: Vec<u8> = sbox
1468            .call(
1469                "ReadMappedBuffer",
1470                (guest_base as u64, expected.len() as u64, true),
1471            )
1472            .unwrap();
1473
1474        assert_eq!(actual, expected);
1475    }
1476
1477    // Makes sure MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE executable but not writable
1478    #[test]
1479    fn test_mmap_write_exec() {
1480        let mut sbox = UninitializedSandbox::new(
1481            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1482            None,
1483        )
1484        .unwrap()
1485        .evolve()
1486        .unwrap();
1487
1488        #[cfg(target_arch = "x86_64")]
1489        let expected = &[0x90, 0x90, 0x90, 0xC3]; // NOOP slide to RET
1490        #[cfg(target_arch = "aarch64")]
1491        let expected = &[0x1f, 0x20, 0x03, 0xd5, 0xc0, 0x03, 0x5f, 0xd6];
1492        let map_mem = page_aligned_memory(expected);
1493        let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1494
1495        unsafe {
1496            sbox.map_region(&region_for_memory(
1497                &map_mem,
1498                guest_base,
1499                MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
1500            ))
1501            .unwrap();
1502        }
1503
1504        let _guard = map_mem.lock.try_read().unwrap();
1505
1506        // Execute should pass since memory is executable
1507        let succeed = sbox
1508            .call::<bool>(
1509                "ExecMappedBuffer",
1510                (guest_base as u64, expected.len() as u64),
1511            )
1512            .unwrap();
1513        assert!(succeed, "Expected execution of mapped buffer to succeed");
1514
1515        // write should fail because the memory is mapped as read-only
1516        let err = sbox
1517            .call::<bool>(
1518                "WriteMappedBuffer",
1519                (guest_base as u64, expected.len() as u64),
1520            )
1521            .unwrap_err();
1522
1523        match err {
1524            HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {}
1525            _ => panic!("Expected MemoryAccessViolation error"),
1526        };
1527    }
1528
1529    fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory {
1530        use hyperlight_common::mem::PAGE_SIZE_USIZE;
1531
1532        let len = src.len().div_ceil(PAGE_SIZE_USIZE) * PAGE_SIZE_USIZE;
1533
1534        let mut mem = ExclusiveSharedMemory::new(len).unwrap();
1535        mem.copy_from_slice(src, 0).unwrap();
1536
1537        let (_, guest_mem) = mem.build();
1538
1539        guest_mem
1540    }
1541
1542    fn region_for_memory(
1543        mem: &GuestSharedMemory,
1544        guest_base: usize,
1545        flags: MemoryRegionFlags,
1546    ) -> MemoryRegion {
1547        let len = mem.mem_size();
1548        MemoryRegion {
1549            host_region: mem.host_region_base()..mem.host_region_end(),
1550            guest_region: guest_base..(guest_base + len),
1551            flags,
1552            region_type: MemoryRegionType::Heap,
1553        }
1554    }
1555
1556    fn allocate_guest_memory() -> GuestSharedMemory {
1557        page_aligned_memory(b"test data for snapshot")
1558    }
1559
1560    #[test]
1561    fn snapshot_restore_handles_remapping_correctly() {
1562        let mut sbox: MultiUseSandbox = {
1563            let path = simple_guest_as_string().unwrap();
1564            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1565            u_sbox.evolve().unwrap()
1566        };
1567
1568        // 1. Take snapshot 1 with no additional regions mapped
1569        let snapshot1 = sbox.snapshot().unwrap();
1570        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1571
1572        // 2. Map a memory region
1573        let map_mem = allocate_guest_memory();
1574        let guest_base = 0x200000000_usize;
1575        let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1576
1577        unsafe { sbox.map_region(&region).unwrap() };
1578        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1579        let orig_read = sbox
1580            .call::<Vec<u8>>(
1581                "ReadMappedBuffer",
1582                (
1583                    guest_base as u64,
1584                    hyperlight_common::vmem::PAGE_SIZE as u64,
1585                    true,
1586                ),
1587            )
1588            .unwrap();
1589
1590        // 3. Take snapshot 2 with 1 region mapped
1591        let snapshot2 = sbox.snapshot().unwrap();
1592        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1593
1594        // 4. Re(store to snapshot 1 (should unmap the region)
1595        sbox.restore(snapshot1.clone()).unwrap();
1596        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1597        let is_mapped = sbox
1598            .call::<bool>("CheckMapped", (guest_base as u64,))
1599            .unwrap();
1600        assert!(!is_mapped);
1601
1602        // 5. Restore forward to snapshot 2 (should have folded the
1603        //    region into the snapshot)
1604        sbox.restore(snapshot2.clone()).unwrap();
1605        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1606        let is_mapped = sbox
1607            .call::<bool>("CheckMapped", (guest_base as u64,))
1608            .unwrap();
1609        assert!(is_mapped);
1610
1611        // Verify the region is the same
1612        let new_read = sbox
1613            .call::<Vec<u8>>(
1614                "ReadMappedBuffer",
1615                (
1616                    guest_base as u64,
1617                    hyperlight_common::vmem::PAGE_SIZE as u64,
1618                    false,
1619                ),
1620            )
1621            .unwrap();
1622        assert_eq!(new_read, orig_read);
1623    }
1624
1625    /// Compaction copies mapped-region pages into the snapshot blob,
1626    /// so cross-instance restore preserves their contents without the
1627    /// target ever mapping the region.
1628    #[test]
1629    fn snapshot_restore_across_sandboxes_preserves_mapped_region_contents() {
1630        let mut source: MultiUseSandbox = {
1631            let path = simple_guest_as_string().unwrap();
1632            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1633            u_sbox.evolve().unwrap()
1634        };
1635
1636        let map_mem = allocate_guest_memory();
1637        let guest_base = 0x200000000_usize;
1638        let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1639        unsafe { source.map_region(&region).unwrap() };
1640
1641        // do_map=true installs the guest PTE for the region.
1642        let orig_read = source
1643            .call::<Vec<u8>>(
1644                "ReadMappedBuffer",
1645                (
1646                    guest_base as u64,
1647                    hyperlight_common::vmem::PAGE_SIZE as u64,
1648                    true,
1649                ),
1650            )
1651            .unwrap();
1652
1653        let snapshot = source.snapshot().unwrap();
1654
1655        let mut target: MultiUseSandbox = {
1656            let path = simple_guest_as_string().unwrap();
1657            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1658            u_sbox.evolve().unwrap()
1659        };
1660        assert_eq!(target.vm.get_mapped_regions().count(), 0);
1661
1662        target.restore(snapshot).unwrap();
1663        assert_eq!(target.vm.get_mapped_regions().count(), 0);
1664
1665        // Snapshot PTEs resolve to GPAs in the snapshot blob, so the
1666        // data is readable without re-mapping.
1667        let new_read = target
1668            .call::<Vec<u8>>(
1669                "ReadMappedBuffer",
1670                (
1671                    guest_base as u64,
1672                    hyperlight_common::vmem::PAGE_SIZE as u64,
1673                    false,
1674                ),
1675            )
1676            .unwrap();
1677        assert_eq!(new_read, orig_read);
1678    }
1679
1680    #[test]
1681    fn snapshot_restore_across_sandboxes() {
1682        let mut sandbox = {
1683            let path = simple_guest_as_string().unwrap();
1684            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1685            u_sbox.evolve().unwrap()
1686        };
1687
1688        let mut sandbox2 = {
1689            let path = simple_guest_as_string().unwrap();
1690            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1691            u_sbox.evolve().unwrap()
1692        };
1693
1694        sandbox.call::<i32>("AddToStatic", 42i32).unwrap();
1695        assert_eq!(sandbox2.call::<i32>("GetStatic", ()).unwrap(), 0);
1696
1697        let snapshot = sandbox.snapshot().unwrap();
1698        sandbox2.restore(snapshot).unwrap();
1699        assert_eq!(sandbox2.call::<i32>("GetStatic", ()).unwrap(), 42);
1700    }
1701
1702    #[test]
1703    fn snapshot_restore_rejects_incompatible_layout() {
1704        let mut sandbox = {
1705            let path = simple_guest_as_string().unwrap();
1706            let mut cfg = SandboxConfiguration::default();
1707            cfg.set_heap_size(0x10_000);
1708            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1709            u_sbox.evolve().unwrap()
1710        };
1711
1712        let mut sandbox2 = {
1713            let path = simple_guest_as_string().unwrap();
1714            let mut cfg = SandboxConfiguration::default();
1715            cfg.set_heap_size(0x20_000);
1716            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1717            u_sbox.evolve().unwrap()
1718        };
1719
1720        let snapshot = sandbox.snapshot().unwrap();
1721        let err = sandbox2.restore(snapshot);
1722        assert!(matches!(err, Err(HyperlightError::SnapshotLayoutMismatch)));
1723    }
1724
1725    /// Validation runs before any memory or vCPU mutation, so a
1726    /// rejected `restore` leaves the target usable.
1727    #[test]
1728    fn snapshot_restore_failure_leaves_target_usable() {
1729        let path = simple_guest_as_string().unwrap();
1730        let mut cfg_a = SandboxConfiguration::default();
1731        cfg_a.set_heap_size(0x10_000);
1732        let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg_a))
1733            .unwrap()
1734            .evolve()
1735            .unwrap();
1736
1737        let path = simple_guest_as_string().unwrap();
1738        let mut cfg_b = SandboxConfiguration::default();
1739        cfg_b.set_heap_size(0x20_000);
1740        let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg_b))
1741            .unwrap()
1742            .evolve()
1743            .unwrap();
1744
1745        target.call::<i32>("AddToStatic", 5i32).unwrap();
1746        let bad_snapshot = source.snapshot().unwrap();
1747        let err = target.restore(bad_snapshot);
1748        assert!(matches!(err, Err(HyperlightError::SnapshotLayoutMismatch)));
1749
1750        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 5);
1751        target.call::<i32>("AddToStatic", 3i32).unwrap();
1752        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 8);
1753
1754        let good_snapshot = target.snapshot().unwrap();
1755        target.call::<i32>("AddToStatic", 100i32).unwrap();
1756        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 108);
1757        target.restore(good_snapshot).unwrap();
1758        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 8);
1759    }
1760
1761    /// `snapshot.regions()` is empty post-compaction, so restore
1762    /// unmaps anything the target had mapped.
1763    #[test]
1764    fn snapshot_restore_across_sandboxes_target_has_mapped_regions() {
1765        let mut source: MultiUseSandbox = {
1766            let path = simple_guest_as_string().unwrap();
1767            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1768            u_sbox.evolve().unwrap()
1769        };
1770        source.call::<i32>("AddToStatic", 23i32).unwrap();
1771        let snapshot = source.snapshot().unwrap();
1772
1773        let mut target: MultiUseSandbox = {
1774            let path = simple_guest_as_string().unwrap();
1775            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1776            u_sbox.evolve().unwrap()
1777        };
1778        let map_mem = allocate_guest_memory();
1779        let guest_base = 0x200000000_usize;
1780        let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1781        unsafe { target.map_region(&region).unwrap() };
1782        assert_eq!(target.vm.get_mapped_regions().count(), 1);
1783
1784        target.restore(snapshot).unwrap();
1785        assert_eq!(target.vm.get_mapped_regions().count(), 0);
1786        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
1787    }
1788
1789    /// Compacted snapshot data is reachable at the source's GVA even
1790    /// when the target had a different region mapped at a different
1791    /// GVA.
1792    #[test]
1793    fn snapshot_restore_across_sandboxes_both_have_different_mapped_regions() {
1794        let mut source: MultiUseSandbox = {
1795            let path = simple_guest_as_string().unwrap();
1796            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1797            u_sbox.evolve().unwrap()
1798        };
1799        let source_mem = allocate_guest_memory();
1800        let source_base = 0x200000000_usize;
1801        let source_region = region_for_memory(&source_mem, source_base, MemoryRegionFlags::READ);
1802        unsafe { source.map_region(&source_region).unwrap() };
1803        let orig_read = source
1804            .call::<Vec<u8>>(
1805                "ReadMappedBuffer",
1806                (
1807                    source_base as u64,
1808                    hyperlight_common::vmem::PAGE_SIZE as u64,
1809                    true,
1810                ),
1811            )
1812            .unwrap();
1813        source.call::<i32>("AddToStatic", 9i32).unwrap();
1814        let snapshot = source.snapshot().unwrap();
1815
1816        let mut target: MultiUseSandbox = {
1817            let path = simple_guest_as_string().unwrap();
1818            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1819            u_sbox.evolve().unwrap()
1820        };
1821        let target_mem = allocate_guest_memory();
1822        let target_base = 0x300000000_usize;
1823        let target_region = region_for_memory(&target_mem, target_base, MemoryRegionFlags::READ);
1824        unsafe { target.map_region(&target_region).unwrap() };
1825        assert_eq!(target.vm.get_mapped_regions().count(), 1);
1826
1827        target.restore(snapshot).unwrap();
1828
1829        assert_eq!(target.vm.get_mapped_regions().count(), 0);
1830        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 9);
1831
1832        let new_read = target
1833            .call::<Vec<u8>>(
1834                "ReadMappedBuffer",
1835                (
1836                    source_base as u64,
1837                    hyperlight_common::vmem::PAGE_SIZE as u64,
1838                    false,
1839                ),
1840            )
1841            .unwrap();
1842        assert_eq!(new_read, orig_read);
1843    }
1844
1845    /// Repeated restore of the same snapshot is idempotent.
1846    #[test]
1847    fn snapshot_restore_across_sandboxes_repeated() {
1848        let mut source: MultiUseSandbox = {
1849            let path = simple_guest_as_string().unwrap();
1850            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1851            u_sbox.evolve().unwrap()
1852        };
1853        source.call::<i32>("AddToStatic", 7i32).unwrap();
1854        let snapshot = source.snapshot().unwrap();
1855
1856        let mut target: MultiUseSandbox = {
1857            let path = simple_guest_as_string().unwrap();
1858            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1859            u_sbox.evolve().unwrap()
1860        };
1861
1862        target.restore(snapshot.clone()).unwrap();
1863        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 7);
1864
1865        target.call::<i32>("AddToStatic", 1000i32).unwrap();
1866        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 1007);
1867
1868        target.restore(snapshot).unwrap();
1869        assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 7);
1870    }
1871
1872    /// Test that snapshot restore properly resets vCPU debug registers. This test verifies
1873    /// that restore() calls reset_vcpu().
1874    #[test]
1875    fn snapshot_restore_resets_debug_registers() {
1876        let mut sandbox: MultiUseSandbox = {
1877            let path = simple_guest_as_string().unwrap();
1878            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1879            u_sbox.evolve().unwrap()
1880        };
1881
1882        let snapshot = sandbox.snapshot().unwrap();
1883
1884        // Verify DR0 is initially 0 (clean state)
1885        let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap();
1886        assert_eq!(dr0_initial, 0, "DR0 should initially be 0");
1887
1888        // Dirty DR0 by setting it to a known non-zero value, avoiding
1889        // bits that are reserved in aarch64 DBGBVR0_EL1
1890        const DIRTY_VALUE: u64 = 0xFFFF_FEDC_7654_3210;
1891        sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap();
1892        let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap();
1893        assert_eq!(
1894            dr0_dirty, DIRTY_VALUE,
1895            "DR0 should be dirty after SetDr0 call"
1896        );
1897
1898        // Restore to the snapshot - this should reset vCPU state including debug registers
1899        sandbox.restore(snapshot).unwrap();
1900
1901        let dr0_after_restore: u64 = sandbox.call("GetDr0", ()).unwrap();
1902        assert_eq!(
1903            dr0_after_restore, 0,
1904            "DR0 should be 0 after restore (reset_vcpu should have been called)"
1905        );
1906    }
1907
1908    /// Test that stale abort buffer bytes from a previous call don't
1909    /// leak into the next call.
1910    #[test]
1911    fn stale_abort_buffer_does_not_leak_across_calls() {
1912        let mut sbox: MultiUseSandbox = {
1913            let path = simple_guest_as_string().unwrap();
1914            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1915            u_sbox.evolve().unwrap()
1916        };
1917
1918        // Simulate a partial abort
1919        sbox.mem_mgr.abort_buffer.extend_from_slice(&[0xAA; 1020]);
1920
1921        let res = sbox.call::<String>("Echo", "hello".to_string());
1922        assert!(
1923            res.is_ok(),
1924            "Expected Ok after stale abort buffer, got: {:?}",
1925            res.unwrap_err()
1926        );
1927
1928        // The buffer should be empty after the call.
1929        assert!(
1930            sbox.mem_mgr.abort_buffer.is_empty(),
1931            "abort_buffer should be empty after a guest call"
1932        );
1933    }
1934
1935    /// Test that sandboxes can be created and evolved with different heap sizes
1936    #[test]
1937    fn test_sandbox_creation_various_sizes() {
1938        let test_cases: [(&str, u64); 3] = [
1939            ("small (8MB heap)", SMALL_HEAP_SIZE),
1940            ("medium (64MB heap)", MEDIUM_HEAP_SIZE),
1941            ("large (256MB heap)", LARGE_HEAP_SIZE),
1942        ];
1943
1944        for (name, heap_size) in test_cases {
1945            let mut cfg = SandboxConfiguration::default();
1946            cfg.set_heap_size(heap_size);
1947            cfg.set_scratch_size(0x100000);
1948
1949            let path = simple_guest_as_string().unwrap();
1950            let sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg))
1951                .unwrap_or_else(|e| panic!("Failed to create {} sandbox: {}", name, e))
1952                .evolve()
1953                .unwrap_or_else(|e| panic!("Failed to evolve {} sandbox: {}", name, e));
1954
1955            drop(sbox);
1956        }
1957    }
1958
1959    /// Helper: create a MultiUseSandbox from the simple guest with default config.
1960    #[cfg(feature = "trace_guest")]
1961    fn sandbox_for_gva_tests() -> MultiUseSandbox {
1962        let path = simple_guest_as_string().unwrap();
1963        UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1964            .unwrap()
1965            .evolve()
1966            .unwrap()
1967    }
1968
1969    /// Helper: read memory at `gva` of length `len` from the guest side via
1970    /// `ReadMappedBuffer(gva, len, false)` and from the host side via
1971    /// `read_guest_memory_by_gva`, then assert both views are identical.
1972    #[cfg(feature = "trace_guest")]
1973    fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
1974        // Guest reads via its own page tables
1975        let expected: Vec<u8> = sbox
1976            .call("ReadMappedBuffer", (gva, len as u64, true))
1977            .unwrap();
1978        assert_eq!(expected.len(), len);
1979
1980        // Host reads by walking the same page tables
1981        let root_pt = sbox.vm.get_root_pt().unwrap();
1982        let actual = sbox
1983            .mem_mgr
1984            .read_guest_memory_by_gva(gva, len, root_pt)
1985            .unwrap();
1986
1987        assert_eq!(
1988            actual, expected,
1989            "read_guest_memory_by_gva at GVA {:#x} (len {}) differs from guest ReadMappedBuffer",
1990            gva, len,
1991        );
1992    }
1993
1994    /// Test reading a small buffer (< 1 page) from guest memory via GVA.
1995    /// Uses the guest code section which is already identity-mapped.
1996    #[test]
1997    #[cfg(feature = "trace_guest")]
1998    fn read_guest_memory_by_gva_single_page() {
1999        let mut sbox = sandbox_for_gva_tests();
2000        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2001        assert_gva_read_matches(&mut sbox, code_gva, 128);
2002    }
2003
2004    /// Test reading exactly one full page (4096 bytes) from guest memory.
2005    /// Uses the guest code section
2006    #[test]
2007    #[cfg(feature = "trace_guest")]
2008    fn read_guest_memory_by_gva_full_page() {
2009        let mut sbox = sandbox_for_gva_tests();
2010        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2011        assert_gva_read_matches(&mut sbox, code_gva, 4096);
2012    }
2013
2014    /// Test that a read starting at an odd (non-page-aligned) address and
2015    /// spanning two page boundaries returns correct data.
2016    #[test]
2017    #[cfg(feature = "trace_guest")]
2018    fn read_guest_memory_by_gva_unaligned_cross_page() {
2019        let mut sbox = sandbox_for_gva_tests();
2020        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2021        // Start 1 byte before the second page boundary and read 4097 bytes
2022        // (spans 2 full page boundaries).
2023        let start = code_gva + 4096 - 1;
2024        println!(
2025            "Testing unaligned cross-page read starting at {:#x} spanning 4097 bytes",
2026            start
2027        );
2028        assert_gva_read_matches(&mut sbox, start, 4097);
2029    }
2030
2031    /// Test reading exactly two full pages (8192 bytes) from guest memory.
2032    #[test]
2033    #[cfg(feature = "trace_guest")]
2034    fn read_guest_memory_by_gva_two_full_pages() {
2035        let mut sbox = sandbox_for_gva_tests();
2036        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2037        assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
2038    }
2039
2040    /// Test reading a region that spans across a page boundary: starts
2041    /// 100 bytes before the end of the first page and reads 200 bytes
2042    /// into the second page.
2043    #[test]
2044    #[cfg(feature = "trace_guest")]
2045    fn read_guest_memory_by_gva_cross_page_boundary() {
2046        let mut sbox = sandbox_for_gva_tests();
2047        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2048        // Start 100 bytes before the first page boundary, read across it.
2049        let start = code_gva + 4096 - 100;
2050        assert_gva_read_matches(&mut sbox, start, 200);
2051    }
2052
2053    /// Helper: create a temp file with known content, padded to be
2054    /// at least page-aligned (4096 bytes). Returns the path and the
2055    /// *original* content bytes (before padding).
2056    fn create_test_file(name: &str, content: &[u8]) -> (std::path::PathBuf, Vec<u8>) {
2057        use std::io::Write;
2058
2059        let page_size = page_size::get();
2060        let padded_len = content.len().max(page_size).div_ceil(page_size) * page_size;
2061        let mut padded = vec![0u8; padded_len];
2062        padded[..content.len()].copy_from_slice(content);
2063
2064        let temp_dir = std::env::temp_dir();
2065        let path = temp_dir.join(name);
2066        let _ = std::fs::remove_file(&path); // clean up from previous runs
2067        let mut f = std::fs::File::create(&path).unwrap();
2068        f.write_all(&padded).unwrap();
2069        (path, content.to_vec())
2070    }
2071
2072    /// Tests the basic `map_file_cow` flow: map a file, read its content
2073    /// from the guest, and verify it matches.
2074    #[test]
2075    fn test_map_file_cow_basic() {
2076        let expected = b"hello world from map_file_cow";
2077        let (path, expected_bytes) =
2078            create_test_file("hyperlight_test_map_file_cow_basic.bin", expected);
2079
2080        let mut sbox = UninitializedSandbox::new(
2081            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2082            None,
2083        )
2084        .unwrap()
2085        .evolve()
2086        .unwrap();
2087
2088        let guest_base: u64 = 0x1_0000_0000;
2089        let mapped_size = sbox.map_file_cow(&path, guest_base).unwrap();
2090        assert!(mapped_size > 0, "mapped_size should be positive");
2091        assert!(
2092            mapped_size >= expected.len() as u64,
2093            "mapped_size should be >= file content length"
2094        );
2095
2096        // Read the content back from the guest
2097        let actual: Vec<u8> = sbox
2098            .call(
2099                "ReadMappedBuffer",
2100                (guest_base, expected_bytes.len() as u64, true),
2101            )
2102            .unwrap();
2103
2104        assert_eq!(
2105            actual, expected_bytes,
2106            "Guest should read back the exact file content"
2107        );
2108
2109        // Clean up
2110        let _ = std::fs::remove_file(&path);
2111    }
2112
2113    /// Tests that `map_file_cow` enforces read-only access: writing to
2114    /// the mapped region from the guest should cause a MemoryAccessViolation.
2115    #[test]
2116    fn test_map_file_cow_read_only_enforcement() {
2117        let content = &[0xBB; 4096];
2118        let (path, _) = create_test_file("hyperlight_test_map_file_cow_readonly.bin", content);
2119
2120        let mut sbox = UninitializedSandbox::new(
2121            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2122            None,
2123        )
2124        .unwrap()
2125        .evolve()
2126        .unwrap();
2127
2128        let guest_base: u64 = 0x1_0000_0000;
2129        sbox.map_file_cow(&path, guest_base).unwrap();
2130
2131        // Writing to the mapped region should fail with MemoryAccessViolation
2132        let err = sbox
2133            .call::<bool>("WriteMappedBuffer", (guest_base, content.len() as u64))
2134            .unwrap_err();
2135
2136        match err {
2137            HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base => {}
2138            _ => panic!(
2139                "Expected MemoryAccessViolation at guest_base, got: {:?}",
2140                err
2141            ),
2142        };
2143
2144        // Clean up
2145        let _ = std::fs::remove_file(&path);
2146    }
2147
2148    /// Tests that `map_file_cow` returns `PoisonedSandbox` when the
2149    /// sandbox is poisoned.
2150    #[test]
2151    fn test_map_file_cow_poisoned() {
2152        let (path, _) = create_test_file("hyperlight_test_map_file_cow_poison.bin", &[0xCC; 4096]);
2153
2154        let mut sbox: MultiUseSandbox = {
2155            let path = simple_guest_as_string().unwrap();
2156            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2157            u_sbox.evolve()
2158        }
2159        .unwrap();
2160        let snapshot = sbox.snapshot().unwrap();
2161
2162        // Poison the sandbox
2163        let _ = sbox
2164            .call::<()>("guest_panic", "hello".to_string())
2165            .unwrap_err();
2166        assert!(sbox.poisoned());
2167
2168        // map_file_cow should fail with PoisonedSandbox
2169        let err = sbox.map_file_cow(&path, 0x1_0000_0000).unwrap_err();
2170        assert!(matches!(err, HyperlightError::PoisonedSandbox));
2171
2172        // Restore and verify map_file_cow works again
2173        sbox.restore(snapshot).unwrap();
2174        assert!(!sbox.poisoned());
2175        let result = sbox.map_file_cow(&path, 0x1_0000_0000);
2176        assert!(result.is_ok());
2177
2178        let _ = std::fs::remove_file(&path);
2179    }
2180
2181    /// Tests that two separate sandboxes can map the same file
2182    /// simultaneously and both read it correctly.
2183    #[test]
2184    fn test_map_file_cow_multi_vm_same_file() {
2185        let expected = b"shared file content across VMs";
2186        let (path, expected_bytes) =
2187            create_test_file("hyperlight_test_map_file_cow_multi_vm.bin", expected);
2188
2189        let guest_base: u64 = 0x1_0000_0000;
2190
2191        let mut sbox1 = UninitializedSandbox::new(
2192            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2193            None,
2194        )
2195        .unwrap()
2196        .evolve()
2197        .unwrap();
2198
2199        let mut sbox2 = UninitializedSandbox::new(
2200            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2201            None,
2202        )
2203        .unwrap()
2204        .evolve()
2205        .unwrap();
2206
2207        // Map the same file into both sandboxes
2208        sbox1.map_file_cow(&path, guest_base).unwrap();
2209        sbox2.map_file_cow(&path, guest_base).unwrap();
2210
2211        // Both should read the correct content
2212        let actual1: Vec<u8> = sbox1
2213            .call(
2214                "ReadMappedBuffer",
2215                (guest_base, expected_bytes.len() as u64, true),
2216            )
2217            .unwrap();
2218        let actual2: Vec<u8> = sbox2
2219            .call(
2220                "ReadMappedBuffer",
2221                (guest_base, expected_bytes.len() as u64, true),
2222            )
2223            .unwrap();
2224
2225        assert_eq!(
2226            actual1, expected_bytes,
2227            "Sandbox 1 should read correct content"
2228        );
2229        assert_eq!(
2230            actual2, expected_bytes,
2231            "Sandbox 2 should read correct content"
2232        );
2233
2234        let _ = std::fs::remove_file(&path);
2235    }
2236
2237    /// Tests that multiple threads can each create a sandbox, map the
2238    /// same file, read it, and drop without errors.
2239    #[test]
2240    fn test_map_file_cow_multi_vm_threaded() {
2241        let expected = b"threaded file mapping test data";
2242        let (path, expected_bytes) =
2243            create_test_file("hyperlight_test_map_file_cow_threaded.bin", expected);
2244
2245        const NUM_THREADS: usize = 5;
2246        let path = Arc::new(path);
2247        let expected_bytes = Arc::new(expected_bytes);
2248        let barrier = Arc::new(Barrier::new(NUM_THREADS));
2249        let mut handles = vec![];
2250
2251        for _ in 0..NUM_THREADS {
2252            let path = path.clone();
2253            let expected_bytes = expected_bytes.clone();
2254            let barrier = barrier.clone();
2255
2256            handles.push(thread::spawn(move || {
2257                barrier.wait();
2258
2259                let mut sbox = UninitializedSandbox::new(
2260                    GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2261                    None,
2262                )
2263                .unwrap()
2264                .evolve()
2265                .unwrap();
2266
2267                let guest_base: u64 = 0x1_0000_0000;
2268                sbox.map_file_cow(&path, guest_base).unwrap();
2269
2270                let actual: Vec<u8> = sbox
2271                    .call(
2272                        "ReadMappedBuffer",
2273                        (guest_base, expected_bytes.len() as u64, true),
2274                    )
2275                    .unwrap();
2276
2277                assert_eq!(actual, *expected_bytes);
2278            }));
2279        }
2280
2281        for h in handles {
2282            h.join().unwrap();
2283        }
2284
2285        let _ = std::fs::remove_file(&*path);
2286    }
2287
2288    /// Tests that file cleanup works after dropping a sandbox that used
2289    /// `map_file_cow` — the file should be deletable (no leaked handles).
2290    #[test]
2291    #[cfg(target_os = "windows")]
2292    fn test_map_file_cow_cleanup_no_handle_leak() {
2293        let (path, _) = create_test_file("hyperlight_test_map_file_cow_cleanup.bin", &[0xDD; 4096]);
2294
2295        {
2296            let mut sbox = UninitializedSandbox::new(
2297                GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2298                None,
2299            )
2300            .unwrap()
2301            .evolve()
2302            .unwrap();
2303
2304            sbox.map_file_cow(&path, 0x1_0000_0000).unwrap();
2305            // sandbox dropped here
2306        }
2307
2308        std::fs::remove_file(&path)
2309            .expect("File should be deletable after sandbox with map_file_cow is dropped");
2310    }
2311
2312    /// Tests snapshot/restore cycle with map_file_cow:
2313    /// snapshot₁ (no file) → map file → snapshot₂ → restore₁ (unmapped)
2314    /// → restore₂ (data folded into snapshot).
2315    #[test]
2316    fn test_map_file_cow_snapshot_remapping_cycle() {
2317        let expected = b"snapshot remapping cycle test!";
2318        let (path, expected_bytes) =
2319            create_test_file("hyperlight_test_map_file_cow_snapshot_remap.bin", expected);
2320
2321        let mut sbox = UninitializedSandbox::new(
2322            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2323            None,
2324        )
2325        .unwrap()
2326        .evolve()
2327        .unwrap();
2328
2329        let guest_base: u64 = 0x1_0000_0000;
2330
2331        // 1. snapshot₁ — no file mapped
2332        let snapshot1 = sbox.snapshot().unwrap();
2333
2334        // 2. Map the file
2335        sbox.map_file_cow(&path, guest_base).unwrap();
2336
2337        // Verify we can read it
2338        let actual: Vec<u8> = sbox
2339            .call(
2340                "ReadMappedBuffer",
2341                (guest_base, expected_bytes.len() as u64, true),
2342            )
2343            .unwrap();
2344        assert_eq!(actual, expected_bytes);
2345
2346        // 3. snapshot₂ — file mapped (data folded into snapshot)
2347        let snapshot2 = sbox.snapshot().unwrap();
2348
2349        // 4. Restore to snapshot₁ — file should be unmapped
2350        sbox.restore(snapshot1.clone()).unwrap();
2351        let is_mapped: bool = sbox.call("CheckMapped", (guest_base,)).unwrap();
2352        assert!(
2353            !is_mapped,
2354            "Region should be unmapped after restoring to snapshot₁"
2355        );
2356
2357        // 5. Restore to snapshot₂ — data should still be readable
2358        //    (folded into snapshot memory, not the original file mapping)
2359        sbox.restore(snapshot2).unwrap();
2360        let is_mapped: bool = sbox.call("CheckMapped", (guest_base,)).unwrap();
2361        assert!(
2362            is_mapped,
2363            "Region should be mapped after restoring to snapshot₂"
2364        );
2365        let actual2: Vec<u8> = sbox
2366            .call(
2367                "ReadMappedBuffer",
2368                (guest_base, expected_bytes.len() as u64, false),
2369            )
2370            .unwrap();
2371        assert_eq!(
2372            actual2, expected_bytes,
2373            "Data should be intact after snapshot₂ restore"
2374        );
2375
2376        let _ = std::fs::remove_file(&path);
2377    }
2378
2379    /// Tests that snapshot correctly captures map_file_cow data and
2380    /// restore brings it back.
2381    #[test]
2382    fn test_map_file_cow_snapshot_restore() {
2383        let expected = b"snapshot restore basic test!!";
2384        let (path, expected_bytes) =
2385            create_test_file("hyperlight_test_map_file_cow_snap_restore.bin", expected);
2386
2387        let mut sbox = UninitializedSandbox::new(
2388            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2389            None,
2390        )
2391        .unwrap()
2392        .evolve()
2393        .unwrap();
2394
2395        let guest_base: u64 = 0x1_0000_0000;
2396        sbox.map_file_cow(&path, guest_base).unwrap();
2397
2398        // Read the content to verify mapping works
2399        let actual: Vec<u8> = sbox
2400            .call(
2401                "ReadMappedBuffer",
2402                (guest_base, expected_bytes.len() as u64, true),
2403            )
2404            .unwrap();
2405        assert_eq!(actual, expected_bytes);
2406
2407        // Take snapshot — folds file data into snapshot memory
2408        let snapshot = sbox.snapshot().unwrap();
2409
2410        // Restore — the file-backed region is unmapped but data is in snapshot
2411        sbox.restore(snapshot).unwrap();
2412
2413        // Data should still be readable from snapshot memory
2414        let actual2: Vec<u8> = sbox
2415            .call(
2416                "ReadMappedBuffer",
2417                (guest_base, expected_bytes.len() as u64, false),
2418            )
2419            .unwrap();
2420        assert_eq!(
2421            actual2, expected_bytes,
2422            "Data should be readable after restore from snapshot"
2423        );
2424
2425        let _ = std::fs::remove_file(&path);
2426    }
2427
2428    /// Tests the deferred `map_file_cow` flow: map a file on
2429    /// `UninitializedSandbox` (before evolve), then evolve and verify
2430    /// the guest can read the mapped content.
2431    #[test]
2432    fn test_map_file_cow_deferred_basic() {
2433        let expected = b"deferred map_file_cow test data";
2434        let (path, expected_bytes) =
2435            create_test_file("hyperlight_test_map_file_cow_deferred.bin", expected);
2436
2437        let guest_base: u64 = 0x1_0000_0000;
2438
2439        let mut u_sbox = UninitializedSandbox::new(
2440            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2441            None,
2442        )
2443        .unwrap();
2444
2445        // Map the file before evolving — this defers the VM-side work.
2446        let mapped_size = u_sbox.map_file_cow(&path, guest_base).unwrap();
2447        assert!(mapped_size > 0, "mapped_size should be positive");
2448        assert!(
2449            mapped_size >= expected.len() as u64,
2450            "mapped_size should be >= file content length"
2451        );
2452
2453        // Evolve — deferred mappings are applied during this step.
2454        let mut sbox: MultiUseSandbox = u_sbox.evolve().unwrap();
2455
2456        // Verify the guest can read the mapped content.
2457        let actual: Vec<u8> = sbox
2458            .call(
2459                "ReadMappedBuffer",
2460                (guest_base, expected_bytes.len() as u64, true),
2461            )
2462            .unwrap();
2463
2464        assert_eq!(
2465            actual, expected_bytes,
2466            "Guest should read back the exact file content after deferred mapping"
2467        );
2468
2469        let _ = std::fs::remove_file(&path);
2470    }
2471
2472    /// Tests that dropping an `UninitializedSandbox` with pending
2473    /// deferred file mappings does not leak or crash — the
2474    /// `PreparedFileMapping::Drop` should clean up host resources.
2475    #[test]
2476    fn test_map_file_cow_deferred_drop_without_evolve() {
2477        let (path, _) = create_test_file(
2478            "hyperlight_test_map_file_cow_deferred_drop.bin",
2479            &[0xAA; 4096],
2480        );
2481
2482        let guest_base: u64 = 0x1_0000_0000;
2483
2484        {
2485            let mut u_sbox = UninitializedSandbox::new(
2486                GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2487                None,
2488            )
2489            .unwrap();
2490
2491            u_sbox.map_file_cow(&path, guest_base).unwrap();
2492            // u_sbox dropped here without evolving — PreparedFileMapping::drop
2493            // should clean up host-side OS resources.
2494        }
2495
2496        // If we get here without a crash/hang, cleanup worked.
2497        // On Windows, also verify the file handle was released.
2498        #[cfg(target_os = "windows")]
2499        std::fs::remove_file(&path)
2500            .expect("File should be deletable after dropping UninitializedSandbox");
2501        #[cfg(not(target_os = "windows"))]
2502        let _ = std::fs::remove_file(&path);
2503    }
2504
2505    /// Tests that `prepare_file_cow` rejects unaligned `guest_base`
2506    /// addresses eagerly, before allocating any OS resources.
2507    #[test]
2508    fn test_map_file_cow_unaligned_guest_base() {
2509        let (path, _) =
2510            create_test_file("hyperlight_test_map_file_cow_unaligned.bin", &[0xBB; 4096]);
2511
2512        let mut u_sbox = UninitializedSandbox::new(
2513            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2514            None,
2515        )
2516        .unwrap();
2517
2518        // Use an intentionally unaligned address (page_size + 1).
2519        let unaligned_base: u64 = (page_size::get() + 1) as u64;
2520        let result = u_sbox.map_file_cow(&path, unaligned_base);
2521        assert!(
2522            result.is_err(),
2523            "map_file_cow should reject unaligned guest_base"
2524        );
2525
2526        let _ = std::fs::remove_file(&path);
2527    }
2528
2529    /// Tests that `prepare_file_cow` rejects empty files.
2530    #[test]
2531    fn test_map_file_cow_empty_file() {
2532        let temp_dir = std::env::temp_dir();
2533        let path = temp_dir.join("hyperlight_test_map_file_cow_empty.bin");
2534        let _ = std::fs::remove_file(&path);
2535        std::fs::File::create(&path).unwrap(); // create empty file
2536
2537        let mut u_sbox = UninitializedSandbox::new(
2538            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2539            None,
2540        )
2541        .unwrap();
2542
2543        let guest_base: u64 = 0x1_0000_0000;
2544        let result = u_sbox.map_file_cow(&path, guest_base);
2545        assert!(result.is_err(), "map_file_cow should reject empty files");
2546
2547        let _ = std::fs::remove_file(&path);
2548    }
2549
2550    /// Tests that mapping two files to overlapping GPA ranges is rejected.
2551    #[test]
2552    fn test_map_file_cow_overlapping_mappings() {
2553        let (path1, _) =
2554            create_test_file("hyperlight_test_map_file_cow_overlap1.bin", &[0xAA; 4096]);
2555        let (path2, _) =
2556            create_test_file("hyperlight_test_map_file_cow_overlap2.bin", &[0xBB; 4096]);
2557
2558        let guest_base: u64 = 0x1_0000_0000;
2559
2560        let mut u_sbox = UninitializedSandbox::new(
2561            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2562            None,
2563        )
2564        .unwrap();
2565
2566        // First mapping should succeed.
2567        u_sbox.map_file_cow(&path1, guest_base).unwrap();
2568
2569        // Second mapping at the same address should fail (overlap).
2570        let result = u_sbox.map_file_cow(&path2, guest_base);
2571        assert!(
2572            result.is_err(),
2573            "map_file_cow should reject overlapping guest address ranges"
2574        );
2575
2576        let _ = std::fs::remove_file(&path1);
2577        let _ = std::fs::remove_file(&path2);
2578    }
2579
2580    /// Tests that `map_file_cow` rejects a guest_base that overlaps
2581    /// the sandbox's shared memory region.
2582    #[test]
2583    fn test_map_file_cow_shared_mem_overlap() {
2584        let (path, _) = create_test_file(
2585            "hyperlight_test_map_file_cow_overlap_shm.bin",
2586            &[0xCC; 4096],
2587        );
2588
2589        let mut u_sbox = UninitializedSandbox::new(
2590            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
2591            None,
2592        )
2593        .unwrap();
2594
2595        // Use BASE_ADDRESS itself — smack in the middle of shared memory.
2596        let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
2597        // page-align it (BASE_ADDRESS is 0x1000, already page-aligned)
2598        let result = u_sbox.map_file_cow(&path, base_addr);
2599        assert!(
2600            result.is_err(),
2601            "map_file_cow should reject guest_base inside shared memory"
2602        );
2603
2604        let _ = std::fs::remove_file(&path);
2605    }
2606
2607    #[test]
2608    fn map_region_rejects_overlapping_regions() {
2609        let mut sbox: MultiUseSandbox = {
2610            let path = simple_guest_as_string().unwrap();
2611            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2612            u_sbox.evolve().unwrap()
2613        };
2614
2615        let mem1 = allocate_guest_memory();
2616        let mem2 = allocate_guest_memory();
2617        let guest_base: usize = 0x200000000;
2618        let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
2619
2620        // First mapping should succeed
2621        unsafe { sbox.map_region(&region1).unwrap() };
2622
2623        // Exact same range should fail
2624        let region2 = region_for_memory(&mem2, guest_base, MemoryRegionFlags::READ);
2625        let err = unsafe { sbox.map_region(&region2) }.unwrap_err();
2626        assert!(
2627            format!("{err:?}").contains("Overlapping"),
2628            "Expected Overlapping error, got: {err:?}"
2629        );
2630    }
2631
2632    #[test]
2633    fn map_region_rejects_partial_overlap() {
2634        let mut sbox: MultiUseSandbox = {
2635            let path = simple_guest_as_string().unwrap();
2636            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2637            u_sbox.evolve().unwrap()
2638        };
2639
2640        // Use multi-page regions so partial overlap is geometrically possible
2641        let mem1 = page_aligned_memory(&[0xAA; 8192]); // 2 pages
2642        let mem2 = page_aligned_memory(&[0xBB; 8192]); // 2 pages
2643        let guest_base: usize = 0x200000000;
2644        let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
2645
2646        unsafe { sbox.map_region(&region1).unwrap() };
2647
2648        // region2 starts one page before region1, overlapping by one page
2649        let overlap_base = guest_base - 0x1000;
2650        let region2 = region_for_memory(&mem2, overlap_base, MemoryRegionFlags::READ);
2651        let err = unsafe { sbox.map_region(&region2) }.unwrap_err();
2652        assert!(
2653            format!("{err:?}").contains("verlap"),
2654            "Expected overlap error for partial overlap, got: {err:?}"
2655        );
2656    }
2657
2658    #[test]
2659    fn map_region_allows_adjacent_non_overlapping() {
2660        let mut sbox: MultiUseSandbox = {
2661            let path = simple_guest_as_string().unwrap();
2662            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2663            u_sbox.evolve().unwrap()
2664        };
2665
2666        let mem1 = allocate_guest_memory();
2667        let mem2 = allocate_guest_memory();
2668        let guest_base: usize = 0x200000000;
2669        let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
2670        let region_size = mem1.mem_size();
2671
2672        unsafe { sbox.map_region(&region1).unwrap() };
2673
2674        // Adjacent region (starts right after the first one ends) should succeed
2675        let adjacent_base = guest_base + region_size;
2676        let region2 = region_for_memory(&mem2, adjacent_base, MemoryRegionFlags::READ);
2677        unsafe { sbox.map_region(&region2).unwrap() };
2678    }
2679
2680    #[test]
2681    fn map_region_rejects_overlap_with_snapshot() {
2682        let mut sbox: MultiUseSandbox = {
2683            let path = simple_guest_as_string().unwrap();
2684            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2685            u_sbox.evolve().unwrap()
2686        };
2687
2688        // Try to map at BASE_ADDRESS (0x1000) which overlaps the snapshot region
2689        let mem = allocate_guest_memory();
2690        let region = region_for_memory(
2691            &mem,
2692            crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS,
2693            MemoryRegionFlags::READ,
2694        );
2695        let err = unsafe { sbox.map_region(&region) }.unwrap_err();
2696        assert!(
2697            format!("{err:?}").contains("Overlapping"),
2698            "Expected Overlapping error for snapshot overlap, got: {err:?}"
2699        );
2700    }
2701
2702    #[test]
2703    fn map_region_rejects_overlap_with_scratch() {
2704        let mut sbox: MultiUseSandbox = {
2705            let path = simple_guest_as_string().unwrap();
2706            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2707            u_sbox.evolve().unwrap()
2708        };
2709
2710        // The scratch region occupies the top of the GPA space
2711        let scratch_addr = hyperlight_common::layout::scratch_base_gpa(
2712            crate::sandbox::SandboxConfiguration::DEFAULT_SCRATCH_SIZE,
2713        ) as usize;
2714        let mem = allocate_guest_memory();
2715        let region = region_for_memory(&mem, scratch_addr, MemoryRegionFlags::READ);
2716        let err = unsafe { sbox.map_region(&region) }.unwrap_err();
2717        assert!(
2718            format!("{err:?}").contains("verlap"),
2719            "Expected overlap error for scratch region, got: {err:?}"
2720        );
2721    }
2722
2723    /// Tests for [`MultiUseSandbox::from_snapshot`] in-memory.
2724    mod from_snapshot {
2725        use std::sync::Arc;
2726
2727        use hyperlight_testing::simple_guest_as_string;
2728
2729        use crate::func::Registerable;
2730        use crate::sandbox::SandboxConfiguration;
2731        use crate::sandbox::snapshot::Snapshot;
2732        use crate::{
2733            GuestBinary, HostFunctions, HyperlightError, MultiUseSandbox, UninitializedSandbox,
2734        };
2735
2736        fn make_sandbox() -> MultiUseSandbox {
2737            let path = simple_guest_as_string().unwrap();
2738            UninitializedSandbox::new(GuestBinary::FilePath(path), None)
2739                .unwrap()
2740                .evolve()
2741                .unwrap()
2742        }
2743
2744        /// Sandbox with an extra `Add(i32, i32) -> i32` host function.
2745        fn make_sandbox_with_add() -> MultiUseSandbox {
2746            let path = simple_guest_as_string().unwrap();
2747            let mut u = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2748            u.register_host_function("Add", |a: i32, b: i32| Ok(a + b))
2749                .unwrap();
2750            u.evolve().unwrap()
2751        }
2752
2753        fn host_funcs_with_matching_add() -> HostFunctions {
2754            let mut hf = HostFunctions::default();
2755            hf.register_host_function("Add", |a: i32, b: i32| Ok(a + b))
2756                .unwrap();
2757            hf
2758        }
2759
2760        #[test]
2761        fn round_trip_running_sandbox() {
2762            let mut sbox = make_sandbox();
2763            sbox.call::<i32>("AddToStatic", 11i32).unwrap();
2764            let snapshot = sbox.snapshot().unwrap();
2765            let mut sbox2 =
2766                MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None).unwrap();
2767            assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 11);
2768            let echoed: String = sbox2.call("Echo", "hi".to_string()).unwrap();
2769            assert_eq!(echoed, "hi");
2770        }
2771
2772        #[test]
2773        fn round_trip_pre_init_snapshot() {
2774            let path = simple_guest_as_string().unwrap();
2775            let snap =
2776                Snapshot::from_env(GuestBinary::FilePath(path), SandboxConfiguration::default())
2777                    .unwrap();
2778            let mut sbox =
2779                MultiUseSandbox::from_snapshot(Arc::new(snap), HostFunctions::default(), None)
2780                    .unwrap();
2781            assert_eq!(sbox.call::<i32>("GetStatic", ()).unwrap(), 0);
2782        }
2783
2784        /// Two sandboxes built from clones of one `Arc<Snapshot>` can
2785        /// each `restore` back to it, and stay memory-isolated from
2786        /// each other in between.
2787        #[test]
2788        fn arc_clone_isolation_and_restore_compat() {
2789            let mut sbox = make_sandbox();
2790            sbox.call::<i32>("AddToStatic", 3i32).unwrap();
2791            let snapshot = sbox.snapshot().unwrap();
2792
2793            let mut a =
2794                MultiUseSandbox::from_snapshot(snapshot.clone(), HostFunctions::default(), None)
2795                    .unwrap();
2796            let mut b =
2797                MultiUseSandbox::from_snapshot(snapshot.clone(), HostFunctions::default(), None)
2798                    .unwrap();
2799            assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 3);
2800            assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
2801
2802            a.call::<i32>("AddToStatic", 7i32).unwrap();
2803            assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 10);
2804            assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
2805
2806            a.restore(snapshot.clone()).unwrap();
2807            b.restore(snapshot).unwrap();
2808            assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 3);
2809            assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
2810        }
2811
2812        #[test]
2813        fn accepts_matching_host_functions() {
2814            let mut sbox = make_sandbox_with_add();
2815            sbox.call::<i32>("AddToStatic", 5i32).unwrap();
2816            let snap = sbox.snapshot().unwrap();
2817            let mut sbox2 =
2818                MultiUseSandbox::from_snapshot(snap, host_funcs_with_matching_add(), None).unwrap();
2819            assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 5);
2820        }
2821
2822        #[test]
2823        fn rejects_missing_host_function() {
2824            let mut sbox = make_sandbox_with_add();
2825            let snap = sbox.snapshot().unwrap();
2826            let err = MultiUseSandbox::from_snapshot(snap, HostFunctions::default(), None)
2827                .expect_err("missing `Add` must be rejected");
2828            assert!(
2829                matches!(
2830                    &err,
2831                    HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
2832                        if missing.iter().any(|n| n == "Add") && signature_mismatches.is_empty()
2833                ),
2834                "got: {:?}",
2835                err
2836            );
2837        }
2838
2839        /// `restore` must also reject a snapshot whose required host
2840        /// functions are not a subset of the target sandbox's. This
2841        /// matters across sandboxes: a snapshot taken from a sandbox
2842        /// with `Add` registered cannot be restored into a layout
2843        /// compatible sandbox that lacks `Add`.
2844        #[test]
2845        fn restore_rejects_missing_host_function() {
2846            let mut sbox_with_add = make_sandbox_with_add();
2847            let snap = sbox_with_add.snapshot().unwrap();
2848            let mut sbox_without_add = make_sandbox();
2849            let err = sbox_without_add
2850                .restore(snap)
2851                .expect_err("missing `Add` must be rejected on restore");
2852            assert!(
2853                matches!(
2854                    &err,
2855                    HyperlightError::SnapshotHostFunctionMismatch { missing, .. }
2856                        if missing.iter().any(|n| n == "Add")
2857                ),
2858                "got: {:?}",
2859                err
2860            );
2861        }
2862
2863        /// `restore` rejects a snapshot whose required host function
2864        /// shares a name with the target's but disagrees on signature.
2865        #[test]
2866        fn restore_rejects_signature_mismatch() {
2867            let mut sbox_with_add = make_sandbox_with_add();
2868            let snap = sbox_with_add.snapshot().unwrap();
2869            let path = simple_guest_as_string().unwrap();
2870            let mut u = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2871            u.register_host_function("Add", |a: String, b: String| Ok(format!("{a}{b}")))
2872                .unwrap();
2873            let mut sbox_wrong_add = u.evolve().unwrap();
2874            let err = sbox_wrong_add
2875                .restore(snap)
2876                .expect_err("signature mismatch on `Add` must be rejected on restore");
2877            assert!(
2878                matches!(
2879                    &err,
2880                    HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
2881                        if missing.is_empty() && signature_mismatches.iter().any(|s| s.contains("Add"))
2882                ),
2883                "got: {:?}",
2884                err
2885            );
2886        }
2887
2888        /// Cross-instance `restore` succeeds when the target registers
2889        /// a strict superset of the snapshot's host functions.
2890        #[test]
2891        fn restore_across_sandboxes_with_superset_host_funcs() {
2892            let mut source = make_sandbox_with_add();
2893            source.call::<i32>("AddToStatic", 17i32).unwrap();
2894            let snap = source.snapshot().unwrap();
2895
2896            let path = simple_guest_as_string().unwrap();
2897            let mut u = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2898            u.register_host_function("Add", |a: i32, b: i32| Ok(a + b))
2899                .unwrap();
2900            u.register_host_function("Mul", |a: i32, b: i32| Ok(a * b))
2901                .unwrap();
2902            let mut target = u.evolve().unwrap();
2903
2904            target.restore(snap).unwrap();
2905            assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 17);
2906        }
2907
2908        #[test]
2909        fn rejects_signature_mismatch() {
2910            let mut sbox = make_sandbox_with_add();
2911            let snap = sbox.snapshot().unwrap();
2912            let mut hf = HostFunctions::default();
2913            hf.register_host_function("Add", |a: String, b: String| Ok(format!("{a}{b}")))
2914                .unwrap();
2915            let err = MultiUseSandbox::from_snapshot(snap, hf, None)
2916                .expect_err("signature mismatch on `Add` must be rejected");
2917            assert!(
2918                matches!(
2919                    &err,
2920                    HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
2921                        if missing.is_empty() && signature_mismatches.iter().any(|s| s.contains("Add"))
2922                ),
2923                "got: {:?}",
2924                err
2925            );
2926        }
2927
2928        /// Supplied host-function set may be a strict superset of the
2929        /// snapshot's required set.
2930        #[test]
2931        fn accepts_extra_host_functions() {
2932            let mut sbox = make_sandbox_with_add();
2933            sbox.call::<i32>("AddToStatic", 9i32).unwrap();
2934            let snap = sbox.snapshot().unwrap();
2935            let mut hf = host_funcs_with_matching_add();
2936            hf.register_host_function("Mul", |a: i32, b: i32| Ok(a * b))
2937                .unwrap();
2938            let mut sbox2 = MultiUseSandbox::from_snapshot(snap, hf, None).unwrap();
2939            assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 9);
2940        }
2941
2942        /// A sandbox built via `from_snapshot` can itself be snapshotted
2943        /// and restored, and its snapshots are restore-compatible with it.
2944        #[test]
2945        fn re_snapshot_after_from_snapshot() {
2946            let mut sbox = make_sandbox();
2947            sbox.call::<i32>("AddToStatic", 4i32).unwrap();
2948            let snap1 = sbox.snapshot().unwrap();
2949
2950            let mut sbox2 =
2951                MultiUseSandbox::from_snapshot(snap1, HostFunctions::default(), None).unwrap();
2952            sbox2.call::<i32>("AddToStatic", 6i32).unwrap();
2953            let snap2 = sbox2.snapshot().unwrap();
2954
2955            sbox2.call::<i32>("AddToStatic", 100i32).unwrap();
2956            assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 110);
2957
2958            sbox2.restore(snap2.clone()).unwrap();
2959            assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 10);
2960
2961            let mut sbox3 =
2962                MultiUseSandbox::from_snapshot(snap2, HostFunctions::default(), None).unwrap();
2963            assert_eq!(sbox3.call::<i32>("GetStatic", ()).unwrap(), 10);
2964        }
2965
2966        /// The host function closure supplied to `from_snapshot` (not the
2967        /// original sandbox's closure) is the one invoked at runtime.
2968        #[test]
2969        fn supplied_host_function_is_callable() {
2970            let path = simple_guest_as_string().unwrap();
2971            let mut u = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
2972            u.register_host_function("Echo42", || Ok(1i64)).unwrap();
2973            let mut sbox = u.evolve().unwrap();
2974            let snap = sbox.snapshot().unwrap();
2975
2976            let mut hf = HostFunctions::default();
2977            hf.register_host_function("Echo42", || Ok(42i64)).unwrap();
2978            let mut sbox2 = MultiUseSandbox::from_snapshot(snap, hf, None).unwrap();
2979
2980            let got: i64 = sbox2
2981                .call(
2982                    "CallGivenParamlessHostFuncThatReturnsI64",
2983                    "Echo42".to_string(),
2984                )
2985                .unwrap();
2986            assert_eq!(got, 42);
2987        }
2988
2989        /// Pre-init snapshots record no required host functions, so any
2990        /// `HostFunctions` set is accepted.
2991        #[test]
2992        fn pre_init_snapshot_accepts_arbitrary_host_functions() {
2993            let path = simple_guest_as_string().unwrap();
2994            let snap =
2995                Snapshot::from_env(GuestBinary::FilePath(path), SandboxConfiguration::default())
2996                    .unwrap();
2997            let mut hf = HostFunctions::default();
2998            hf.register_host_function("Unrelated", |a: i32| Ok(a + 1))
2999                .unwrap();
3000            let mut sbox = MultiUseSandbox::from_snapshot(Arc::new(snap), hf, None).unwrap();
3001            assert_eq!(sbox.call::<i32>("GetStatic", ()).unwrap(), 0);
3002        }
3003
3004        /// Snapshots taken from a sandbox built via `from_snapshot`
3005        /// must continue the generation counter of the snapshot they
3006        /// were constructed from, matching `restore`.
3007        #[test]
3008        fn snapshot_generation_propagates() {
3009            let mut sbox = make_sandbox();
3010            sbox.call::<i32>("AddToStatic", 1i32).unwrap();
3011            let snap1 = sbox.snapshot().unwrap();
3012            let gen1 = snap1.snapshot_generation();
3013            sbox.call::<i32>("AddToStatic", 1i32).unwrap();
3014            let snap2 = sbox.snapshot().unwrap();
3015            let gen2 = snap2.snapshot_generation();
3016            assert_eq!(gen2, gen1 + 1);
3017
3018            let mut sbox2 =
3019                MultiUseSandbox::from_snapshot(snap2, HostFunctions::default(), None).unwrap();
3020            sbox2.call::<i32>("AddToStatic", 1i32).unwrap();
3021            let snap3 = sbox2.snapshot().unwrap();
3022            assert_eq!(snap3.snapshot_generation(), gen2 + 1);
3023        }
3024
3025        /// Registering a host function on an already-evolved
3026        /// `MultiUseSandbox` must invalidate its cached snapshot, so
3027        /// that the next `snapshot()` reflects the new required
3028        /// host-function set.
3029        #[test]
3030        fn late_register_invalidates_snapshot_cache() {
3031            let mut sbox = make_sandbox();
3032            // Force a cached snapshot to exist.
3033            let _ = sbox.snapshot().unwrap();
3034
3035            sbox.register_host_function("Echo42", || Ok(42i64)).unwrap();
3036
3037            // The next snapshot must include `Echo42` as a required
3038            // host function, so building a sandbox from it without
3039            // `Echo42` must fail.
3040            let snap = sbox.snapshot().unwrap();
3041            let err = MultiUseSandbox::from_snapshot(snap, HostFunctions::default(), None)
3042                .expect_err("late-registered `Echo42` must be required by the new snapshot");
3043            let msg = format!("{}", err);
3044            assert!(msg.contains("Echo42"), "got: {}", msg);
3045        }
3046    }
3047}