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