hyperlight_host/sandbox/
mod.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
17/// Configuration needed to establish a sandbox.
18pub mod config;
19/// Functionality for reading, but not modifying host functions
20pub(crate) mod host_funcs;
21/// Functionality for dealing with `Sandbox`es that contain Hypervisors
22pub(crate) mod hypervisor;
23/// Functionality for dealing with initialized sandboxes that can
24/// call 0 or more guest functions
25pub mod initialized_multi_use;
26pub(crate) mod outb;
27/// Functionality for creating uninitialized sandboxes, manipulating them,
28/// and converting them to initialized sandboxes.
29pub mod uninitialized;
30/// Functionality for properly converting `UninitializedSandbox`es to
31/// initialized `Sandbox`es.
32pub(crate) mod uninitialized_evolve;
33
34/// Representation of a snapshot of a `Sandbox`.
35pub mod snapshot;
36
37/// Trait used by the macros to paper over the differences between hyperlight and hyperlight-wasm
38mod callable;
39
40#[cfg(feature = "unwind_guest")]
41use std::io::Write;
42#[cfg(feature = "trace_guest")]
43use std::sync::{Arc, Mutex};
44
45/// Trait used by the macros to paper over the differences between hyperlight and hyperlight-wasm
46pub use callable::Callable;
47/// Re-export for `SandboxConfiguration` type
48pub use config::SandboxConfiguration;
49#[cfg(feature = "unwind_guest")]
50use framehop::Unwinder;
51/// Re-export for the `MultiUseSandbox` type
52pub use initialized_multi_use::MultiUseSandbox;
53use tracing::{Span, instrument};
54/// Re-export for `GuestBinary` type
55pub use uninitialized::GuestBinary;
56/// Re-export for `UninitializedSandbox` type
57pub use uninitialized::UninitializedSandbox;
58
59#[cfg(target_os = "windows")]
60use crate::hypervisor::windows_hypervisor_platform;
61
62// In case its not obvious why there are separate is_supported_platform and is_hypervisor_present functions its because
63// Hyperlight is designed to be able to run on a host that doesn't have a hypervisor.
64// In that case, the sandbox will be in process, we plan on making this a dev only feature and fixing up Linux support
65// so we should review the need for this function at that time.
66
67/// Determine if this is a supported platform for Hyperlight
68///
69/// Returns a boolean indicating whether this is a supported platform.
70#[instrument(skip_all, parent = Span::current())]
71pub fn is_supported_platform() -> bool {
72    #[cfg(not(target_os = "linux"))]
73    #[cfg(not(target_os = "windows"))]
74    return false;
75
76    true
77}
78
79/// Alias for the type of extra allowed syscalls.
80pub type ExtraAllowedSyscall = i64;
81
82/// Determine whether a suitable hypervisor is available to run
83/// this sandbox.
84///
85///  Returns a boolean indicating whether a suitable hypervisor is present.
86#[instrument(skip_all, parent = Span::current())]
87pub fn is_hypervisor_present() -> bool {
88    hypervisor::get_available_hypervisor().is_some()
89}
90
91#[cfg(feature = "trace_guest")]
92#[derive(Clone)]
93/// The information that trace collection requires in order to write
94/// an accurate trace.
95pub(crate) struct TraceInfo {
96    /// The epoch against which trace events are timed; at least as
97    /// early as the creation of the sandbox being traced.
98    pub epoch: std::time::Instant,
99    /// The frequency of the timestamp counter.
100    pub tsc_freq: Option<u64>,
101    /// The epoch at which the guest started, if it has started.
102    /// This is used to calculate the time spent in the guest relative to the
103    /// time when the host started.
104    pub guest_start_epoch: Option<std::time::Instant>,
105    /// The start guest time, in TSC cycles, for the current guest has a double purpose.
106    /// This field is used in two ways:
107    /// 1. It contains the TSC value recorded on the host when the guest started.
108    ///    This is used to calculate the TSC frequency which is the same on the host and guest.
109    ///    The TSC frequency is used to convert TSC values to timestamps in the trace.
110    ///    **NOTE**: This is only used until the TSC frequency is calculated, when the first
111    ///    records are received.
112    /// 2. To store the TSC value at recorded on the guest when the guest started (first record
113    ///    received)
114    ///    This is used to calculate the records timestamps relative to when guest started.
115    pub guest_start_tsc: Option<u64>,
116    /// The file to which the trace is being written
117    #[allow(dead_code)]
118    pub file: Arc<Mutex<std::fs::File>>,
119    /// The unwind information for the current guest
120    #[cfg(feature = "unwind_guest")]
121    #[allow(dead_code)]
122    pub unwind_module: Arc<dyn crate::mem::exe::UnwindInfo>,
123    /// The framehop unwinder for the current guest
124    #[cfg(feature = "unwind_guest")]
125    pub unwinder: framehop::x86_64::UnwinderX86_64<Vec<u8>>,
126    /// The framehop cache
127    #[cfg(feature = "unwind_guest")]
128    pub unwind_cache: Arc<Mutex<framehop::x86_64::CacheX86_64>>,
129}
130#[cfg(feature = "trace_guest")]
131impl TraceInfo {
132    /// Create a new TraceInfo by saving the current time as the epoch
133    /// and generating a random filename.
134    pub fn new(
135        #[cfg(feature = "unwind_guest")] unwind_module: Arc<dyn crate::mem::exe::UnwindInfo>,
136    ) -> crate::Result<Self> {
137        let mut path = std::env::current_dir()?;
138        path.push("trace");
139
140        // create directory if it does not exist
141        if !path.exists() {
142            std::fs::create_dir(&path)?;
143        }
144        path.push(uuid::Uuid::new_v4().to_string());
145        path.set_extension("trace");
146
147        log::info!("Creating trace file at: {}", path.display());
148        println!("Creating trace file at: {}", path.display());
149
150        #[cfg(feature = "unwind_guest")]
151        let hash = unwind_module.hash();
152        #[cfg(feature = "unwind_guest")]
153        let (unwinder, unwind_cache) = {
154            let mut unwinder = framehop::x86_64::UnwinderX86_64::new();
155            unwinder.add_module(unwind_module.clone().as_module());
156            let cache = framehop::x86_64::CacheX86_64::new();
157            (unwinder, Arc::new(Mutex::new(cache)))
158        };
159        if !hyperlight_guest_tracing::invariant_tsc::has_invariant_tsc() {
160            // If the platform does not support invariant TSC, warn the user.
161            // On Azure nested virtualization, the TSC invariant bit is not correctly reported, this is a known issue.
162            log::warn!(
163                "Invariant TSC is not supported on this platform, trace timestamps may be inaccurate"
164            );
165        }
166
167        let ret = Self {
168            epoch: std::time::Instant::now(),
169            tsc_freq: None,
170            guest_start_epoch: None,
171            guest_start_tsc: None,
172            file: Arc::new(Mutex::new(std::fs::File::create_new(path)?)),
173            #[cfg(feature = "unwind_guest")]
174            unwind_module,
175            #[cfg(feature = "unwind_guest")]
176            unwinder,
177            #[cfg(feature = "unwind_guest")]
178            unwind_cache,
179        };
180        /* write a frame identifying the binary */
181        #[cfg(feature = "unwind_guest")]
182        self::outb::record_trace_frame(&ret, 0, |f| {
183            let _ = f.write_all(hash.as_bytes());
184        })?;
185        Ok(ret)
186    }
187
188    /// Calculate the TSC frequency based on the RDTSC instruction on the host.
189    fn calculate_tsc_freq(&mut self) -> crate::Result<()> {
190        let (start, start_time) = match (
191            self.guest_start_tsc.as_ref(),
192            self.guest_start_epoch.as_ref(),
193        ) {
194            (Some(start), Some(start_time)) => (*start, *start_time),
195            _ => {
196                // If the guest start TSC and time are not set, we use the current time and TSC.
197                // This is not ideal, but it allows us to calculate the TSC frequency without
198                // failing.
199                // This is a fallback mechanism to ensure that we can still calculate, however it
200                // should be noted that this may lead to inaccuracies in the TSC frequency.
201                // The start time should be already set before running the guest for each sandbox.
202                log::error!(
203                    "Guest start TSC and time are not set. Calculating TSC frequency will use current time and TSC."
204                );
205                (
206                    hyperlight_guest_tracing::invariant_tsc::read_tsc(),
207                    std::time::Instant::now(),
208                )
209            }
210        };
211
212        let end_time = std::time::Instant::now();
213        let end = hyperlight_guest_tracing::invariant_tsc::read_tsc();
214
215        let elapsed = end_time.duration_since(start_time).as_secs_f64();
216        let tsc_freq = ((end - start) as f64 / elapsed) as u64;
217
218        log::info!("Calculated TSC frequency: {} Hz", tsc_freq);
219        self.tsc_freq = Some(tsc_freq);
220
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use std::sync::Arc;
228    use std::thread;
229
230    use crossbeam_queue::ArrayQueue;
231    use hyperlight_testing::simple_guest_as_string;
232
233    use crate::sandbox::uninitialized::GuestBinary;
234    use crate::{MultiUseSandbox, UninitializedSandbox, new_error};
235
236    #[test]
237    // TODO: add support for testing on WHP
238    #[cfg(target_os = "linux")]
239    fn is_hypervisor_present() {
240        use std::path::Path;
241
242        cfg_if::cfg_if! {
243            if #[cfg(all(kvm, mshv))] {
244                assert_eq!(Path::new("/dev/kvm").exists() || Path::new("/dev/mshv").exists(), super::is_hypervisor_present());
245            } else if #[cfg(kvm)] {
246                assert_eq!(Path::new("/dev/kvm").exists(), super::is_hypervisor_present());
247            } else if #[cfg(mshv)] {
248                assert_eq!(Path::new("/dev/mshv").exists(), super::is_hypervisor_present());
249            } else {
250                assert!(!super::is_hypervisor_present());
251            }
252        }
253    }
254
255    #[test]
256    fn check_create_and_use_sandbox_on_different_threads() {
257        let unintializedsandbox_queue = Arc::new(ArrayQueue::<UninitializedSandbox>::new(10));
258        let sandbox_queue = Arc::new(ArrayQueue::<MultiUseSandbox>::new(10));
259
260        for i in 0..10 {
261            let simple_guest_path = simple_guest_as_string().expect("Guest Binary Missing");
262            let unintializedsandbox =
263                UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_path), None)
264                    .unwrap_or_else(|_| panic!("Failed to create UninitializedSandbox {}", i));
265
266            unintializedsandbox_queue
267                .push(unintializedsandbox)
268                .unwrap_or_else(|_| panic!("Failed to push UninitializedSandbox {}", i));
269        }
270
271        let thread_handles = (0..10)
272            .map(|i| {
273                let uq = unintializedsandbox_queue.clone();
274                let sq = sandbox_queue.clone();
275                thread::spawn(move || {
276                    let uninitialized_sandbox = uq.pop().unwrap_or_else(|| {
277                        panic!("Failed to pop UninitializedSandbox thread {}", i)
278                    });
279                    let host_funcs = uninitialized_sandbox
280                        .host_funcs
281                        .try_lock()
282                        .map_err(|_| new_error!("Error locking"));
283
284                    assert!(host_funcs.is_ok());
285
286                    host_funcs
287                        .unwrap()
288                        .host_print(format!(
289                            "Printing from UninitializedSandbox on Thread {}\n",
290                            i
291                        ))
292                        .unwrap();
293
294                    let sandbox = uninitialized_sandbox.evolve().unwrap_or_else(|_| {
295                        panic!("Failed to initialize UninitializedSandbox thread {}", i)
296                    });
297
298                    sq.push(sandbox).unwrap_or_else(|_| {
299                        panic!("Failed to push UninitializedSandbox thread {}", i)
300                    })
301                })
302            })
303            .collect::<Vec<_>>();
304
305        for handle in thread_handles {
306            handle.join().unwrap();
307        }
308
309        let thread_handles = (0..10)
310            .map(|i| {
311                let sq = sandbox_queue.clone();
312                thread::spawn(move || {
313                    let sandbox = sq
314                        .pop()
315                        .unwrap_or_else(|| panic!("Failed to pop Sandbox thread {}", i));
316                    let host_funcs = sandbox
317                        ._host_funcs
318                        .try_lock()
319                        .map_err(|_| new_error!("Error locking"));
320
321                    assert!(host_funcs.is_ok());
322
323                    host_funcs
324                        .unwrap()
325                        .host_print(format!("Print from Sandbox on Thread {}\n", i))
326                        .unwrap();
327                })
328            })
329            .collect::<Vec<_>>();
330
331        for handle in thread_handles {
332            handle.join().unwrap();
333        }
334    }
335}