Skip to main content

hyperlight_host/sandbox/
uninitialized.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use std::fmt::Debug;
5use std::option::Option;
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex};
8
9use tracing::{Span, instrument};
10use tracing_core::LevelFilter;
11
12use super::host_funcs::FunctionRegistry;
13use super::snapshot::Snapshot;
14use super::uninitialized_evolve::evolve_impl_multi_use;
15use crate::func::host_functions::{HostFunction, register_host_function};
16use crate::func::{ParameterTuple, SupportedReturnType};
17#[cfg(feature = "build-metadata")]
18use crate::log_build_details;
19use crate::mem::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionFlags};
20use crate::mem::mgr::SandboxMemoryManager;
21use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory};
22use crate::sandbox::SandboxConfiguration;
23use crate::{MultiUseSandbox, Result, new_error};
24
25#[cfg(any(crashdump, gdb))]
26#[derive(Clone, Debug, Default)]
27pub(crate) struct SandboxRuntimeConfig {
28    #[cfg(crashdump)]
29    pub(crate) binary_path: Option<PathBuf>,
30    #[cfg(gdb)]
31    pub(crate) debug_info: Option<super::config::DebugInfo>,
32    #[cfg(crashdump)]
33    pub(crate) guest_core_dump: bool,
34    /// The original entry point address of the loaded guest binary
35    /// (load_addr + ELF entry offset). Used for AT_ENTRY in core dumps
36    /// so GDB can compute the correct load offset for PIE binaries.
37    ///
38    /// `None` until resolved from the snapshot's `NextAction::Initialise`
39    /// in `set_up_hypervisor_partition`.
40    #[cfg(crashdump)]
41    pub(crate) entry_point: Option<u64>,
42}
43
44/// A preliminary sandbox that represents allocated memory and registered host functions,
45/// but has not yet created the underlying virtual machine.
46///
47/// This struct holds the configuration and setup needed for a sandbox without actually
48/// creating the VM. It allows you to:
49/// - Set up memory layout and load guest binary data
50/// - Register host functions that will be available to the guest
51/// - Configure sandbox settings before VM creation
52///
53/// The virtual machine is not created until you call [`evolve`](Self::evolve) to transform
54/// this into an initialized [`MultiUseSandbox`].
55pub struct UninitializedSandbox {
56    /// Registered host functions
57    pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
58    /// The memory manager for the sandbox.
59    pub(crate) mgr: SandboxMemoryManager<ExclusiveSharedMemory>,
60    pub(crate) max_guest_log_level: Option<LevelFilter>,
61    pub(crate) config: SandboxConfiguration,
62    #[cfg(any(crashdump, gdb))]
63    pub(crate) rt_cfg: SandboxRuntimeConfig,
64    pub(crate) load_info: crate::mem::exe::LoadInfo,
65    // This is needed to convey the stack pointer between the snapshot
66    // and the HyperlightVm creation
67    pub(crate) stack_top_gva: u64,
68    /// File mappings prepared by [`Self::map_file_cow`] that will be
69    /// applied to the VM during [`Self::evolve`].
70    pub(crate) pending_file_mappings: Vec<super::file_mapping::PreparedFileMapping>,
71}
72
73impl Debug for UninitializedSandbox {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("UninitializedSandbox")
76            .field("memory_layout", &self.mgr.layout)
77            .finish()
78    }
79}
80
81/// A `GuestBinary` is either a buffer or the file path to some data (e.g., a guest binary).
82#[derive(Debug)]
83pub enum GuestBinary {
84    /// A buffer containing the GuestBinary
85    Buffer(Vec<u8>),
86    /// A path to the GuestBinary
87    FilePath(PathBuf),
88}
89impl GuestBinary {
90    /// If the guest binary is identified by a file, canonicalise the path
91    ///
92    /// For [`GuestBinary::FilePath`], this resolves the path to its canonical
93    /// form. For [`GuestBinary::Buffer`], this method is a no-op.
94    /// TODO: Maybe we should make the GuestEnvironment or
95    ///       GuestBinary constructors crate-private and turn this
96    ///       into an invariant on one of those types.
97    pub fn canonicalize(&mut self) -> Result<()> {
98        if let GuestBinary::FilePath(p) = self {
99            *p = p
100                .canonicalize()
101                .map_err(|e| new_error!("GuestBinary not found: '{}': {}", p.display(), e))?;
102        }
103        Ok(())
104    }
105}
106
107/// A `GuestBlob` containing data and the permissions for its use.
108#[derive(Debug)]
109pub struct GuestBlob<'a> {
110    /// The data contained in the blob.
111    pub data: &'a [u8],
112    /// The permissions for the blob in memory.
113    /// By default, it's READ
114    pub permissions: MemoryRegionFlags,
115}
116
117impl<'a> From<&'a [u8]> for GuestBlob<'a> {
118    fn from(data: &'a [u8]) -> Self {
119        GuestBlob {
120            data,
121            permissions: DEFAULT_GUEST_BLOB_MEM_FLAGS,
122        }
123    }
124}
125
126/// Container for a guest binary and optional initialization data.
127///
128/// This struct combines a guest binary (either from a file or memory buffer) with
129/// optional data that will be available to the guest during execution.
130///
131/// The guest binary is owned. `'b` is the lifetime of the borrowed init data.
132#[derive(Debug)]
133pub struct GuestEnvironment<'b> {
134    /// The guest binary, which can be a file path or a buffer.
135    pub guest_binary: GuestBinary,
136    /// An optional guest blob, which can be used to provide additional data to the guest.
137    pub init_data: Option<GuestBlob<'b>>,
138}
139
140impl<'b> GuestEnvironment<'b> {
141    /// Creates a new `GuestEnvironment` with the given guest binary and an optional guest blob.
142    pub fn new(guest_binary: GuestBinary, init_data: Option<&'b [u8]>) -> Self {
143        GuestEnvironment {
144            guest_binary,
145            init_data: init_data.map(GuestBlob::from),
146        }
147    }
148}
149
150impl From<GuestBinary> for GuestEnvironment<'_> {
151    fn from(guest_binary: GuestBinary) -> Self {
152        GuestEnvironment {
153            guest_binary,
154            init_data: None,
155        }
156    }
157}
158
159impl UninitializedSandbox {
160    // Creates a new uninitialized sandbox from a pre-built snapshot.
161    // Note that since memory configuration is part of the snapshot the only configuration
162    // that can be changed (from the original snapshot) is the configuration defines the behaviour of
163    // `InterruptHandler` on Linux.
164    //
165    // This is ok for now as this is not a public function
166    fn from_snapshot(
167        snapshot: Arc<Snapshot>,
168        cfg: Option<SandboxConfiguration>,
169        #[cfg(crashdump)] binary_path: Option<PathBuf>,
170    ) -> Result<Self> {
171        #[cfg(feature = "build-metadata")]
172        log_build_details();
173
174        // hyperlight is only supported on Windows 11 and Windows Server 2022 and later
175        #[cfg(target_os = "windows")]
176        check_windows_version()?;
177
178        let sandbox_cfg = cfg.unwrap_or_default();
179
180        #[cfg(any(crashdump, gdb))]
181        let rt_cfg = {
182            #[cfg(crashdump)]
183            let guest_core_dump = sandbox_cfg.get_guest_core_dump();
184
185            #[cfg(gdb)]
186            let debug_info = sandbox_cfg.get_guest_debug_info();
187
188            SandboxRuntimeConfig {
189                #[cfg(crashdump)]
190                binary_path,
191                #[cfg(gdb)]
192                debug_info,
193                #[cfg(crashdump)]
194                guest_core_dump,
195                // entry_point is set later in set_up_hypervisor_partition
196                // once the entrypoint is resolved from the snapshot
197                #[cfg(crashdump)]
198                entry_point: None,
199            }
200        };
201
202        let mem_mgr_wrapper =
203            SandboxMemoryManager::<ExclusiveSharedMemory>::from_snapshot(snapshot.as_ref())?;
204
205        let host_funcs = Arc::new(Mutex::new(FunctionRegistry::with_default_host_print()));
206
207        let sandbox = Self {
208            host_funcs,
209            mgr: mem_mgr_wrapper,
210            max_guest_log_level: None,
211            config: sandbox_cfg,
212            #[cfg(any(crashdump, gdb))]
213            rt_cfg,
214            load_info: snapshot.load_info(),
215            stack_top_gva: snapshot.stack_top_gva(),
216            pending_file_mappings: Vec::new(),
217        };
218
219        crate::debug!("Sandbox created:  {:#?}", sandbox);
220
221        Ok(sandbox)
222    }
223
224    /// Creates a new uninitialized sandbox for the given guest environment.
225    ///
226    /// The guest binary can be provided as either a file path or memory buffer.
227    /// An optional configuration can customize memory sizes and sandbox settings.
228    /// After creation, register host functions using [`register`](Self::register)
229    /// before calling [`evolve`](Self::evolve) to complete initialization and create the VM.
230    #[instrument(
231        err(Debug),
232        skip(env),
233        parent = Span::current()
234    )]
235    pub fn new<'b>(
236        env: impl Into<GuestEnvironment<'b>>,
237        cfg: Option<SandboxConfiguration>,
238    ) -> Result<Self> {
239        let cfg = cfg.unwrap_or_default();
240        let env = env.into();
241        #[cfg(crashdump)]
242        let binary_path = match &env.guest_binary {
243            GuestBinary::FilePath(path) => Some(path.clone()),
244            GuestBinary::Buffer(_) => None,
245        };
246        let snapshot = Snapshot::from_env(env, cfg)?;
247        Self::from_snapshot(
248            Arc::new(snapshot),
249            Some(cfg),
250            #[cfg(crashdump)]
251            binary_path,
252        )
253    }
254
255    /// Creates and initializes the virtual machine, transforming this into a ready-to-use sandbox.
256    ///
257    /// This method consumes the `UninitializedSandbox` and performs the final initialization
258    /// steps to create the underlying virtual machine. Once evolved, the resulting
259    /// [`MultiUseSandbox`] can execute guest code and handle function calls.
260    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
261    pub fn evolve(self) -> Result<MultiUseSandbox> {
262        evolve_impl_multi_use(self)
263    }
264
265    /// Map the contents of a file into the guest at a particular address.
266    ///
267    /// The file mapping is prepared immediately (host-side OS work) but
268    /// the actual VM-side mapping is deferred until [`evolve()`](Self::evolve).
269    ///
270    /// The `guest_base` must be page-aligned and must lie **outside**
271    /// the sandbox's primary shared memory region (`BASE_ADDRESS` to
272    /// `BASE_ADDRESS + shared_mem_size`).
273    ///
274    /// Returns the length of the mapping in bytes.
275    #[instrument(err(Debug), skip(self, file_path, guest_base), parent = Span::current())]
276    pub fn map_file_cow(
277        &mut self,
278        file_path: &std::path::Path,
279        guest_base: u64,
280    ) -> crate::Result<u64> {
281        // Validate that guest_base is outside the sandbox's primary memory slot.
282        // (Full range check happens after prepare_file_cow when we know the mapped size.)
283        let shared_size = self.mgr.shared_mem.mem_size() as u64;
284        let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
285
286        let prepared = super::file_mapping::prepare_file_cow(file_path, guest_base)?;
287
288        // Validate full mapped range doesn't overlap shared memory.
289        let mapping_end = guest_base
290            .checked_add(prepared.size as u64)
291            .ok_or_else(|| {
292                crate::HyperlightError::Error(format!(
293                    "map_file_cow: guest address overflow: {:#x} + {:#x}",
294                    guest_base, prepared.size
295                ))
296            })?;
297        let shared_end = base_addr.checked_add(shared_size).ok_or_else(|| {
298            crate::HyperlightError::Error("shared memory end overflow".to_string())
299        })?;
300        if guest_base < shared_end && mapping_end > base_addr {
301            return Err(crate::HyperlightError::Error(format!(
302                "map_file_cow: mapping [{:#x}..{:#x}) overlaps sandbox shared memory [{:#x}..{:#x})",
303                guest_base, mapping_end, base_addr, shared_end,
304            )));
305        }
306
307        let size = prepared.size as u64;
308
309        // Check for overlaps with existing pending file mappings.
310        let new_start = guest_base;
311        let new_end = mapping_end;
312        for existing in &self.pending_file_mappings {
313            let ex_start = existing.guest_base;
314            let ex_end = ex_start.checked_add(existing.size as u64).ok_or_else(|| {
315                crate::HyperlightError::Error(format!(
316                    "map_file_cow: existing mapping address overflow: {:#x} + {:#x}",
317                    ex_start, existing.size
318                ))
319            })?;
320            if new_start < ex_end && new_end > ex_start {
321                return Err(crate::HyperlightError::Error(format!(
322                    "map_file_cow: mapping [{:#x}..{:#x}) overlaps existing mapping [{:#x}..{:#x})",
323                    new_start, new_end, ex_start, ex_end,
324                )));
325            }
326        }
327
328        self.pending_file_mappings.push(prepared);
329        Ok(size)
330    }
331
332    /// Returns the total size of the sandbox shared memory region in bytes.
333    ///
334    /// This is useful for placing file mappings at guest physical addresses
335    /// that don't overlap the primary shared memory slot.
336    pub fn shared_mem_size(&self) -> usize {
337        self.mgr.shared_mem.mem_size()
338    }
339
340    /// Sets the maximum log level for guest code execution.
341    ///
342    /// If not set, the log level is determined by the `RUST_LOG` environment variable,
343    /// defaulting to [`LevelFilter::Error`] if unset.
344    pub fn set_max_guest_log_level(&mut self, log_level: LevelFilter) {
345        self.max_guest_log_level = Some(log_level);
346    }
347
348    /// Registers a host function that the guest can call.
349    pub fn register<Args: ParameterTuple, Output: SupportedReturnType>(
350        &mut self,
351        name: impl AsRef<str>,
352        host_func: impl Into<HostFunction<Output, Args>>,
353    ) -> Result<()> {
354        register_host_function(host_func, self, name.as_ref())
355    }
356
357    /// Registers the special "HostPrint" function for guest printing.
358    ///
359    /// This overrides the default behavior of writing to stdout.
360    /// The function expects the signature `FnMut(String) -> i32`
361    /// and will be called when the guest wants to print output.
362    pub fn register_print(
363        &mut self,
364        print_func: impl Into<HostFunction<i32, (String,)>>,
365    ) -> Result<()> {
366        self.register("HostPrint", print_func)
367    }
368}
369// Check to see if the current version of Windows is supported
370// Hyperlight is only supported on Windows 11 and Windows Server 2022 and later
371#[cfg(target_os = "windows")]
372fn check_windows_version() -> Result<()> {
373    use windows_version::{OsVersion, is_server};
374    const WINDOWS_MAJOR: u32 = 10;
375    const WINDOWS_MINOR: u32 = 0;
376    const WINDOWS_PACK: u32 = 0;
377
378    // Windows Server 2022 has version numbers 10.0.20348 or greater
379    if is_server() {
380        if OsVersion::current() < OsVersion::new(WINDOWS_MAJOR, WINDOWS_MINOR, WINDOWS_PACK, 20348)
381        {
382            return Err(new_error!(
383                "Hyperlight Requires Windows Server 2022 or newer"
384            ));
385        }
386    } else if OsVersion::current()
387        < OsVersion::new(WINDOWS_MAJOR, WINDOWS_MINOR, WINDOWS_PACK, 22000)
388    {
389        return Err(new_error!("Hyperlight Requires Windows 11 or newer"));
390    }
391    Ok(())
392}
393
394#[cfg(test)]
395mod tests {
396    use std::sync::Arc;
397    use std::sync::mpsc::channel;
398    use std::{fs, thread};
399
400    use crossbeam_queue::ArrayQueue;
401    use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnValue};
402    use hyperlight_testing::simple_guest_as_pathbuf;
403
404    use crate::sandbox::SandboxConfiguration;
405    use crate::sandbox::uninitialized::{GuestBinary, GuestEnvironment};
406    use crate::{MultiUseSandbox, Result, UninitializedSandbox, new_error};
407
408    #[cfg(target_os = "linux")]
409    #[test]
410    fn guest_binary_loads_from_non_utf8_path() {
411        use std::ffi::OsString;
412        use std::os::unix::ffi::OsStringExt;
413
414        let temp_dir = tempfile::tempdir().unwrap();
415        let guest_path = temp_dir
416            .path()
417            .join(OsString::from_vec(b"guest-\xff".to_vec()));
418        fs::copy(simple_guest_as_pathbuf(), &guest_path).unwrap();
419
420        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(guest_path), None);
421
422        assert!(sandbox.is_ok());
423    }
424
425    #[test]
426    fn test_load_extra_blob() {
427        let binary_path = simple_guest_as_pathbuf();
428        let buffer = [0xde, 0xad, 0xbe, 0xef];
429        let guest_env =
430            GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), Some(&buffer));
431
432        let uninitialized_sandbox = UninitializedSandbox::new(guest_env, None).unwrap();
433        let mut sandbox = uninitialized_sandbox.evolve().unwrap();
434
435        let res = sandbox
436            .call::<Vec<u8>>("ReadFromUserMemory", (4u64, buffer.to_vec()))
437            .expect("Failed to call ReadFromUserMemory");
438
439        assert_eq!(res, buffer.to_vec());
440    }
441
442    #[test]
443    fn test_new_sandbox() {
444        // Guest Binary exists at path
445
446        let binary_path = simple_guest_as_pathbuf();
447        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(binary_path.clone()), None);
448        assert!(sandbox.is_ok());
449
450        // Guest Binary does not exist at path
451
452        let mut binary_path_does_not_exist = binary_path.clone();
453        binary_path_does_not_exist
454            .as_mut_os_string()
455            .push(".nonexistent");
456        let uninitialized_sandbox =
457            UninitializedSandbox::new(GuestBinary::FilePath(binary_path_does_not_exist), None);
458        assert!(uninitialized_sandbox.is_err());
459
460        // Non default memory configuration
461        let cfg = {
462            let mut cfg = SandboxConfiguration::default();
463            cfg.set_input_data_size(0x1000);
464            cfg.set_output_data_size(0x1000);
465            cfg.set_heap_size(0x1000);
466            Some(cfg)
467        };
468
469        let uninitialized_sandbox =
470            UninitializedSandbox::new(GuestBinary::FilePath(binary_path.clone()), cfg);
471        assert!(uninitialized_sandbox.is_ok());
472
473        let uninitialized_sandbox =
474            UninitializedSandbox::new(GuestBinary::FilePath(binary_path), None).unwrap();
475
476        // Get a Sandbox from an uninitialized sandbox without a call back function
477
478        let _sandbox = uninitialized_sandbox.evolve().unwrap();
479
480        // Test with a valid guest binary buffer
481
482        let binary_path = simple_guest_as_pathbuf();
483        let sandbox =
484            UninitializedSandbox::new(GuestBinary::Buffer(fs::read(binary_path).unwrap()), None);
485        assert!(sandbox.is_ok());
486
487        // Test with a invalid guest binary buffer
488
489        let binary_path = simple_guest_as_pathbuf();
490        let mut bytes = fs::read(binary_path).unwrap();
491        let _ = bytes.split_off(100);
492        let sandbox = UninitializedSandbox::new(GuestBinary::Buffer(bytes), None);
493        assert!(sandbox.is_err());
494    }
495
496    #[test]
497    fn test_host_functions() {
498        let uninitialized_sandbox = || {
499            UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
500                .unwrap()
501        };
502
503        // simple register + call
504        {
505            let mut usbox = uninitialized_sandbox();
506
507            usbox.register("test0", |arg: i32| Ok(arg + 1)).unwrap();
508
509            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
510            assert!(sandbox.is_ok());
511            let sandbox = sandbox.unwrap();
512
513            let host_funcs = sandbox
514                .host_funcs
515                .try_lock()
516                .map_err(|_| new_error!("Error locking"));
517
518            assert!(host_funcs.is_ok());
519
520            let res = host_funcs
521                .unwrap()
522                .call_host_function("test0", vec![ParameterValue::Int(1)])
523                .unwrap();
524
525            assert_eq!(res, ReturnValue::Int(2));
526        }
527
528        // multiple parameters register + call
529        {
530            let mut usbox = uninitialized_sandbox();
531
532            usbox.register("test1", |a: i32, b: i32| Ok(a + b)).unwrap();
533
534            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
535            assert!(sandbox.is_ok());
536            let sandbox = sandbox.unwrap();
537
538            let host_funcs = sandbox
539                .host_funcs
540                .try_lock()
541                .map_err(|_| new_error!("Error locking"));
542
543            assert!(host_funcs.is_ok());
544
545            let res = host_funcs
546                .unwrap()
547                .call_host_function(
548                    "test1",
549                    vec![ParameterValue::Int(1), ParameterValue::Int(2)],
550                )
551                .unwrap();
552
553            assert_eq!(res, ReturnValue::Int(3));
554        }
555
556        // incorrect arguments register + call
557        {
558            let mut usbox = uninitialized_sandbox();
559
560            usbox
561                .register("test2", |msg: String| {
562                    println!("test2 called: {}", msg);
563                    Ok(())
564                })
565                .unwrap();
566
567            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
568            assert!(sandbox.is_ok());
569            let sandbox = sandbox.unwrap();
570
571            let host_funcs = sandbox
572                .host_funcs
573                .try_lock()
574                .map_err(|_| new_error!("Error locking"));
575
576            assert!(host_funcs.is_ok());
577
578            let res = host_funcs.unwrap().call_host_function("test2", vec![]);
579            assert!(res.is_err());
580        }
581
582        // calling a function that doesn't exist
583        {
584            let usbox = uninitialized_sandbox();
585            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
586            assert!(sandbox.is_ok());
587            let sandbox = sandbox.unwrap();
588
589            let host_funcs = sandbox
590                .host_funcs
591                .try_lock()
592                .map_err(|_| new_error!("Error locking"));
593
594            assert!(host_funcs.is_ok());
595
596            let res = host_funcs.unwrap().call_host_function("test4", vec![]);
597            assert!(res.is_err());
598        }
599    }
600
601    #[test]
602    fn test_host_print() {
603        // writer as a FnMut closure mutating a captured variable and then trying to access the captured variable
604        // after the Sandbox instance has been dropped
605        // this example is fairly contrived but we should still support such an approach.
606
607        let (tx, rx) = channel();
608
609        let writer = move |msg| {
610            let _ = tx.send(msg);
611            Ok(0)
612        };
613
614        let mut sandbox =
615            UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
616                .expect("Failed to create sandbox");
617
618        sandbox
619            .register_print(writer)
620            .expect("Failed to register host print function");
621
622        let host_funcs = sandbox
623            .host_funcs
624            .try_lock()
625            .map_err(|_| new_error!("Error locking"));
626
627        assert!(host_funcs.is_ok());
628
629        host_funcs.unwrap().host_print("test".to_string()).unwrap();
630
631        drop(sandbox);
632
633        let received_msgs: Vec<_> = rx.into_iter().collect();
634        assert_eq!(received_msgs, ["test"]);
635
636        // There may be cases where a mutable reference to the captured variable is not required to be used outside the closure
637        // e.g. if the function is writing to a file or a socket etc.
638
639        // writer as a FnMut closure mutating a captured variable but not trying to access the captured variable
640
641        // This seems more realistic as the client is creating a file to be written to in the closure
642        // and then accessing the file a different handle.
643        // The problem is that captured_file still needs static lifetime so even though we can access the data through the second file handle
644        // this still does not work as the captured_file is dropped at the end of the function
645
646        // TODO: Currently, we block any writes that are not to
647        // the stdout/stderr file handles, so this code is commented
648        // out until we can register writer functions like any other
649        // host functions with their own set of extra allowed syscalls.
650        // In particular, this code should be brought back once we have addressed the issue
651
652        // let captured_file = Arc::new(Mutex::new(NamedTempFile::new().unwrap()));
653        // let capture_file_clone = captured_file.clone();
654        //
655        // let capture_file_lock = captured_file
656        //     .try_lock()
657        //     .map_err(|_| new_error!("Error locking"))
658        //     .unwrap();
659        // let mut file = capture_file_lock.reopen().unwrap();
660        // drop(capture_file_lock);
661        //
662        // let writer = move |msg: String| -> Result<i32> {
663        //     let mut captured_file = capture_file_clone
664        //         .try_lock()
665        //         .map_err(|_| new_error!("Error locking"))
666        //         .unwrap();
667        //     captured_file.write_all(msg.as_bytes()).unwrap();
668        //     Ok(0)
669        // };
670        //
671        // let writer_func = Arc::new(Mutex::new(writer));
672        //
673        // let sandbox = UninitializedSandbox::new(
674        //     GuestBinary::FilePath(simple_guest_as_pathbuf()),
675        //     None,
676        //     None,
677        //     Some(&writer_func),
678        // )
679        // .expect("Failed to create sandbox");
680        //
681        // let host_funcs = sandbox
682        //     .host_funcs
683        //     .try_lock()
684        //     .map_err(|_| new_error!("Error locking"));
685        //
686        // assert!(host_funcs.is_ok());
687        //
688        // host_funcs.unwrap().host_print("test2".to_string()).unwrap();
689        //
690        // let mut buffer = String::new();
691        // file.read_to_string(&mut buffer).unwrap();
692        // assert_eq!(buffer, "test2");
693
694        // writer as a function
695
696        fn fn_writer(msg: String) -> Result<i32> {
697            assert_eq!(msg, "test2");
698            Ok(0)
699        }
700
701        let mut sandbox =
702            UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
703                .expect("Failed to create sandbox");
704
705        sandbox
706            .register_print(fn_writer)
707            .expect("Failed to register host print function");
708
709        let host_funcs = sandbox
710            .host_funcs
711            .try_lock()
712            .map_err(|_| new_error!("Error locking"));
713
714        assert!(host_funcs.is_ok());
715
716        host_funcs.unwrap().host_print("test2".to_string()).unwrap();
717
718        // writer as a method
719
720        let mut test_host_print = TestHostPrint::new();
721
722        // create a closure over the struct method
723
724        let writer_closure = move |s| test_host_print.write(s);
725
726        let mut sandbox =
727            UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
728                .expect("Failed to create sandbox");
729
730        sandbox
731            .register_print(writer_closure)
732            .expect("Failed to register host print function");
733
734        let host_funcs = sandbox
735            .host_funcs
736            .try_lock()
737            .map_err(|_| new_error!("Error locking"));
738
739        assert!(host_funcs.is_ok());
740
741        host_funcs.unwrap().host_print("test3".to_string()).unwrap();
742    }
743
744    struct TestHostPrint {}
745
746    impl TestHostPrint {
747        fn new() -> Self {
748            TestHostPrint {}
749        }
750
751        fn write(&mut self, msg: String) -> Result<i32> {
752            assert_eq!(msg, "test3");
753            Ok(0)
754        }
755    }
756
757    #[test]
758    fn check_create_and_use_sandbox_on_different_threads() {
759        let unintializedsandbox_queue = Arc::new(ArrayQueue::<UninitializedSandbox>::new(10));
760        let sandbox_queue = Arc::new(ArrayQueue::<MultiUseSandbox>::new(10));
761
762        for i in 0..10 {
763            let simple_guest_path = simple_guest_as_pathbuf();
764            let unintializedsandbox = {
765                let err_string = format!("failed to create UninitializedSandbox {i}");
766                let err_str = err_string.as_str();
767                UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_path), None)
768                    .expect(err_str)
769            };
770
771            {
772                let err_string = format!("Failed to push UninitializedSandbox {i}");
773                let err_str = err_string.as_str();
774
775                unintializedsandbox_queue
776                    .push(unintializedsandbox)
777                    .expect(err_str);
778            }
779        }
780
781        let thread_handles = (0..10)
782            .map(|i| {
783                let uq = unintializedsandbox_queue.clone();
784                let sq = sandbox_queue.clone();
785                thread::spawn(move || {
786                    let uninitialized_sandbox = uq.pop().unwrap_or_else(|| {
787                        panic!("Failed to pop UninitializedSandbox thread {}", i)
788                    });
789
790                    let host_funcs = uninitialized_sandbox
791                        .host_funcs
792                        .try_lock()
793                        .map_err(|_| new_error!("Error locking"));
794
795                    assert!(host_funcs.is_ok());
796
797                    host_funcs
798                        .unwrap()
799                        .host_print(format!("Print from UninitializedSandbox on Thread {}\n", i))
800                        .unwrap();
801
802                    let sandbox = uninitialized_sandbox.evolve().unwrap_or_else(|_| {
803                        panic!("Failed to initialize UninitializedSandbox thread {}", i)
804                    });
805
806                    sq.push(sandbox).unwrap_or_else(|_| {
807                        panic!("Failed to push UninitializedSandbox thread {}", i)
808                    })
809                })
810            })
811            .collect::<Vec<_>>();
812
813        for handle in thread_handles {
814            handle.join().unwrap();
815        }
816
817        let thread_handles = (0..10)
818            .map(|i| {
819                let sq = sandbox_queue.clone();
820                thread::spawn(move || {
821                    let sandbox = sq
822                        .pop()
823                        .unwrap_or_else(|| panic!("Failed to pop Sandbox thread {}", i));
824
825                    let host_funcs = sandbox
826                        .host_funcs
827                        .try_lock()
828                        .map_err(|_| new_error!("Error locking"));
829
830                    assert!(host_funcs.is_ok());
831
832                    host_funcs
833                        .unwrap()
834                        .host_print(format!("Print from Sandbox on Thread {}\n", i))
835                        .unwrap();
836                })
837            })
838            .collect::<Vec<_>>();
839
840        for handle in thread_handles {
841            handle.join().unwrap();
842        }
843    }
844
845    /// Tests that tracing spans and events are properly emitted when a tracing subscriber is set.
846    ///
847    /// This test verifies:
848    /// 1. Spans are created with correct attributes (correlation_id)
849    /// 2. Nested spans from UninitializedSandbox::new are properly parented
850    /// 3. Error events are emitted when sandbox creation fails
851    ///
852    /// NOTE: The `#[instrument]` callsite on `UninitializedSandbox::new` uses
853    /// tracing's global interest cache. If another test thread registers that
854    /// callsite first (with the no-op subscriber), the cached `Interest::never()`
855    /// will suppress span creation on our thread. To work around this, we:
856    /// 1. Make a warmup call to force-register the callsite
857    /// 2. Call `rebuild_interest_cache()` to overwrite the cached interest with
858    ///    our subscriber's `Interest::sometimes()`
859    /// 3. Clear recorded state and run the real test
860    #[test]
861    #[cfg(feature = "build-metadata")]
862    fn test_trace_trace() {
863        use hyperlight_testing::tracing_subscriber::TracingSubscriber;
864        use tracing::Level;
865        use tracing_core::Subscriber;
866        use tracing_core::callsite::rebuild_interest_cache;
867        use uuid::Uuid;
868
869        /// Helper to extract a string value from nested JSON: obj["span"]["attributes"][key]
870        fn get_span_attr<'a>(span: &'a serde_json::Value, key: &str) -> Option<&'a str> {
871            span.get("span")?.get("attributes")?.get(key)?.as_str()
872        }
873
874        /// Helper to extract event field: obj["event"][field]
875        fn get_event_field<'a>(event: &'a serde_json::Value, field: &str) -> Option<&'a str> {
876            event.get("event")?.get(field)?.as_str()
877        }
878
879        /// Helper to extract event metadata field: obj["event"]["metadata"][field]
880        fn get_event_metadata<'a>(event: &'a serde_json::Value, field: &str) -> Option<&'a str> {
881            event.get("event")?.get("metadata")?.get(field)?.as_str()
882        }
883
884        let subscriber = TracingSubscriber::new(Level::TRACE);
885
886        tracing::subscriber::with_default(subscriber.clone(), || {
887            // Warmup: force-register the #[instrument] callsite on
888            // UninitializedSandbox::new by calling it once. This ensures the
889            // callsite exists in the global registry regardless of whether
890            // another thread already registered it.
891            let mut bad_path = simple_guest_as_pathbuf();
892            bad_path.as_mut_os_string().push("does_not_exist");
893            let _ = UninitializedSandbox::new(GuestBinary::FilePath(bad_path.clone()), None);
894
895            // Rebuild the interest cache. Now that the callsite is guaranteed
896            // to be registered, this will overwrite any cached Interest::never()
897            // (from another thread's no-op subscriber) with our subscriber's
898            // Interest::sometimes(), ensuring subsequent calls create spans.
899            rebuild_interest_cache();
900
901            // Clear all state from the warmup call
902            subscriber.clear();
903
904            let correlation_id = Uuid::new_v4().to_string();
905            let _span = tracing::error_span!("test_trace_logs", %correlation_id).entered();
906
907            // Verify we're in a span with correct name
908            let (test_span_id, span_meta) = subscriber
909                .current_span()
910                .into_inner()
911                .expect("Should be inside a span");
912            assert_eq!(span_meta.name(), "test_trace_logs");
913
914            // Verify correlation_id was recorded
915            let span_data = subscriber.get_span(test_span_id.into_u64());
916            let recorded_id =
917                get_span_attr(&span_data, "correlation_id").expect("correlation_id not found");
918            assert_eq!(recorded_id, correlation_id);
919
920            // Try to create a sandbox with a non-existent binary - this should fail
921            // and emit an error event
922            let result = UninitializedSandbox::new(GuestBinary::FilePath(bad_path), None);
923            assert!(result.is_err(), "Sandbox creation should fail");
924
925            // Verify we're still in our test span
926            let (current_id, _) = subscriber
927                .current_span()
928                .into_inner()
929                .expect("Should still be inside a span");
930            assert_eq!(
931                current_id.into_u64(),
932                test_span_id.into_u64(),
933                "Should still be in the test span"
934            );
935
936            // Verify a span named "new" was created by UninitializedSandbox::new
937            // (look up by name rather than hardcoded ID to avoid fragility)
938            let all_spans = subscriber.get_all_spans();
939            let _new_span_entry = all_spans
940                .iter()
941                .find(|&(&id, _)| {
942                    id != test_span_id.into_u64()
943                        && subscriber.get_span_metadata(id).name() == "new"
944                })
945                .expect("Expected a span named 'new' from UninitializedSandbox::new");
946
947            // Verify the error event was emitted
948            let events = subscriber.get_events();
949            assert_eq!(events.len(), 1, "Expected exactly one error event");
950
951            let event = &events[0];
952            let level = get_event_metadata(event, "level").expect("event should have level");
953            let error = get_event_field(event, "error").expect("event should have error field");
954            let target = get_event_metadata(event, "target").expect("event should have target");
955            let module_path =
956                get_event_metadata(event, "module_path").expect("event should have module_path");
957
958            assert_eq!(level, "ERROR");
959            assert!(
960                error.contains("GuestBinary not found"),
961                "Error should mention 'GuestBinary not found', got: {error}"
962            );
963            assert_eq!(target, "hyperlight_host::sandbox::uninitialized");
964            assert_eq!(module_path, "hyperlight_host::sandbox::uninitialized");
965        });
966    }
967
968    #[test]
969    #[ignore]
970    // Tests that traces are emitted as log records when there is no trace
971    // subscriber configured.
972    #[cfg(feature = "build-metadata")]
973    fn test_log_trace() {
974        use std::path::PathBuf;
975
976        use hyperlight_testing::logger::{LOGGER as TEST_LOGGER, Logger as TestLogger};
977        use log::Level;
978        use tracing_core::callsite::rebuild_interest_cache;
979
980        {
981            TestLogger::initialize_test_logger();
982            TEST_LOGGER.set_max_level(log::LevelFilter::Trace);
983
984            // This makes sure that the metadata interest cache is rebuilt so that
985            // the log records are emitted for the trace records
986
987            rebuild_interest_cache();
988
989            let mut invalid_binary_path = simple_guest_as_pathbuf();
990            invalid_binary_path
991                .as_mut_os_string()
992                .push("does_not_exist");
993
994            let sbox = UninitializedSandbox::new(GuestBinary::FilePath(invalid_binary_path), None);
995            assert!(sbox.is_err());
996
997            // When tracing is creating log records it will create a log
998            // record for the creation of the span (from the instrument
999            // attribute), and will then create a log record for the entry to
1000            // and exit from the span.
1001            //
1002            // It also creates a log record for the span being dropped.
1003            //
1004            // In addition there are 14 info log records created for build information
1005            //
1006            // So we expect 19 log records for this test, four for the span and
1007            // then one for the error as the file that we are attempting to
1008            // load into the sandbox does not exist, plus the 14 info log records
1009
1010            let num_calls = TEST_LOGGER.num_log_calls();
1011            assert_eq!(13, num_calls);
1012
1013            // Log record 1
1014
1015            let logcall = TEST_LOGGER.get_log_call(0).unwrap();
1016            assert_eq!(Level::Info, logcall.level);
1017
1018            assert!(logcall.args.starts_with("new; cfg"));
1019            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1020
1021            // Log record 2
1022
1023            let logcall = TEST_LOGGER.get_log_call(1).unwrap();
1024            assert_eq!(Level::Trace, logcall.level);
1025            assert_eq!(logcall.args, "-> new;");
1026            assert_eq!("tracing::span::active", logcall.target);
1027
1028            // Log record 17
1029
1030            let logcall = TEST_LOGGER.get_log_call(10).unwrap();
1031            assert_eq!(Level::Error, logcall.level);
1032            assert!(
1033                logcall
1034                    .args
1035                    .starts_with("error=Error(\"GuestBinary not found:")
1036            );
1037            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1038
1039            // Log record 18
1040
1041            let logcall = TEST_LOGGER.get_log_call(11).unwrap();
1042            assert_eq!(Level::Trace, logcall.level);
1043            assert_eq!(logcall.args, "<- new;");
1044            assert_eq!("tracing::span::active", logcall.target);
1045
1046            // Log record 19
1047
1048            let logcall = TEST_LOGGER.get_log_call(12).unwrap();
1049            assert_eq!(Level::Trace, logcall.level);
1050            assert_eq!(logcall.args, "-- new;");
1051            assert_eq!("tracing::span", logcall.target);
1052        }
1053        {
1054            // test to ensure an invalid binary logs & traces properly
1055            TEST_LOGGER.clear_log_calls();
1056            TEST_LOGGER.set_max_level(log::LevelFilter::Info);
1057
1058            let mut valid_binary_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1059            valid_binary_path.push("src");
1060            valid_binary_path.push("sandbox");
1061            valid_binary_path.push("initialized.rs");
1062
1063            let sbox = UninitializedSandbox::new(GuestBinary::FilePath(valid_binary_path), None);
1064            assert!(sbox.is_err());
1065
1066            // There should be 2 calls this time when we change to the log
1067            // LevelFilter to Info.
1068            let num_calls = TEST_LOGGER.num_log_calls();
1069            assert_eq!(2, num_calls);
1070
1071            // Log record 1
1072
1073            let logcall = TEST_LOGGER.get_log_call(0).unwrap();
1074            assert_eq!(Level::Info, logcall.level);
1075
1076            assert!(logcall.args.starts_with("new; cfg"));
1077            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1078
1079            // Log record 2
1080
1081            let logcall = TEST_LOGGER.get_log_call(1).unwrap();
1082            assert_eq!(Level::Error, logcall.level);
1083            assert!(
1084                logcall
1085                    .args
1086                    .starts_with("error=Error(\"GuestBinary not found:")
1087            );
1088            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1089        }
1090        {
1091            TEST_LOGGER.clear_log_calls();
1092            TEST_LOGGER.set_max_level(log::LevelFilter::Error);
1093
1094            let sbox = {
1095                let res = UninitializedSandbox::new(
1096                    GuestBinary::FilePath(simple_guest_as_pathbuf()),
1097                    None,
1098                );
1099                res.unwrap()
1100            };
1101            let _: Result<MultiUseSandbox> = sbox.evolve();
1102
1103            let num_calls = TEST_LOGGER.num_log_calls();
1104
1105            assert_eq!(0, num_calls);
1106        }
1107    }
1108
1109    #[test]
1110    fn test_invalid_path() {
1111        let invalid_path = "some/path/that/does/not/exist";
1112        let sbox = UninitializedSandbox::new(GuestBinary::FilePath(invalid_path.into()), None);
1113        println!("{:?}", sbox);
1114        #[cfg(target_os = "windows")]
1115        assert!(
1116            matches!(sbox, Err(e) if e.to_string().contains("GuestBinary not found: 'some/path/that/does/not/exist': The system cannot find the path specified. (os error 3)"))
1117        );
1118        #[cfg(target_os = "linux")]
1119        assert!(
1120            matches!(sbox, Err(e) if e.to_string().contains("GuestBinary not found: 'some/path/that/does/not/exist': No such file or directory (os error 2)"))
1121        );
1122    }
1123
1124    #[test]
1125    fn test_from_snapshot_various_configurations() {
1126        use crate::sandbox::snapshot::Snapshot;
1127
1128        let binary_path = simple_guest_as_pathbuf();
1129
1130        // Test 1: Create snapshot with default config, create multiple sandboxes from it
1131        {
1132            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1133
1134            let snapshot = Arc::new(
1135                Snapshot::from_env(env, Default::default())
1136                    .expect("Failed to create snapshot with default config"),
1137            );
1138
1139            // Create first sandbox from snapshot
1140            let sandbox1 = UninitializedSandbox::from_snapshot(
1141                snapshot.clone(),
1142                None,
1143                #[cfg(crashdump)]
1144                Some(binary_path.clone()),
1145            )
1146            .expect("Failed to create first sandbox from snapshot");
1147
1148            // Create second sandbox from same snapshot
1149            let sandbox2 = UninitializedSandbox::from_snapshot(
1150                snapshot.clone(),
1151                None,
1152                #[cfg(crashdump)]
1153                Some(binary_path.clone()),
1154            )
1155            .expect("Failed to create second sandbox from snapshot");
1156
1157            // Both should be able to evolve independently
1158            let _evolved1 = sandbox1.evolve().expect("Failed to evolve sandbox1");
1159            let _evolved2 = sandbox2.evolve().expect("Failed to evolve sandbox2");
1160        }
1161
1162        // Test 2: Create snapshot with custom heap size
1163        {
1164            let mut cfg = SandboxConfiguration::default();
1165            cfg.set_heap_size(16 * 1024 * 1024); // 16MB heap
1166
1167            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1168
1169            let snapshot = Arc::new(
1170                Snapshot::from_env(env, cfg)
1171                    .expect("Failed to create snapshot with custom heap size"),
1172            );
1173
1174            let sandbox = UninitializedSandbox::from_snapshot(
1175                snapshot,
1176                None,
1177                #[cfg(crashdump)]
1178                Some(binary_path.clone()),
1179            )
1180            .expect("Failed to create sandbox from snapshot with custom heap");
1181
1182            let _evolved = sandbox.evolve().expect("Failed to evolve sandbox");
1183        }
1184
1185        // Test 3: Create snapshot with custom scratch size
1186        {
1187            let mut cfg = SandboxConfiguration::default();
1188            cfg.set_scratch_size(256 * 1024); // 256KB scratch
1189
1190            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1191
1192            let snapshot = Arc::new(
1193                Snapshot::from_env(env, cfg)
1194                    .expect("Failed to create snapshot with custom stack size"),
1195            );
1196
1197            let sandbox = UninitializedSandbox::from_snapshot(
1198                snapshot,
1199                None,
1200                #[cfg(crashdump)]
1201                Some(binary_path.clone()),
1202            )
1203            .expect("Failed to create sandbox from snapshot with custom stack");
1204
1205            let _evolved = sandbox.evolve().expect("Failed to evolve sandbox");
1206        }
1207
1208        // Test 4: Create snapshot with custom input/output buffer sizes
1209        {
1210            let mut cfg = SandboxConfiguration::default();
1211            cfg.set_input_data_size(64 * 1024); // 64KB input
1212            cfg.set_output_data_size(64 * 1024); // 64KB output
1213
1214            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1215
1216            let snapshot = Arc::new(
1217                Snapshot::from_env(env, cfg)
1218                    .expect("Failed to create snapshot with custom buffer sizes"),
1219            );
1220
1221            let sandbox = UninitializedSandbox::from_snapshot(
1222                snapshot,
1223                None,
1224                #[cfg(crashdump)]
1225                Some(binary_path.clone()),
1226            )
1227            .expect("Failed to create sandbox from snapshot with custom buffers");
1228
1229            let _evolved = sandbox.evolve().expect("Failed to evolve sandbox");
1230        }
1231
1232        // Test 5: Create snapshot with all custom settings
1233        {
1234            let mut cfg = SandboxConfiguration::default();
1235            cfg.set_heap_size(32 * 1024 * 1024); // 32MB heap
1236            cfg.set_scratch_size(256 * 1024 * 2); // 512KB scratch (256KB will be input/output)
1237            cfg.set_input_data_size(128 * 1024); // 128KB input
1238            cfg.set_output_data_size(128 * 1024); // 128KB output
1239
1240            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1241
1242            let snapshot = Arc::new(
1243                Snapshot::from_env(env, cfg)
1244                    .expect("Failed to create snapshot with all custom settings"),
1245            );
1246
1247            // Create multiple sandboxes from the same snapshot
1248            let sandbox1 = UninitializedSandbox::from_snapshot(
1249                snapshot.clone(),
1250                None,
1251                #[cfg(crashdump)]
1252                Some(binary_path.clone()),
1253            )
1254            .expect("Failed to create sandbox1 from fully customized snapshot");
1255            let sandbox2 = UninitializedSandbox::from_snapshot(
1256                snapshot.clone(),
1257                None,
1258                #[cfg(crashdump)]
1259                Some(binary_path.clone()),
1260            )
1261            .expect("Failed to create sandbox2 from fully customized snapshot");
1262            let sandbox3 = UninitializedSandbox::from_snapshot(
1263                snapshot.clone(),
1264                None,
1265                #[cfg(crashdump)]
1266                Some(binary_path.clone()),
1267            )
1268            .expect("Failed to create sandbox3 from fully customized snapshot");
1269
1270            let _evolved1 = sandbox1.evolve().expect("Failed to evolve sandbox1");
1271            let _evolved2 = sandbox2.evolve().expect("Failed to evolve sandbox2");
1272            let _evolved3 = sandbox3.evolve().expect("Failed to evolve sandbox3");
1273        }
1274
1275        // Test 6: Create snapshot from binary buffer instead of file path
1276        {
1277            let binary_bytes = fs::read(&binary_path).expect("Failed to read binary file");
1278
1279            let snapshot = Arc::new(
1280                Snapshot::from_env(GuestBinary::Buffer(binary_bytes), Default::default())
1281                    .expect("Failed to create snapshot from buffer"),
1282            );
1283
1284            let sandbox = UninitializedSandbox::from_snapshot(
1285                snapshot,
1286                None,
1287                #[cfg(crashdump)]
1288                None,
1289            )
1290            .expect("Failed to create sandbox from buffer-based snapshot");
1291
1292            let _evolved = sandbox.evolve().expect("Failed to evolve sandbox");
1293        }
1294
1295        // Test 7: Register host functions on sandboxes created from snapshot
1296        {
1297            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1298
1299            let snapshot = Arc::new(
1300                Snapshot::from_env(env, Default::default()).expect("Failed to create snapshot"),
1301            );
1302
1303            let mut sandbox = UninitializedSandbox::from_snapshot(
1304                snapshot,
1305                None,
1306                #[cfg(crashdump)]
1307                Some(binary_path.clone()),
1308            )
1309            .expect("Failed to create sandbox from snapshot");
1310
1311            // Register a custom host function
1312            sandbox
1313                .register("CustomAdd", |a: i32, b: i32| Ok(a + b))
1314                .expect("Failed to register custom function");
1315
1316            let evolved = sandbox.evolve().expect("Failed to evolve sandbox");
1317
1318            // Verify the host function was registered
1319            let host_funcs = evolved
1320                .host_funcs
1321                .try_lock()
1322                .expect("Failed to lock host funcs");
1323
1324            let result = host_funcs
1325                .call_host_function(
1326                    "CustomAdd",
1327                    vec![ParameterValue::Int(10), ParameterValue::Int(20)],
1328                )
1329                .expect("Failed to call CustomAdd");
1330
1331            assert_eq!(result, ReturnValue::Int(30));
1332        }
1333
1334        // Test 8: Create snapshot with init data (guest blob)
1335        {
1336            let init_data = [0xCA, 0xFE, 0xBA, 0xBE];
1337            let guest_env =
1338                GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), Some(&init_data));
1339
1340            let snapshot = Arc::new(
1341                Snapshot::from_env(guest_env, Default::default())
1342                    .expect("Failed to create snapshot with init data"),
1343            );
1344
1345            let sandbox = UninitializedSandbox::from_snapshot(
1346                snapshot,
1347                None,
1348                #[cfg(crashdump)]
1349                Some(binary_path.clone()),
1350            )
1351            .expect("Failed to create sandbox from snapshot with init data");
1352
1353            let _evolved = sandbox.evolve().expect("Failed to evolve sandbox");
1354        }
1355
1356        // Test 9: Create snapshot from existing sandbox
1357        {
1358            let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None);
1359            let orig_snapshot = Arc::new(
1360                Snapshot::from_env(env, Default::default())
1361                    .expect("Failed to create snapshot with default config"),
1362            );
1363            let orig_sandbox = UninitializedSandbox::from_snapshot(
1364                orig_snapshot,
1365                None,
1366                #[cfg(crashdump)]
1367                Some(binary_path.clone()),
1368            )
1369            .expect("Failed to create orig_sandbox");
1370            let mut initialized_sandbox = orig_sandbox
1371                .evolve()
1372                .expect("Failed to evolve orig_sandbox");
1373            let new_snapshot = initialized_sandbox
1374                .snapshot()
1375                .expect("Failed to create new_snapshot");
1376            let new_sandbox = UninitializedSandbox::from_snapshot(
1377                new_snapshot,
1378                None,
1379                #[cfg(crashdump)]
1380                Some(binary_path.clone()),
1381            )
1382            .expect("Failed to create new_sandbox");
1383            let _evolved = new_sandbox.evolve().expect("Failed to evolve new_sandbox");
1384        }
1385    }
1386}