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