Skip to main content

hyperlight_host/sandbox/
initialized_multi_use.rs

1/*
2Copyright 2025  The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17use std::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::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::util::estimate_flatbuffer_capacity;
32use tracing::{Span, instrument};
33
34use super::Callable;
35use super::host_funcs::FunctionRegistry;
36use super::snapshot::Snapshot;
37use crate::HyperlightError::{self, SnapshotSandboxMismatch};
38use crate::func::{ParameterTuple, SupportedReturnType};
39use crate::hypervisor::InterruptHandle;
40use crate::hypervisor::hyperlight_vm::{HyperlightVm, HyperlightVmError};
41use crate::mem::memory_region::MemoryRegion;
42#[cfg(unix)]
43use crate::mem::memory_region::{MemoryRegionFlags, MemoryRegionType};
44use crate::mem::mgr::SandboxMemoryManager;
45use crate::mem::shared_mem::HostSharedMemory;
46use crate::metrics::{
47    METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
48};
49use crate::{Result, log_then_return};
50
51/// A fully initialized sandbox that can execute guest functions multiple times.
52///
53/// Guest functions can be called repeatedly while maintaining state between calls.
54/// The sandbox supports creating snapshots and restoring to previous states.
55///
56/// ## Sandbox Poisoning
57///
58/// The sandbox becomes **poisoned** when the guest is not run to completion, leaving it in
59/// an inconsistent state that could compromise memory safety, data integrity, or security.
60///
61/// ### When Does Poisoning Occur?
62///
63/// Poisoning happens when guest execution is interrupted before normal completion:
64///
65/// - **Guest panics or aborts** - When a guest function panics, crashes, or calls `abort()`,
66///   the normal cleanup and unwinding process is interrupted
67/// - **Invalid memory access** - Attempts to read/write/execute memory outside allowed regions
68/// - **Stack overflow** - Guest exhausts its stack space during execution
69/// - **Heap exhaustion** - Guest runs out of heap memory
70/// - **Host-initiated cancellation** - Calling [`InterruptHandle::kill()`] to forcefully
71///   terminate an in-progress guest function
72///
73/// ### Why This Is Unsafe
74///
75/// When guest execution doesn't complete normally, critical cleanup operations are skipped:
76///
77/// - **Memory leaks** - Heap allocations remain unreachable as the call stack is unwound
78/// - **Corrupted allocator state** - Memory allocator metadata (free lists, heap headers)
79///   left inconsistent
80/// - **Locked resources** - Mutexes or other synchronization primitives remain locked
81/// - **Partial state updates** - Data structures left half-modified (corrupted linked lists,
82///   inconsistent hash tables, etc.)
83///
84/// ### Recovery
85///
86/// Use [`restore()`](Self::restore) with a snapshot taken before poisoning occurred.
87/// This is the **only safe way** to recover - it completely replaces all memory state,
88/// eliminating any inconsistencies. See [`restore()`](Self::restore) for details.
89pub struct MultiUseSandbox {
90    /// Unique identifier for this sandbox instance
91    id: u64,
92    /// Whether this sandbox is poisoned
93    poisoned: bool,
94    pub(super) host_funcs: Arc<Mutex<FunctionRegistry>>,
95    pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
96    vm: HyperlightVm,
97    #[cfg(gdb)]
98    dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
99    /// If the current state of the sandbox has been captured in a snapshot,
100    /// that snapshot is stored here.
101    snapshot: Option<Arc<Snapshot>>,
102}
103
104impl MultiUseSandbox {
105    /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance.
106    ///
107    /// This function is not equivalent to doing an `evolve` from uninitialized
108    /// to initialized, and is purposely not exposed publicly outside the crate
109    /// (as a `From` implementation would be)
110    #[instrument(skip_all, parent = Span::current(), level = "Trace")]
111    pub(super) fn from_uninit(
112        host_funcs: Arc<Mutex<FunctionRegistry>>,
113        mgr: SandboxMemoryManager<HostSharedMemory>,
114        vm: HyperlightVm,
115        #[cfg(gdb)] dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
116    ) -> MultiUseSandbox {
117        Self {
118            id: super::snapshot::SANDBOX_CONFIGURATION_COUNTER.fetch_add(1, Ordering::Relaxed),
119            poisoned: false,
120            host_funcs,
121            mem_mgr: mgr,
122            vm,
123            #[cfg(gdb)]
124            dbg_mem_access_fn,
125            snapshot: None,
126        }
127    }
128
129    /// Creates a snapshot of the sandbox's current memory state.
130    ///
131    /// The snapshot is tied to this specific sandbox instance and can only be
132    /// restored to the same sandbox it was created from.
133    ///
134    /// ## Poisoned Sandbox
135    ///
136    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
137    /// is currently poisoned. Snapshots can only be taken from non-poisoned sandboxes.
138    ///
139    /// # Examples
140    ///
141    /// ```no_run
142    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
143    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
144    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
145    ///     GuestBinary::FilePath("guest.bin".into()),
146    ///     None
147    /// )?.evolve()?;
148    ///
149    /// // Modify sandbox state
150    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 42)?;
151    ///
152    /// // Create snapshot belonging to this sandbox
153    /// let snapshot = sandbox.snapshot()?;
154    /// # Ok(())
155    /// # }
156    /// ```
157    #[instrument(err(Debug), skip_all, parent = Span::current())]
158    pub fn snapshot(&mut self) -> Result<Arc<Snapshot>> {
159        if self.poisoned {
160            return Err(crate::HyperlightError::PoisonedSandbox);
161        }
162
163        if let Some(snapshot) = &self.snapshot {
164            return Ok(snapshot.clone());
165        }
166        let mapped_regions_iter = self.vm.get_mapped_regions();
167        let mapped_regions_vec: Vec<MemoryRegion> = mapped_regions_iter.cloned().collect();
168        let root_pt_gpa = self
169            .vm
170            .get_root_pt()
171            .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
172        let stack_top_gpa = self.vm.get_stack_top();
173        let sregs = self
174            .vm
175            .get_snapshot_sregs()
176            .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
177        let entrypoint = self.vm.get_entrypoint();
178        let memory_snapshot = self.mem_mgr.snapshot(
179            self.id,
180            mapped_regions_vec,
181            root_pt_gpa,
182            stack_top_gpa,
183            sregs,
184            entrypoint,
185        )?;
186        let snapshot = Arc::new(memory_snapshot);
187        self.snapshot = Some(snapshot.clone());
188        Ok(snapshot)
189    }
190
191    /// Restores the sandbox's memory to a previously captured snapshot state.
192    ///
193    /// The snapshot must have been created from this same sandbox instance.
194    /// Attempting to restore a snapshot from a different sandbox will return
195    /// a [`SnapshotSandboxMismatch`](crate::HyperlightError::SnapshotSandboxMismatch) error.
196    ///
197    /// ## Poison State Recovery
198    ///
199    /// This method automatically clears any poison state when successful. This is safe because:
200    /// - Snapshots can only be taken from non-poisoned sandboxes
201    /// - Restoration completely replaces all memory state, eliminating any inconsistencies
202    ///   caused by incomplete guest execution
203    ///
204    /// ### What Gets Fixed During Restore
205    ///
206    /// When a poisoned sandbox is restored, the memory state is completely reset:
207    /// - **Leaked heap memory** - All allocations from interrupted execution are discarded
208    /// - **Corrupted allocator metadata** - Free lists and heap headers restored to consistent state
209    /// - **Locked mutexes** - All lock state is reset
210    /// - **Partial updates** - Data structures restored to their pre-execution state
211    ///
212    /// # Examples
213    ///
214    /// ```no_run
215    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
216    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
217    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
218    ///     GuestBinary::FilePath("guest.bin".into()),
219    ///     None
220    /// )?.evolve()?;
221    ///
222    /// // Take initial snapshot from this sandbox
223    /// let snapshot = sandbox.snapshot()?;
224    ///
225    /// // Modify sandbox state
226    /// sandbox.call_guest_function_by_name::<i32>("SetValue", 100)?;
227    /// let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
228    /// assert_eq!(value, 100);
229    ///
230    /// // Restore to previous state (same sandbox)
231    /// sandbox.restore(snapshot)?;
232    /// let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
233    /// assert_eq!(restored_value, 0); // Back to initial state
234    /// # Ok(())
235    /// # }
236    /// ```
237    ///
238    /// ## Recovering from Poison
239    ///
240    /// ```no_run
241    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary, HyperlightError};
242    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
243    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
244    ///     GuestBinary::FilePath("guest.bin".into()),
245    ///     None
246    /// )?.evolve()?;
247    ///
248    /// // Take snapshot before potentially poisoning operation
249    /// let snapshot = sandbox.snapshot()?;
250    ///
251    /// // This might poison the sandbox (guest not run to completion)
252    /// let result = sandbox.call::<()>("guest_panic", ());
253    /// if result.is_err() {
254    ///     if sandbox.poisoned() {
255    ///         // Restore from snapshot to clear poison
256    ///         sandbox.restore(snapshot.clone())?;
257    ///         assert!(!sandbox.poisoned());
258    ///         
259    ///         // Sandbox is now usable again
260    ///         sandbox.call::<String>("Echo", "hello".to_string())?;
261    ///     }
262    /// }
263    /// # Ok(())
264    /// # }
265    /// ```
266    #[instrument(err(Debug), skip_all, parent = Span::current())]
267    pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()> {
268        // Currently, we do not try to optimise restore to the
269        // most-current snapshot. This is because the most-current
270        // snapshot, while it must have identical virtual memory
271        // layout to the current sandbox, does not necessarily have
272        // the exact same /physical/ memory contents. It is not
273        // entirely inconceivable that this could lead to breakage of
274        // cross-request isolation in some way, although it would
275        // require some /very/ odd code.  For example, suppose that a
276        // service uses Hyperlight to sandbox native code from
277        // clients, and promises cross-request isolation. A tenant
278        // provides a binary that can process two forms of request,
279        // either writing a secret into physical memory, or reading
280        // from arbitrary physical memory, assuming that the two kinds
281        // of requests can never (dangerously) meet in the same
282        // sandbox.
283        //
284        // It is presently unclear whether this is a sensible threat
285        // model, especially since Hyperlight is often used with
286        // managed-code runtimes which do not allow even arbitrary
287        // access to virtual memory, much less physical memory.
288        // However, out of an abundance of caution, the optimisation
289        // is presently disabled.
290
291        if self.id != snapshot.sandbox_id() {
292            return Err(SnapshotSandboxMismatch);
293        }
294
295        let (gsnapshot, gscratch) = self.mem_mgr.restore_snapshot(&snapshot)?;
296        if let Some(gsnapshot) = gsnapshot {
297            self.vm
298                .update_snapshot_mapping(gsnapshot)
299                .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
300        }
301        if let Some(gscratch) = gscratch {
302            self.vm
303                .update_scratch_mapping(gscratch)
304                .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
305        }
306
307        let sregs = snapshot.sregs().ok_or_else(|| {
308            HyperlightError::Error("snapshot from running sandbox should have sregs".to_string())
309        })?;
310        // TODO (ludfjig): Go through the rest of possible errors in this `MultiUseSandbox::restore` function
311        // and determine if they should also poison the sandbox.
312        self.vm
313            .reset_vcpu(snapshot.root_pt_gpa(), sregs)
314            .map_err(|e| {
315                self.poisoned = true;
316                HyperlightVmError::Restore(e)
317            })?;
318
319        self.vm.set_stack_top(snapshot.stack_top_gva());
320        self.vm.set_entrypoint(snapshot.entrypoint());
321
322        let current_regions: HashSet<_> = self.vm.get_mapped_regions().cloned().collect();
323        let snapshot_regions: HashSet<_> = snapshot.regions().iter().cloned().collect();
324
325        let regions_to_unmap = current_regions.difference(&snapshot_regions);
326        let regions_to_map = snapshot_regions.difference(&current_regions);
327
328        for region in regions_to_unmap {
329            self.vm
330                .unmap_region(region)
331                .map_err(HyperlightVmError::UnmapRegion)?;
332        }
333
334        for region in regions_to_map {
335            // Safety: The region has been mapped before, and at that point the caller promised that the memory region is valid
336            // in their call to `MultiUseSandbox::map_region`
337            unsafe { self.vm.map_region(region) }.map_err(HyperlightVmError::MapRegion)?;
338        }
339
340        // The restored snapshot is now our most current snapshot
341        self.snapshot = Some(snapshot.clone());
342
343        // Clear poison state when successfully restoring from snapshot.
344        //
345        // # Safety:
346        // This is safe because:
347        // 1. Snapshots can only be taken from non-poisoned sandboxes (verified at snapshot creation)
348        // 2. Restoration completely replaces all memory state, eliminating:
349        //    - All leaked heap allocations (memory is restored to snapshot state)
350        //    - All corrupted data structures (overwritten with consistent snapshot data)
351        //    - All inconsistent global state (reset to snapshot values)
352        self.poisoned = false;
353
354        Ok(())
355    }
356
357    /// Calls a guest function by name with the specified arguments.
358    ///
359    /// Changes made to the sandbox during execution are *not* persisted.
360    ///
361    /// ## Poisoned Sandbox
362    ///
363    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
364    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
365    ///
366    /// # Examples
367    ///
368    /// ```no_run
369    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
370    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
371    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
372    ///     GuestBinary::FilePath("guest.bin".into()),
373    ///     None
374    /// )?.evolve()?;
375    ///
376    /// // Call function with no arguments
377    /// let result: i32 = sandbox.call_guest_function_by_name("GetCounter", ())?;
378    ///
379    /// // Call function with single argument
380    /// let doubled: i32 = sandbox.call_guest_function_by_name("Double", 21)?;
381    /// assert_eq!(doubled, 42);
382    ///
383    /// // Call function with multiple arguments
384    /// let sum: i32 = sandbox.call_guest_function_by_name("Add", (10, 32))?;
385    /// assert_eq!(sum, 42);
386    ///
387    /// // Call function returning string
388    /// let message: String = sandbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?;
389    /// assert_eq!(message, "Hello, World!");
390    /// # Ok(())
391    /// # }
392    /// ```
393    #[doc(hidden)]
394    #[deprecated(
395        since = "0.8.0",
396        note = "Deprecated in favour of call and snapshot/restore."
397    )]
398    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
399    pub fn call_guest_function_by_name<Output: SupportedReturnType>(
400        &mut self,
401        func_name: &str,
402        args: impl ParameterTuple,
403    ) -> Result<Output> {
404        if self.poisoned {
405            return Err(crate::HyperlightError::PoisonedSandbox);
406        }
407        let snapshot = self.snapshot()?;
408        let res = self.call(func_name, args);
409        self.restore(snapshot)?;
410        res
411    }
412
413    /// Calls a guest function by name with the specified arguments.
414    ///
415    /// Changes made to the sandbox during execution are persisted.
416    ///
417    /// ## Poisoned Sandbox
418    ///
419    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
420    /// is already poisoned before the call. Use [`restore()`](Self::restore) to recover from
421    /// a poisoned state.
422    ///
423    /// ## Sandbox Poisoning
424    ///
425    /// If this method returns an error, the sandbox may be poisoned if the guest was not run
426    /// to completion (due to panic, abort, memory violation, stack/heap exhaustion, or forced
427    /// termination). Use [`poisoned()`](Self::poisoned) to check the poison state and
428    /// [`restore()`](Self::restore) to recover if needed.
429    ///
430    /// If this method returns `Ok`, the sandbox is guaranteed to **not** be poisoned - the guest
431    /// function completed successfully and the sandbox state is consistent.
432    ///
433    /// # Examples
434    ///
435    /// ```no_run
436    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
437    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
438    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
439    ///     GuestBinary::FilePath("guest.bin".into()),
440    ///     None
441    /// )?.evolve()?;
442    ///
443    /// // Call function with no arguments
444    /// let result: i32 = sandbox.call("GetCounter", ())?;
445    ///
446    /// // Call function with single argument
447    /// let doubled: i32 = sandbox.call("Double", 21)?;
448    /// assert_eq!(doubled, 42);
449    ///
450    /// // Call function with multiple arguments
451    /// let sum: i32 = sandbox.call("Add", (10, 32))?;
452    /// assert_eq!(sum, 42);
453    ///
454    /// // Call function returning string
455    /// let message: String = sandbox.call("Echo", "Hello, World!".to_string())?;
456    /// assert_eq!(message, "Hello, World!");
457    /// # Ok(())
458    /// # }
459    /// ```
460    ///
461    /// ## Handling Potential Poisoning
462    ///
463    /// ```no_run
464    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
465    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
466    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
467    ///     GuestBinary::FilePath("guest.bin".into()),
468    ///     None
469    /// )?.evolve()?;
470    ///
471    /// // Take snapshot before risky operation
472    /// let snapshot = sandbox.snapshot()?;
473    ///
474    /// // Call potentially unsafe guest function
475    /// let result = sandbox.call::<String>("RiskyOperation", "input".to_string());
476    ///
477    /// // Check if the call failed and poisoned the sandbox
478    /// if let Err(e) = result {
479    ///     eprintln!("Guest function failed: {}", e);
480    ///     
481    ///     if sandbox.poisoned() {
482    ///         eprintln!("Sandbox was poisoned, restoring from snapshot");
483    ///         sandbox.restore(snapshot.clone())?;
484    ///     }
485    /// }
486    /// # Ok(())
487    /// # }
488    /// ```
489    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
490    pub fn call<Output: SupportedReturnType>(
491        &mut self,
492        func_name: &str,
493        args: impl ParameterTuple,
494    ) -> Result<Output> {
495        if self.poisoned {
496            return Err(crate::HyperlightError::PoisonedSandbox);
497        }
498        // Reset snapshot since we are mutating the sandbox state
499        self.snapshot = None;
500        maybe_time_and_emit_guest_call(func_name, || {
501            let ret = self.call_guest_function_by_name_no_reset(
502                func_name,
503                Output::TYPE,
504                args.into_value(),
505            );
506            // Use the ? operator to allow converting any hyperlight_common::func::Error
507            // returned by from_value into a HyperlightError
508            let ret = Output::from_value(ret?)?;
509            Ok(ret)
510        })
511    }
512
513    /// Maps a region of host memory into the sandbox address space.
514    ///
515    /// The base address and length must meet platform alignment requirements
516    /// (typically page-aligned). The `region_type` field is ignored as guest
517    /// page table entries are not created.
518    ///
519    /// ## Poisoned Sandbox
520    ///
521    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
522    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
523    ///
524    /// # Safety
525    ///
526    /// The caller must ensure the host memory region remains valid and unmodified
527    /// for the lifetime of `self`.
528    #[instrument(err(Debug), skip(self, rgn), parent = Span::current())]
529    #[cfg(target_os = "linux")]
530    unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> {
531        if self.poisoned {
532            return Err(crate::HyperlightError::PoisonedSandbox);
533        }
534        if rgn.flags.contains(MemoryRegionFlags::WRITE) {
535            // TODO: Implement support for writable mappings, which
536            // need to be registered with the memory manager so that
537            // writes can be rolled back when necessary.
538            log_then_return!("TODO: Writable mappings not yet supported");
539        }
540        // Reset snapshot since we are mutating the sandbox state
541        self.snapshot = None;
542        unsafe { self.vm.map_region(rgn) }.map_err(HyperlightVmError::MapRegion)?;
543        self.mem_mgr.mapped_rgns += 1;
544        Ok(())
545    }
546
547    /// Map the contents of a file into the guest at a particular address
548    ///
549    /// Returns the length of the mapping in bytes.
550    ///
551    /// ## Poisoned Sandbox
552    ///
553    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
554    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
555    #[instrument(err(Debug), skip(self, _fp, _guest_base), parent = Span::current())]
556    pub fn map_file_cow(&mut self, _fp: &Path, _guest_base: u64) -> Result<u64> {
557        if self.poisoned {
558            return Err(crate::HyperlightError::PoisonedSandbox);
559        }
560        #[cfg(windows)]
561        log_then_return!("mmap'ing a file into the guest is not yet supported on Windows");
562        #[cfg(unix)]
563        unsafe {
564            let file = std::fs::File::options().read(true).write(true).open(_fp)?;
565            let file_size = file.metadata()?.st_size();
566            let page_size = page_size::get();
567            let size = (file_size as usize).div_ceil(page_size) * page_size;
568            let base = libc::mmap(
569                std::ptr::null_mut(),
570                size,
571                libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
572                libc::MAP_PRIVATE,
573                file.as_raw_fd(),
574                0,
575            );
576            if base == libc::MAP_FAILED {
577                log_then_return!("mmap error: {:?}", std::io::Error::last_os_error());
578            }
579
580            if let Err(err) = self.map_region(&MemoryRegion {
581                host_region: base as usize..base.wrapping_add(size) as usize,
582                guest_region: _guest_base as usize.._guest_base as usize + size,
583                flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
584                region_type: MemoryRegionType::Heap,
585            }) {
586                libc::munmap(base, size);
587                return Err(err);
588            };
589
590            Ok(size as u64)
591        }
592    }
593
594    /// Calls a guest function with type-erased parameters and return values.
595    ///
596    /// This function is used for fuzz testing parameter and return type handling.
597    ///
598    /// ## Poisoned Sandbox
599    ///
600    /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
601    /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
602    #[cfg(feature = "fuzzing")]
603    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
604    pub fn call_type_erased_guest_function_by_name(
605        &mut self,
606        func_name: &str,
607        ret_type: ReturnType,
608        args: Vec<ParameterValue>,
609    ) -> Result<ReturnValue> {
610        if self.poisoned {
611            return Err(crate::HyperlightError::PoisonedSandbox);
612        }
613        // Reset snapshot since we are mutating the sandbox state
614        self.snapshot = None;
615        maybe_time_and_emit_guest_call(func_name, || {
616            self.call_guest_function_by_name_no_reset(func_name, ret_type, args)
617        })
618    }
619
620    fn call_guest_function_by_name_no_reset(
621        &mut self,
622        function_name: &str,
623        return_type: ReturnType,
624        args: Vec<ParameterValue>,
625    ) -> Result<ReturnValue> {
626        if self.poisoned {
627            return Err(crate::HyperlightError::PoisonedSandbox);
628        }
629        // ===== KILL() TIMING POINT 1 =====
630        // Clear any stale cancellation from a previous guest function call or if kill() was called too early.
631        // Any kill() that completed (even partially) BEFORE this line has NO effect on this call.
632        self.vm.clear_cancel();
633
634        let res = (|| {
635            let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args);
636
637            let fc = FunctionCall::new(
638                function_name.to_string(),
639                Some(args),
640                FunctionCallType::Guest,
641                return_type,
642            );
643
644            let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
645            let buffer = fc.encode(&mut builder);
646
647            self.mem_mgr.write_guest_function_call(buffer)?;
648
649            let dispatch_res = self.vm.dispatch_call_from_host(
650                &mut self.mem_mgr,
651                &self.host_funcs,
652                #[cfg(gdb)]
653                self.dbg_mem_access_fn.clone(),
654            );
655
656            // Convert dispatch errors to HyperlightErrors to maintain backwards compatibility
657            // but first determine if sandbox should be poisoned
658            if let Err(e) = dispatch_res {
659                let (error, should_poison) = e.promote();
660                self.poisoned |= should_poison;
661                return Err(error);
662            }
663
664            let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner();
665
666            match guest_result {
667                Ok(val) => Ok(val),
668                Err(guest_error) => {
669                    metrics::counter!(
670                        METRIC_GUEST_ERROR,
671                        METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string()
672                    )
673                    .increment(1);
674
675                    Err(HyperlightError::GuestError(
676                        guest_error.code,
677                        guest_error.message,
678                    ))
679                }
680            }
681        })();
682
683        // In the happy path we do not need to clear io-buffers from the host because:
684        // - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::<FunctionCall>()`
685        // - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result`
686        // - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call`
687        // - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value`
688        if let Err(e) = &res {
689            self.mem_mgr.clear_io_buffers();
690
691            // Determine if we should poison the sandbox.
692            self.poisoned |= e.is_poison_error();
693        }
694
695        // Note: clear_call_active() is automatically called when _guard is dropped here
696
697        res
698    }
699
700    /// Returns a handle for interrupting guest execution.
701    ///
702    /// # Examples
703    ///
704    /// ```no_run
705    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
706    /// # use std::thread;
707    /// # use std::time::Duration;
708    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
709    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
710    ///     GuestBinary::FilePath("guest.bin".into()),
711    ///     None
712    /// )?.evolve()?;
713    ///
714    /// // Get interrupt handle before starting long-running operation
715    /// let interrupt_handle = sandbox.interrupt_handle();
716    ///
717    /// // Spawn thread to interrupt after timeout
718    /// let handle_clone = interrupt_handle.clone();
719    /// thread::spawn(move || {
720    ///     thread::sleep(Duration::from_secs(5));
721    ///     handle_clone.kill();
722    /// });
723    ///
724    /// // This call may be interrupted by the spawned thread
725    /// let result = sandbox.call_guest_function_by_name::<i32>("LongRunningFunction", ());
726    /// # Ok(())
727    /// # }
728    /// ```
729    pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
730        self.vm.interrupt_handle()
731    }
732
733    /// Generate a crash dump of the current state of the VM underlying this sandbox.
734    ///
735    /// Creates an ELF core dump file that can be used for debugging. The dump
736    /// captures the current state of the sandbox including registers, memory regions,
737    /// and other execution context.
738    ///
739    /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
740    /// environment variable. If not set, it defaults to the system's temporary directory.
741    ///
742    /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
743    /// is also configured to allow core dumps (which is the default behavior).
744    ///
745    /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
746    /// guest that dont cause crashes (e.g. a guest function that does not return)
747    ///
748    /// # Examples
749    ///
750    /// Attach to your running process with gdb and call this function:
751    ///
752    /// ```shell
753    /// sudo gdb -p <pid_of_your_process>
754    /// (gdb) info threads
755    /// # find the thread that is running the guest function you want to debug
756    /// (gdb) thread <thread_number>
757    /// # switch to the frame where you have access to your MultiUseSandbox instance
758    /// (gdb) backtrace
759    /// (gdb) frame <frame_number>
760    /// # get the pointer to your MultiUseSandbox instance
761    /// # Get the sandbox pointer
762    /// (gdb) print sandbox
763    /// # Call the crashdump function
764    /// call sandbox.generate_crashdump()
765    /// ```
766    /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
767    ///
768    #[cfg(crashdump)]
769    #[instrument(err(Debug), skip_all, parent = Span::current())]
770    pub fn generate_crashdump(&mut self) -> Result<()> {
771        crate::hypervisor::crashdump::generate_crashdump(&self.vm, &mut self.mem_mgr, None)
772    }
773
774    /// Generate a crash dump of the current state of the VM, writing to `dir`.
775    ///
776    /// Like [`generate_crashdump`](Self::generate_crashdump), but the core dump
777    /// file is placed in `dir` instead of consulting the `HYPERLIGHT_CORE_DUMP_DIR`
778    /// environment variable.  This avoids the need for callers to use
779    /// `unsafe { std::env::set_var(...) }`.
780    #[cfg(crashdump)]
781    #[instrument(err(Debug), skip_all, parent = Span::current())]
782    pub fn generate_crashdump_to_dir(&mut self, dir: impl Into<String>) -> Result<()> {
783        crate::hypervisor::crashdump::generate_crashdump(
784            &self.vm,
785            &mut self.mem_mgr,
786            Some(dir.into()),
787        )
788    }
789
790    /// Returns whether the sandbox is currently poisoned.
791    ///
792    /// A poisoned sandbox is in an inconsistent state due to the guest not running to completion.
793    /// All operations will be rejected until the sandbox is restored from a non-poisoned snapshot.
794    ///
795    /// ## Causes of Poisoning
796    ///
797    /// The sandbox becomes poisoned when guest execution is interrupted:
798    /// - **Panics/Aborts** - Guest code panics or calls `abort()`
799    /// - **Invalid Memory Access** - Read/write/execute violations  
800    /// - **Stack Overflow** - Guest exhausts stack space
801    /// - **Heap Exhaustion** - Guest runs out of heap memory
802    /// - **Forced Termination** - [`InterruptHandle::kill()`] called during execution
803    ///
804    /// ## Recovery
805    ///
806    /// To clear the poison state, use [`restore()`](Self::restore) with a snapshot
807    /// that was taken before the sandbox became poisoned.
808    ///
809    /// # Examples
810    ///
811    /// ```no_run
812    /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
813    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
814    /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
815    ///     GuestBinary::FilePath("guest.bin".into()),
816    ///     None
817    /// )?.evolve()?;
818    ///
819    /// // Check if sandbox is poisoned
820    /// if sandbox.poisoned() {
821    ///     println!("Sandbox is poisoned and needs attention");
822    /// }
823    /// # Ok(())
824    /// # }
825    /// ```
826    pub fn poisoned(&self) -> bool {
827        self.poisoned
828    }
829}
830
831impl Callable for MultiUseSandbox {
832    fn call<Output: SupportedReturnType>(
833        &mut self,
834        func_name: &str,
835        args: impl ParameterTuple,
836    ) -> Result<Output> {
837        if self.poisoned {
838            return Err(crate::HyperlightError::PoisonedSandbox);
839        }
840        self.call(func_name, args)
841    }
842}
843
844impl std::fmt::Debug for MultiUseSandbox {
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        f.debug_struct("MultiUseSandbox").finish()
847    }
848}
849
850#[cfg(test)]
851mod tests {
852    use std::sync::{Arc, Barrier};
853    use std::thread;
854
855    use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
856    use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE};
857    use hyperlight_testing::simple_guest_as_string;
858
859    #[cfg(target_os = "linux")]
860    use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
861    #[cfg(target_os = "linux")]
862    use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
863    use crate::sandbox::SandboxConfiguration;
864    use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox};
865
866    #[test]
867    fn poison() {
868        let mut sbox: MultiUseSandbox = {
869            let path = simple_guest_as_string().unwrap();
870            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
871            u_sbox.evolve()
872        }
873        .unwrap();
874        let snapshot = sbox.snapshot().unwrap();
875
876        // poison on purpose
877        let res = sbox
878            .call::<()>("guest_panic", "hello".to_string())
879            .unwrap_err();
880        assert!(
881            matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
882        );
883        assert!(sbox.poisoned());
884
885        // guest calls should fail when poisoned
886        let res = sbox
887            .call::<()>("guest_panic", "hello2".to_string())
888            .unwrap_err();
889        assert!(matches!(res, HyperlightError::PoisonedSandbox));
890
891        // snapshot should fail when poisoned
892        if let Err(e) = sbox.snapshot() {
893            assert!(sbox.poisoned());
894            assert!(matches!(e, HyperlightError::PoisonedSandbox));
895        } else {
896            panic!("Snapshot should fail");
897        }
898
899        // map_region should fail when poisoned
900        #[cfg(target_os = "linux")]
901        {
902            let map_mem = allocate_guest_memory();
903            let guest_base = 0x0;
904            let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
905            let res = unsafe { sbox.map_region(&region) }.unwrap_err();
906            assert!(matches!(res, HyperlightError::PoisonedSandbox));
907        }
908
909        // map_file_cow should fail when poisoned
910        #[cfg(target_os = "linux")]
911        {
912            let temp_file = std::env::temp_dir().join("test_poison_map_file.bin");
913            let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err();
914            assert!(matches!(res, HyperlightError::PoisonedSandbox));
915            std::fs::remove_file(&temp_file).ok(); // Clean up
916        }
917
918        // call_guest_function_by_name (deprecated) should fail when poisoned
919        #[allow(deprecated)]
920        let res = sbox
921            .call_guest_function_by_name::<String>("Echo", "test".to_string())
922            .unwrap_err();
923        assert!(matches!(res, HyperlightError::PoisonedSandbox));
924
925        // restore to non-poisoned snapshot should work and clear poison
926        sbox.restore(snapshot.clone()).unwrap();
927        assert!(!sbox.poisoned());
928
929        // guest calls should work again after restore
930        let res = sbox.call::<String>("Echo", "hello2".to_string()).unwrap();
931        assert_eq!(res, "hello2".to_string());
932        assert!(!sbox.poisoned());
933
934        // re-poison on purpose
935        let res = sbox
936            .call::<()>("guest_panic", "hello".to_string())
937            .unwrap_err();
938        assert!(
939            matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
940        );
941        assert!(sbox.poisoned());
942
943        // restore to non-poisoned snapshot should work again
944        sbox.restore(snapshot.clone()).unwrap();
945        assert!(!sbox.poisoned());
946
947        // guest calls should work again
948        let res = sbox.call::<String>("Echo", "hello3".to_string()).unwrap();
949        assert_eq!(res, "hello3".to_string());
950        assert!(!sbox.poisoned());
951
952        // snapshot should work again
953        let _ = sbox.snapshot().unwrap();
954    }
955
956    /// Make sure input/output buffers are properly reset after guest call (with host call)
957    #[test]
958    fn host_func_error() {
959        let path = simple_guest_as_string().unwrap();
960        let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
961        sandbox
962            .register("HostError", || -> Result<()> {
963                Err(HyperlightError::Error("hi".to_string()))
964            })
965            .unwrap();
966        let mut sandbox = sandbox.evolve().unwrap();
967
968        // will exhaust io if leaky
969        for _ in 0..1000 {
970            let result = sandbox
971                .call::<i64>(
972                    "CallGivenParamlessHostFuncThatReturnsI64",
973                    "HostError".to_string(),
974                )
975                .unwrap_err();
976
977            assert!(
978                matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"),
979            );
980        }
981    }
982
983    #[test]
984    fn call_host_func_expect_error() {
985        let path = simple_guest_as_string().unwrap();
986        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
987        let mut sandbox = sandbox.evolve().unwrap();
988        sandbox
989            .call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string())
990            .unwrap();
991    }
992
993    /// Make sure input/output buffers are properly reset after guest call (with host call)
994    #[test]
995    fn io_buffer_reset() {
996        let mut cfg = SandboxConfiguration::default();
997        cfg.set_input_data_size(4096);
998        cfg.set_output_data_size(4096);
999        let path = simple_guest_as_string().unwrap();
1000        let mut sandbox =
1001            UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1002        sandbox.register("HostAdd", |a: i32, b: i32| a + b).unwrap();
1003        let mut sandbox = sandbox.evolve().unwrap();
1004
1005        // will exhaust io if leaky. Tests both success and error paths
1006        for _ in 0..1000 {
1007            let result = sandbox.call::<i32>("Add", (5i32, 10i32)).unwrap();
1008            assert_eq!(result, 15);
1009            let result = sandbox.call::<i32>("AddToStaticAndFail", ()).unwrap_err();
1010            assert!(
1011                matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose")
1012            );
1013        }
1014    }
1015
1016    /// Tests that call_guest_function_by_name restores the state correctly
1017    #[test]
1018    fn test_call_guest_function_by_name() {
1019        let mut sbox: MultiUseSandbox = {
1020            let path = simple_guest_as_string().unwrap();
1021            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1022            u_sbox.evolve()
1023        }
1024        .unwrap();
1025
1026        let snapshot = sbox.snapshot().unwrap();
1027
1028        let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1029        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1030        assert_eq!(res, 5);
1031
1032        sbox.restore(snapshot).unwrap();
1033        #[allow(deprecated)]
1034        let _ = sbox
1035            .call_guest_function_by_name::<i32>("AddToStatic", 5i32)
1036            .unwrap();
1037        #[allow(deprecated)]
1038        let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap();
1039        assert_eq!(res, 0);
1040    }
1041
1042    // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (24K) and heap(20K).
1043    // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
1044    #[test]
1045    fn test_with_small_stack_and_heap() {
1046        let mut cfg = SandboxConfiguration::default();
1047        cfg.set_heap_size(20 * 1024);
1048        // min_scratch_size already includes 1 page (4k on most
1049        // platforms) of guest stack, so add 20k more to get 24k
1050        // total, and then add some more for the eagerly-copied page
1051        // tables on amd64
1052        let min_scratch = hyperlight_common::layout::min_scratch_size(
1053            cfg.get_input_data_size(),
1054            cfg.get_output_data_size(),
1055        );
1056        cfg.set_scratch_size(min_scratch + 0x10000 + 0x10000);
1057
1058        let mut sbox1: MultiUseSandbox = {
1059            let path = simple_guest_as_string().unwrap();
1060            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1061            u_sbox.evolve()
1062        }
1063        .unwrap();
1064
1065        for _ in 0..1000 {
1066            sbox1.call::<String>("Echo", "hello".to_string()).unwrap();
1067        }
1068
1069        let mut sbox2: MultiUseSandbox = {
1070            let path = simple_guest_as_string().unwrap();
1071            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1072            u_sbox.evolve()
1073        }
1074        .unwrap();
1075
1076        for i in 0..1000 {
1077            sbox2
1078                .call::<i32>(
1079                    "PrintUsingPrintf",
1080                    format!("Hello World {}\n", i).to_string(),
1081                )
1082                .unwrap();
1083        }
1084    }
1085
1086    /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
1087    /// and restoring a snapshot from before evolving restores the previous state
1088    #[test]
1089    fn snapshot_evolve_restore_handles_state_correctly() {
1090        let mut sbox: MultiUseSandbox = {
1091            let path = simple_guest_as_string().unwrap();
1092            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1093            u_sbox.evolve()
1094        }
1095        .unwrap();
1096
1097        let snapshot = sbox.snapshot().unwrap();
1098
1099        let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1100
1101        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1102        assert_eq!(res, 5);
1103
1104        sbox.restore(snapshot).unwrap();
1105        let res: i32 = sbox.call("GetStatic", ()).unwrap();
1106        assert_eq!(res, 0);
1107    }
1108
1109    #[test]
1110    fn test_trigger_exception_on_guest() {
1111        let usbox = UninitializedSandbox::new(
1112            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1113            None,
1114        )
1115        .unwrap();
1116
1117        let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap();
1118
1119        let res: Result<()> = multi_use_sandbox.call("TriggerException", ());
1120
1121        assert!(res.is_err());
1122
1123        match res.unwrap_err() {
1124            HyperlightError::GuestAborted(_, msg) => {
1125                // msg should indicate we got an invalid opcode exception
1126                assert!(msg.contains("InvalidOpcode"));
1127            }
1128            e => panic!(
1129                "Expected HyperlightError::GuestExecutionError but got {:?}",
1130                e
1131            ),
1132        }
1133    }
1134
1135    #[test]
1136    fn create_200_sandboxes() {
1137        const NUM_THREADS: usize = 10;
1138        const SANDBOXES_PER_THREAD: usize = 20;
1139
1140        // barrier to make sure all threads start their work simultaneously
1141        let start_barrier = Arc::new(Barrier::new(NUM_THREADS + 1));
1142        let mut thread_handles = vec![];
1143
1144        for _ in 0..NUM_THREADS {
1145            let barrier = start_barrier.clone();
1146
1147            let handle = thread::spawn(move || {
1148                barrier.wait();
1149
1150                for _ in 0..SANDBOXES_PER_THREAD {
1151                    let guest_path = simple_guest_as_string().expect("Guest Binary Missing");
1152                    let uninit =
1153                        UninitializedSandbox::new(GuestBinary::FilePath(guest_path), None).unwrap();
1154
1155                    let mut sandbox: MultiUseSandbox = uninit.evolve().unwrap();
1156
1157                    let result: i32 = sandbox.call("GetStatic", ()).unwrap();
1158                    assert_eq!(result, 0);
1159                }
1160            });
1161
1162            thread_handles.push(handle);
1163        }
1164
1165        start_barrier.wait();
1166
1167        for handle in thread_handles {
1168            handle.join().unwrap();
1169        }
1170    }
1171
1172    #[cfg(target_os = "linux")]
1173    #[test]
1174    fn test_mmap() {
1175        let mut sbox = UninitializedSandbox::new(
1176            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1177            None,
1178        )
1179        .unwrap()
1180        .evolve()
1181        .unwrap();
1182
1183        let expected = b"hello world";
1184        let map_mem = page_aligned_memory(expected);
1185        let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1186
1187        unsafe {
1188            sbox.map_region(&region_for_memory(
1189                &map_mem,
1190                guest_base,
1191                MemoryRegionFlags::READ,
1192            ))
1193            .unwrap();
1194        }
1195
1196        let _guard = map_mem.lock.try_read().unwrap();
1197        let actual: Vec<u8> = sbox
1198            .call(
1199                "ReadMappedBuffer",
1200                (guest_base as u64, expected.len() as u64, true),
1201            )
1202            .unwrap();
1203
1204        assert_eq!(actual, expected);
1205    }
1206
1207    // Makes sure MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE executable but not writable
1208    #[cfg(target_os = "linux")]
1209    #[test]
1210    fn test_mmap_write_exec() {
1211        let mut sbox = UninitializedSandbox::new(
1212            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1213            None,
1214        )
1215        .unwrap()
1216        .evolve()
1217        .unwrap();
1218
1219        let expected = &[0x90, 0x90, 0x90, 0xC3]; // NOOP slide to RET
1220        let map_mem = page_aligned_memory(expected);
1221        let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1222
1223        unsafe {
1224            sbox.map_region(&region_for_memory(
1225                &map_mem,
1226                guest_base,
1227                MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
1228            ))
1229            .unwrap();
1230        }
1231
1232        let _guard = map_mem.lock.try_read().unwrap();
1233
1234        // Execute should pass since memory is executable
1235        let succeed = sbox
1236            .call::<bool>(
1237                "ExecMappedBuffer",
1238                (guest_base as u64, expected.len() as u64),
1239            )
1240            .unwrap();
1241        assert!(succeed, "Expected execution of mapped buffer to succeed");
1242
1243        // write should fail because the memory is mapped as read-only
1244        let err = sbox
1245            .call::<bool>(
1246                "WriteMappedBuffer",
1247                (guest_base as u64, expected.len() as u64),
1248            )
1249            .unwrap_err();
1250
1251        match err {
1252            HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {}
1253            _ => panic!("Expected MemoryAccessViolation error"),
1254        };
1255    }
1256
1257    #[cfg(target_os = "linux")]
1258    fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory {
1259        use hyperlight_common::mem::PAGE_SIZE_USIZE;
1260
1261        let len = src.len().div_ceil(PAGE_SIZE_USIZE) * PAGE_SIZE_USIZE;
1262
1263        let mut mem = ExclusiveSharedMemory::new(len).unwrap();
1264        mem.copy_from_slice(src, 0).unwrap();
1265
1266        let (_, guest_mem) = mem.build();
1267
1268        guest_mem
1269    }
1270
1271    #[cfg(target_os = "linux")]
1272    fn region_for_memory(
1273        mem: &GuestSharedMemory,
1274        guest_base: usize,
1275        flags: MemoryRegionFlags,
1276    ) -> MemoryRegion {
1277        let ptr = mem.base_addr();
1278        let len = mem.mem_size();
1279        MemoryRegion {
1280            host_region: ptr..(ptr + len),
1281            guest_region: guest_base..(guest_base + len),
1282            flags,
1283            region_type: MemoryRegionType::Heap,
1284        }
1285    }
1286
1287    #[cfg(target_os = "linux")]
1288    fn allocate_guest_memory() -> GuestSharedMemory {
1289        page_aligned_memory(b"test data for snapshot")
1290    }
1291
1292    #[test]
1293    #[cfg(target_os = "linux")]
1294    fn snapshot_restore_handles_remapping_correctly() {
1295        let mut sbox: MultiUseSandbox = {
1296            let path = simple_guest_as_string().unwrap();
1297            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1298            u_sbox.evolve().unwrap()
1299        };
1300
1301        // 1. Take snapshot 1 with no additional regions mapped
1302        let snapshot1 = sbox.snapshot().unwrap();
1303        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1304
1305        // 2. Map a memory region
1306        let map_mem = allocate_guest_memory();
1307        let guest_base = 0x200000000_usize;
1308        let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1309
1310        unsafe { sbox.map_region(&region).unwrap() };
1311        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1312        let orig_read = sbox
1313            .call::<Vec<u8>>(
1314                "ReadMappedBuffer",
1315                (
1316                    guest_base as u64,
1317                    hyperlight_common::vmem::PAGE_SIZE as u64,
1318                    true,
1319                ),
1320            )
1321            .unwrap();
1322
1323        // 3. Take snapshot 2 with 1 region mapped
1324        let snapshot2 = sbox.snapshot().unwrap();
1325        assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
1326
1327        // 4. Re(store to snapshot 1 (should unmap the region)
1328        sbox.restore(snapshot1.clone()).unwrap();
1329        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1330        let is_mapped = sbox
1331            .call::<bool>("CheckMapped", (guest_base as u64,))
1332            .unwrap();
1333        assert!(!is_mapped);
1334
1335        // 5. Restore forward to snapshot 2 (should have folded the
1336        //    region into the snapshot)
1337        sbox.restore(snapshot2.clone()).unwrap();
1338        assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
1339        let is_mapped = sbox
1340            .call::<bool>("CheckMapped", (guest_base as u64,))
1341            .unwrap();
1342        assert!(is_mapped);
1343
1344        // Verify the region is the same
1345        let new_read = sbox
1346            .call::<Vec<u8>>(
1347                "ReadMappedBuffer",
1348                (
1349                    guest_base as u64,
1350                    hyperlight_common::vmem::PAGE_SIZE as u64,
1351                    false,
1352                ),
1353            )
1354            .unwrap();
1355        assert_eq!(new_read, orig_read);
1356    }
1357
1358    #[test]
1359    fn snapshot_different_sandbox() {
1360        let mut sandbox = {
1361            let path = simple_guest_as_string().unwrap();
1362            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1363            u_sbox.evolve().unwrap()
1364        };
1365
1366        let mut sandbox2 = {
1367            let path = simple_guest_as_string().unwrap();
1368            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1369            u_sbox.evolve().unwrap()
1370        };
1371        assert_ne!(sandbox.id, sandbox2.id);
1372
1373        let snapshot = sandbox.snapshot().unwrap();
1374        let err = sandbox2.restore(snapshot.clone());
1375        assert!(matches!(err, Err(HyperlightError::SnapshotSandboxMismatch)));
1376
1377        let sandbox_id = sandbox.id;
1378        drop(sandbox);
1379        drop(sandbox2);
1380        drop(snapshot);
1381
1382        let sandbox3 = {
1383            let path = simple_guest_as_string().unwrap();
1384            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1385            u_sbox.evolve().unwrap()
1386        };
1387        assert_ne!(sandbox3.id, sandbox_id);
1388    }
1389
1390    /// Test that snapshot restore properly resets vCPU debug registers. This test verifies
1391    /// that restore() calls reset_vcpu().
1392    #[test]
1393    fn snapshot_restore_resets_debug_registers() {
1394        let mut sandbox: MultiUseSandbox = {
1395            let path = simple_guest_as_string().unwrap();
1396            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1397            u_sbox.evolve().unwrap()
1398        };
1399
1400        let snapshot = sandbox.snapshot().unwrap();
1401
1402        // Verify DR0 is initially 0 (clean state)
1403        let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap();
1404        assert_eq!(dr0_initial, 0, "DR0 should initially be 0");
1405
1406        // Dirty DR0 by setting it to a known non-zero value
1407        const DIRTY_VALUE: u64 = 0xDEAD_BEEF_CAFE_BABE;
1408        sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap();
1409        let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap();
1410        assert_eq!(
1411            dr0_dirty, DIRTY_VALUE,
1412            "DR0 should be dirty after SetDr0 call"
1413        );
1414
1415        // Restore to the snapshot - this should reset vCPU state including debug registers
1416        sandbox.restore(snapshot).unwrap();
1417
1418        let dr0_after_restore: u64 = sandbox.call("GetDr0", ()).unwrap();
1419        assert_eq!(
1420            dr0_after_restore, 0,
1421            "DR0 should be 0 after restore (reset_vcpu should have been called)"
1422        );
1423    }
1424
1425    /// Test that sandboxes can be created and evolved with different heap sizes
1426    #[test]
1427    fn test_sandbox_creation_various_sizes() {
1428        let test_cases: [(&str, u64); 3] = [
1429            ("small (8MB heap)", SMALL_HEAP_SIZE),
1430            ("medium (64MB heap)", MEDIUM_HEAP_SIZE),
1431            ("large (256MB heap)", LARGE_HEAP_SIZE),
1432        ];
1433
1434        for (name, heap_size) in test_cases {
1435            let mut cfg = SandboxConfiguration::default();
1436            cfg.set_heap_size(heap_size);
1437            cfg.set_scratch_size(0x100000);
1438
1439            let path = simple_guest_as_string().unwrap();
1440            let sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg))
1441                .unwrap_or_else(|e| panic!("Failed to create {} sandbox: {}", name, e))
1442                .evolve()
1443                .unwrap_or_else(|e| panic!("Failed to evolve {} sandbox: {}", name, e));
1444
1445            drop(sbox);
1446        }
1447    }
1448
1449    /// Helper: create a MultiUseSandbox from the simple guest with default config.
1450    #[cfg(feature = "trace_guest")]
1451    fn sandbox_for_gva_tests() -> MultiUseSandbox {
1452        let path = simple_guest_as_string().unwrap();
1453        UninitializedSandbox::new(GuestBinary::FilePath(path), None)
1454            .unwrap()
1455            .evolve()
1456            .unwrap()
1457    }
1458
1459    /// Helper: read memory at `gva` of length `len` from the guest side via
1460    /// `ReadMappedBuffer(gva, len, false)` and from the host side via
1461    /// `read_guest_memory_by_gva`, then assert both views are identical.
1462    #[cfg(feature = "trace_guest")]
1463    fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
1464        // Guest reads via its own page tables
1465        let expected: Vec<u8> = sbox
1466            .call("ReadMappedBuffer", (gva, len as u64, true))
1467            .unwrap();
1468        assert_eq!(expected.len(), len);
1469
1470        // Host reads by walking the same page tables
1471        let root_pt = sbox.vm.get_root_pt().unwrap();
1472        let actual = sbox
1473            .mem_mgr
1474            .read_guest_memory_by_gva(gva, len, root_pt)
1475            .unwrap();
1476
1477        assert_eq!(
1478            actual, expected,
1479            "read_guest_memory_by_gva at GVA {:#x} (len {}) differs from guest ReadMappedBuffer",
1480            gva, len,
1481        );
1482    }
1483
1484    /// Test reading a small buffer (< 1 page) from guest memory via GVA.
1485    /// Uses the guest code section which is already identity-mapped.
1486    #[test]
1487    #[cfg(feature = "trace_guest")]
1488    fn read_guest_memory_by_gva_single_page() {
1489        let mut sbox = sandbox_for_gva_tests();
1490        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
1491        assert_gva_read_matches(&mut sbox, code_gva, 128);
1492    }
1493
1494    /// Test reading exactly one full page (4096 bytes) from guest memory.
1495    /// Uses the guest code section
1496    #[test]
1497    #[cfg(feature = "trace_guest")]
1498    fn read_guest_memory_by_gva_full_page() {
1499        let mut sbox = sandbox_for_gva_tests();
1500        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
1501        assert_gva_read_matches(&mut sbox, code_gva, 4096);
1502    }
1503
1504    /// Test that a read starting at an odd (non-page-aligned) address and
1505    /// spanning two page boundaries returns correct data.
1506    #[test]
1507    #[cfg(feature = "trace_guest")]
1508    fn read_guest_memory_by_gva_unaligned_cross_page() {
1509        let mut sbox = sandbox_for_gva_tests();
1510        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
1511        // Start 1 byte before the second page boundary and read 4097 bytes
1512        // (spans 2 full page boundaries).
1513        let start = code_gva + 4096 - 1;
1514        println!(
1515            "Testing unaligned cross-page read starting at {:#x} spanning 4097 bytes",
1516            start
1517        );
1518        assert_gva_read_matches(&mut sbox, start, 4097);
1519    }
1520
1521    /// Test reading exactly two full pages (8192 bytes) from guest memory.
1522    #[test]
1523    #[cfg(feature = "trace_guest")]
1524    fn read_guest_memory_by_gva_two_full_pages() {
1525        let mut sbox = sandbox_for_gva_tests();
1526        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
1527        assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
1528    }
1529
1530    /// Test reading a region that spans across a page boundary: starts
1531    /// 100 bytes before the end of the first page and reads 200 bytes
1532    /// into the second page.
1533    #[test]
1534    #[cfg(feature = "trace_guest")]
1535    fn read_guest_memory_by_gva_cross_page_boundary() {
1536        let mut sbox = sandbox_for_gva_tests();
1537        let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
1538        // Start 100 bytes before the first page boundary, read across it.
1539        let start = code_gva + 4096 - 100;
1540        assert_gva_read_matches(&mut sbox, start, 200);
1541    }
1542}