Skip to main content

hyperlight_host/sandbox/
config.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::cmp::max;
18use std::time::Duration;
19
20#[cfg(target_os = "linux")]
21use libc::c_int;
22use tracing::{Span, instrument};
23
24/// Used for passing debug configuration to a sandbox
25#[cfg(gdb)]
26#[derive(Copy, Clone, Debug, Eq, PartialEq)]
27pub struct DebugInfo {
28    /// Guest debug port
29    pub port: u16,
30}
31
32/// The complete set of configuration needed to create a Sandbox
33#[derive(Copy, Clone, Debug, Eq, PartialEq)]
34#[repr(C)]
35pub struct SandboxConfiguration {
36    /// Guest core dump output directory
37    /// This field is by default set to true which means the value core dumps will be placed in:
38    /// - HYPERLIGHT_CORE_DUMP_DIR environment variable if it is set
39    /// - default value of the temporary directory
40    ///
41    /// The core dump files generation can be disabled by setting this field to false.
42    #[cfg(crashdump)]
43    guest_core_dump: bool,
44    /// Guest gdb debug port
45    #[cfg(gdb)]
46    guest_debug_info: Option<DebugInfo>,
47    /// The size of the memory buffer that is made available for input to the
48    /// Guest Binary
49    input_data_size: usize,
50    /// The size of the memory buffer that is made available for input to the
51    /// Guest Binary
52    output_data_size: usize,
53    /// The heap size to use in the guest sandbox. If set to 0, the heap
54    /// size will be determined from the PE file header
55    ///
56    /// Note: this is a C-compatible struct, so even though this optional
57    /// field should be represented as an `Option`, that type is not
58    /// FFI-safe, so it cannot be.
59    heap_size_override: u64,
60    /// Delay between interrupt retries. This duration specifies how long to wait
61    /// between attempts to send signals to the thread running the sandbox's VCPU.
62    /// Multiple retries may be necessary because signals only interrupt the VCPU
63    /// thread when the vcpu thread is in kernel space. There's a narrow window during which a
64    /// signal can be delivered to the thread, but the thread may not yet
65    /// have entered kernel space.
66    interrupt_retry_delay: Duration,
67    /// Offset from `SIGRTMIN` used to determine the signal number for interrupting
68    /// the VCPU thread. The actual signal sent is `SIGRTMIN + interrupt_vcpu_sigrtmin_offset`.
69    ///
70    /// This signal must fall within the valid real-time signal range supported by the host.
71    ///
72    /// Note: Since real-time signals can vary across platforms, ensure that the offset
73    /// results in a signal number that is not already in use by other components of the system.
74    interrupt_vcpu_sigrtmin_offset: u8,
75    /// How much writable memory to offer the guest
76    scratch_size: usize,
77}
78
79impl SandboxConfiguration {
80    /// The default size of input data
81    pub const DEFAULT_INPUT_SIZE: usize = 0x4000;
82    /// The minimum size of input data
83    pub const MIN_INPUT_SIZE: usize = 0x2000;
84    /// The default size of output data
85    pub const DEFAULT_OUTPUT_SIZE: usize = 0x4000;
86    /// The minimum size of output data
87    pub const MIN_OUTPUT_SIZE: usize = 0x2000;
88    /// The default interrupt retry delay
89    pub const DEFAULT_INTERRUPT_RETRY_DELAY: Duration = Duration::from_micros(500);
90    /// The default signal offset from `SIGRTMIN` used to determine the signal number for interrupting
91    pub const INTERRUPT_VCPU_SIGRTMIN_OFFSET: u8 = 0;
92    /// The default heap size of a hyperlight sandbox
93    pub const DEFAULT_HEAP_SIZE: u64 = 131072;
94    /// The default size of the scratch region
95    pub const DEFAULT_SCRATCH_SIZE: usize = 0x48000;
96
97    #[allow(clippy::too_many_arguments)]
98    /// Create a new configuration for a sandbox with the given sizes.
99    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
100    fn new(
101        input_data_size: usize,
102        output_data_size: usize,
103        heap_size_override: Option<u64>,
104        scratch_size: usize,
105        interrupt_retry_delay: Duration,
106        interrupt_vcpu_sigrtmin_offset: u8,
107        #[cfg(gdb)] guest_debug_info: Option<DebugInfo>,
108        #[cfg(crashdump)] guest_core_dump: bool,
109    ) -> Self {
110        Self {
111            input_data_size: max(input_data_size, Self::MIN_INPUT_SIZE),
112            output_data_size: max(output_data_size, Self::MIN_OUTPUT_SIZE),
113            heap_size_override: heap_size_override.unwrap_or(0),
114            scratch_size,
115            interrupt_retry_delay,
116            interrupt_vcpu_sigrtmin_offset,
117            #[cfg(gdb)]
118            guest_debug_info,
119            #[cfg(crashdump)]
120            guest_core_dump,
121        }
122    }
123
124    /// Set the size of the memory buffer that is made available for input to the guest
125    /// the minimum value is MIN_INPUT_SIZE
126    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
127    pub fn set_input_data_size(&mut self, input_data_size: usize) {
128        self.input_data_size = max(input_data_size, Self::MIN_INPUT_SIZE);
129    }
130
131    /// Set the size of the memory buffer that is made available for output from the guest
132    /// the minimum value is MIN_OUTPUT_SIZE
133    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
134    pub fn set_output_data_size(&mut self, output_data_size: usize) {
135        self.output_data_size = max(output_data_size, Self::MIN_OUTPUT_SIZE);
136    }
137
138    /// 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
139    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
140    pub fn set_heap_size(&mut self, heap_size: u64) {
141        self.heap_size_override = heap_size;
142    }
143
144    /// Sets the interrupt retry delay
145    #[cfg(target_os = "linux")]
146    pub fn set_interrupt_retry_delay(&mut self, delay: Duration) {
147        self.interrupt_retry_delay = delay;
148    }
149
150    /// Get the delay between retries for interrupts
151    #[cfg(target_os = "linux")]
152    pub fn get_interrupt_retry_delay(&self) -> Duration {
153        self.interrupt_retry_delay
154    }
155
156    /// Get the signal offset from `SIGRTMIN` used to determine the signal number for interrupting the VCPU thread
157    #[cfg(target_os = "linux")]
158    pub fn get_interrupt_vcpu_sigrtmin_offset(&self) -> u8 {
159        self.interrupt_vcpu_sigrtmin_offset
160    }
161
162    /// Sets the offset from `SIGRTMIN` to determine the real-time signal used for
163    /// interrupting the VCPU thread.
164    ///
165    /// The final signal number is computed as `SIGRTMIN + offset`, and it must fall within
166    /// the valid range of real-time signals supported by the host system.
167    ///
168    /// Returns Ok(()) if the offset is valid, or an error if it exceeds the maximum real-time signal number.
169    #[cfg(target_os = "linux")]
170    pub fn set_interrupt_vcpu_sigrtmin_offset(&mut self, offset: u8) -> crate::Result<()> {
171        if libc::SIGRTMIN() + offset as c_int > libc::SIGRTMAX() {
172            return Err(crate::new_error!(
173                "Invalid SIGRTMIN offset: {}. It exceeds the maximum real-time signal number.",
174                offset
175            ));
176        }
177        self.interrupt_vcpu_sigrtmin_offset = offset;
178        Ok(())
179    }
180
181    /// Toggles the guest core dump generation for a sandbox
182    /// Setting this to false disables the core dump generation
183    /// This is only used when the `crashdump` feature is enabled
184    #[cfg(crashdump)]
185    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
186    pub fn set_guest_core_dump(&mut self, enable: bool) {
187        self.guest_core_dump = enable;
188    }
189
190    /// Sets the configuration for the guest debug
191    #[cfg(gdb)]
192    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
193    pub fn set_guest_debug_info(&mut self, debug_info: DebugInfo) {
194        self.guest_debug_info = Some(debug_info);
195    }
196
197    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
198    pub(crate) fn get_input_data_size(&self) -> usize {
199        self.input_data_size
200    }
201
202    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
203    pub(crate) fn get_output_data_size(&self) -> usize {
204        self.output_data_size
205    }
206
207    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
208    pub(crate) fn get_scratch_size(&self) -> usize {
209        self.scratch_size
210    }
211
212    /// Set the size of the scratch regiong
213    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
214    pub fn set_scratch_size(&mut self, scratch_size: usize) {
215        self.scratch_size = scratch_size;
216    }
217
218    #[cfg(crashdump)]
219    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
220    pub(crate) fn get_guest_core_dump(&self) -> bool {
221        self.guest_core_dump
222    }
223
224    #[cfg(gdb)]
225    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
226    pub(crate) fn get_guest_debug_info(&self) -> Option<DebugInfo> {
227        self.guest_debug_info
228    }
229
230    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
231    fn heap_size_override_opt(&self) -> Option<u64> {
232        (self.heap_size_override > 0).then_some(self.heap_size_override)
233    }
234
235    /// If self.heap_size_override is non-zero, return it. Otherwise,
236    /// return exe_info.heap_reserve()
237    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
238    pub(crate) fn get_heap_size(&self) -> u64 {
239        self.heap_size_override_opt()
240            .unwrap_or(Self::DEFAULT_HEAP_SIZE)
241    }
242}
243
244impl Default for SandboxConfiguration {
245    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
246    fn default() -> Self {
247        Self::new(
248            Self::DEFAULT_INPUT_SIZE,
249            Self::DEFAULT_OUTPUT_SIZE,
250            None,
251            Self::DEFAULT_SCRATCH_SIZE,
252            Self::DEFAULT_INTERRUPT_RETRY_DELAY,
253            Self::INTERRUPT_VCPU_SIGRTMIN_OFFSET,
254            #[cfg(gdb)]
255            None,
256            #[cfg(crashdump)]
257            true,
258        )
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::SandboxConfiguration;
265
266    #[test]
267    fn overrides() {
268        const HEAP_SIZE_OVERRIDE: u64 = 0x50000;
269        const INPUT_DATA_SIZE_OVERRIDE: usize = 0x4000;
270        const OUTPUT_DATA_SIZE_OVERRIDE: usize = 0x4001;
271        const SCRATCH_SIZE_OVERRIDE: usize = 0x60000;
272        let mut cfg = SandboxConfiguration::new(
273            INPUT_DATA_SIZE_OVERRIDE,
274            OUTPUT_DATA_SIZE_OVERRIDE,
275            Some(HEAP_SIZE_OVERRIDE),
276            SCRATCH_SIZE_OVERRIDE,
277            SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY,
278            SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET,
279            #[cfg(gdb)]
280            None,
281            #[cfg(crashdump)]
282            true,
283        );
284
285        let heap_size = cfg.get_heap_size();
286        let scratch_size = cfg.get_scratch_size();
287        assert_eq!(HEAP_SIZE_OVERRIDE, heap_size);
288        assert_eq!(SCRATCH_SIZE_OVERRIDE, scratch_size);
289
290        cfg.heap_size_override = 2048;
291        cfg.scratch_size = 0x40000;
292        assert_eq!(2048, cfg.heap_size_override);
293        assert_eq!(0x40000, cfg.scratch_size);
294        assert_eq!(INPUT_DATA_SIZE_OVERRIDE, cfg.input_data_size);
295        assert_eq!(OUTPUT_DATA_SIZE_OVERRIDE, cfg.output_data_size);
296    }
297
298    #[test]
299    fn min_sizes() {
300        let mut cfg = SandboxConfiguration::new(
301            SandboxConfiguration::MIN_INPUT_SIZE - 1,
302            SandboxConfiguration::MIN_OUTPUT_SIZE - 1,
303            None,
304            SandboxConfiguration::DEFAULT_SCRATCH_SIZE,
305            SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY,
306            SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET,
307            #[cfg(gdb)]
308            None,
309            #[cfg(crashdump)]
310            true,
311        );
312        assert_eq!(SandboxConfiguration::MIN_INPUT_SIZE, cfg.input_data_size);
313        assert_eq!(SandboxConfiguration::MIN_OUTPUT_SIZE, cfg.output_data_size);
314        assert_eq!(0, cfg.heap_size_override);
315
316        cfg.set_input_data_size(SandboxConfiguration::MIN_INPUT_SIZE - 1);
317        cfg.set_output_data_size(SandboxConfiguration::MIN_OUTPUT_SIZE - 1);
318
319        assert_eq!(SandboxConfiguration::MIN_INPUT_SIZE, cfg.input_data_size);
320        assert_eq!(SandboxConfiguration::MIN_OUTPUT_SIZE, cfg.output_data_size);
321    }
322
323    mod proptests {
324        use proptest::prelude::*;
325
326        use super::SandboxConfiguration;
327        #[cfg(gdb)]
328        use crate::sandbox::config::DebugInfo;
329
330        proptest! {
331            #[test]
332            fn input_data_size(size in SandboxConfiguration::MIN_INPUT_SIZE..=SandboxConfiguration::MIN_INPUT_SIZE * 10) {
333                let mut cfg = SandboxConfiguration::default();
334                cfg.set_input_data_size(size);
335                prop_assert_eq!(size, cfg.get_input_data_size());
336            }
337
338            #[test]
339            fn output_data_size(size in SandboxConfiguration::MIN_OUTPUT_SIZE..=SandboxConfiguration::MIN_OUTPUT_SIZE * 10) {
340                let mut cfg = SandboxConfiguration::default();
341                cfg.set_output_data_size(size);
342                prop_assert_eq!(size, cfg.get_output_data_size());
343            }
344
345
346            #[test]
347            fn heap_size_override(size in 0x1000..=0x10000u64) {
348                let mut cfg = SandboxConfiguration::default();
349                cfg.set_heap_size(size);
350                prop_assert_eq!(size, cfg.heap_size_override);
351            }
352
353            #[test]
354            #[cfg(gdb)]
355            fn guest_debug_info(port in 9000..=u16::MAX) {
356                let mut cfg = SandboxConfiguration::default();
357                let debug_info = DebugInfo { port };
358                cfg.set_guest_debug_info(debug_info);
359                prop_assert_eq!(debug_info, *cfg.get_guest_debug_info().as_ref().unwrap());
360            }
361        }
362    }
363}