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