Skip to main content

hyperlight_host/sandbox/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use std::cmp::max;
5use std::time::Duration;
6
7#[cfg(target_os = "linux")]
8use libc::c_int;
9use tracing::{Span, instrument};
10
11/// Used for passing debug configuration to a sandbox
12#[cfg(gdb)]
13#[derive(Copy, Clone, Debug, Eq, PartialEq)]
14pub struct DebugInfo {
15    /// Guest debug port
16    pub port: u16,
17}
18
19/// Errors returned when declaring guest MSRs.
20#[cfg(target_arch = "x86_64")]
21#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
22pub enum GuestMsrError {
23    /// The declared MSR set exceeds its fixed capacity.
24    #[error("declared guest MSRs exceed the maximum of {maximum} distinct entries")]
25    CapacityExceeded {
26        /// Maximum number of distinct declared MSRs.
27        maximum: usize,
28    },
29}
30
31/// The complete set of configuration needed to create a Sandbox
32#[derive(Copy, Clone, Debug, Eq, PartialEq)]
33#[repr(C)]
34pub struct SandboxConfiguration {
35    /// Guest core dump output directory
36    /// This field is by default set to true which means the value core dumps will be placed in:
37    /// - HYPERLIGHT_CORE_DUMP_DIR environment variable if it is set
38    /// - default value of the temporary directory
39    ///
40    /// The core dump files generation can be disabled by setting this field to false.
41    #[cfg(crashdump)]
42    guest_core_dump: bool,
43    /// Guest gdb debug port
44    #[cfg(gdb)]
45    guest_debug_info: Option<DebugInfo>,
46    /// The size of the memory buffer that is made available for input to the
47    /// Guest Binary
48    input_data_size: usize,
49    /// The size of the memory buffer that is made available for input to the
50    /// Guest Binary
51    output_data_size: usize,
52    /// The heap size to use in the guest sandbox. If set to 0, the heap
53    /// size will be determined from the PE file header
54    ///
55    /// Note: this is a C-compatible struct, so even though this optional
56    /// field should be represented as an `Option`, that type is not
57    /// FFI-safe, so it cannot be.
58    heap_size_override: u64,
59    /// Delay between interrupt retries. This duration specifies how long to wait
60    /// between attempts to send signals to the thread running the sandbox's VCPU.
61    /// Multiple retries may be necessary because signals only interrupt the VCPU
62    /// thread when the vcpu thread is in kernel space. There's a narrow window during which a
63    /// signal can be delivered to the thread, but the thread may not yet
64    /// have entered kernel space.
65    interrupt_retry_delay: Duration,
66    /// Offset from `SIGRTMIN` used to determine the signal number for interrupting
67    /// the VCPU thread. The actual signal sent is `SIGRTMIN + interrupt_vcpu_sigrtmin_offset`.
68    ///
69    /// This signal must fall within the valid real-time signal range supported by the host.
70    ///
71    /// Note: Since real-time signals can vary across platforms, ensure that the offset
72    /// results in a signal number that is not already in use by other components of the system.
73    interrupt_vcpu_sigrtmin_offset: u8,
74    /// How much writable memory to offer the guest
75    scratch_size: usize,
76    /// Declared guest MSRs, stored inline to keep this type `Copy`.
77    #[cfg(target_arch = "x86_64")]
78    guest_msrs: [u32; Self::MAX_GUEST_MSRS],
79    /// Number of valid entries in `guest_msrs`.
80    #[cfg(target_arch = "x86_64")]
81    guest_msrs_count: usize,
82}
83
84impl SandboxConfiguration {
85    /// The default size of input data
86    pub const DEFAULT_INPUT_SIZE: usize = 0x4000;
87    /// The minimum size of input data
88    pub const MIN_INPUT_SIZE: usize = 0x2000;
89    /// The default size of output data
90    pub const DEFAULT_OUTPUT_SIZE: usize = 0x4000;
91    /// The minimum size of output data
92    pub const MIN_OUTPUT_SIZE: usize = 0x2000;
93    /// The default interrupt retry delay
94    pub const DEFAULT_INTERRUPT_RETRY_DELAY: Duration = Duration::from_micros(500);
95    /// The default signal offset from `SIGRTMIN` used to determine the signal number for interrupting
96    pub const INTERRUPT_VCPU_SIGRTMIN_OFFSET: u8 = 0;
97    /// The default heap size of a hyperlight sandbox
98    pub const DEFAULT_HEAP_SIZE: u64 = 131072;
99    /// The default size of the scratch region
100    pub const DEFAULT_SCRATCH_SIZE: usize = 0x48000;
101    /// Maximum number of distinct guest MSRs that can be declared.
102    /// KVM supports at most 16 MSR filter ranges. Each index may require its
103    /// own range, so 16 is the portable limit across backends.
104    #[cfg(target_arch = "x86_64")]
105    pub const MAX_GUEST_MSRS: usize = 16;
106
107    #[allow(clippy::too_many_arguments)]
108    /// Create a new configuration for a sandbox with the given sizes.
109    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
110    fn new(
111        input_data_size: usize,
112        output_data_size: usize,
113        heap_size_override: Option<u64>,
114        scratch_size: usize,
115        interrupt_retry_delay: Duration,
116        interrupt_vcpu_sigrtmin_offset: u8,
117        #[cfg(gdb)] guest_debug_info: Option<DebugInfo>,
118        #[cfg(crashdump)] guest_core_dump: bool,
119    ) -> Self {
120        Self {
121            input_data_size: max(input_data_size, Self::MIN_INPUT_SIZE),
122            output_data_size: max(output_data_size, Self::MIN_OUTPUT_SIZE),
123            heap_size_override: heap_size_override.unwrap_or(0),
124            scratch_size,
125            interrupt_retry_delay,
126            interrupt_vcpu_sigrtmin_offset,
127            #[cfg(gdb)]
128            guest_debug_info,
129            #[cfg(crashdump)]
130            guest_core_dump,
131            #[cfg(target_arch = "x86_64")]
132            guest_msrs: [0; Self::MAX_GUEST_MSRS],
133            #[cfg(target_arch = "x86_64")]
134            guest_msrs_count: 0,
135        }
136    }
137
138    /// Set the size of the memory buffer that is made available for input to the guest
139    /// the minimum value is MIN_INPUT_SIZE
140    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
141    pub fn set_input_data_size(&mut self, input_data_size: usize) {
142        self.input_data_size = max(input_data_size, Self::MIN_INPUT_SIZE);
143    }
144
145    /// Set the size of the memory buffer that is made available for output from the guest
146    /// the minimum value is MIN_OUTPUT_SIZE
147    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
148    pub fn set_output_data_size(&mut self, output_data_size: usize) {
149        self.output_data_size = max(output_data_size, Self::MIN_OUTPUT_SIZE);
150    }
151
152    /// Set the heap size to use in the guest sandbox. If set to 0, the heap size will be determined from the PE file header
153    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
154    pub fn set_heap_size(&mut self, heap_size: u64) {
155        self.heap_size_override = heap_size;
156    }
157
158    /// Sets the interrupt retry delay
159    #[cfg(any(kvm, mshv3, hvf))]
160    pub fn set_interrupt_retry_delay(&mut self, delay: Duration) {
161        self.interrupt_retry_delay = delay;
162    }
163
164    /// Get the delay between retries for interrupts
165    #[cfg(any(kvm, mshv3, hvf))]
166    pub fn get_interrupt_retry_delay(&self) -> Duration {
167        self.interrupt_retry_delay
168    }
169
170    /// Get the signal offset from `SIGRTMIN` used to determine the signal number for interrupting the VCPU thread
171    #[cfg(target_os = "linux")]
172    pub fn get_interrupt_vcpu_sigrtmin_offset(&self) -> u8 {
173        self.interrupt_vcpu_sigrtmin_offset
174    }
175
176    /// Declares the MSRs the guest depends on.
177    ///
178    /// A declared MSR's value is part of the sandbox's saved state: captured by
179    /// [`MultiUseSandbox::snapshot`](crate::MultiUseSandbox::snapshot) and written
180    /// back on [`MultiUseSandbox::restore`](crate::MultiUseSandbox::restore). Every
181    /// MSR you do not declare is reset to a clean default on each restore.
182    ///
183    /// If this method is not called, only a small core of essential CPU state
184    /// (kernel GS base, TSC) is saved and restored.
185    ///
186    /// # Platform-specific behavior
187    ///
188    /// * On KVM, declaring an MSR is also what lets the guest access it. The
189    ///   guest faults on any `RDMSR`/`WRMSR` of an undeclared MSR.
190    /// * On MSHV and WHP there is no such enforcement, so declaration only
191    ///   controls what is saved and restored, not what the guest may touch.
192    ///
193    /// Duplicate indices, within the slice or against the existing set, are
194    /// ignored and do not count toward capacity.
195    ///
196    /// # Errors
197    ///
198    /// Returns [`GuestMsrError::CapacityExceeded`] if the distinct entries
199    /// would exceed [`Self::MAX_GUEST_MSRS`]. The declared set is unchanged on
200    /// error.
201    #[cfg(target_arch = "x86_64")]
202    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
203    pub fn guest_msrs(&mut self, indices: &[u32]) -> Result<&mut Self, GuestMsrError> {
204        let additional = indices
205            .iter()
206            .enumerate()
207            .filter(|(position, index)| {
208                !self.guest_msrs[..self.guest_msrs_count].contains(index)
209                    && !indices[..*position].contains(index)
210            })
211            .count();
212        if additional > Self::MAX_GUEST_MSRS - self.guest_msrs_count {
213            return Err(GuestMsrError::CapacityExceeded {
214                maximum: Self::MAX_GUEST_MSRS,
215            });
216        }
217        for &index in indices {
218            if !self.guest_msrs[..self.guest_msrs_count].contains(&index) {
219                self.guest_msrs[self.guest_msrs_count] = index;
220                self.guest_msrs_count += 1;
221            }
222        }
223        Ok(self)
224    }
225
226    /// Returns the declared guest MSRs.
227    #[cfg(target_arch = "x86_64")]
228    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
229    pub(crate) fn get_guest_msrs(&self) -> &[u32] {
230        &self.guest_msrs[..self.guest_msrs_count]
231    }
232
233    /// Sets the offset from `SIGRTMIN` to determine the real-time signal used for
234    /// interrupting the VCPU thread.
235    ///
236    /// The final signal number is computed as `SIGRTMIN + offset`, and it must fall within
237    /// the valid range of real-time signals supported by the host system.
238    ///
239    /// Returns Ok(()) if the offset is valid, or an error if it exceeds the maximum real-time signal number.
240    #[cfg(target_os = "linux")]
241    pub fn set_interrupt_vcpu_sigrtmin_offset(&mut self, offset: u8) -> crate::Result<()> {
242        if libc::SIGRTMIN() + offset as c_int > libc::SIGRTMAX() {
243            return Err(crate::new_error!(
244                "Invalid SIGRTMIN offset: {}. It exceeds the maximum real-time signal number.",
245                offset
246            ));
247        }
248        self.interrupt_vcpu_sigrtmin_offset = offset;
249        Ok(())
250    }
251
252    /// Toggles the guest core dump generation for a sandbox
253    /// Setting this to false disables the core dump generation
254    /// This is only used when the `crashdump` feature is enabled
255    #[cfg(crashdump)]
256    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
257    pub fn set_guest_core_dump(&mut self, enable: bool) {
258        self.guest_core_dump = enable;
259    }
260
261    /// Sets the configuration for the guest debug
262    #[cfg(gdb)]
263    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
264    pub fn set_guest_debug_info(&mut self, debug_info: DebugInfo) {
265        self.guest_debug_info = Some(debug_info);
266    }
267
268    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
269    pub(crate) fn get_input_data_size(&self) -> usize {
270        self.input_data_size
271    }
272
273    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
274    pub(crate) fn get_output_data_size(&self) -> usize {
275        self.output_data_size
276    }
277
278    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
279    pub(crate) fn get_scratch_size(&self) -> usize {
280        self.scratch_size
281    }
282
283    /// Set the size of the scratch regiong
284    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
285    pub fn set_scratch_size(&mut self, scratch_size: usize) {
286        self.scratch_size = scratch_size;
287    }
288
289    #[cfg(crashdump)]
290    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
291    pub(crate) fn get_guest_core_dump(&self) -> bool {
292        self.guest_core_dump
293    }
294
295    #[cfg(gdb)]
296    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
297    pub(crate) fn get_guest_debug_info(&self) -> Option<DebugInfo> {
298        self.guest_debug_info
299    }
300
301    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
302    fn heap_size_override_opt(&self) -> Option<u64> {
303        (self.heap_size_override > 0).then_some(self.heap_size_override)
304    }
305
306    /// If self.heap_size_override is non-zero, return it. Otherwise,
307    /// return exe_info.heap_reserve()
308    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
309    pub(crate) fn get_heap_size(&self) -> u64 {
310        self.heap_size_override_opt()
311            .unwrap_or(Self::DEFAULT_HEAP_SIZE)
312    }
313}
314
315impl Default for SandboxConfiguration {
316    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
317    fn default() -> Self {
318        Self::new(
319            Self::DEFAULT_INPUT_SIZE,
320            Self::DEFAULT_OUTPUT_SIZE,
321            None,
322            Self::DEFAULT_SCRATCH_SIZE,
323            Self::DEFAULT_INTERRUPT_RETRY_DELAY,
324            Self::INTERRUPT_VCPU_SIGRTMIN_OFFSET,
325            #[cfg(gdb)]
326            None,
327            #[cfg(crashdump)]
328            true,
329        )
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    #[cfg(target_arch = "x86_64")]
336    use super::GuestMsrError;
337    use super::SandboxConfiguration;
338
339    #[test]
340    #[cfg(target_arch = "x86_64")]
341    fn guest_msrs_reports_overflow() {
342        let mut cfg = SandboxConfiguration::default();
343        for index in 0..SandboxConfiguration::MAX_GUEST_MSRS as u32 {
344            cfg.guest_msrs(&[index]).unwrap();
345        }
346
347        cfg.guest_msrs(&[0]).unwrap();
348        assert_eq!(
349            cfg.guest_msrs(&[SandboxConfiguration::MAX_GUEST_MSRS as u32]),
350            Err(GuestMsrError::CapacityExceeded {
351                maximum: SandboxConfiguration::MAX_GUEST_MSRS,
352            })
353        );
354    }
355
356    #[test]
357    #[cfg(target_arch = "x86_64")]
358    fn bulk_guest_msrs_overflow_is_atomic() {
359        let mut cfg = SandboxConfiguration::default();
360        cfg.guest_msrs(&[1, 2]).unwrap();
361        let oversized: Vec<u32> = (3..=SandboxConfiguration::MAX_GUEST_MSRS as u32 + 1).collect();
362
363        assert!(matches!(
364            cfg.guest_msrs(&oversized),
365            Err(GuestMsrError::CapacityExceeded { .. })
366        ));
367        assert_eq!(cfg.get_guest_msrs(), &[1, 2]);
368    }
369
370    #[test]
371    #[cfg(target_arch = "x86_64")]
372    fn guest_msrs_dedups_and_preserves_order() {
373        let mut cfg = SandboxConfiguration::default();
374        cfg.guest_msrs(&[0x10]).unwrap();
375        cfg.guest_msrs(&[0x20, 0x20, 0x10, 0x30, 0x20]).unwrap();
376        // 0x10 already present, 0x20 and 0x30 added once each in first-seen order.
377        assert_eq!(cfg.get_guest_msrs(), &[0x10, 0x20, 0x30]);
378    }
379
380    #[test]
381    #[cfg(target_arch = "x86_64")]
382    fn guest_msrs_duplicates_do_not_count_toward_capacity() {
383        let mut cfg = SandboxConfiguration::default();
384        let fill: Vec<u32> = (0..SandboxConfiguration::MAX_GUEST_MSRS as u32 - 1).collect();
385        cfg.guest_msrs(&fill).unwrap();
386        // One slot remains. Three copies of one new index count as a single
387        // distinct entry and fit.
388        cfg.guest_msrs(&[u32::MAX, u32::MAX, u32::MAX]).unwrap();
389        assert_eq!(
390            cfg.get_guest_msrs().len(),
391            SandboxConfiguration::MAX_GUEST_MSRS
392        );
393    }
394
395    #[test]
396    fn overrides() {
397        const HEAP_SIZE_OVERRIDE: u64 = 0x50000;
398        const INPUT_DATA_SIZE_OVERRIDE: usize = 0x4000;
399        const OUTPUT_DATA_SIZE_OVERRIDE: usize = 0x4001;
400        const SCRATCH_SIZE_OVERRIDE: usize = 0x60000;
401        let mut cfg = SandboxConfiguration::new(
402            INPUT_DATA_SIZE_OVERRIDE,
403            OUTPUT_DATA_SIZE_OVERRIDE,
404            Some(HEAP_SIZE_OVERRIDE),
405            SCRATCH_SIZE_OVERRIDE,
406            SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY,
407            SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET,
408            #[cfg(gdb)]
409            None,
410            #[cfg(crashdump)]
411            true,
412        );
413
414        let heap_size = cfg.get_heap_size();
415        let scratch_size = cfg.get_scratch_size();
416        assert_eq!(HEAP_SIZE_OVERRIDE, heap_size);
417        assert_eq!(SCRATCH_SIZE_OVERRIDE, scratch_size);
418
419        cfg.heap_size_override = 2048;
420        cfg.scratch_size = 0x40000;
421        assert_eq!(2048, cfg.heap_size_override);
422        assert_eq!(0x40000, cfg.scratch_size);
423        assert_eq!(INPUT_DATA_SIZE_OVERRIDE, cfg.input_data_size);
424        assert_eq!(OUTPUT_DATA_SIZE_OVERRIDE, cfg.output_data_size);
425    }
426
427    #[test]
428    fn min_sizes() {
429        let mut cfg = SandboxConfiguration::new(
430            SandboxConfiguration::MIN_INPUT_SIZE - 1,
431            SandboxConfiguration::MIN_OUTPUT_SIZE - 1,
432            None,
433            SandboxConfiguration::DEFAULT_SCRATCH_SIZE,
434            SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY,
435            SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET,
436            #[cfg(gdb)]
437            None,
438            #[cfg(crashdump)]
439            true,
440        );
441        assert_eq!(SandboxConfiguration::MIN_INPUT_SIZE, cfg.input_data_size);
442        assert_eq!(SandboxConfiguration::MIN_OUTPUT_SIZE, cfg.output_data_size);
443        assert_eq!(0, cfg.heap_size_override);
444
445        cfg.set_input_data_size(SandboxConfiguration::MIN_INPUT_SIZE - 1);
446        cfg.set_output_data_size(SandboxConfiguration::MIN_OUTPUT_SIZE - 1);
447
448        assert_eq!(SandboxConfiguration::MIN_INPUT_SIZE, cfg.input_data_size);
449        assert_eq!(SandboxConfiguration::MIN_OUTPUT_SIZE, cfg.output_data_size);
450    }
451
452    mod proptests {
453        use proptest::prelude::*;
454
455        use super::SandboxConfiguration;
456        #[cfg(gdb)]
457        use crate::sandbox::config::DebugInfo;
458
459        proptest! {
460            #[test]
461            fn input_data_size(size in SandboxConfiguration::MIN_INPUT_SIZE..=SandboxConfiguration::MIN_INPUT_SIZE * 10) {
462                let mut cfg = SandboxConfiguration::default();
463                cfg.set_input_data_size(size);
464                prop_assert_eq!(size, cfg.get_input_data_size());
465            }
466
467            #[test]
468            fn output_data_size(size in SandboxConfiguration::MIN_OUTPUT_SIZE..=SandboxConfiguration::MIN_OUTPUT_SIZE * 10) {
469                let mut cfg = SandboxConfiguration::default();
470                cfg.set_output_data_size(size);
471                prop_assert_eq!(size, cfg.get_output_data_size());
472            }
473
474
475            #[test]
476            fn heap_size_override(size in 0x1000..=0x10000u64) {
477                let mut cfg = SandboxConfiguration::default();
478                cfg.set_heap_size(size);
479                prop_assert_eq!(size, cfg.heap_size_override);
480            }
481
482            #[test]
483            #[cfg(gdb)]
484            fn guest_debug_info(port in 9000..=u16::MAX) {
485                let mut cfg = SandboxConfiguration::default();
486                let debug_info = DebugInfo { port };
487                cfg.set_guest_debug_info(debug_info);
488                prop_assert_eq!(debug_info, *cfg.get_guest_debug_info().as_ref().unwrap());
489            }
490        }
491    }
492}