Skip to main content

hyperlight_host/sandbox/
builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6#[cfg(target_os = "linux")]
7use std::time::Duration;
8
9use hyperlight_common::func::{ParameterTuple, SupportedReturnType};
10use tracing_core::LevelFilter;
11
12use crate::func::HostFunction;
13use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
14use crate::sandbox::SandboxConfiguration;
15#[cfg(gdb)]
16use crate::sandbox::config::DebugInfo;
17#[cfg(target_arch = "x86_64")]
18use crate::sandbox::config::GuestMsrError;
19use crate::sandbox::host_funcs::FunctionEntry;
20use crate::sandbox::snapshot::Snapshot;
21use crate::sandbox::uninitialized::{GuestBlob, GuestEnvironment};
22use crate::{
23    GuestBinary, HostFunctions, MultiUseSandbox as Sandbox, Result, UninitializedSandbox, new_error,
24};
25
26/// What a [`SandboxBuilder`] builds the sandbox from.
27enum Source {
28    GuestBinary(GuestBinary),
29    Snapshot(Arc<Snapshot>),
30}
31
32impl Source {
33    fn file(path: impl AsRef<Path>) -> Self {
34        Self::GuestBinary(GuestBinary::FilePath(path.as_ref().to_path_buf()))
35    }
36
37    fn bytes(buffer: impl Into<Vec<u8>>) -> Self {
38        Self::GuestBinary(GuestBinary::Buffer(buffer.into()))
39    }
40}
41
42/// Builds a [`Sandbox`].
43///
44/// Start from [`SandboxBuilder::from_file`],
45/// [`SandboxBuilder::from_bytes`] or [`SandboxBuilder::from_snapshot`],
46/// chain the settings you need, then call [`SandboxBuilder::build`]. Every
47/// setting has a default, so a builder with no adjustments is valid.
48///
49/// By default only the `HostPrint` host function is registered, which writes
50/// guest output to the host's stdout. Replace it with [`Self::host_print`].
51///
52/// # Examples
53///
54/// From a guest binary on disk:
55///
56/// ```no_run
57/// # use hyperlight_host::{Result, SandboxBuilder};
58/// # fn example() -> Result<()> {
59/// let mut sandbox = SandboxBuilder::from_file("guest.bin")
60///     .heap_size(1024 * 1024)
61///     .host_function("Add", |a: i32, b: i32| a + b)
62///     .build()?;
63///
64/// let result: String = sandbox.call("Echo", "hello".to_string())?;
65/// # Ok(())
66/// # }
67/// ```
68///
69/// From a snapshot. The snapshot carries the guest binary and the state it was
70/// taken in, so no guest binary is given here. The builder must still register
71/// every host function the snapshot was taken with:
72///
73/// ```no_run
74/// # use hyperlight_host::{Result, SandboxBuilder};
75/// # fn example() -> Result<()> {
76/// let mut sandbox = SandboxBuilder::from_file("guest.bin")
77///     .host_function("Add", |a: i32, b: i32| a + b)
78///     .build()?;
79/// let snapshot = sandbox.snapshot()?;
80///
81/// let mut restored = SandboxBuilder::from_snapshot(snapshot)
82///     .host_function("Add", |a: i32, b: i32| a + b)
83///     .build()?;
84///
85/// let result: String = restored.call("Echo", "hello".to_string())?;
86/// # Ok(())
87/// # }
88/// ```
89pub struct SandboxBuilder {
90    source: Source,
91    cfg: SandboxConfiguration,
92    host_funcs: HostFunctions,
93    init_data: Option<(Vec<u8>, MemoryRegionFlags)>,
94    mapped_file_cow: Vec<(std::path::PathBuf, u64)>,
95    mapped_memory_regions: Vec<MemoryRegion>,
96    guest_log_level: Option<LevelFilter>,
97}
98
99impl SandboxBuilder {
100    fn with_source(source: Source) -> Self {
101        Self {
102            source,
103            cfg: SandboxConfiguration::default(),
104            host_funcs: HostFunctions::default(),
105            init_data: None,
106            mapped_file_cow: Vec::new(),
107            mapped_memory_regions: Vec::new(),
108            guest_log_level: None,
109        }
110    }
111
112    /// Build a sandbox running the guest binary at `path`, an ELF file.
113    pub fn from_file(path: impl AsRef<Path>) -> Self {
114        Self::with_source(Source::file(path))
115    }
116
117    /// Build a sandbox running the guest binary held in `buffer`, the contents
118    /// of an ELF file.
119    pub fn from_bytes(buffer: impl Into<Vec<u8>>) -> Self {
120        Self::with_source(Source::bytes(buffer))
121    }
122
123    /// Build a sandbox restoring the guest from `snapshot`.
124    pub fn from_snapshot(snapshot: Arc<Snapshot>) -> Self {
125        Self::with_source(Source::Snapshot(snapshot))
126    }
127
128    /// Create the sandbox.
129    ///
130    /// # Errors
131    ///
132    /// When building from a snapshot, returns an error if [`Self::init_data`]
133    /// or [`Self::guest_log_level`] are set. The snapshot already carries
134    /// both, so they have no effect there.
135    pub fn build(self) -> Result<Sandbox> {
136        let Self {
137            source,
138            cfg,
139            host_funcs,
140            init_data,
141            mapped_file_cow,
142            mapped_memory_regions,
143            guest_log_level,
144        } = self;
145
146        let mut sandbox = match source {
147            Source::GuestBinary(guest_binary) => {
148                let env = GuestEnvironment {
149                    init_data: init_data.as_ref().map(|(data, flags)| GuestBlob {
150                        data,
151                        permissions: *flags,
152                    }),
153                    guest_binary,
154                };
155
156                let mut uninitialized_sandbox = UninitializedSandbox::new(env, Some(cfg))?;
157
158                uninitialized_sandbox.host_funcs = Arc::new(Mutex::new(host_funcs.into_inner()));
159
160                for (path, guest_base) in mapped_file_cow {
161                    uninitialized_sandbox.map_file_cow(&path, guest_base)?;
162                }
163
164                if let Some(log_level) = guest_log_level {
165                    uninitialized_sandbox.set_max_guest_log_level(log_level);
166                }
167
168                uninitialized_sandbox.evolve()?
169            }
170            Source::Snapshot(snapshot) => {
171                if init_data.is_some() {
172                    return Err(new_error!(
173                        "init_data has no effect when building from a snapshot, as the snapshot already contains it"
174                    ));
175                }
176
177                if guest_log_level.is_some() {
178                    return Err(new_error!(
179                        "guest_log_level has no effect when building from a snapshot, as the snapshot already contains it"
180                    ));
181                }
182
183                let mut sandbox = Sandbox::from_snapshot(snapshot, host_funcs, Some(cfg))?;
184
185                for (path, guest_base) in mapped_file_cow {
186                    sandbox.map_file_cow(&path, guest_base)?;
187                }
188
189                sandbox
190            }
191        };
192
193        for region in mapped_memory_regions {
194            // SAFETY: the caller of `mapped_memory_region` guaranteed each region
195            // stays valid and unmodified for the lifetime of this sandbox.
196            unsafe { sandbox.map_region(&region)? };
197        }
198
199        Ok(sandbox)
200    }
201}
202
203impl SandboxBuilder {
204    /// Sets the sandbox `init_data` into the sandbox's memory when it is built, with `flags` as
205    /// the guest's permissions on that region.
206    ///
207    /// Note: [`Self::build`] errors if this setting is set and the builder's
208    /// source is a snapshot, as the snapshot already contains the init data.
209    pub fn init_data(mut self, data: impl Into<Vec<u8>>, flags: MemoryRegionFlags) -> Self {
210        self.init_data = Some((data.into(), flags));
211        self
212    }
213
214    /// Map the contents of the file at `path` into the guest at `guest_base`,
215    /// copy-on-write.
216    ///
217    /// `guest_base` must be page-aligned and lie outside the sandbox's primary
218    /// shared memory region. Violations surface as an error from
219    /// [`Self::build`], not here. Call this once per file to map several.
220    pub fn mapped_file_cow(mut self, path: impl AsRef<Path>, guest_base: u64) -> Self {
221        self.mapped_file_cow
222            .push((path.as_ref().to_path_buf(), guest_base));
223        self
224    }
225
226    /// Maps a region of host memory into the sandbox address space.
227    ///
228    /// The base address and length must meet platform alignment requirements
229    /// (typically page-aligned). The `region_type` field is ignored as guest
230    /// page table entries are not created.
231    ///
232    /// # Safety
233    ///
234    /// The caller must ensure the host memory region remains valid and
235    /// unmodified for the lifetime of the sandbox this builder produces.
236    pub unsafe fn mapped_memory_region(mut self, region: MemoryRegion) -> Self {
237        self.mapped_memory_regions.push(region);
238        self
239    }
240
241    /// Sets the maximum log level for guest code execution.
242    ///
243    /// If not set, the log level is determined by the `RUST_LOG` environment variable,
244    /// defaulting to [`LevelFilter::ERROR`] if unset.
245    ///
246    /// Note: [`Self::build`] errors if this setting is set and the builder's
247    /// source is a snapshot, as the log level is already captured in the snapshot.
248    pub fn guest_log_level(mut self, level: LevelFilter) -> Self {
249        self.guest_log_level = Some(level);
250        self
251    }
252
253    /// The maximum log level for guest code execution, or `None` if not set.
254    pub fn get_guest_log_level(&self) -> Option<LevelFilter> {
255        self.guest_log_level
256    }
257}
258
259impl SandboxBuilder {
260    /// Registers a host function that the guest can call.
261    ///
262    /// Note: registering under the name `HostPrint` overrides guest printing.
263    /// Prefer [`Self::host_print`], which checks the signature at compile time.
264    pub fn host_function<Args: ParameterTuple, Output: SupportedReturnType>(
265        mut self,
266        name: impl AsRef<str>,
267        host_func: impl Into<HostFunction<Output, Args>>,
268    ) -> Self {
269        let func = host_func.into().into();
270        let name = name.as_ref().to_string();
271
272        let entry = FunctionEntry {
273            function: func,
274            parameter_types: Args::TYPE,
275            return_type: Output::TYPE,
276        };
277
278        self.host_funcs
279            .inner_mut()
280            .register_host_function(name, entry);
281        self
282    }
283
284    /// Registers the special "HostPrint" function for guest printing.
285    ///
286    /// This overrides the default behavior of writing to stdout.
287    /// The function expects the signature `FnMut(String) -> i32`
288    /// and will be called when the guest wants to print output.
289    pub fn host_print(self, print_func: impl Into<HostFunction<i32, (String,)>>) -> Self {
290        self.host_function("HostPrint", print_func)
291    }
292
293    /// Registers every host function in `host_funcs`.
294    ///
295    /// Entries whose names are already registered are overwritten.
296    ///
297    /// Note: an entry named `HostPrint` overrides guest printing. Prefer
298    /// [`Self::host_print`], which checks the signature at compile time.
299    pub fn host_functions(mut self, host_funcs: HostFunctions) -> Self {
300        for (func_name, func_entry) in host_funcs.into_iter() {
301            self.host_funcs
302                .inner_mut()
303                .register_host_function(func_name, func_entry);
304        }
305        self
306    }
307}
308
309impl SandboxBuilder {
310    /// Set the size of the memory buffer made available for input to the guest.
311    /// Values below [`SandboxConfiguration::MIN_INPUT_SIZE`] are clamped up.
312    pub fn input_data_size(mut self, size: usize) -> Self {
313        self.cfg.set_input_data_size(size);
314        self
315    }
316
317    /// The size of the memory buffer made available for input to the guest.
318    pub fn get_input_data_size(&self) -> usize {
319        self.cfg.get_input_data_size()
320    }
321
322    /// Set the size of the memory buffer made available for output from the guest.
323    /// Values below [`SandboxConfiguration::MIN_OUTPUT_SIZE`] are clamped up.
324    pub fn output_data_size(mut self, size: usize) -> Self {
325        self.cfg.set_output_data_size(size);
326        self
327    }
328
329    /// The size of the memory buffer made available for output from the guest.
330    pub fn get_output_data_size(&self) -> usize {
331        self.cfg.get_output_data_size()
332    }
333
334    /// Set the guest heap size. A size of 0 selects
335    /// [`SandboxConfiguration::DEFAULT_HEAP_SIZE`].
336    pub fn heap_size(mut self, size: u64) -> Self {
337        self.cfg.set_heap_size(size);
338        self
339    }
340
341    /// The guest heap size, defaulting to
342    /// [`SandboxConfiguration::DEFAULT_HEAP_SIZE`] when no override is set.
343    pub fn get_heap_size(&self) -> u64 {
344        self.cfg.get_heap_size()
345    }
346
347    /// Set how much writable memory to offer the guest.
348    pub fn scratch_size(mut self, size: usize) -> Self {
349        self.cfg.set_scratch_size(size);
350        self
351    }
352
353    /// How much writable memory is offered to the guest.
354    pub fn get_scratch_size(&self) -> usize {
355        self.cfg.get_scratch_size()
356    }
357
358    /// Declare MSRs the guest owns, saved and restored with the rest of the
359    /// sandbox state. Adds to the declared set, so repeated calls accumulate.
360    ///
361    /// See [`SandboxConfiguration::guest_msrs`] for the platform-specific
362    /// behavior and the capacity limit.
363    ///
364    /// # Errors
365    ///
366    /// Returns [`GuestMsrError::CapacityExceeded`] if the distinct entries
367    /// would exceed [`SandboxConfiguration::MAX_GUEST_MSRS`]. The declared set
368    /// is unchanged on error.
369    #[cfg(target_arch = "x86_64")]
370    pub fn guest_msrs(mut self, indices: &[u32]) -> std::result::Result<Self, GuestMsrError> {
371        self.cfg.guest_msrs(indices)?;
372        Ok(self)
373    }
374
375    /// Set how long to wait between attempts to signal the VCPU thread.
376    #[cfg(target_os = "linux")]
377    pub fn interrupt_retry_delay(mut self, delay: Duration) -> Self {
378        self.cfg.set_interrupt_retry_delay(delay);
379        self
380    }
381
382    /// How long to wait between attempts to signal the VCPU thread.
383    #[cfg(target_os = "linux")]
384    pub fn get_interrupt_retry_delay(&self) -> Duration {
385        self.cfg.get_interrupt_retry_delay()
386    }
387
388    /// Set the offset from `SIGRTMIN` for the signal used to interrupt the VCPU
389    /// thread.
390    ///
391    /// # Errors
392    ///
393    /// Returns an error if `SIGRTMIN + offset` exceeds `SIGRTMAX`.
394    #[cfg(target_os = "linux")]
395    pub fn interrupt_vcpu_sigrtmin_offset(mut self, offset: u8) -> Result<Self> {
396        self.cfg.set_interrupt_vcpu_sigrtmin_offset(offset)?;
397        Ok(self)
398    }
399
400    /// The offset from `SIGRTMIN` for the signal used to interrupt the VCPU thread.
401    #[cfg(target_os = "linux")]
402    pub fn get_interrupt_vcpu_sigrtmin_offset(&self) -> u8 {
403        self.cfg.get_interrupt_vcpu_sigrtmin_offset()
404    }
405
406    /// Toggle guest core dump generation.
407    #[cfg(crashdump)]
408    pub fn guest_core_dump(mut self, enabled: bool) -> Self {
409        self.cfg.set_guest_core_dump(enabled);
410        self
411    }
412
413    /// Whether guest core dump generation is enabled.
414    #[cfg(crashdump)]
415    pub fn get_guest_core_dump(&self) -> bool {
416        self.cfg.get_guest_core_dump()
417    }
418
419    /// Set the guest debug configuration.
420    #[cfg(gdb)]
421    pub fn guest_debug_info(mut self, debug_info: DebugInfo) -> Self {
422        self.cfg.set_guest_debug_info(debug_info);
423        self
424    }
425
426    /// The guest debug configuration, or `None` when debugging is not configured.
427    #[cfg(gdb)]
428    pub fn get_guest_debug_info(&self) -> Option<DebugInfo> {
429        self.cfg.get_guest_debug_info()
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use hyperlight_testing::simple_guest_as_string;
436    use tracing_core::LevelFilter;
437
438    use super::SandboxBuilder;
439    use crate::mem::memory_region::MemoryRegionFlags;
440
441    #[test]
442    fn build_from_file() {
443        let path = simple_guest_as_string().unwrap();
444        let mut sandbox = SandboxBuilder::from_file(path)
445            .input_data_size(0x8000)
446            .build()
447            .unwrap();
448
449        let result = sandbox.call::<String>("Echo", "hello".to_string()).unwrap();
450        assert_eq!(result, "hello");
451    }
452
453    #[test]
454    fn build_from_bytes() {
455        let bytes = std::fs::read(simple_guest_as_string().unwrap()).unwrap();
456        let mut sandbox = SandboxBuilder::from_bytes(bytes).build().unwrap();
457
458        let result = sandbox.call::<String>("Echo", "hello".to_string()).unwrap();
459        assert_eq!(result, "hello");
460    }
461
462    #[test]
463    fn build_from_snapshot() {
464        let path = simple_guest_as_string().unwrap();
465        let mut sandbox = SandboxBuilder::from_file(path).build().unwrap();
466        let snapshot = sandbox.snapshot().unwrap();
467
468        let mut restored = SandboxBuilder::from_snapshot(snapshot).build().unwrap();
469
470        let result = restored
471            .call::<String>("Echo", "hello".to_string())
472            .unwrap();
473        assert_eq!(result, "hello");
474    }
475
476    #[test]
477    fn build_from_snapshot_errors_on_ignored_settings() {
478        let path = simple_guest_as_string().unwrap();
479        let mut sandbox = SandboxBuilder::from_file(path).build().unwrap();
480        let snapshot = sandbox.snapshot().unwrap();
481
482        assert!(
483            SandboxBuilder::from_snapshot(snapshot.clone())
484                .init_data([0u8; 8], MemoryRegionFlags::READ)
485                .build()
486                .is_err()
487        );
488
489        assert!(
490            SandboxBuilder::from_snapshot(snapshot)
491                .guest_log_level(LevelFilter::INFO)
492                .build()
493                .is_err()
494        );
495    }
496}