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 log::LevelFilter;
23use tracing::{Span, instrument};
24
25use super::host_funcs::{FunctionRegistry, default_writer_func};
26use super::mem_mgr::MemMgrWrapper;
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::exe::ExeInfo;
33use crate::mem::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionFlags};
34use crate::mem::mgr::{STACK_COOKIE_LEN, SandboxMemoryManager};
35use crate::mem::shared_mem::ExclusiveSharedMemory;
36use crate::sandbox::SandboxConfiguration;
37use crate::{MultiUseSandbox, Result, new_error};
38
39#[cfg(all(target_os = "linux", feature = "seccomp"))]
40const EXTRA_ALLOWED_SYSCALLS_FOR_WRITER_FUNC: &[super::ExtraAllowedSyscall] = &[
41    // Fuzzing fails without `mmap` being an allowed syscall on our seccomp filter.
42    // All fuzzing does is call `PrintOutput` (which calls `HostPrint` ). Thing is, `println!`
43    // is designed to be thread-safe in Rust and the std lib ensures this by using
44    // buffered I/O, which I think relies on `mmap`. This gets surfaced in fuzzing with an
45    // OOM error, which I think is happening because `println!` is not being able to allocate
46    // more memory for its buffers for the fuzzer's huge inputs.
47    libc::SYS_mmap,
48    libc::SYS_brk,
49    libc::SYS_mprotect,
50    #[cfg(mshv)]
51    libc::SYS_close,
52];
53
54#[cfg(any(crashdump, gdb))]
55#[derive(Clone, Debug, Default)]
56pub(crate) struct SandboxRuntimeConfig {
57    #[cfg(crashdump)]
58    pub(crate) binary_path: Option<String>,
59    #[cfg(gdb)]
60    pub(crate) debug_info: Option<super::config::DebugInfo>,
61    #[cfg(crashdump)]
62    pub(crate) guest_core_dump: bool,
63}
64
65/// A preliminary sandbox that represents allocated memory and registered host functions,
66/// but has not yet created the underlying virtual machine.
67///
68/// This struct holds the configuration and setup needed for a sandbox without actually
69/// creating the VM. It allows you to:
70/// - Set up memory layout and load guest binary data
71/// - Register host functions that will be available to the guest
72/// - Configure sandbox settings before VM creation
73///
74/// The virtual machine is not created until you call [`evolve`](Self::evolve) to transform
75/// this into an initialized [`MultiUseSandbox`].
76pub struct UninitializedSandbox {
77    /// Registered host functions
78    pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
79    /// The memory manager for the sandbox.
80    pub(crate) mgr: MemMgrWrapper<ExclusiveSharedMemory>,
81    pub(crate) max_guest_log_level: Option<LevelFilter>,
82    pub(crate) config: SandboxConfiguration,
83    #[cfg(any(crashdump, gdb))]
84    pub(crate) rt_cfg: SandboxRuntimeConfig,
85    pub(crate) load_info: crate::mem::exe::LoadInfo,
86}
87
88impl Debug for UninitializedSandbox {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("UninitializedSandbox")
91            .field("memory_layout", &self.mgr.unwrap_mgr().layout)
92            .finish()
93    }
94}
95
96impl UninitializedSandbox {
97    /// Creates and initializes the virtual machine, transforming this into a ready-to-use sandbox.
98    ///
99    /// This method consumes the `UninitializedSandbox` and performs the final initialization
100    /// steps to create the underlying virtual machine. Once evolved, the resulting
101    /// [`MultiUseSandbox`] can execute guest code and handle function calls.
102    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
103    pub fn evolve(self) -> Result<MultiUseSandbox> {
104        evolve_impl_multi_use(self)
105    }
106}
107
108/// A `GuestBinary` is either a buffer or the file path to some data (e.g., a guest binary).
109#[derive(Debug)]
110pub enum GuestBinary<'a> {
111    /// A buffer containing the GuestBinary
112    Buffer(&'a [u8]),
113    /// A path to the GuestBinary
114    FilePath(String),
115}
116
117/// A `GuestBlob` containing data and the permissions for its use.
118#[derive(Debug)]
119pub struct GuestBlob<'a> {
120    /// The data contained in the blob.
121    pub data: &'a [u8],
122    /// The permissions for the blob in memory.
123    /// By default, it's READ
124    pub permissions: MemoryRegionFlags,
125}
126
127impl<'a> From<&'a [u8]> for GuestBlob<'a> {
128    fn from(data: &'a [u8]) -> Self {
129        GuestBlob {
130            data,
131            permissions: DEFAULT_GUEST_BLOB_MEM_FLAGS,
132        }
133    }
134}
135
136/// Container for a guest binary and optional initialization data.
137///
138/// This struct combines a guest binary (either from a file or memory buffer) with
139/// optional data that will be available to the guest during execution.
140#[derive(Debug)]
141pub struct GuestEnvironment<'a, 'b> {
142    /// The guest binary, which can be a file path or a buffer.
143    pub guest_binary: GuestBinary<'a>,
144    /// An optional guest blob, which can be used to provide additional data to the guest.
145    pub init_data: Option<GuestBlob<'b>>,
146}
147
148impl<'a, 'b> GuestEnvironment<'a, 'b> {
149    /// Creates a new `GuestEnvironment` with the given guest binary and an optional guest blob.
150    pub fn new(guest_binary: GuestBinary<'a>, init_data: Option<&'b [u8]>) -> Self {
151        GuestEnvironment {
152            guest_binary,
153            init_data: init_data.map(GuestBlob::from),
154        }
155    }
156}
157
158impl<'a> From<GuestBinary<'a>> for GuestEnvironment<'a, '_> {
159    fn from(guest_binary: GuestBinary<'a>) -> Self {
160        GuestEnvironment {
161            guest_binary,
162            init_data: None,
163        }
164    }
165}
166
167impl UninitializedSandbox {
168    /// Creates a new uninitialized sandbox for the given guest environment.
169    ///
170    /// The guest binary can be provided as either a file path or memory buffer.
171    /// An optional configuration can customize memory sizes and sandbox settings.
172    /// After creation, register host functions using [`register`](Self::register)
173    /// before calling [`evolve`](Self::evolve) to complete initialization and create the VM.
174    #[instrument(
175        err(Debug),
176        skip(env),
177        parent = Span::current()
178    )]
179    pub fn new<'a, 'b>(
180        env: impl Into<GuestEnvironment<'a, 'b>>,
181        cfg: Option<SandboxConfiguration>,
182    ) -> Result<Self> {
183        #[cfg(feature = "build-metadata")]
184        log_build_details();
185
186        // hyperlight is only supported on Windows 11 and Windows Server 2022 and later
187        #[cfg(target_os = "windows")]
188        check_windows_version()?;
189
190        let env: GuestEnvironment<'_, '_> = env.into();
191        let guest_binary = env.guest_binary;
192        let guest_blob = env.init_data;
193
194        // If the guest binary is a file make sure it exists
195        let guest_binary = match guest_binary {
196            GuestBinary::FilePath(binary_path) => {
197                let path = Path::new(&binary_path)
198                    .canonicalize()
199                    .map_err(|e| new_error!("GuestBinary not found: '{}': {}", binary_path, e))?
200                    .into_os_string()
201                    .into_string()
202                    .map_err(|e| new_error!("Error converting OsString to String: {:?}", e))?;
203
204                GuestBinary::FilePath(path)
205            }
206            buffer @ GuestBinary::Buffer(_) => buffer,
207        };
208
209        let sandbox_cfg = cfg.unwrap_or_default();
210
211        #[cfg(any(crashdump, gdb))]
212        let rt_cfg = {
213            #[cfg(crashdump)]
214            let guest_core_dump = sandbox_cfg.get_guest_core_dump();
215
216            #[cfg(gdb)]
217            let debug_info = sandbox_cfg.get_guest_debug_info();
218
219            #[cfg(crashdump)]
220            let binary_path = if let GuestBinary::FilePath(ref path) = guest_binary {
221                Some(path.clone())
222            } else {
223                None
224            };
225
226            SandboxRuntimeConfig {
227                #[cfg(crashdump)]
228                binary_path,
229                #[cfg(gdb)]
230                debug_info,
231                #[cfg(crashdump)]
232                guest_core_dump,
233            }
234        };
235
236        let (mut mem_mgr_wrapper, load_info) = {
237            let (mut mgr, load_info) = UninitializedSandbox::load_guest_binary(
238                sandbox_cfg,
239                &guest_binary,
240                guest_blob.as_ref(),
241            )?;
242
243            let stack_guard = Self::create_stack_guard();
244            mgr.set_stack_guard(&stack_guard)?;
245            (MemMgrWrapper::new(mgr, stack_guard), load_info)
246        };
247
248        mem_mgr_wrapper.write_memory_layout()?;
249
250        // if env has a guest blob, load it into shared mem
251        if let Some(blob) = guest_blob {
252            mem_mgr_wrapper.write_init_data(blob.data)?;
253        }
254
255        let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default()));
256
257        let mut sandbox = Self {
258            host_funcs,
259            mgr: mem_mgr_wrapper,
260            max_guest_log_level: None,
261            config: sandbox_cfg,
262            #[cfg(any(crashdump, gdb))]
263            rt_cfg,
264            load_info,
265        };
266
267        // If we were passed a writer for host print register it otherwise use the default.
268        sandbox.register_print(default_writer_func)?;
269
270        crate::debug!("Sandbox created:  {:#?}", sandbox);
271
272        Ok(sandbox)
273    }
274
275    #[instrument(skip_all, parent = Span::current(), level = "Trace")]
276    fn create_stack_guard() -> [u8; STACK_COOKIE_LEN] {
277        rand::random::<[u8; STACK_COOKIE_LEN]>()
278    }
279
280    /// Load the file at `bin_path_str` into a PE file, then attempt to
281    /// load the PE file into a `SandboxMemoryManager` and return it.
282    ///
283    /// If `run_from_guest_binary` is passed as `true`, and this code is
284    /// running on windows, this function will call
285    /// `SandboxMemoryManager::load_guest_binary_using_load_library` to
286    /// create the new `SandboxMemoryManager`. If `run_from_guest_binary` is
287    /// passed as `true` and we're not running on windows, this function will
288    /// return an `Err`. Otherwise, if `run_from_guest_binary` is passed
289    /// as `false`, this function calls `SandboxMemoryManager::load_guest_binary_into_memory`.
290    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
291    pub(super) fn load_guest_binary(
292        cfg: SandboxConfiguration,
293        guest_binary: &GuestBinary,
294        guest_blob: Option<&GuestBlob>,
295    ) -> Result<(
296        SandboxMemoryManager<ExclusiveSharedMemory>,
297        crate::mem::exe::LoadInfo,
298    )> {
299        let exe_info = match guest_binary {
300            GuestBinary::FilePath(bin_path_str) => ExeInfo::from_file(bin_path_str)?,
301            GuestBinary::Buffer(buffer) => ExeInfo::from_buf(buffer)?,
302        };
303
304        SandboxMemoryManager::load_guest_binary_into_memory(cfg, exe_info, guest_blob)
305    }
306
307    /// Sets the maximum log level for guest code execution.
308    ///
309    /// If not set, the log level is determined by the `RUST_LOG` environment variable,
310    /// defaulting to [`LevelFilter::Error`] if unset.
311    pub fn set_max_guest_log_level(&mut self, log_level: LevelFilter) {
312        self.max_guest_log_level = Some(log_level);
313    }
314
315    /// Registers a host function that the guest can call.
316    pub fn register<Args: ParameterTuple, Output: SupportedReturnType>(
317        &mut self,
318        name: impl AsRef<str>,
319        host_func: impl Into<HostFunction<Output, Args>>,
320    ) -> Result<()> {
321        register_host_function(host_func, self, name.as_ref(), None)
322    }
323
324    /// Registers a host function with additional allowed syscalls during execution.
325    ///
326    /// Unlike [`register`](Self::register), this variant allows specifying extra syscalls
327    /// that will be permitted when the function handler runs.
328    #[cfg(all(feature = "seccomp", target_os = "linux"))]
329    pub fn register_with_extra_allowed_syscalls<
330        Args: ParameterTuple,
331        Output: SupportedReturnType,
332    >(
333        &mut self,
334        name: impl AsRef<str>,
335        host_func: impl Into<HostFunction<Output, Args>>,
336        extra_allowed_syscalls: impl IntoIterator<Item = crate::sandbox::ExtraAllowedSyscall>,
337    ) -> Result<()> {
338        let extra_allowed_syscalls: Vec<_> = extra_allowed_syscalls.into_iter().collect();
339        register_host_function(host_func, self, name.as_ref(), Some(extra_allowed_syscalls))
340    }
341
342    /// Registers the special "HostPrint" function for guest printing.
343    ///
344    /// This overrides the default behavior of writing to stdout.
345    /// The function expects the signature `FnMut(String) -> i32`
346    /// and will be called when the guest wants to print output.
347    pub fn register_print(
348        &mut self,
349        print_func: impl Into<HostFunction<i32, (String,)>>,
350    ) -> Result<()> {
351        #[cfg(not(all(target_os = "linux", feature = "seccomp")))]
352        self.register("HostPrint", print_func)?;
353
354        #[cfg(all(target_os = "linux", feature = "seccomp"))]
355        self.register_with_extra_allowed_syscalls(
356            "HostPrint",
357            print_func,
358            EXTRA_ALLOWED_SYSCALLS_FOR_WRITER_FUNC.iter().copied(),
359        )?;
360
361        Ok(())
362    }
363
364    /// Registers the "HostPrint" function with additional allowed syscalls.
365    ///
366    /// Like [`register_print`](Self::register_print), but allows specifying extra syscalls
367    /// that will be permitted during function execution.
368    #[cfg(all(feature = "seccomp", target_os = "linux"))]
369    pub fn register_print_with_extra_allowed_syscalls(
370        &mut self,
371        print_func: impl Into<HostFunction<i32, (String,)>>,
372        extra_allowed_syscalls: impl IntoIterator<Item = crate::sandbox::ExtraAllowedSyscall>,
373    ) -> Result<()> {
374        #[cfg(all(target_os = "linux", feature = "seccomp"))]
375        self.register_with_extra_allowed_syscalls(
376            "HostPrint",
377            print_func,
378            EXTRA_ALLOWED_SYSCALLS_FOR_WRITER_FUNC
379                .iter()
380                .copied()
381                .chain(extra_allowed_syscalls),
382        )?;
383
384        Ok(())
385    }
386}
387// Check to see if the current version of Windows is supported
388// Hyperlight is only supported on Windows 11 and Windows Server 2022 and later
389#[cfg(target_os = "windows")]
390fn check_windows_version() -> Result<()> {
391    use windows_version::{OsVersion, is_server};
392    const WINDOWS_MAJOR: u32 = 10;
393    const WINDOWS_MINOR: u32 = 0;
394    const WINDOWS_PACK: u32 = 0;
395
396    // Windows Server 2022 has version numbers 10.0.20348 or greater
397    if is_server() {
398        if OsVersion::current() < OsVersion::new(WINDOWS_MAJOR, WINDOWS_MINOR, WINDOWS_PACK, 20348)
399        {
400            return Err(new_error!(
401                "Hyperlight Requires Windows Server 2022 or newer"
402            ));
403        }
404    } else if OsVersion::current()
405        < OsVersion::new(WINDOWS_MAJOR, WINDOWS_MINOR, WINDOWS_PACK, 22000)
406    {
407        return Err(new_error!("Hyperlight Requires Windows 11 or newer"));
408    }
409    Ok(())
410}
411
412#[cfg(test)]
413mod tests {
414    use std::sync::Arc;
415    use std::sync::mpsc::channel;
416    use std::{fs, thread};
417
418    use crossbeam_queue::ArrayQueue;
419    use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnValue};
420    use hyperlight_testing::simple_guest_as_string;
421
422    use crate::sandbox::SandboxConfiguration;
423    use crate::sandbox::uninitialized::{GuestBinary, GuestEnvironment};
424    use crate::{MultiUseSandbox, Result, UninitializedSandbox, new_error};
425
426    #[test]
427    fn test_load_extra_blob() {
428        let binary_path = simple_guest_as_string().unwrap();
429        let buffer = [0xde, 0xad, 0xbe, 0xef];
430        let guest_env =
431            GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), Some(&buffer));
432
433        let uninitialized_sandbox = UninitializedSandbox::new(guest_env, None).unwrap();
434        let mut sandbox: MultiUseSandbox = uninitialized_sandbox.evolve().unwrap();
435
436        let res = sandbox
437            .call::<Vec<u8>>("ReadFromUserMemory", (4u64, buffer.to_vec()))
438            .expect("Failed to call ReadFromUserMemory");
439
440        assert_eq!(res, buffer.to_vec());
441    }
442
443    #[test]
444    fn test_new_sandbox() {
445        // Guest Binary exists at path
446
447        let binary_path = simple_guest_as_string().unwrap();
448        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(binary_path.clone()), None);
449        assert!(sandbox.is_ok());
450
451        // Guest Binary does not exist at path
452
453        let mut binary_path_does_not_exist = binary_path.clone();
454        binary_path_does_not_exist.push_str(".nonexistent");
455        let uninitialized_sandbox =
456            UninitializedSandbox::new(GuestBinary::FilePath(binary_path_does_not_exist), None);
457        assert!(uninitialized_sandbox.is_err());
458
459        // Non default memory configuration
460        let cfg = {
461            let mut cfg = SandboxConfiguration::default();
462            cfg.set_input_data_size(0x1000);
463            cfg.set_output_data_size(0x1000);
464            cfg.set_stack_size(0x1000);
465            cfg.set_heap_size(0x1000);
466            Some(cfg)
467        };
468
469        let uninitialized_sandbox =
470            UninitializedSandbox::new(GuestBinary::FilePath(binary_path.clone()), cfg);
471        assert!(uninitialized_sandbox.is_ok());
472
473        let uninitialized_sandbox =
474            UninitializedSandbox::new(GuestBinary::FilePath(binary_path), None).unwrap();
475
476        // Get a Sandbox from an uninitialized sandbox without a call back function
477
478        let _sandbox: MultiUseSandbox = uninitialized_sandbox.evolve().unwrap();
479
480        // Test with a valid guest binary buffer
481
482        let binary_path = simple_guest_as_string().unwrap();
483        let sandbox =
484            UninitializedSandbox::new(GuestBinary::Buffer(&fs::read(binary_path).unwrap()), None);
485        assert!(sandbox.is_ok());
486
487        // Test with a invalid guest binary buffer
488
489        let binary_path = simple_guest_as_string().unwrap();
490        let mut bytes = fs::read(binary_path).unwrap();
491        let _ = bytes.split_off(100);
492        let sandbox = UninitializedSandbox::new(GuestBinary::Buffer(&bytes), None);
493        assert!(sandbox.is_err());
494    }
495
496    #[test]
497    fn test_load_guest_binary_manual() {
498        let cfg = SandboxConfiguration::default();
499
500        let simple_guest_path = simple_guest_as_string().unwrap();
501
502        UninitializedSandbox::load_guest_binary(
503            cfg,
504            &GuestBinary::FilePath(simple_guest_path),
505            None.as_ref(),
506        )
507        .unwrap();
508    }
509
510    #[test]
511    fn test_host_functions() {
512        let uninitialized_sandbox = || {
513            UninitializedSandbox::new(
514                GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
515                None,
516            )
517            .unwrap()
518        };
519
520        // simple register + call
521        {
522            let mut usbox = uninitialized_sandbox();
523
524            usbox.register("test0", |arg: i32| Ok(arg + 1)).unwrap();
525
526            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
527            assert!(sandbox.is_ok());
528            let sandbox = sandbox.unwrap();
529
530            let host_funcs = sandbox
531                ._host_funcs
532                .try_lock()
533                .map_err(|_| new_error!("Error locking"));
534
535            assert!(host_funcs.is_ok());
536
537            let res = host_funcs
538                .unwrap()
539                .call_host_function("test0", vec![ParameterValue::Int(1)])
540                .unwrap();
541
542            assert_eq!(res, ReturnValue::Int(2));
543        }
544
545        // multiple parameters register + call
546        {
547            let mut usbox = uninitialized_sandbox();
548
549            usbox.register("test1", |a: i32, b: i32| Ok(a + b)).unwrap();
550
551            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
552            assert!(sandbox.is_ok());
553            let sandbox = sandbox.unwrap();
554
555            let host_funcs = sandbox
556                ._host_funcs
557                .try_lock()
558                .map_err(|_| new_error!("Error locking"));
559
560            assert!(host_funcs.is_ok());
561
562            let res = host_funcs
563                .unwrap()
564                .call_host_function(
565                    "test1",
566                    vec![ParameterValue::Int(1), ParameterValue::Int(2)],
567                )
568                .unwrap();
569
570            assert_eq!(res, ReturnValue::Int(3));
571        }
572
573        // incorrect arguments register + call
574        {
575            let mut usbox = uninitialized_sandbox();
576
577            usbox
578                .register("test2", |msg: String| {
579                    println!("test2 called: {}", msg);
580                    Ok(())
581                })
582                .unwrap();
583
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("test2", vec![]);
596            assert!(res.is_err());
597        }
598
599        // calling a function that doesn't exist
600        {
601            let usbox = uninitialized_sandbox();
602            let sandbox: Result<MultiUseSandbox> = usbox.evolve();
603            assert!(sandbox.is_ok());
604            let sandbox = sandbox.unwrap();
605
606            let host_funcs = sandbox
607                ._host_funcs
608                .try_lock()
609                .map_err(|_| new_error!("Error locking"));
610
611            assert!(host_funcs.is_ok());
612
613            let res = host_funcs.unwrap().call_host_function("test4", vec![]);
614            assert!(res.is_err());
615        }
616    }
617
618    #[test]
619    fn test_host_print() {
620        // writer as a FnMut closure mutating a captured variable and then trying to access the captured variable
621        // after the Sandbox instance has been dropped
622        // this example is fairly contrived but we should still support such an approach.
623
624        let (tx, rx) = channel();
625
626        let writer = move |msg| {
627            let _ = tx.send(msg);
628            Ok(0)
629        };
630
631        let mut sandbox = UninitializedSandbox::new(
632            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
633            None,
634        )
635        .expect("Failed to create sandbox");
636
637        sandbox
638            .register_print(writer)
639            .expect("Failed to register host print function");
640
641        let host_funcs = sandbox
642            .host_funcs
643            .try_lock()
644            .map_err(|_| new_error!("Error locking"));
645
646        assert!(host_funcs.is_ok());
647
648        host_funcs.unwrap().host_print("test".to_string()).unwrap();
649
650        drop(sandbox);
651
652        let received_msgs: Vec<_> = rx.into_iter().collect();
653        assert_eq!(received_msgs, ["test"]);
654
655        // There may be cases where a mutable reference to the captured variable is not required to be used outside the closure
656        // e.g. if the function is writing to a file or a socket etc.
657
658        // writer as a FnMut closure mutating a captured variable but not trying to access the captured variable
659
660        // This seems more realistic as the client is creating a file to be written to in the closure
661        // and then accessing the file a different handle.
662        // The problem is that captured_file still needs static lifetime so even though we can access the data through the second file handle
663        // this still does not work as the captured_file is dropped at the end of the function
664
665        // TODO: Currently, we block any writes that are not to
666        // the stdout/stderr file handles, so this code is commented
667        // out until we can register writer functions like any other
668        // host functions with their own set of extra allowed syscalls.
669        // In particular, this code should be brought back once we have addressed the issue
670
671        // let captured_file = Arc::new(Mutex::new(NamedTempFile::new().unwrap()));
672        // let capture_file_clone = captured_file.clone();
673        //
674        // let capture_file_lock = captured_file
675        //     .try_lock()
676        //     .map_err(|_| new_error!("Error locking"))
677        //     .unwrap();
678        // let mut file = capture_file_lock.reopen().unwrap();
679        // drop(capture_file_lock);
680        //
681        // let writer = move |msg: String| -> Result<i32> {
682        //     let mut captured_file = capture_file_clone
683        //         .try_lock()
684        //         .map_err(|_| new_error!("Error locking"))
685        //         .unwrap();
686        //     captured_file.write_all(msg.as_bytes()).unwrap();
687        //     Ok(0)
688        // };
689        //
690        // let writer_func = Arc::new(Mutex::new(writer));
691        //
692        // let sandbox = UninitializedSandbox::new(
693        //     GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
694        //     None,
695        //     None,
696        //     Some(&writer_func),
697        // )
698        // .expect("Failed to create sandbox");
699        //
700        // let host_funcs = sandbox
701        //     .host_funcs
702        //     .try_lock()
703        //     .map_err(|_| new_error!("Error locking"));
704        //
705        // assert!(host_funcs.is_ok());
706        //
707        // host_funcs.unwrap().host_print("test2".to_string()).unwrap();
708        //
709        // let mut buffer = String::new();
710        // file.read_to_string(&mut buffer).unwrap();
711        // assert_eq!(buffer, "test2");
712
713        // writer as a function
714
715        fn fn_writer(msg: String) -> Result<i32> {
716            assert_eq!(msg, "test2");
717            Ok(0)
718        }
719
720        let mut sandbox = UninitializedSandbox::new(
721            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
722            None,
723        )
724        .expect("Failed to create sandbox");
725
726        sandbox
727            .register_print(fn_writer)
728            .expect("Failed to register host print function");
729
730        let host_funcs = sandbox
731            .host_funcs
732            .try_lock()
733            .map_err(|_| new_error!("Error locking"));
734
735        assert!(host_funcs.is_ok());
736
737        host_funcs.unwrap().host_print("test2".to_string()).unwrap();
738
739        // writer as a method
740
741        let mut test_host_print = TestHostPrint::new();
742
743        // create a closure over the struct method
744
745        let writer_closure = move |s| test_host_print.write(s);
746
747        let mut sandbox = UninitializedSandbox::new(
748            GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
749            None,
750        )
751        .expect("Failed to create sandbox");
752
753        sandbox
754            .register_print(writer_closure)
755            .expect("Failed to register host print function");
756
757        let host_funcs = sandbox
758            .host_funcs
759            .try_lock()
760            .map_err(|_| new_error!("Error locking"));
761
762        assert!(host_funcs.is_ok());
763
764        host_funcs.unwrap().host_print("test3".to_string()).unwrap();
765    }
766
767    struct TestHostPrint {}
768
769    impl TestHostPrint {
770        fn new() -> Self {
771            TestHostPrint {}
772        }
773
774        fn write(&mut self, msg: String) -> Result<i32> {
775            assert_eq!(msg, "test3");
776            Ok(0)
777        }
778    }
779
780    #[test]
781    fn check_create_and_use_sandbox_on_different_threads() {
782        let unintializedsandbox_queue = Arc::new(ArrayQueue::<UninitializedSandbox>::new(10));
783        let sandbox_queue = Arc::new(ArrayQueue::<MultiUseSandbox>::new(10));
784
785        for i in 0..10 {
786            let simple_guest_path = simple_guest_as_string().expect("Guest Binary Missing");
787            let unintializedsandbox = {
788                let err_string = format!("failed to create UninitializedSandbox {i}");
789                let err_str = err_string.as_str();
790                UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_path), None)
791                    .expect(err_str)
792            };
793
794            {
795                let err_string = format!("Failed to push UninitializedSandbox {i}");
796                let err_str = err_string.as_str();
797
798                unintializedsandbox_queue
799                    .push(unintializedsandbox)
800                    .expect(err_str);
801            }
802        }
803
804        let thread_handles = (0..10)
805            .map(|i| {
806                let uq = unintializedsandbox_queue.clone();
807                let sq = sandbox_queue.clone();
808                thread::spawn(move || {
809                    let uninitialized_sandbox = uq.pop().unwrap_or_else(|| {
810                        panic!("Failed to pop UninitializedSandbox thread {}", i)
811                    });
812
813                    let host_funcs = uninitialized_sandbox
814                        .host_funcs
815                        .try_lock()
816                        .map_err(|_| new_error!("Error locking"));
817
818                    assert!(host_funcs.is_ok());
819
820                    host_funcs
821                        .unwrap()
822                        .host_print(format!("Print from UninitializedSandbox on Thread {}\n", i))
823                        .unwrap();
824
825                    let sandbox = uninitialized_sandbox.evolve().unwrap_or_else(|_| {
826                        panic!("Failed to initialize UninitializedSandbox thread {}", i)
827                    });
828
829                    sq.push(sandbox).unwrap_or_else(|_| {
830                        panic!("Failed to push UninitializedSandbox thread {}", i)
831                    })
832                })
833            })
834            .collect::<Vec<_>>();
835
836        for handle in thread_handles {
837            handle.join().unwrap();
838        }
839
840        let thread_handles = (0..10)
841            .map(|i| {
842                let sq = sandbox_queue.clone();
843                thread::spawn(move || {
844                    let sandbox = sq
845                        .pop()
846                        .unwrap_or_else(|| panic!("Failed to pop Sandbox thread {}", i));
847
848                    let host_funcs = sandbox
849                        ._host_funcs
850                        .try_lock()
851                        .map_err(|_| new_error!("Error locking"));
852
853                    assert!(host_funcs.is_ok());
854
855                    host_funcs
856                        .unwrap()
857                        .host_print(format!("Print from Sandbox on Thread {}\n", i))
858                        .unwrap();
859                })
860            })
861            .collect::<Vec<_>>();
862
863        for handle in thread_handles {
864            handle.join().unwrap();
865        }
866    }
867
868    #[test]
869    // Tests that trace data are emitted when a trace subscriber is set
870    // this test is ignored because it is incompatible with other tests , specifically those which require a logger for tracing
871    // marking  this test as ignored means that running `cargo test` will not run this test but will allow a developer who runs that command
872    // from their workstation to be successful without needed to know about test interdependencies
873    // this test will be run explicitly as a part of the CI pipeline
874    #[ignore]
875    #[cfg(feature = "build-metadata")]
876    fn test_trace_trace() {
877        use hyperlight_testing::logger::Logger as TestLogger;
878        use hyperlight_testing::tracing_subscriber::TracingSubscriber as TestSubscriber;
879        use serde_json::{Map, Value};
880        use tracing::Level as tracing_level;
881        use tracing_core::Subscriber;
882        use tracing_core::callsite::rebuild_interest_cache;
883        use uuid::Uuid;
884
885        use crate::testing::log_values::build_metadata_testing::try_to_strings;
886        use crate::testing::log_values::test_value_as_str;
887
888        TestLogger::initialize_log_tracer();
889        rebuild_interest_cache();
890        let subscriber = TestSubscriber::new(tracing_level::TRACE);
891        tracing::subscriber::with_default(subscriber.clone(), || {
892            let correlation_id = Uuid::new_v4().as_hyphenated().to_string();
893            let span = tracing::error_span!("test_trace_logs", correlation_id).entered();
894
895            // We should be in span 1
896
897            let current_span = subscriber.current_span();
898            assert!(current_span.is_known(), "Current span is unknown");
899            let current_span_metadata = current_span.into_inner().unwrap();
900            assert_eq!(
901                current_span_metadata.0.into_u64(),
902                1,
903                "Current span is not span 1"
904            );
905            assert_eq!(current_span_metadata.1.name(), "test_trace_logs");
906
907            // Get the span data and check the correlation id
908
909            let span_data = subscriber.get_span(1);
910            let span_attributes: &Map<String, Value> = span_data
911                .get("span")
912                .unwrap()
913                .get("attributes")
914                .unwrap()
915                .as_object()
916                .unwrap();
917
918            test_value_as_str(span_attributes, "correlation_id", correlation_id.as_str());
919
920            let mut binary_path = simple_guest_as_string().unwrap();
921            binary_path.push_str("does_not_exist");
922
923            let sbox = UninitializedSandbox::new(GuestBinary::FilePath(binary_path), None);
924            assert!(sbox.is_err());
925
926            // Now we should still be in span 1 but span 2 should be created (we created entered and exited span 2 when we called UninitializedSandbox::new)
927
928            let current_span = subscriber.current_span();
929            assert!(current_span.is_known(), "Current span is unknown");
930            let current_span_metadata = current_span.into_inner().unwrap();
931            assert_eq!(
932                current_span_metadata.0.into_u64(),
933                1,
934                "Current span is not span 1"
935            );
936
937            let span_metadata = subscriber.get_span_metadata(2);
938            assert_eq!(span_metadata.name(), "new");
939
940            // There should be one event for the error that the binary path does not exist plus 14 info events for the logging of the crate info
941
942            let events = subscriber.get_events();
943            assert_eq!(events.len(), 15);
944
945            let mut count_matching_events = 0;
946
947            for json_value in events {
948                let event_values = json_value.as_object().unwrap().get("event").unwrap();
949                let metadata_values_map =
950                    event_values.get("metadata").unwrap().as_object().unwrap();
951                let event_values_map = event_values.as_object().unwrap();
952
953                let expected_error_start = "Error(\"GuestBinary not found:";
954
955                let err_vals_res = try_to_strings([
956                    (metadata_values_map, "level"),
957                    (event_values_map, "error"),
958                    (metadata_values_map, "module_path"),
959                    (metadata_values_map, "target"),
960                ]);
961                if let Ok(err_vals) = err_vals_res {
962                    if err_vals[0] == "ERROR"
963                        && err_vals[1].starts_with(expected_error_start)
964                        && err_vals[2] == "hyperlight_host::sandbox::uninitialized"
965                        && err_vals[3] == "hyperlight_host::sandbox::uninitialized"
966                    {
967                        count_matching_events += 1;
968                    }
969                }
970            }
971            assert!(
972                count_matching_events == 1,
973                "Unexpected number of matching events {}",
974                count_matching_events
975            );
976            span.exit();
977            subscriber.clear();
978        });
979    }
980
981    #[test]
982    #[ignore]
983    // Tests that traces are emitted as log records when there is no trace
984    // subscriber configured.
985    #[cfg(feature = "build-metadata")]
986    fn test_log_trace() {
987        use std::path::PathBuf;
988
989        use hyperlight_testing::logger::{LOGGER as TEST_LOGGER, Logger as TestLogger};
990        use log::Level;
991        use tracing_core::callsite::rebuild_interest_cache;
992
993        {
994            TestLogger::initialize_test_logger();
995            TEST_LOGGER.set_max_level(log::LevelFilter::Trace);
996
997            // This makes sure that the metadata interest cache is rebuilt so that
998            // the log records are emitted for the trace records
999
1000            rebuild_interest_cache();
1001
1002            let mut invalid_binary_path = simple_guest_as_string().unwrap();
1003            invalid_binary_path.push_str("does_not_exist");
1004
1005            let sbox = UninitializedSandbox::new(GuestBinary::FilePath(invalid_binary_path), None);
1006            assert!(sbox.is_err());
1007
1008            // When tracing is creating log records it will create a log
1009            // record for the creation of the span (from the instrument
1010            // attribute), and will then create a log record for the entry to
1011            // and exit from the span.
1012            //
1013            // It also creates a log record for the span being dropped.
1014            //
1015            // In addition there are 14 info log records created for build information
1016            //
1017            // So we expect 19 log records for this test, four for the span and
1018            // then one for the error as the file that we are attempting to
1019            // load into the sandbox does not exist, plus the 14 info log records
1020
1021            let num_calls = TEST_LOGGER.num_log_calls();
1022            assert_eq!(19, num_calls);
1023
1024            // Log record 1
1025
1026            let logcall = TEST_LOGGER.get_log_call(0).unwrap();
1027            assert_eq!(Level::Info, logcall.level);
1028
1029            assert!(logcall.args.starts_with("new; cfg"));
1030            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1031
1032            // Log record 2
1033
1034            let logcall = TEST_LOGGER.get_log_call(1).unwrap();
1035            assert_eq!(Level::Trace, logcall.level);
1036            assert_eq!(logcall.args, "-> new;");
1037            assert_eq!("tracing::span::active", logcall.target);
1038
1039            // Log record 17
1040
1041            let logcall = TEST_LOGGER.get_log_call(16).unwrap();
1042            assert_eq!(Level::Error, logcall.level);
1043            assert!(
1044                logcall
1045                    .args
1046                    .starts_with("error=Error(\"GuestBinary not found:")
1047            );
1048            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1049
1050            // Log record 18
1051
1052            let logcall = TEST_LOGGER.get_log_call(17).unwrap();
1053            assert_eq!(Level::Trace, logcall.level);
1054            assert_eq!(logcall.args, "<- new;");
1055            assert_eq!("tracing::span::active", logcall.target);
1056
1057            // Log record 19
1058
1059            let logcall = TEST_LOGGER.get_log_call(18).unwrap();
1060            assert_eq!(Level::Trace, logcall.level);
1061            assert_eq!(logcall.args, "-- new;");
1062            assert_eq!("tracing::span", logcall.target);
1063        }
1064        {
1065            // test to ensure an invalid binary logs & traces properly
1066            TEST_LOGGER.clear_log_calls();
1067            TEST_LOGGER.set_max_level(log::LevelFilter::Info);
1068
1069            let mut valid_binary_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1070            valid_binary_path.push("src");
1071            valid_binary_path.push("sandbox");
1072            valid_binary_path.push("initialized.rs");
1073
1074            let sbox = UninitializedSandbox::new(
1075                GuestBinary::FilePath(valid_binary_path.into_os_string().into_string().unwrap()),
1076                None,
1077            );
1078            assert!(sbox.is_err());
1079
1080            // There should be 2 calls this time when we change to the log
1081            // LevelFilter to Info.
1082            let num_calls = TEST_LOGGER.num_log_calls();
1083            assert_eq!(2, num_calls);
1084
1085            // Log record 1
1086
1087            let logcall = TEST_LOGGER.get_log_call(0).unwrap();
1088            assert_eq!(Level::Info, logcall.level);
1089
1090            assert!(logcall.args.starts_with("new; cfg"));
1091            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1092
1093            // Log record 2
1094
1095            let logcall = TEST_LOGGER.get_log_call(1).unwrap();
1096            assert_eq!(Level::Error, logcall.level);
1097            assert!(
1098                logcall
1099                    .args
1100                    .starts_with("error=Error(\"GuestBinary not found:")
1101            );
1102            assert_eq!("hyperlight_host::sandbox::uninitialized", logcall.target);
1103        }
1104        {
1105            TEST_LOGGER.clear_log_calls();
1106            TEST_LOGGER.set_max_level(log::LevelFilter::Error);
1107
1108            let sbox = {
1109                let res = UninitializedSandbox::new(
1110                    GuestBinary::FilePath(simple_guest_as_string().unwrap()),
1111                    None,
1112                );
1113                res.unwrap()
1114            };
1115            let _: Result<MultiUseSandbox> = sbox.evolve();
1116
1117            let num_calls = TEST_LOGGER.num_log_calls();
1118
1119            assert_eq!(0, num_calls);
1120        }
1121    }
1122
1123    #[test]
1124    fn test_invalid_path() {
1125        let invalid_path = "some/path/that/does/not/exist";
1126        let sbox = UninitializedSandbox::new(GuestBinary::FilePath(invalid_path.to_string()), None);
1127        println!("{:?}", sbox);
1128        #[cfg(target_os = "windows")]
1129        assert!(
1130            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)"))
1131        );
1132        #[cfg(target_os = "linux")]
1133        assert!(
1134            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)"))
1135        );
1136    }
1137}