diff --git a/src/hyperlight_host/src/lib.rs b/src/hyperlight_host/src/lib.rs index 162d0420f..785b5414d 100644 --- a/src/hyperlight_host/src/lib.rs +++ b/src/hyperlight_host/src/lib.rs @@ -91,6 +91,8 @@ pub use hypervisor::virtual_machine::is_hypervisor_present; pub use sandbox::MultiUseSandbox; /// The re-export for the `UninitializedSandbox` type pub use sandbox::UninitializedSandbox; +/// The re-export for the `SandboxBuilder` type +pub use sandbox::builder::SandboxBuilder; /// A collection of host functions that can be supplied to a sandbox /// constructor (e.g. [`MultiUseSandbox::from_snapshot`]). pub use sandbox::host_funcs::HostFunctions; diff --git a/src/hyperlight_host/src/sandbox/builder.rs b/src/hyperlight_host/src/sandbox/builder.rs new file mode 100644 index 000000000..6f287f103 --- /dev/null +++ b/src/hyperlight_host/src/sandbox/builder.rs @@ -0,0 +1,525 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use std::path::Path; +use std::sync::Arc; +#[cfg(target_os = "linux")] +use std::time::Duration; + +use hyperlight_common::func::{ParameterTuple, SupportedReturnType}; +use tracing_core::LevelFilter; + +use crate::func::HostFunction; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +use crate::sandbox::SandboxConfiguration; +#[cfg(gdb)] +use crate::sandbox::config::DebugInfo; +use crate::sandbox::host_funcs::FunctionEntry; +use crate::sandbox::snapshot::Snapshot; +use crate::sandbox::uninitialized::{GuestBlob, GuestEnvironment}; +use crate::{ + GuestBinary, HostFunctions, MultiUseSandbox as Sandbox, Result, UninitializedSandbox, new_error, +}; + +/// Builds a [`Sandbox`]. +/// +/// Start from [`SandboxBuilder::new`], adjust settings through the `with_*` +/// (consuming, chainable) or bare-named (in place) accessors, then call one of +/// the `build_from_*` methods to create the sandbox from a guest binary on +/// disk, a guest binary in memory, or a [`Snapshot`]. Every setting has a +/// default, so a builder with no adjustments is valid. +#[derive(Default)] +pub struct SandboxBuilder { + cfg: SandboxConfiguration, + host_funcs: HostFunctions, + init_data: Option<(Vec, MemoryRegionFlags)>, + mapped_file_cow: Vec<(std::path::PathBuf, u64)>, + mapped_memory_regions: Vec, + max_guest_log_level: Option, +} + +impl SandboxBuilder { + /// Create a builder with the default configuration and no host functions. + pub fn new() -> Self { + Self::default() + } + + /// Build a sandbox running the guest binary at `path`. + pub fn build_from_file(self, path: impl AsRef) -> Result { + let path = path.as_ref().to_path_buf(); + self.build_from_guest_binary(GuestBinary::FilePath(path)) + } + + /// Build a sandbox running the guest binary held in `buffer`. + pub fn build_from_bytes(self, buffer: impl AsRef<[u8]>) -> Result { + let buffer = buffer.as_ref(); + self.build_from_guest_binary(GuestBinary::Buffer(buffer)) + } + + fn build_from_guest_binary(self, guest_binary: GuestBinary) -> Result { + let init_data = self.init_data.as_ref().map(|(data, flags)| GuestBlob { + data, + permissions: *flags, + }); + + let env = GuestEnvironment { + init_data, + guest_binary, + }; + + let mut uninitialized_sandbox = UninitializedSandbox::new(env, Some(self.cfg))?; + + let mut func_registry = uninitialized_sandbox + .host_funcs + .try_lock() + .map_err(|_| new_error!("Error locking"))?; + + let host_funcs = self.host_funcs.into_inner().functions_map; + for (func_name, func_entry) in host_funcs { + func_registry.register_host_function(func_name, func_entry); + } + + drop(func_registry); + + for (path, guest_base) in self.mapped_file_cow { + uninitialized_sandbox.map_file_cow(&path, guest_base)?; + } + + if let Some(max_log_level) = self.max_guest_log_level { + uninitialized_sandbox.set_max_guest_log_level(max_log_level); + } + + let mut sandbox = uninitialized_sandbox.evolve()?; + + for region in self.mapped_memory_regions { + // SAFETY: the caller of `map_memory_region` guaranteed each region + // stays valid and unmodified for the lifetime of this sandbox. + unsafe { sandbox.map_region(®ion)? }; + } + + Ok(sandbox) + } + + /// Build a sandbox restored from `snapshot`. + /// + /// # Errors + /// + /// Returns an error if [`Self::init_data`] or [`Self::max_guest_log_level`] + /// are set. The snapshot already carries both, so they have no effect here. + pub fn build_from_snapshot(self, snapshot: Arc) -> Result { + if self.init_data.is_some() { + return Err(new_error!( + "init_data has no effect when building from a snapshot, as the snapshot already contains it" + )); + } + + if self.max_guest_log_level.is_some() { + return Err(new_error!( + "max_guest_log_level has no effect when building from a snapshot, as the snapshot already contains it" + )); + } + + let mut sandbox = Sandbox::from_snapshot(snapshot, self.host_funcs, Some(self.cfg))?; + + for (path, guest_base) in self.mapped_file_cow { + sandbox.map_file_cow(&path, guest_base)?; + } + + for region in self.mapped_memory_regions { + // SAFETY: the caller of `map_memory_region` guaranteed each region + // stays valid and unmodified for the lifetime of this sandbox. + unsafe { sandbox.map_region(®ion)? }; + } + + Ok(sandbox) + } +} + +impl SandboxBuilder { + /// Sets the sandbox `init_data` into the sandbox's memory when it is built, with `flags` as + /// the guest's permissions on that region. + /// + /// Note: [`Self::build_from_snapshot`] errors if this setting is set, as the snapshot already + /// contains the init data. + pub fn init_data(&mut self, data: impl Into>, flags: MemoryRegionFlags) -> &mut Self { + self.init_data = Some((data.into(), flags)); + self + } + + /// Like [`Self::init_data`], but consumes and returns `self` for chaining. + pub fn with_init_data(mut self, data: impl Into>, flags: MemoryRegionFlags) -> Self { + self.init_data(data, flags); + self + } + + /// Map the contents of the file at `path` into the guest at `guest_base`, + /// copy-on-write. + /// + /// `guest_base` must be page-aligned and lie outside the sandbox's primary + /// shared memory region. Violations surface as an error from the + /// `build_from_*` call, not here. Call this once per file to map several. + pub fn map_file_cow(&mut self, path: impl AsRef, guest_base: u64) -> &mut Self { + self.mapped_file_cow + .push((path.as_ref().to_path_buf(), guest_base)); + self + } + + /// Like [`Self::map_file_cow`], but consumes and returns `self` for chaining. + pub fn with_mapped_file_cow(mut self, path: impl AsRef, guest_base: u64) -> Self { + self.map_file_cow(path, guest_base); + self + } + + /// Maps a region of host memory into the sandbox address space. + /// + /// The base address and length must meet platform alignment requirements + /// (typically page-aligned). The `region_type` field is ignored as guest + /// page table entries are not created. + /// + /// # Safety + /// + /// The caller must ensure the host memory region remains valid and + /// unmodified for the lifetime of the sandbox this builder produces. + pub unsafe fn map_memory_region(&mut self, region: MemoryRegion) -> &mut Self { + self.mapped_memory_regions.push(region); + self + } + + /// Like [`Self::map_memory_region`], but consumes and returns `self` for chaining. + /// + /// # Safety + /// + /// Same as [`Self::map_memory_region`]. + pub unsafe fn with_mapped_memory_region(mut self, region: MemoryRegion) -> Self { + unsafe { self.map_memory_region(region) }; + self + } + + /// Sets the maximum log level for guest code execution. + /// + /// If not set, the log level is determined by the `RUST_LOG` environment variable, + /// defaulting to [`LevelFilter::ERROR`] if unset. + /// + /// Note: [`Self::build_from_snapshot`] errors if this setting is set, as the log level is + /// already captured in the snapshot. + pub fn max_guest_log_level(&mut self, level: LevelFilter) -> &mut Self { + self.max_guest_log_level = Some(level); + self + } + + /// Like [`Self::max_guest_log_level`], but consumes and returns `self` for chaining. + pub fn with_max_guest_log_level(mut self, level: LevelFilter) -> Self { + self.max_guest_log_level(level); + self + } + + /// The maximum log level for guest code execution, or `None` if not set. + pub fn get_max_guest_log_level(&self) -> Option { + self.max_guest_log_level + } +} + +impl SandboxBuilder { + /// Registers a host function that the guest can call. + pub fn host_function( + &mut self, + name: impl AsRef, + host_func: impl Into>, + ) -> &mut Self { + let func = host_func.into().into(); + let name = name.as_ref().to_string(); + + let entry = FunctionEntry { + function: func, + parameter_types: Args::TYPE, + return_type: Output::TYPE, + }; + + self.host_funcs + .inner_mut() + .register_host_function(name, entry); + self + } + + /// Like [`Self::host_function`], but consumes and returns `self` for chaining. + pub fn with_host_function( + mut self, + name: impl AsRef, + host_func: impl Into>, + ) -> Self { + self.host_function(name, host_func); + self + } + + /// Registers the special "HostPrint" function for guest printing. + /// + /// This overrides the default behavior of writing to stdout. + /// The function expects the signature `FnMut(String) -> i32` + /// and will be called when the guest wants to print output. + pub fn host_print(&mut self, print_func: impl Into>) -> &mut Self { + self.host_function("HostPrint", print_func); + self + } + + /// Like [`Self::host_print`], but consumes and returns `self` for chaining. + pub fn with_host_print(mut self, print_func: impl Into>) -> Self { + self.host_print(print_func); + self + } + + /// Registers every host function in `host_funcs`. + /// + /// Entries whose names are already registered are overwritten. + pub fn host_functions(&mut self, host_funcs: HostFunctions) -> &mut Self { + let host_funcs = host_funcs.into_inner().functions_map; + for (func_name, func_entry) in host_funcs { + self.host_funcs + .inner_mut() + .register_host_function(func_name, func_entry); + } + self + } + + /// Like [`Self::host_functions`], but consumes and returns `self` for chaining. + pub fn with_host_functions(mut self, host_funcs: HostFunctions) -> Self { + self.host_functions(host_funcs); + self + } +} + +impl SandboxBuilder { + /// Set the size of the memory buffer made available for input to the guest. + /// Values below [`SandboxConfiguration::MIN_INPUT_SIZE`] are clamped up. + pub fn input_data_size(&mut self, size: usize) -> &mut Self { + self.cfg.set_input_data_size(size); + self + } + + /// Like [`Self::input_data_size`], but consumes and returns `self` for chaining. + pub fn with_input_data_size(mut self, size: usize) -> Self { + self.input_data_size(size); + self + } + + /// The size of the memory buffer made available for input to the guest. + pub fn get_input_data_size(&self) -> usize { + self.cfg.get_input_data_size() + } + + /// Set the size of the memory buffer made available for output from the guest. + /// Values below [`SandboxConfiguration::MIN_OUTPUT_SIZE`] are clamped up. + pub fn output_data_size(&mut self, size: usize) -> &mut Self { + self.cfg.set_output_data_size(size); + self + } + + /// Like [`Self::output_data_size`], but consumes and returns `self` for chaining. + pub fn with_output_data_size(mut self, size: usize) -> Self { + self.output_data_size(size); + self + } + + /// The size of the memory buffer made available for output from the guest. + pub fn get_output_data_size(&self) -> usize { + self.cfg.get_output_data_size() + } + + /// Set the guest heap size. A size of 0 selects + /// [`SandboxConfiguration::DEFAULT_HEAP_SIZE`]. + pub fn heap_size(&mut self, size: u64) -> &mut Self { + self.cfg.set_heap_size(size); + self + } + + /// Like [`Self::heap_size`], but consumes and returns `self` for chaining. + pub fn with_heap_size(mut self, size: u64) -> Self { + self.heap_size(size); + self + } + + /// The guest heap size, defaulting to + /// [`SandboxConfiguration::DEFAULT_HEAP_SIZE`] when no override is set. + pub fn get_heap_size(&self) -> u64 { + self.cfg.get_heap_size() + } + + /// Set how much writable memory to offer the guest. + pub fn scratch_size(&mut self, size: usize) -> &mut Self { + self.cfg.set_scratch_size(size); + self + } + + /// Like [`Self::scratch_size`], but consumes and returns `self` for chaining. + pub fn with_scratch_size(mut self, size: usize) -> Self { + self.scratch_size(size); + self + } + + /// How much writable memory is offered to the guest. + pub fn get_scratch_size(&self) -> usize { + self.cfg.get_scratch_size() + } + + /// Set how long to wait between attempts to signal the VCPU thread. + #[cfg(target_os = "linux")] + pub fn interrupt_retry_delay(&mut self, delay: Duration) -> &mut Self { + self.cfg.set_interrupt_retry_delay(delay); + self + } + + /// Like [`Self::interrupt_retry_delay`], but consumes and returns `self` for chaining. + #[cfg(target_os = "linux")] + pub fn with_interrupt_retry_delay(mut self, delay: Duration) -> Self { + self.interrupt_retry_delay(delay); + self + } + + /// How long to wait between attempts to signal the VCPU thread. + #[cfg(target_os = "linux")] + pub fn get_interrupt_retry_delay(&self) -> Duration { + self.cfg.get_interrupt_retry_delay() + } + + /// Set the offset from `SIGRTMIN` for the signal used to interrupt the VCPU + /// thread. + /// + /// # Errors + /// + /// Returns an error if `SIGRTMIN + offset` exceeds `SIGRTMAX`. + #[cfg(target_os = "linux")] + pub fn interrupt_vcpu_sigrtmin_offset(&mut self, offset: u8) -> Result<&mut Self> { + self.cfg.set_interrupt_vcpu_sigrtmin_offset(offset)?; + Ok(self) + } + + /// Like [`Self::interrupt_vcpu_sigrtmin_offset`], but consumes and returns `self` for chaining. + #[cfg(target_os = "linux")] + pub fn with_interrupt_vcpu_sigrtmin_offset(mut self, offset: u8) -> Result { + self.interrupt_vcpu_sigrtmin_offset(offset)?; + Ok(self) + } + + /// The offset from `SIGRTMIN` for the signal used to interrupt the VCPU thread. + #[cfg(target_os = "linux")] + pub fn get_interrupt_vcpu_sigrtmin_offset(&self) -> u8 { + self.cfg.get_interrupt_vcpu_sigrtmin_offset() + } + + /// Toggle guest core dump generation. + #[cfg(crashdump)] + pub fn guest_core_dump(&mut self, enabled: bool) -> &mut Self { + self.cfg.set_guest_core_dump(enabled); + self + } + + /// Like [`Self::guest_core_dump`], but consumes and returns `self` for chaining. + #[cfg(crashdump)] + pub fn with_guest_core_dump(mut self, enabled: bool) -> Self { + self.guest_core_dump(enabled); + self + } + + /// Whether guest core dump generation is enabled. + #[cfg(crashdump)] + pub fn get_guest_core_dump(&self) -> bool { + self.cfg.get_guest_core_dump() + } + + /// Set the guest debug configuration. + #[cfg(gdb)] + pub fn guest_debug_info(&mut self, debug_info: DebugInfo) -> &mut Self { + self.cfg.set_guest_debug_info(debug_info); + self + } + + /// Like [`Self::guest_debug_info`], but consumes and returns `self` for chaining. + #[cfg(gdb)] + pub fn with_guest_debug_info(mut self, debug_info: DebugInfo) -> Self { + self.guest_debug_info(debug_info); + self + } + + /// The guest debug configuration, or `None` when debugging is not configured. + #[cfg(gdb)] + pub fn get_guest_debug_info(&self) -> Option { + self.cfg.get_guest_debug_info() + } +} + +#[cfg(test)] +mod tests { + use hyperlight_testing::simple_guest_as_string; + use tracing_core::LevelFilter; + + use super::SandboxBuilder; + use crate::mem::memory_region::MemoryRegionFlags; + + #[test] + fn build_from_file() { + let path = simple_guest_as_string().unwrap(); + let mut sandbox = SandboxBuilder::new() + .with_input_data_size(0x8000) + .build_from_file(path) + .unwrap(); + + let result = sandbox.call::("Echo", "hello".to_string()).unwrap(); + assert_eq!(result, "hello"); + } + + #[test] + fn build_from_bytes() { + let bytes = std::fs::read(simple_guest_as_string().unwrap()).unwrap(); + let mut sandbox = SandboxBuilder::new().build_from_bytes(bytes).unwrap(); + + let result = sandbox.call::("Echo", "hello".to_string()).unwrap(); + assert_eq!(result, "hello"); + } + + #[test] + fn build_from_snapshot() { + let path = simple_guest_as_string().unwrap(); + let mut sandbox = SandboxBuilder::new().build_from_file(path).unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + + let mut restored = SandboxBuilder::new().build_from_snapshot(snapshot).unwrap(); + + let result = restored + .call::("Echo", "hello".to_string()) + .unwrap(); + assert_eq!(result, "hello"); + } + + #[test] + fn build_from_snapshot_errors_on_ignored_settings() { + let path = simple_guest_as_string().unwrap(); + let mut sandbox = SandboxBuilder::new().build_from_file(path).unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + + assert!( + SandboxBuilder::new() + .with_init_data([0u8; 8], MemoryRegionFlags::READ) + .build_from_snapshot(snapshot.clone()) + .is_err() + ); + + assert!( + SandboxBuilder::new() + .with_max_guest_log_level(LevelFilter::INFO) + .build_from_snapshot(snapshot) + .is_err() + ); + } +} diff --git a/src/hyperlight_host/src/sandbox/host_funcs.rs b/src/hyperlight_host/src/sandbox/host_funcs.rs index e885c1f5e..1c05ad894 100644 --- a/src/hyperlight_host/src/sandbox/host_funcs.rs +++ b/src/hyperlight_host/src/sandbox/host_funcs.rs @@ -32,7 +32,7 @@ use crate::func::host_functions::TypeErasedHostFunction; #[derive(Default)] /// A Wrapper around details of functions exposed by the Host pub struct FunctionRegistry { - functions_map: HashMap, + pub(super) functions_map: HashMap, } /// A collection of host functions that can be supplied to a sandbox diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 623c24667..9a5a51786 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -40,6 +40,7 @@ use crate::mem::shared_mem::{HostSharedMemory, SharedMemory as _}; use crate::metrics::{ METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call, }; +use crate::sandbox::builder::SandboxBuilder; use crate::{HyperlightError, Result, log_then_return}; /// A fully initialized sandbox that can execute guest functions multiple times. @@ -110,6 +111,14 @@ pub struct MultiUseSandbox { pub type PtRootFinder = Box Vec + Send>; impl MultiUseSandbox { + /// Start building a sandbox. + /// + /// Returns a [`SandboxBuilder`] with default settings. Adjust it, then call + /// one of its `build_from_*` methods to get a `MultiUseSandbox`. + pub fn builder() -> SandboxBuilder { + SandboxBuilder::new() + } + /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance. /// /// This function is not equivalent to doing an `evolve` from uninitialized diff --git a/src/hyperlight_host/src/sandbox/mod.rs b/src/hyperlight_host/src/sandbox/mod.rs index 822b1e388..51d59945e 100644 --- a/src/hyperlight_host/src/sandbox/mod.rs +++ b/src/hyperlight_host/src/sandbox/mod.rs @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +/// Functionality for creating and configuring `Sandbox`es. +pub mod builder; /// Configuration needed to establish a sandbox. pub mod config; /// Host-side file mapping preparation for `map_file_cow`.