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