hyperlight_host/sandbox/
uninitialized.rs

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