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::collections::HashSet;
18#[cfg(unix)]
19use std::os::fd::AsRawFd;
20#[cfg(unix)]
21use std::os::linux::fs::MetadataExt;
22use std::path::Path;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::{Arc, Mutex};
25
26use flatbuffers::FlatBufferBuilder;
27use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
28use hyperlight_common::flatbuffer_wrappers::function_types::{
29    ParameterValue, ReturnType, ReturnValue,
30};
31use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
32use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity;
33use tracing::{Span, instrument};
34
35use super::Callable;
36use super::host_funcs::FunctionRegistry;
37use super::snapshot::Snapshot;
38use crate::HyperlightError::{self, SnapshotSandboxMismatch};
39use crate::func::{ParameterTuple, SupportedReturnType};
40use crate::hypervisor::InterruptHandle;
41use crate::hypervisor::hyperlight_vm::HyperlightVm;
42#[cfg(unix)]
43use crate::mem::memory_region::MemoryRegionType;
44use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
45use crate::mem::mgr::SandboxMemoryManager;
46use crate::mem::ptr::RawPtr;
47use crate::mem::shared_mem::HostSharedMemory;
48use crate::metrics::{
49    METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
50};
51use crate::{Result, log_then_return};
52
53/// Global counter for assigning unique IDs to sandboxes
54static SANDBOX_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
55
56/// A fully initialized sandbox that can execute guest functions multiple times.
57///
58/// Guest functions can be called repeatedly while maintaining state between calls.
59/// The sandbox supports creating snapshots and restoring to previous states.
60///
61/// ## Sandbox Poisoning
62///
63/// The sandbox becomes **poisoned** when the guest is not run to completion, leaving it in
64/// an inconsistent state that could compromise memory safety, data integrity, or security.
65///
66/// ### When Does Poisoning Occur?
67///
68/// Poisoning happens when guest execution is interrupted before normal completion:
69///
70/// - **Guest panics or aborts** - When a guest function panics, crashes, or calls `abort()`,
71///   the normal cleanup and unwinding process is interrupted
72/// - **Invalid memory access** - Attempts to read/write/execute memory outside allowed regions
73/// - **Stack overflow** - Guest exhausts its stack space during execution
74/// - **Heap exhaustion** - Guest runs out of heap memory
75/// - **Host-initiated cancellation** - Calling [`InterruptHandle::kill()`] to forcefully
76///   terminate an in-progress guest function
77///
78/// ### Why This Is Unsafe
79///
80/// When guest execution doesn't complete normally, critical cleanup operations are skipped:
81///
82/// - **Memory leaks** - Heap allocations remain unreachable as the call stack is unwound
83/// - **Corrupted allocator state** - Memory allocator metadata (free lists, heap headers)
84///   left inconsistent
85/// - **Locked resources** - Mutexes or other synchronization primitives remain locked
86/// - **Partial state updates** - Data structures left half-modified (corrupted linked lists,
87///   inconsistent hash tables, etc.)
88///
89/// ### Recovery
90///
91/// Use [`restore()`](Self::restore) with a snapshot taken before poisoning occurred.
92/// This is the **only safe way** to recover - it completely replaces all memory state,
93/// eliminating any inconsistencies. See [`restore()`](Self::restore) for details.
94pub struct MultiUseSandbox {
95    /// Unique identifier for this sandbox instance
96    id: u64,
97    /// Whether this sandbox is poisoned
98    poisoned: bool,
99    pub(super) host_funcs: Arc<Mutex<FunctionRegistry>>,
100    pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
101    vm: HyperlightVm,
102    dispatch_ptr: RawPtr,
103    #[cfg(gdb)]
104    dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
105    /// If the current state of the sandbox has been captured in a snapshot,
106    /// that snapshot is stored here.
107    snapshot: Option<Snapshot>,
108}
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        dispatch_ptr: RawPtr,
122        #[cfg(gdb)] dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
123    ) -> MultiUseSandbox {
124        Self {
125            id: SANDBOX_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
126            poisoned: false,
127            host_funcs,
128            mem_mgr: mgr,
129            vm,
130            dispatch_ptr,
131            #[cfg(gdb)]
132            dbg_mem_access_fn,
133            snapshot: None,
134        }
135    }
136
137    /// Creates a snapshot of the sandbox's current memory state.
138    ///
139    /// The snapshot is tied to this specific sandbox instance and can only be
140    /// restored to the same sandbox it was created from.
141    ///
142    /// ## Poisoned Sandbox
143    ///
144    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
145    /// is currently poisoned. Snapshots can only be taken from non-poisoned sandboxes.
146    ///
147    /// # Examples
148    ///
149    /// ```no_run
150    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
151    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
152    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
153    ///     GuestBinary::FilePath("guest.bin".into()),
154    ///     None
155    /// )?.evolve()?;
156    ///
157    /// // Modify sandbox state
158    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 42)?;
159    ///
160    /// // Create snapshot belonging to this sandbox
161    /// let snapshot = sandbox.snapshot()?;
162    /// # Ok(())
163    /// # }
164    /// ```
165    #[instrument(err(Debug), skip_all, parent = Span::current())]
166    pub fn snapshot(&mut self) -> Result<Snapshot> {
167        if self.poisoned {
168            return Err(crate::HyperlightError::PoisonedSandbox);
169        }
170
171        if let Some(snapshot) = &self.snapshot {
172            return Ok(snapshot.clone());
173        }
174        let mapped_regions_iter = self.vm.get_mapped_regions();
175        let mapped_regions_vec: Vec<MemoryRegion> = mapped_regions_iter.cloned().collect();
176        let memory_snapshot = self.mem_mgr.snapshot(self.id, mapped_regions_vec)?;
177        let inner = Arc::new(memory_snapshot);
178        let snapshot = Snapshot { inner };
179        self.snapshot = Some(snapshot.clone());
180        Ok(snapshot)
181    }
182
183    /// Restores the sandbox's memory to a previously captured snapshot state.
184    ///
185    /// The snapshot must have been created from this same sandbox instance.
186    /// Attempting to restore a snapshot from a different sandbox will return
187    /// a [`SnapshotSandboxMismatch`](crate::HyperlightError::SnapshotSandboxMismatch) error.
188    ///
189    /// ## Poison State Recovery
190    ///
191    /// This method automatically clears any poison state when successful. This is safe because:
192    /// - Snapshots can only be taken from non-poisoned sandboxes
193    /// - Restoration completely replaces all memory state, eliminating any inconsistencies
194    ///   caused by incomplete guest execution
195    ///
196    /// ### What Gets Fixed During Restore
197    ///
198    /// When a poisoned sandbox is restored, the memory state is completely reset:
199    /// - **Leaked heap memory** - All allocations from interrupted execution are discarded
200    /// - **Corrupted allocator metadata** - Free lists and heap headers restored to consistent state
201    /// - **Locked mutexes** - All lock state is reset
202    /// - **Partial updates** - Data structures restored to their pre-execution state
203    ///
204    ///
205    /// # Examples
206    ///
207    /// ```no_run
208    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
209    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
210    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
211    ///     GuestBinary::FilePath("guest.bin".into()),
212    ///     None
213    /// )?.evolve()?;
214    ///
215    /// // Take initial snapshot from this sandbox
216    /// let snapshot = sandbox.snapshot()?;
217    ///
218    /// // Modify sandbox state
219    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 100)?;
220    /// let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
221    /// assert_eq!(value, 100);
222    ///
223    /// // Restore to previous state (same sandbox)
224    /// sandbox.restore(&snapshot)?;
225    /// let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
226    /// assert_eq!(restored_value, 0); // Back to initial state
227    /// # Ok(())
228    /// # }
229    /// ```
230    ///
231    /// ## Recovering from Poison
232    ///
233    /// ```no_run
234    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary, HyperlightError};
235    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
236    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
237    ///     GuestBinary::FilePath("guest.bin".into()),
238    ///     None
239    /// )?.evolve()?;
240    ///
241    /// // Take snapshot before potentially poisoning operation
242    /// let snapshot = sandbox.snapshot()?;
243    ///
244    /// // This might poison the sandbox (guest not run to completion)
245    /// let result = sandbox.call::<()>("guest_panic", ());
246    /// if result.is_err() {
247    ///     if sandbox.poisoned() {
248    ///         // Restore from snapshot to clear poison
249    ///         sandbox.restore(&snapshot)?;
250    ///         assert!(!sandbox.poisoned());
251    ///         
252    ///         // Sandbox is now usable again
253    ///         sandbox.call::<String>("Echo", "hello".to_string())?;
254    ///     }
255    /// }
256    /// # Ok(())
257    /// # }
258    /// ```
259    #[instrument(err(Debug), skip_all, parent = Span::current())]
260    pub fn restore(&mut self, snapshot: &Snapshot) -> Result<()> {
261        if let Some(snap) = &self.snapshot
262            && Arc::ptr_eq(&snap.inner, &snapshot.inner)
263        {
264            // If the snapshot is already the current one, no need to restore
265            return Ok(());
266        }
267
268        if self.id != snapshot.inner.sandbox_id() {
269            return Err(SnapshotSandboxMismatch);
270        }
271
272        self.mem_mgr.restore_snapshot(&snapshot.inner)?;
273
274        let current_regions: HashSet<_> = self.vm.get_mapped_regions().cloned().collect();
275        let snapshot_regions: HashSet<_> = snapshot.inner.regions().iter().cloned().collect();
276
277        let regions_to_unmap = current_regions.difference(&snapshot_regions);
278        let regions_to_map = snapshot_regions.difference(&current_regions);
279
280        for region in regions_to_unmap {
281            self.vm.unmap_region(region)?;
282        }
283
284        for region in regions_to_map {
285            // Safety: The region has been mapped before, and at that point the caller promised that the memory region is valid
286            // in their call to `MultiUseSandbox::map_region`
287            unsafe { self.vm.map_region(region)? };
288        }
289
290        // The restored snapshot is now our most current snapshot
291        self.snapshot = Some(snapshot.clone());
292
293        // Clear poison state when successfully restoring from snapshot.
294        //
295        // # Safety:
296        // This is safe because:
297        // 1. Snapshots can only be taken from non-poisoned sandboxes (verified at snapshot creation)
298        // 2. Restoration completely replaces all memory state, eliminating:
299        //    - All leaked heap allocations (memory is restored to snapshot state)
300        //    - All corrupted data structures (overwritten with consistent snapshot data)
301        //    - All inconsistent global state (reset to snapshot values)
302        self.poisoned = false;
303
304        Ok(())
305    }
306
307    /// Calls a guest function by name with the specified arguments.
308    ///
309    /// Changes made to the sandbox during execution are *not* persisted.
310    ///
311    /// ## Poisoned Sandbox
312    ///
313    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
314    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
315    ///
316    /// # Examples
317    ///
318    /// ```no_run
319    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
320    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
321    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
322    ///     GuestBinary::FilePath("guest.bin".into()),
323    ///     None
324    /// )?.evolve()?;
325    ///
326    /// // Call function with no arguments
327    /// let result: i32 = sandbox.call_guest_function_by_name("GetCounter", ())?;
328    ///
329    /// // Call function with single argument
330    /// let doubled: i32 = sandbox.call_guest_function_by_name("Double", 21)?;
331    /// assert_eq!(doubled, 42);
332    ///
333    /// // Call function with multiple arguments
334    /// let sum: i32 = sandbox.call_guest_function_by_name("Add", (10, 32))?;
335    /// assert_eq!(sum, 42);
336    ///
337    /// // Call function returning string
338    /// let message: String = sandbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?;
339    /// assert_eq!(message, "Hello, World!");
340    /// # Ok(())
341    /// # }
342    /// ```
343    #[doc(hidden)]
344    #[deprecated(
345        since = "0.8.0",
346        note = "Deprecated in favour of call and snapshot/restore."
347    )]
348    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
349    pub fn call_guest_function_by_name<Output: SupportedReturnType>(
350        &mut self,
351        func_name: &str,
352        args: impl ParameterTuple,
353    ) -> Result<Output> {
354        if self.poisoned {
355            return Err(crate::HyperlightError::PoisonedSandbox);
356        }
357        let snapshot = self.snapshot()?;
358        let res = self.call(func_name, args);
359        self.restore(&snapshot)?;
360        res
361    }
362
363    /// Calls a guest function by name with the specified arguments.
364    ///
365    /// Changes made to the sandbox during execution are persisted.
366    ///
367    /// ## Poisoned Sandbox
368    ///
369    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
370    /// is already poisoned before the call. Use [`restore()`](Self::restore) to recover from
371    /// a poisoned state.
372    ///
373    /// ## Sandbox Poisoning
374    ///
375    /// If this method returns an error, the sandbox may be poisoned if the guest was not run
376    /// to completion (due to panic, abort, memory violation, stack/heap exhaustion, or forced
377    /// termination). Use [`poisoned()`](Self::poisoned) to check the poison state and
378    /// [`restore()`](Self::restore) to recover if needed.
379    ///
380    /// If this method returns `Ok`, the sandbox is guaranteed to **not** be poisoned - the guest
381    /// function completed successfully and the sandbox state is consistent.
382    ///
383    /// # Examples
384    ///
385    /// ```no_run
386    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
387    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
388    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
389    ///     GuestBinary::FilePath("guest.bin".into()),
390    ///     None
391    /// )?.evolve()?;
392    ///
393    /// // Call function with no arguments
394    /// let result: i32 = sandbox.call("GetCounter", ())?;
395    ///
396    /// // Call function with single argument
397    /// let doubled: i32 = sandbox.call("Double", 21)?;
398    /// assert_eq!(doubled, 42);
399    ///
400    /// // Call function with multiple arguments
401    /// let sum: i32 = sandbox.call("Add", (10, 32))?;
402    /// assert_eq!(sum, 42);
403    ///
404    /// // Call function returning string
405    /// let message: String = sandbox.call("Echo", "Hello, World!".to_string())?;
406    /// assert_eq!(message, "Hello, World!");
407    /// # Ok(())
408    /// # }
409    /// ```
410    ///
411    /// ## Handling Potential Poisoning
412    ///
413    /// ```no_run
414    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
415    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
416    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
417    ///     GuestBinary::FilePath("guest.bin".into()),
418    ///     None
419    /// )?.evolve()?;
420    ///
421    /// // Take snapshot before risky operation
422    /// let snapshot = sandbox.snapshot()?;
423    ///
424    /// // Call potentially unsafe guest function
425    /// let result = sandbox.call::<String>("RiskyOperation", "input".to_string());
426    ///
427    /// // Check if the call failed and poisoned the sandbox
428    /// if let Err(e) = result {
429    ///     eprintln!("Guest function failed: {}", e);
430    ///     
431    ///     if sandbox.poisoned() {
432    ///         eprintln!("Sandbox was poisoned, restoring from snapshot");
433    ///         sandbox.restore(&snapshot)?;
434    ///     }
435    /// }
436    /// # Ok(())
437    /// # }
438    /// ```
439    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
440    pub fn call<Output: SupportedReturnType>(
441        &mut self,
442        func_name: &str,
443        args: impl ParameterTuple,
444    ) -> Result<Output> {
445        if self.poisoned {
446            return Err(crate::HyperlightError::PoisonedSandbox);
447        }
448        // Reset snapshot since we are mutating the sandbox state
449        self.snapshot = None;
450        maybe_time_and_emit_guest_call(func_name, || {
451            let ret = self.call_guest_function_by_name_no_reset(
452                func_name,
453                Output::TYPE,
454                args.into_value(),
455            );
456            // Use the ? operator to allow converting any hyperlight_common::func::Error
457            // returned by from_value into a HyperlightError
458            let ret = Output::from_value(ret?)?;
459            Ok(ret)
460        })
461    }
462
463    /// Maps a region of host memory into the sandbox address space.
464    ///
465    /// The base address and length must meet platform alignment requirements
466    /// (typically page-aligned). The `region_type` field is ignored as guest
467    /// page table entries are not created.
468    ///
469    /// ## Poisoned Sandbox
470    ///
471    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
472    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
473    ///
474    /// # Safety
475    ///
476    /// The caller must ensure the host memory region remains valid and unmodified
477    /// for the lifetime of `self`.
478    #[instrument(err(Debug), skip(self, rgn), parent = Span::current())]
479    pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> {
480        if self.poisoned {
481            return Err(crate::HyperlightError::PoisonedSandbox);
482        }
483        if rgn.flags.contains(MemoryRegionFlags::STACK_GUARD) {
484            // Stack guard pages are an internal implementation detail
485            // (which really should be moved into the guest)
486            log_then_return!("Cannot map host memory as a stack guard page");
487        }
488        if rgn.flags.contains(MemoryRegionFlags::WRITE) {
489            // TODO: Implement support for writable mappings, which
490            // need to be registered with the memory manager so that
491            // writes can be rolled back when necessary.
492            log_then_return!("TODO: Writable mappings not yet supported");
493        }
494        // Reset snapshot since we are mutating the sandbox state
495        self.snapshot = None;
496        unsafe { self.vm.map_region(rgn) }?;
497        self.mem_mgr.mapped_rgns += 1;
498        Ok(())
499    }
500
501    /// Map the contents of a file into the guest at a particular address
502    ///
503    /// Returns the length of the mapping in bytes.
504    ///
505    /// ## Poisoned Sandbox
506    ///
507    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
508    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
509    #[instrument(err(Debug), skip(self, _fp, _guest_base), parent = Span::current())]
510    pub fn map_file_cow(&mut self, _fp: &Path, _guest_base: u64) -> Result<u64> {
511        if self.poisoned {
512            return Err(crate::HyperlightError::PoisonedSandbox);
513        }
514        #[cfg(windows)]
515        log_then_return!("mmap'ing a file into the guest is not yet supported on Windows");
516        #[cfg(unix)]
517        unsafe {
518            let file = std::fs::File::options().read(true).write(true).open(_fp)?;
519            let file_size = file.metadata()?.st_size();
520            let page_size = page_size::get();
521            let size = (file_size as usize).div_ceil(page_size) * page_size;
522            let base = libc::mmap(
523                std::ptr::null_mut(),
524                size,
525                libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
526                libc::MAP_PRIVATE,
527                file.as_raw_fd(),
528                0,
529            );
530            if base == libc::MAP_FAILED {
531                log_then_return!("mmap error: {:?}", std::io::Error::last_os_error());
532            }
533
534            if let Err(err) = self.map_region(&MemoryRegion {
535                host_region: base as usize..base.wrapping_add(size) as usize,
536                guest_region: _guest_base as usize.._guest_base as usize + size,
537                flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
538                region_type: MemoryRegionType::Heap,
539            }) {
540                libc::munmap(base, size);
541                return Err(err);
542            };
543
544            Ok(size as u64)
545        }
546    }
547
548    /// Calls a guest function with type-erased parameters and return values.
549    ///
550    /// This function is used for fuzz testing parameter and return type handling.
551    ///
552    /// ## Poisoned Sandbox
553    ///
554    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
555    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
556    #[cfg(feature = "fuzzing")]
557    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
558    pub fn call_type_erased_guest_function_by_name(
559        &mut self,
560        func_name: &str,
561        ret_type: ReturnType,
562        args: Vec<ParameterValue>,
563    ) -> Result<ReturnValue> {
564        if self.poisoned {
565            return Err(crate::HyperlightError::PoisonedSandbox);
566        }
567        // Reset snapshot since we are mutating the sandbox state
568        self.snapshot = None;
569        maybe_time_and_emit_guest_call(func_name, || {
570            self.call_guest_function_by_name_no_reset(func_name, ret_type, args)
571        })
572    }
573
574    fn call_guest_function_by_name_no_reset(
575        &mut self,
576        function_name: &str,
577        return_type: ReturnType,
578        args: Vec<ParameterValue>,
579    ) -> Result<ReturnValue> {
580        if self.poisoned {
581            return Err(crate::HyperlightError::PoisonedSandbox);
582        }
583        // ===== KILL() TIMING POINT 1 =====
584        // Clear any stale cancellation from a previous guest function call or if kill() was called too early.
585        // Any kill() that completed (even partially) BEFORE this line has NO effect on this call.
586        self.vm.clear_cancel();
587
588        let res = (|| {
589            let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args);
590
591            let fc = FunctionCall::new(
592                function_name.to_string(),
593                Some(args),
594                FunctionCallType::Guest,
595                return_type,
596            );
597
598            let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
599            let buffer = fc.encode(&mut builder);
600
601            self.mem_mgr.write_guest_function_call(buffer)?;
602
603            self.vm.dispatch_call_from_host(
604                self.dispatch_ptr.clone(),
605                &mut self.mem_mgr,
606                &self.host_funcs,
607                #[cfg(gdb)]
608                self.dbg_mem_access_fn.clone(),
609            )?;
610
611            self.mem_mgr.check_stack_guard()?;
612
613            let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner();
614
615            match guest_result {
616                Ok(val) => Ok(val),
617                Err(guest_error) => {
618                    metrics::counter!(
619                        METRIC_GUEST_ERROR,
620                        METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string()
621                    )
622                    .increment(1);
623
624                    Err(match guest_error.code {
625                        ErrorCode::StackOverflow => HyperlightError::StackOverflow(),
626                        _ => HyperlightError::GuestError(guest_error.code, guest_error.message),
627                    })
628                }
629            }
630        })();
631
632        // In the happy path we do not need to clear io-buffers from the host because:
633        // - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::<FunctionCall>()`
634        // - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result`
635        // - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call`
636        // - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value`
637        if let Err(e) = &res {
638            self.mem_mgr.clear_io_buffers();
639
640            // Determine if we should poison the sandbox.
641            self.poisoned |= e.is_poison_error();
642        }
643
644        // Note: clear_call_active() is automatically called when _guard is dropped here
645
646        res
647    }
648
649    /// Returns a handle for interrupting guest execution.
650    ///
651    /// # Examples
652    ///
653    /// ```no_run
654    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
655    /// # use std::thread;
656    /// # use std::time::Duration;
657    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
658    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
659    ///     GuestBinary::FilePath("guest.bin".into()),
660    ///     None
661    /// )?.evolve()?;
662    ///
663    /// // Get interrupt handle before starting long-running operation
664    /// let interrupt_handle = sandbox.interrupt_handle();
665    ///
666    /// // Spawn thread to interrupt after timeout
667    /// let handle_clone = interrupt_handle.clone();
668    /// thread::spawn(move || {
669    ///     thread::sleep(Duration::from_secs(5));
670    ///     handle_clone.kill();
671    /// });
672    ///
673    /// // This call may be interrupted by the spawned thread
674    /// let result = sandbox.call_guest_function_by_name::<i32>("LongRunningFunction", ());
675    /// # Ok(())
676    /// # }
677    /// ```
678    pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
679        self.vm.interrupt_handle()
680    }
681
682    /// Generate a crash dump of the current state of the VM underlying this sandbox.
683    ///
684    /// Creates an ELF core dump file that can be used for debugging. The dump
685    /// captures the current state of the sandbox including registers, memory regions,
686    /// and other execution context.
687    ///
688    /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
689    /// environment variable. If not set, it defaults to the system's temporary directory.
690    ///
691    /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
692    /// is also configured to allow core dumps (which is the default behavior).
693    ///
694    /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
695    /// guest that dont cause crashes (e.g. a guest function that does not return)
696    ///
697    /// # Examples
698    ///
699    /// Attach to your running process with gdb and call this function:
700    ///
701    /// ```shell
702    /// sudo gdb -p <pid_of_your_process>
703    /// (gdb) info threads
704    /// # find the thread that is running the guest function you want to debug
705    /// (gdb) thread <thread_number>
706    /// # switch to the frame where you have access to your MultiUseSandbox instance
707    /// (gdb) backtrace
708    /// (gdb) frame <frame_number>
709    /// # get the pointer to your MultiUseSandbox instance
710    /// # Get the sandbox pointer
711    /// (gdb) print sandbox
712    /// # Call the crashdump function
713    /// call sandbox.generate_crashdump()
714    /// ```
715    /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
716    ///
717    #[cfg(crashdump)]
718    #[instrument(err(Debug), skip_all, parent = Span::current())]
719    pub fn generate_crashdump(&self) -> Result<()> {
720        crate::hypervisor::crashdump::generate_crashdump(&self.vm)
721    }
722
723    /// Returns whether the sandbox is currently poisoned.
724    ///
725    /// A poisoned sandbox is in an inconsistent state due to the guest not running to completion.
726    /// All operations will be rejected until the sandbox is restored from a non-poisoned snapshot.
727    ///
728    /// ## Causes of Poisoning
729    ///
730    /// The sandbox becomes poisoned when guest execution is interrupted:
731    /// - **Panics/Aborts** - Guest code panics or calls `abort()`
732    /// - **Invalid Memory Access** - Read/write/execute violations  
733    /// - **Stack Overflow** - Guest exhausts stack space
734    /// - **Heap Exhaustion** - Guest runs out of heap memory
735    /// - **Forced Termination** - [`InterruptHandle::kill()`] called during execution
736    ///
737    /// ## Recovery
738    ///
739    /// To clear the poison state, use [`restore()`](Self::restore) with a snapshot
740    /// that was taken before the sandbox became poisoned.
741    ///
742    /// # Examples
743    ///
744    /// ```no_run
745    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
746    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
747    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
748    ///     GuestBinary::FilePath("guest.bin".into()),
749    ///     None
750    /// )?.evolve()?;
751    ///
752    /// // Check if sandbox is poisoned
753    /// if sandbox.poisoned() {
754    ///     println!("Sandbox is poisoned and needs attention");
755    /// }
756    /// # Ok(())
757    /// # }
758    /// ```
759    pub fn poisoned(&self) -> bool {
760        self.poisoned
761    }
762}
763
764impl Callable for MultiUseSandbox {
765    fn call<Output: SupportedReturnType>(
766        &mut self,
767        func_name: &str,
768        args: impl ParameterTuple,
769    ) -> Result<Output> {
770        if self.poisoned {
771            return Err(crate::HyperlightError::PoisonedSandbox);
772        }
773        self.call(func_name, args)
774    }
775}
776
777impl std::fmt::Debug for MultiUseSandbox {
778    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
779        f.debug_struct("MultiUseSandbox")
780            .field("stack_guard", &self.mem_mgr.get_stack_cookie())
781            .finish()
782    }
783}
784
785#[cfg(test)]
786mod tests {
787    use std::sync::{Arc, Barrier};
788    use std::thread;
789
790    use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
791    use hyperlight_testing::simple_guest_as_string;
792
793    #[cfg(target_os = "linux")]
794    use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
795    #[cfg(target_os = "linux")]
796    use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
797    use crate::sandbox::SandboxConfiguration;
798    use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox};
799
800    #[test]
801    fn poison() {
802        let mut sbox: MultiUseSandbox = {
803            let path = simple_guest_as_string().unwrap();
804            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
805            u_sbox.evolve()
806        }
807        .unwrap();
808        let snapshot = sbox.snapshot().unwrap();
809
810        // poison on purpose
811        let res = sbox
812            .call::<()>("guest_panic", "hello".to_string())
813            .unwrap_err();
814        assert!(
815            matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
816        );
817        assert!(sbox.poisoned());
818
819        // guest calls should fail when poisoned
820        let res = sbox
821            .call::<()>("guest_panic", "hello2".to_string())
822            .unwrap_err();
823        assert!(matches!(res, HyperlightError::PoisonedSandbox));
824
825        // snapshot should fail when poisoned
826        if let Err(e) = sbox.snapshot() {
827            assert!(sbox.poisoned());
828            assert!(matches!(e, HyperlightError::PoisonedSandbox));
829        } else {
830            panic!("Snapshot should fail");
831        }
832
833        // map_region should fail when poisoned
834        #[cfg(target_os = "linux")]
835        {
836            let map_mem = allocate_guest_memory();
837            let guest_base = 0x0;
838            let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
839            let res = unsafe { sbox.map_region(&region) }.unwrap_err();
840            assert!(matches!(res, HyperlightError::PoisonedSandbox));
841        }
842
843        // map_file_cow should fail when poisoned
844        #[cfg(target_os = "linux")]
845        {
846            let temp_file = std::env::temp_dir().join("test_poison_map_file.bin");
847            let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err();
848            assert!(matches!(res, HyperlightError::PoisonedSandbox));
849            std::fs::remove_file(&temp_file).ok(); // Clean up
850        }
851
852        // call_guest_function_by_name (deprecated) should fail when poisoned
853        #[allow(deprecated)]
854        let res = sbox
855            .call_guest_function_by_name::<String>("Echo", "test".to_string())
856            .unwrap_err();
857        assert!(matches!(res, HyperlightError::PoisonedSandbox));
858
859        // restore to non-poisoned snapshot should work and clear poison
860        sbox.restore(&snapshot).unwrap();
861        assert!(!sbox.poisoned());
862
863        // guest calls should work again after restore
864        let res = sbox.call::<String>("Echo", "hello2".to_string()).unwrap();
865        assert_eq!(res, "hello2".to_string());
866        assert!(!sbox.poisoned());
867
868        // re-poison on purpose
869        let res = sbox
870            .call::<()>("guest_panic", "hello".to_string())
871            .unwrap_err();
872        assert!(
873            matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
874        );
875        assert!(sbox.poisoned());
876
877        // restore to non-poisoned snapshot should work again
878        sbox.restore(&snapshot).unwrap();
879        assert!(!sbox.poisoned());
880
881        // guest calls should work again
882        let res = sbox.call::<String>("Echo", "hello3".to_string()).unwrap();
883        assert_eq!(res, "hello3".to_string());
884        assert!(!sbox.poisoned());
885
886        // snapshot should work again
887        let _ = sbox.snapshot().unwrap();
888    }
889
890    /// Make sure input/output buffers are properly reset after guest call (with host call)
891    #[test]
892    fn host_func_error() {
893        let path = simple_guest_as_string().unwrap();
894        let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
895        sandbox
896            .register("HostError", || -> Result<()> {
897                Err(HyperlightError::Error("hi".to_string()))
898            })
899            .unwrap();
900        let mut sandbox = sandbox.evolve().unwrap();
901
902        // will exhaust io if leaky
903        for _ in 0..1000 {
904            let result = sandbox
905                .call::<i64>(
906                    "CallGivenParamlessHostFuncThatReturnsI64",
907                    "HostError".to_string(),
908                )
909                .unwrap_err();
910
911            assert!(
912                matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"),
913            );
914        }
915    }
916
917    #[test]
918    fn call_host_func_expect_error() {
919        let path = simple_guest_as_string().unwrap();
920        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
921        let mut sandbox = sandbox.evolve().unwrap();
922        sandbox
923            .call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string())
924            .unwrap();
925    }
926
927    /// Make sure input/output buffers are properly reset after guest call (with host call)
928    #[test]
929    fn io_buffer_reset() {
930        let mut cfg = SandboxConfiguration::default();
931        cfg.set_input_data_size(4096);
932        cfg.set_output_data_size(4096);
933        let path = simple_guest_as_string().unwrap();
934        let mut sandbox =
935            UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
936        sandbox.register("HostAdd", |a: i32, b: i32| a + b).unwrap();
937        let mut sandbox = sandbox.evolve().unwrap();
938
939        // will exhaust io if leaky. Tests both success and error paths
940        for _ in 0..1000 {
941            let result = sandbox.call::<i32>("Add", (5i32, 10i32)).unwrap();
942            assert_eq!(result, 15);
943            let result = sandbox.call::<i32>("AddToStaticAndFail", ()).unwrap_err();
944            assert!(
945                matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose")
946            );
947        }
948    }
949
950    /// Tests that call_guest_function_by_name restores the state correctly
951    #[test]
952    fn test_call_guest_function_by_name() {
953        let mut sbox: MultiUseSandbox = {
954            let path = simple_guest_as_string().unwrap();
955            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
956            u_sbox.evolve()
957        }
958        .unwrap();
959
960        let snapshot = sbox.snapshot().unwrap();
961
962        let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
963        let res: i32 = sbox.call("GetStatic", ()).unwrap();
964        assert_eq!(res, 5);
965
966        sbox.restore(&snapshot).unwrap();
967        #[allow(deprecated)]
968        let _ = sbox
969            .call_guest_function_by_name::<i32>("AddToStatic", 5i32)
970            .unwrap();
971        #[allow(deprecated)]
972        let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap();
973        assert_eq!(res, 0);
974    }
975
976    // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (1K) and heap(14K).
977    // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
978    #[test]
979    fn test_with_small_stack_and_heap() {
980        let mut cfg = SandboxConfiguration::default();
981        cfg.set_heap_size(20 * 1024);
982        cfg.set_stack_size(18 * 1024);
983
984        let mut sbox1: MultiUseSandbox = {
985            let path = simple_guest_as_string().unwrap();
986            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
987            u_sbox.evolve()
988        }
989        .unwrap();
990
991        for _ in 0..1000 {
992            sbox1.call::<String>("Echo", "hello".to_string()).unwrap();
993        }
994
995        let mut sbox2: MultiUseSandbox = {
996            let path = simple_guest_as_string().unwrap();
997            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
998            u_sbox.evolve()
999        }
1000        .unwrap();
1001
1002        for i in 0..1000 {
1003            sbox2
1004                .call::<i32>(
1005                    "PrintUsingPrintf",
1006                    format!("Hello World {}\n", i).to_string(),
1007                )
1008                .unwrap();
1009        }
1010    }
1011
1012    /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
1013    /// and restoring a snapshot from before evolving restores the previous state
1014    #[test]
1015    fn snapshot_evolve_restore_handles_state_correctly() {
1016        let mut sbox: MultiUseSandbox = {
1017            let path = simple_guest_as_string().unwrap();
1018            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1019            u_sbox.evolve()
1020        }
1021        .unwrap();
1022
1023        let snapshot = sbox.snapshot().unwrap();
1024
1025        let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1026
1027        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1028        assert_eq!(res, 5);
1029
1030        sbox.restore(&snapshot).unwrap();
1031        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1032        assert_eq!(res, 0);
1033    }
1034
1035    #[test]
1036    fn test_trigger_exception_on_guest() {
1037        let usbox = UninitializedSandbox::new(
1038            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1039            None,
1040        )
1041        .unwrap();
1042
1043        let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap();
1044
1045        let res: Result<()> = multi_use_sandbox.call("TriggerException", ());
1046
1047        assert!(res.is_err());
1048
1049        match res.unwrap_err() {
1050            HyperlightError::GuestAborted(_, msg) => {
1051                // msg should indicate we got an invalid opcode exception
1052                assert!(msg.contains("InvalidOpcode"));
1053            }
1054            e => panic!(
1055                "Expected HyperlightError::GuestExecutionError but got {:?}",
1056                e
1057            ),
1058        }
1059    }
1060
1061    #[test]
1062    #[ignore] // this test runs by itself because it uses a lot of system resources
1063    fn create_1000_sandboxes() {
1064        let barrier = Arc::new(Barrier::new(21));
1065
1066        let mut handles = vec![];
1067
1068        for _ in 0..20 {
1069            let c = barrier.clone();
1070
1071            let handle = thread::spawn(move || {
1072                c.wait();
1073
1074                for _ in 0..50 {
1075                    let usbox = UninitializedSandbox::new(
1076                        GuestBinary::FilePath(
1077                            simple_guest_as_string().expect("Guest Binary Missing"),
1078                        ),
1079                        None,
1080                    )
1081                    .unwrap();
1082
1083                    let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap();
1084
1085                    let res: i32 = multi_use_sandbox.call("GetStatic", ()).unwrap();
1086
1087                    assert_eq!(res, 0);
1088                }
1089            });
1090
1091            handles.push(handle);
1092        }
1093
1094        barrier.wait();
1095
1096        for handle in handles {
1097            handle.join().unwrap();
1098        }
1099    }
1100
1101    #[cfg(target_os = "linux")]
1102    #[test]
1103    fn test_mmap() {
1104        let mut sbox = UninitializedSandbox::new(
1105            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1106            None,
1107        )
1108        .unwrap()
1109        .evolve()
1110        .unwrap();
1111
1112        let expected = b"hello world";
1113        let map_mem = page_aligned_memory(expected);
1114        let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1115
1116        unsafe {
1117            sbox.map_region(&region_for_memory(
1118                &map_mem,
1119                guest_base,
1120                MemoryRegionFlags::READ,
1121            ))
1122            .unwrap();
1123        }
1124
1125        let _guard = map_mem.lock.try_read().unwrap();
1126        let actual: Vec<u8> = sbox
1127            .call(
1128                "ReadMappedBuffer",
1129                (guest_base as u64, expected.len() as u64),
1130            )
1131            .unwrap();
1132
1133        assert_eq!(actual, expected);
1134    }
1135
1136    // Makes sure MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE executable but not writable
1137    #[cfg(target_os = "linux")]
1138    #[test]
1139    fn test_mmap_write_exec() {
1140        let mut sbox = UninitializedSandbox::new(
1141            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1142            None,
1143        )
1144        .unwrap()
1145        .evolve()
1146        .unwrap();
1147
1148        let expected = &[0x90, 0x90, 0x90, 0xC3]; // NOOP slide to RET
1149        let map_mem = page_aligned_memory(expected);
1150        let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1151
1152        unsafe {
1153            sbox.map_region(&region_for_memory(
1154                &map_mem,
1155                guest_base,
1156                MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
1157            ))
1158            .unwrap();
1159        }
1160
1161        let _guard = map_mem.lock.try_read().unwrap();
1162
1163        // Execute should pass since memory is executable
1164        let succeed = sbox
1165            .call::<bool>(
1166                "ExecMappedBuffer",
1167                (guest_base as u64, expected.len() as u64),
1168            )
1169            .unwrap();
1170        assert!(succeed, "Expected execution of mapped buffer to succeed");
1171
1172        // write should fail because the memory is mapped as read-only
1173        let err = sbox
1174            .call::<bool>(
1175                "WriteMappedBuffer",
1176                (guest_base as u64, expected.len() as u64),
1177            )
1178            .unwrap_err();
1179
1180        match err {
1181            HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {}
1182            _ => panic!("Expected MemoryAccessViolation error"),
1183        };
1184    }
1185
1186    #[cfg(target_os = "linux")]
1187    fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory {
1188        use hyperlight_common::mem::PAGE_SIZE_USIZE;
1189
1190        let len = src.len().div_ceil(PAGE_SIZE_USIZE) * PAGE_SIZE_USIZE;
1191
1192        let mut mem = ExclusiveSharedMemory::new(len).unwrap();
1193        mem.copy_from_slice(src, 0).unwrap();
1194
1195        let (_, guest_mem) = mem.build();
1196
1197        guest_mem
1198    }
1199
1200    #[cfg(target_os = "linux")]
1201    fn region_for_memory(
1202        mem: &GuestSharedMemory,
1203        guest_base: usize,
1204        flags: MemoryRegionFlags,
1205    ) -> MemoryRegion {
1206        let ptr = mem.base_addr();
1207        let len = mem.mem_size();
1208        MemoryRegion {
1209            host_region: ptr..(ptr + len),
1210            guest_region: guest_base..(guest_base + len),
1211            flags,
1212            region_type: MemoryRegionType::Heap,
1213        }
1214    }
1215
1216    #[cfg(target_os = "linux")]
1217    fn allocate_guest_memory() -> GuestSharedMemory {
1218        page_aligned_memory(b"test data for snapshot")
1219    }
1220
1221    #[test]
1222    #[cfg(target_os = "linux")]
1223    fn snapshot_restore_handles_remapping_correctly() {
1224        let mut sbox: MultiUseSandbox = {
1225            let path = simple_guest_as_string().unwrap();
1226            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1227            u_sbox.evolve().unwrap()
1228        };
1229
1230        // 1. Take snapshot 1 with no additional regions mapped
1231        let snapshot1 = sbox.snapshot().unwrap();
1232        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1233
1234        // 2. Map a memory region
1235        let map_mem = allocate_guest_memory();
1236        let guest_base = 0x200000000_usize;
1237        let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1238
1239        unsafe { sbox.map_region(&region).unwrap() };
1240        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1241
1242        // 3. Take snapshot 2 with 1 region mapped
1243        let snapshot2 = sbox.snapshot().unwrap();
1244        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1245
1246        // 4. Restore to snapshot 1 (should unmap the region)
1247        sbox.restore(&snapshot1).unwrap();
1248        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1249
1250        // 5. Restore forward to snapshot 2 (should remap the region)
1251        sbox.restore(&snapshot2).unwrap();
1252        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1253
1254        // Verify the region is the same
1255        let mut restored_regions = sbox.vm.get_mapped_regions();
1256        assert_eq!(restored_regions.next().unwrap(), &region);
1257        assert!(restored_regions.next().is_none());
1258        drop(restored_regions);
1259
1260        // 6. Try map the region again (should fail since already mapped)
1261        let err = unsafe { sbox.map_region(&region) };
1262        assert!(
1263            err.is_err(),
1264            "Expected error when remapping existing region: {:?}",
1265            err
1266        );
1267    }
1268
1269    #[test]
1270    fn snapshot_different_sandbox() {
1271        let mut sandbox = {
1272            let path = simple_guest_as_string().unwrap();
1273            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1274            u_sbox.evolve().unwrap()
1275        };
1276
1277        let mut sandbox2 = {
1278            let path = simple_guest_as_string().unwrap();
1279            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1280            u_sbox.evolve().unwrap()
1281        };
1282        assert_ne!(sandbox.id, sandbox2.id);
1283
1284        let snapshot = sandbox.snapshot().unwrap();
1285        let err = sandbox2.restore(&snapshot);
1286        assert!(matches!(err, Err(HyperlightError::SnapshotSandboxMismatch)));
1287
1288        let sandbox_id = sandbox.id;
1289        drop(sandbox);
1290        drop(sandbox2);
1291        drop(snapshot);
1292
1293        let sandbox3 = {
1294            let path = simple_guest_as_string().unwrap();
1295            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1296            u_sbox.evolve().unwrap()
1297        };
1298        assert_ne!(sandbox3.id, sandbox_id);
1299    }
1300}