Skip to main content

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