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