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