pub struct MultiUseSandbox { /* private fields */ }Expand description
A fully initialized sandbox that can execute guest functions multiple times.
Guest functions can be called repeatedly while maintaining state between calls. The sandbox supports creating snapshots and restoring to previous states.
§Sandbox Poisoning
The sandbox becomes poisoned when the guest is not run to completion, leaving it in an inconsistent state that could compromise memory safety, data integrity, or security.
§When Does Poisoning Occur?
Poisoning happens when guest execution is interrupted before normal completion:
- Guest panics or aborts - When a guest function panics, crashes, or calls
abort(), the normal cleanup and unwinding process is interrupted - Invalid memory access - Attempts to read/write/execute memory outside allowed regions
- Stack overflow - Guest exhausts its stack space during execution
- Heap exhaustion - Guest runs out of heap memory
- Host-initiated cancellation - Calling
InterruptHandle::kill()to forcefully terminate an in-progress guest function
§Why This Is Unsafe
When guest execution doesn’t complete normally, critical cleanup operations are skipped:
- Memory leaks - Heap allocations remain unreachable as the call stack is unwound
- Corrupted allocator state - Memory allocator metadata (free lists, heap headers) left inconsistent
- Locked resources - Mutexes or other synchronization primitives remain locked
- Partial state updates - Data structures left half-modified (corrupted linked lists, inconsistent hash tables, etc.)
§Recovery
Use restore() with a snapshot taken before poisoning occurred.
This is the only safe way to recover - it completely replaces all memory state,
eliminating any inconsistencies. See restore() for details.
Implementations§
Source§impl MultiUseSandbox
impl MultiUseSandbox
Sourcepub fn set_pt_root_finder(&mut self, finder: PtRootFinder)
pub fn set_pt_root_finder(&mut self, finder: PtRootFinder)
Set a callback that discovers page table roots from guest memory. The callback receives (snapshot_mem, scratch_mem, cr3) and returns the list of root GPAs to walk during snapshot creation.
Sourcepub fn from_snapshot(
snapshot: Arc<Snapshot>,
host_funcs: HostFunctions,
config: Option<SandboxConfiguration>,
) -> Result<Self>
pub fn from_snapshot( snapshot: Arc<Snapshot>, host_funcs: HostFunctions, config: Option<SandboxConfiguration>, ) -> Result<Self>
Create a MultiUseSandbox directly from a Snapshot,
bypassing UninitializedSandbox
and evolve().
This is useful for fast sandbox creation when a snapshot of an already-initialized guest is available, either saved to disk or captured in memory from another sandbox.
The provided [HostFunctions] must include every host function
that was registered on the sandbox at the time the snapshot was
taken (matched by name and signature). Additional host functions
not present in the snapshot are allowed. A mismatch returns
SnapshotHostFunctionMismatch
carrying the missing names and signature differences.
An optional SandboxConfiguration
can be supplied to override runtime settings such as timeouts and
interrupt behavior. Memory layout fields
(input_data_size, output_data_size, heap_size, scratch_size)
are always taken from the snapshot. Any values supplied in
config for those fields are ignored.
§Examples
From a snapshot taken on another sandbox:
// Create and initialize a sandbox the normal way
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None,
)?.evolve()?;
// Capture a snapshot of the initialized state
let snapshot = sandbox.snapshot()?;
// Create a new sandbox directly from the snapshot
let mut sandbox2 = MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None)?;
let result: i32 = sandbox2.call("GetValue", ())?;From a snapshot loaded from disk:
let tag = OciTag::new("latest")?;
let snapshot = Arc::new(Snapshot::load("./guest_snapshot", tag)?);
let mut sandbox = MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None)?;
let result: String = sandbox.call("Echo", "hello".to_string())?;Sourcepub fn snapshot(&mut self) -> Result<Arc<Snapshot>>
pub fn snapshot(&mut self) -> Result<Arc<Snapshot>>
Creates a snapshot of the sandbox’s current memory state.
The returned snapshot can be applied to any
MultiUseSandbox whose memory layout is structurally
compatible with this sandbox’s layout and whose registered
host functions are a superset of those registered here at the
time of capture. See MultiUseSandbox::restore and
MultiUseSandbox::from_snapshot for the exact compatibility
rules and the error variants returned on mismatch.
§Poisoned Sandbox
This method will return crate::HyperlightError::PoisonedSandbox if the sandbox
is currently poisoned. Snapshots can only be taken from non-poisoned sandboxes.
§Examples
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Modify sandbox state
sandbox.call_guest_function_by_name::<i32>("SetValue", 42)?;
// Capture a snapshot of the current memory state
let snapshot = sandbox.snapshot()?;Sourcepub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()>
pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()>
Restores the sandbox’s memory to a previously captured snapshot state.
The snapshot’s memory layout must be structurally compatible
with this sandbox’s layout, otherwise this returns
SnapshotLayoutMismatch.
The sandbox’s registered host functions must be a superset of
those required by the snapshot (matched by name and
signature). Extras on the sandbox are allowed. The registry
itself is left unchanged. A mismatch returns
SnapshotHostFunctionMismatch
carrying the missing names and signature differences.
§Poison State Recovery
This method automatically clears any poison state when successful. This is safe because:
- Snapshots can only be taken from non-poisoned sandboxes
- Restoration completely replaces all memory state, eliminating any inconsistencies caused by incomplete guest execution
§What Gets Fixed During Restore
When a poisoned sandbox is restored, the memory state is completely reset:
- Leaked heap memory - All allocations from interrupted execution are discarded
- Corrupted allocator metadata - Free lists and heap headers restored to consistent state
- Locked mutexes - All lock state is reset
- Partial updates - Data structures restored to their pre-execution state
§Examples
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Take initial snapshot from this sandbox
let snapshot = sandbox.snapshot()?;
// Modify sandbox state
sandbox.call_guest_function_by_name::<i32>("SetValue", 100)?;
let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
assert_eq!(value, 100);
// Restore to previous state (same sandbox)
sandbox.restore(snapshot)?;
let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
assert_eq!(restored_value, 0); // Back to initial state§Recovering from Poison
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Take snapshot before potentially poisoning operation
let snapshot = sandbox.snapshot()?;
// This might poison the sandbox (guest not run to completion)
let result = sandbox.call::<()>("guest_panic", ());
if result.is_err() {
if sandbox.poisoned() {
// Restore from snapshot to clear poison
sandbox.restore(snapshot.clone())?;
assert!(!sandbox.poisoned());
// Sandbox is now usable again
sandbox.call::<String>("Echo", "hello".to_string())?;
}
}Sourcepub fn call<Output: SupportedReturnType>(
&mut self,
func_name: &str,
args: impl ParameterTuple,
) -> Result<Output>
pub fn call<Output: SupportedReturnType>( &mut self, func_name: &str, args: impl ParameterTuple, ) -> Result<Output>
Calls a guest function by name with the specified arguments.
Changes made to the sandbox during execution are persisted.
§Poisoned Sandbox
This method will return crate::HyperlightError::PoisonedSandbox if the sandbox
is already poisoned before the call. Use restore() to recover from
a poisoned state.
§Sandbox Poisoning
If this method returns an error, the sandbox may be poisoned if the guest was not run
to completion (due to panic, abort, memory violation, stack/heap exhaustion, or forced
termination). Use poisoned() to check the poison state and
restore() to recover if needed.
If this method returns Ok, the sandbox is guaranteed to not be poisoned - the guest
function completed successfully and the sandbox state is consistent.
§Examples
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Call function with no arguments
let result: i32 = sandbox.call("GetCounter", ())?;
// Call function with single argument
let doubled: i32 = sandbox.call("Double", 21)?;
assert_eq!(doubled, 42);
// Call function with multiple arguments
let sum: i32 = sandbox.call("Add", (10, 32))?;
assert_eq!(sum, 42);
// Call function returning string
let message: String = sandbox.call("Echo", "Hello, World!".to_string())?;
assert_eq!(message, "Hello, World!");§Handling Potential Poisoning
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Take snapshot before risky operation
let snapshot = sandbox.snapshot()?;
// Call potentially unsafe guest function
let result = sandbox.call::<String>("RiskyOperation", "input".to_string());
// Check if the call failed and poisoned the sandbox
if let Err(e) = result {
eprintln!("Guest function failed: {}", e);
if sandbox.poisoned() {
eprintln!("Sandbox was poisoned, restoring from snapshot");
sandbox.restore(snapshot.clone())?;
}
}Sourcepub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()>
pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()>
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.
§Poisoned Sandbox
This method will return crate::HyperlightError::PoisonedSandbox if the sandbox
is currently poisoned. Use restore() to recover from a poisoned state.
§Safety
The caller must ensure the host memory region remains valid and unmodified
for the lifetime of self.
Sourcepub fn map_file_cow(&mut self, file_path: &Path, guest_base: u64) -> Result<u64>
pub fn map_file_cow(&mut self, file_path: &Path, guest_base: u64) -> Result<u64>
Map the contents of a file into the guest at a particular address
Returns the length of the mapping in bytes.
§Poisoned Sandbox
This method will return crate::HyperlightError::PoisonedSandbox if the sandbox
is currently poisoned. Use restore() to recover from a poisoned state.
Sourcepub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle>
pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle>
Returns a handle for interrupting guest execution.
§Examples
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Get interrupt handle before starting long-running operation
let interrupt_handle = sandbox.interrupt_handle();
// Spawn thread to interrupt after timeout
let handle_clone = interrupt_handle.clone();
thread::spawn(move || {
thread::sleep(Duration::from_secs(5));
handle_clone.kill();
});
// This call may be interrupted by the spawned thread
let result = sandbox.call_guest_function_by_name::<i32>("LongRunningFunction", ());Sourcepub fn poisoned(&self) -> bool
pub fn poisoned(&self) -> bool
Returns whether the sandbox is currently poisoned.
A poisoned sandbox is in an inconsistent state due to the guest not running to completion. All operations will be rejected until the sandbox is restored from a non-poisoned snapshot.
§Causes of Poisoning
The sandbox becomes poisoned when guest execution is interrupted:
- Panics/Aborts - Guest code panics or calls
abort() - Invalid Memory Access - Read/write/execute violations
- Stack Overflow - Guest exhausts stack space
- Heap Exhaustion - Guest runs out of heap memory
- Forced Termination -
InterruptHandle::kill()called during execution
§Recovery
To clear the poison state, use restore() with a snapshot
that was taken before the sandbox became poisoned.
§Examples
let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
GuestBinary::FilePath("guest.bin".into()),
None
)?.evolve()?;
// Check if sandbox is poisoned
if sandbox.poisoned() {
println!("Sandbox is poisoned and needs attention");
}Trait Implementations§
Source§impl Callable for MultiUseSandbox
impl Callable for MultiUseSandbox
Source§fn call<Output: SupportedReturnType>(
&mut self,
func_name: &str,
args: impl ParameterTuple,
) -> Result<Output>
fn call<Output: SupportedReturnType>( &mut self, func_name: &str, args: impl ParameterTuple, ) -> Result<Output>
Source§impl Debug for MultiUseSandbox
impl Debug for MultiUseSandbox
Source§impl Registerable for MultiUseSandbox
Allow registering host functions on an already-evolved
crate::MultiUseSandbox.
impl Registerable for MultiUseSandbox
Allow registering host functions on an already-evolved
crate::MultiUseSandbox.
The primary entry point for host-function registration is the
UninitializedSandbox impl above — that’s the lifecycle phase
where the guest hasn’t yet been allowed to issue host calls.
There are, however, cases where a MultiUseSandbox is obtained
without traversing the Uninitialized → evolve() path:
- Sandboxes loaded from a persisted snapshot.
- Any future API that yields a
MultiUseSandboxdirectly.
In those cases the caller never had a chance to call
register_host_function on an UninitializedSandbox, so we
expose the same trait implementation here for late registration.
The guest’s host-function dispatcher resolves by name at call
time, so inserting into the registry after evolve() is
semantically safe as long as the first host-function invocation
happens after registration completes.