hyperlight_host/sandbox/initialized_multi_use.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::collections::HashSet;
18#[cfg(unix)]
19use std::os::fd::AsRawFd;
20#[cfg(unix)]
21use std::os::linux::fs::MetadataExt;
22use std::path::Path;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::{Arc, Mutex};
25
26use flatbuffers::FlatBufferBuilder;
27use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
28use hyperlight_common::flatbuffer_wrappers::function_types::{
29 ParameterValue, ReturnType, ReturnValue,
30};
31use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
32use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity;
33use tracing::{Span, instrument};
34
35use super::Callable;
36use super::host_funcs::FunctionRegistry;
37use super::snapshot::Snapshot;
38use crate::HyperlightError::{self, SnapshotSandboxMismatch};
39use crate::func::{ParameterTuple, SupportedReturnType};
40use crate::hypervisor::{Hypervisor, InterruptHandle};
41#[cfg(unix)]
42use crate::mem::memory_region::MemoryRegionType;
43use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
44use crate::mem::mgr::SandboxMemoryManager;
45use crate::mem::ptr::RawPtr;
46use crate::mem::shared_mem::HostSharedMemory;
47use crate::metrics::{
48 METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
49};
50use crate::{Result, log_then_return, new_error};
51
52/// Global counter for assigning unique IDs to sandboxes
53static SANDBOX_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
54
55/// RAII guard that automatically calls `clear_call_active()` when dropped.
56///
57/// This ensures that the call_active flag is always cleared when a guest function
58/// call completes, even if the function returns early due to an error.
59///
60/// Only one guard can exist per interrupt handle at a time - attempting to create
61/// a second guard will return an error.
62struct CallActiveGuard<T: crate::hypervisor::InterruptHandleInternal + ?Sized> {
63 interrupt_handle: Arc<T>,
64}
65
66impl<T: crate::hypervisor::InterruptHandleInternal + ?Sized> CallActiveGuard<T> {
67 /// Creates a new guard and marks a guest function call as active.
68 ///
69 /// # Errors
70 ///
71 /// Returns an error if `call_active` is already true (i.e., another guard already exists).
72 fn new(interrupt_handle: Arc<T>) -> Result<Self> {
73 // Atomically check that call_active is false and set it to true.
74 // This prevents creating multiple guards for the same interrupt handle.
75 let was_active = interrupt_handle.set_call_active();
76 if was_active {
77 return Err(new_error!(
78 "Attempted to create CallActiveGuard when a call is already active"
79 ));
80 }
81 Ok(Self { interrupt_handle })
82 }
83}
84
85impl<T: crate::hypervisor::InterruptHandleInternal + ?Sized> Drop for CallActiveGuard<T> {
86 fn drop(&mut self) {
87 self.interrupt_handle.clear_call_active();
88 }
89}
90
91/// A fully initialized sandbox that can execute guest functions multiple times.
92///
93/// Guest functions can be called repeatedly while maintaining state between calls.
94/// The sandbox supports creating snapshots and restoring to previous states.
95///
96/// ## Sandbox Poisoning
97///
98/// The sandbox becomes **poisoned** when the guest is not run to completion, leaving it in
99/// an inconsistent state that could compromise memory safety, data integrity, or security.
100///
101/// ### When Does Poisoning Occur?
102///
103/// Poisoning happens when guest execution is interrupted before normal completion:
104///
105/// - **Guest panics or aborts** - When a guest function panics, crashes, or calls `abort()`,
106/// the normal cleanup and unwinding process is interrupted
107/// - **Invalid memory access** - Attempts to read/write/execute memory outside allowed regions
108/// - **Stack overflow** - Guest exhausts its stack space during execution
109/// - **Heap exhaustion** - Guest runs out of heap memory
110/// - **Host-initiated cancellation** - Calling [`InterruptHandle::kill()`] to forcefully
111/// terminate an in-progress guest function
112///
113/// ### Why This Is Unsafe
114///
115/// When guest execution doesn't complete normally, critical cleanup operations are skipped:
116///
117/// - **Memory leaks** - Heap allocations remain unreachable as the call stack is unwound
118/// - **Corrupted allocator state** - Memory allocator metadata (free lists, heap headers)
119/// left inconsistent
120/// - **Locked resources** - Mutexes or other synchronization primitives remain locked
121/// - **Partial state updates** - Data structures left half-modified (corrupted linked lists,
122/// inconsistent hash tables, etc.)
123///
124/// ### Recovery
125///
126/// Use [`restore()`](Self::restore) with a snapshot taken before poisoning occurred.
127/// This is the **only safe way** to recover - it completely replaces all memory state,
128/// eliminating any inconsistencies. See [`restore()`](Self::restore) for details.
129pub struct MultiUseSandbox {
130 /// Unique identifier for this sandbox instance
131 id: u64,
132 /// Whether this sandbox is poisoned
133 poisoned: bool,
134 // We need to keep a reference to the host functions, even if the compiler marks it as unused. The compiler cannot detect our dynamic usages of the host function in `HyperlightFunction::call`.
135 pub(super) _host_funcs: Arc<Mutex<FunctionRegistry>>,
136 pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
137 vm: Box<dyn Hypervisor>,
138 dispatch_ptr: RawPtr,
139 #[cfg(gdb)]
140 dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
141 /// If the current state of the sandbox has been captured in a snapshot,
142 /// that snapshot is stored here.
143 snapshot: Option<Snapshot>,
144}
145
146impl MultiUseSandbox {
147 /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance.
148 ///
149 /// This function is not equivalent to doing an `evolve` from uninitialized
150 /// to initialized, and is purposely not exposed publicly outside the crate
151 /// (as a `From` implementation would be)
152 #[instrument(skip_all, parent = Span::current(), level = "Trace")]
153 pub(super) fn from_uninit(
154 host_funcs: Arc<Mutex<FunctionRegistry>>,
155 mgr: SandboxMemoryManager<HostSharedMemory>,
156 vm: Box<dyn Hypervisor>,
157 dispatch_ptr: RawPtr,
158 #[cfg(gdb)] dbg_mem_access_fn: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
159 ) -> MultiUseSandbox {
160 Self {
161 id: SANDBOX_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
162 poisoned: false,
163 _host_funcs: host_funcs,
164 mem_mgr: mgr,
165 vm,
166 dispatch_ptr,
167 #[cfg(gdb)]
168 dbg_mem_access_fn,
169 snapshot: None,
170 }
171 }
172
173 /// Creates a snapshot of the sandbox's current memory state.
174 ///
175 /// The snapshot is tied to this specific sandbox instance and can only be
176 /// restored to the same sandbox it was created from.
177 ///
178 /// ## Poisoned Sandbox
179 ///
180 /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
181 /// is currently poisoned. Snapshots can only be taken from non-poisoned sandboxes.
182 ///
183 /// # Examples
184 ///
185 /// ```no_run
186 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
187 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
188 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
189 /// GuestBinary::FilePath("guest.bin".into()),
190 /// None
191 /// )?.evolve()?;
192 ///
193 /// // Modify sandbox state
194 /// sandbox.call_guest_function_by_name::<i32>("SetValue", 42)?;
195 ///
196 /// // Create snapshot belonging to this sandbox
197 /// let snapshot = sandbox.snapshot()?;
198 /// # Ok(())
199 /// # }
200 /// ```
201 #[instrument(err(Debug), skip_all, parent = Span::current())]
202 pub fn snapshot(&mut self) -> Result<Snapshot> {
203 if self.poisoned {
204 return Err(crate::HyperlightError::PoisonedSandbox);
205 }
206
207 if let Some(snapshot) = &self.snapshot {
208 return Ok(snapshot.clone());
209 }
210 let mapped_regions_iter = self.vm.get_mapped_regions();
211 let mapped_regions_vec: Vec<MemoryRegion> = mapped_regions_iter.cloned().collect();
212 let memory_snapshot = self.mem_mgr.snapshot(self.id, mapped_regions_vec)?;
213 let inner = Arc::new(memory_snapshot);
214 let snapshot = Snapshot { inner };
215 self.snapshot = Some(snapshot.clone());
216 Ok(snapshot)
217 }
218
219 /// Restores the sandbox's memory to a previously captured snapshot state.
220 ///
221 /// The snapshot must have been created from this same sandbox instance.
222 /// Attempting to restore a snapshot from a different sandbox will return
223 /// a [`SnapshotSandboxMismatch`](crate::HyperlightError::SnapshotSandboxMismatch) error.
224 ///
225 /// ## Poison State Recovery
226 ///
227 /// This method automatically clears any poison state when successful. This is safe because:
228 /// - Snapshots can only be taken from non-poisoned sandboxes
229 /// - Restoration completely replaces all memory state, eliminating any inconsistencies
230 /// caused by incomplete guest execution
231 ///
232 /// ### What Gets Fixed During Restore
233 ///
234 /// When a poisoned sandbox is restored, the memory state is completely reset:
235 /// - **Leaked heap memory** - All allocations from interrupted execution are discarded
236 /// - **Corrupted allocator metadata** - Free lists and heap headers restored to consistent state
237 /// - **Locked mutexes** - All lock state is reset
238 /// - **Partial updates** - Data structures restored to their pre-execution state
239 ///
240 ///
241 /// # Examples
242 ///
243 /// ```no_run
244 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
245 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
246 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
247 /// GuestBinary::FilePath("guest.bin".into()),
248 /// None
249 /// )?.evolve()?;
250 ///
251 /// // Take initial snapshot from this sandbox
252 /// let snapshot = sandbox.snapshot()?;
253 ///
254 /// // Modify sandbox state
255 /// sandbox.call_guest_function_by_name::<i32>("SetValue", 100)?;
256 /// let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
257 /// assert_eq!(value, 100);
258 ///
259 /// // Restore to previous state (same sandbox)
260 /// sandbox.restore(&snapshot)?;
261 /// let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?;
262 /// assert_eq!(restored_value, 0); // Back to initial state
263 /// # Ok(())
264 /// # }
265 /// ```
266 ///
267 /// ## Recovering from Poison
268 ///
269 /// ```no_run
270 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary, HyperlightError};
271 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
272 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
273 /// GuestBinary::FilePath("guest.bin".into()),
274 /// None
275 /// )?.evolve()?;
276 ///
277 /// // Take snapshot before potentially poisoning operation
278 /// let snapshot = sandbox.snapshot()?;
279 ///
280 /// // This might poison the sandbox (guest not run to completion)
281 /// let result = sandbox.call::<()>("guest_panic", ());
282 /// if result.is_err() {
283 /// if sandbox.poisoned() {
284 /// // Restore from snapshot to clear poison
285 /// sandbox.restore(&snapshot)?;
286 /// assert!(!sandbox.poisoned());
287 ///
288 /// // Sandbox is now usable again
289 /// sandbox.call::<String>("Echo", "hello".to_string())?;
290 /// }
291 /// }
292 /// # Ok(())
293 /// # }
294 /// ```
295 #[instrument(err(Debug), skip_all, parent = Span::current())]
296 pub fn restore(&mut self, snapshot: &Snapshot) -> Result<()> {
297 if let Some(snap) = &self.snapshot
298 && Arc::ptr_eq(&snap.inner, &snapshot.inner)
299 {
300 // If the snapshot is already the current one, no need to restore
301 return Ok(());
302 }
303
304 if self.id != snapshot.inner.sandbox_id() {
305 return Err(SnapshotSandboxMismatch);
306 }
307
308 self.mem_mgr.restore_snapshot(&snapshot.inner)?;
309
310 let current_regions: HashSet<_> = self.vm.get_mapped_regions().cloned().collect();
311 let snapshot_regions: HashSet<_> = snapshot.inner.regions().iter().cloned().collect();
312
313 let regions_to_unmap = current_regions.difference(&snapshot_regions);
314 let regions_to_map = snapshot_regions.difference(¤t_regions);
315
316 for region in regions_to_unmap {
317 unsafe { self.vm.unmap_region(region)? };
318 }
319
320 for region in regions_to_map {
321 unsafe { self.vm.map_region(region)? };
322 }
323
324 // The restored snapshot is now our most current snapshot
325 self.snapshot = Some(snapshot.clone());
326
327 // Clear poison state when successfully restoring from snapshot.
328 //
329 // # Safety:
330 // This is safe because:
331 // 1. Snapshots can only be taken from non-poisoned sandboxes (verified at snapshot creation)
332 // 2. Restoration completely replaces all memory state, eliminating:
333 // - All leaked heap allocations (memory is restored to snapshot state)
334 // - All corrupted data structures (overwritten with consistent snapshot data)
335 // - All inconsistent global state (reset to snapshot values)
336 self.poisoned = false;
337
338 Ok(())
339 }
340
341 /// Calls a guest function by name with the specified arguments.
342 ///
343 /// Changes made to the sandbox during execution are *not* persisted.
344 ///
345 /// ## Poisoned Sandbox
346 ///
347 /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
348 /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
349 ///
350 /// # Examples
351 ///
352 /// ```no_run
353 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
354 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
355 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
356 /// GuestBinary::FilePath("guest.bin".into()),
357 /// None
358 /// )?.evolve()?;
359 ///
360 /// // Call function with no arguments
361 /// let result: i32 = sandbox.call_guest_function_by_name("GetCounter", ())?;
362 ///
363 /// // Call function with single argument
364 /// let doubled: i32 = sandbox.call_guest_function_by_name("Double", 21)?;
365 /// assert_eq!(doubled, 42);
366 ///
367 /// // Call function with multiple arguments
368 /// let sum: i32 = sandbox.call_guest_function_by_name("Add", (10, 32))?;
369 /// assert_eq!(sum, 42);
370 ///
371 /// // Call function returning string
372 /// let message: String = sandbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?;
373 /// assert_eq!(message, "Hello, World!");
374 /// # Ok(())
375 /// # }
376 /// ```
377 #[doc(hidden)]
378 #[deprecated(
379 since = "0.8.0",
380 note = "Deprecated in favour of call and snapshot/restore."
381 )]
382 #[instrument(err(Debug), skip(self, args), parent = Span::current())]
383 pub fn call_guest_function_by_name<Output: SupportedReturnType>(
384 &mut self,
385 func_name: &str,
386 args: impl ParameterTuple,
387 ) -> Result<Output> {
388 if self.poisoned {
389 return Err(crate::HyperlightError::PoisonedSandbox);
390 }
391 let snapshot = self.snapshot()?;
392 let res = self.call(func_name, args);
393 self.restore(&snapshot)?;
394 res
395 }
396
397 /// Calls a guest function by name with the specified arguments.
398 ///
399 /// Changes made to the sandbox during execution are persisted.
400 ///
401 /// ## Poisoned Sandbox
402 ///
403 /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
404 /// is already poisoned before the call. Use [`restore()`](Self::restore) to recover from
405 /// a poisoned state.
406 ///
407 /// ## Sandbox Poisoning
408 ///
409 /// If this method returns an error, the sandbox may be poisoned if the guest was not run
410 /// to completion (due to panic, abort, memory violation, stack/heap exhaustion, or forced
411 /// termination). Use [`poisoned()`](Self::poisoned) to check the poison state and
412 /// [`restore()`](Self::restore) to recover if needed.
413 ///
414 /// If this method returns `Ok`, the sandbox is guaranteed to **not** be poisoned - the guest
415 /// function completed successfully and the sandbox state is consistent.
416 ///
417 /// # Examples
418 ///
419 /// ```no_run
420 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
421 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
422 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
423 /// GuestBinary::FilePath("guest.bin".into()),
424 /// None
425 /// )?.evolve()?;
426 ///
427 /// // Call function with no arguments
428 /// let result: i32 = sandbox.call("GetCounter", ())?;
429 ///
430 /// // Call function with single argument
431 /// let doubled: i32 = sandbox.call("Double", 21)?;
432 /// assert_eq!(doubled, 42);
433 ///
434 /// // Call function with multiple arguments
435 /// let sum: i32 = sandbox.call("Add", (10, 32))?;
436 /// assert_eq!(sum, 42);
437 ///
438 /// // Call function returning string
439 /// let message: String = sandbox.call("Echo", "Hello, World!".to_string())?;
440 /// assert_eq!(message, "Hello, World!");
441 /// # Ok(())
442 /// # }
443 /// ```
444 ///
445 /// ## Handling Potential Poisoning
446 ///
447 /// ```no_run
448 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
449 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
450 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
451 /// GuestBinary::FilePath("guest.bin".into()),
452 /// None
453 /// )?.evolve()?;
454 ///
455 /// // Take snapshot before risky operation
456 /// let snapshot = sandbox.snapshot()?;
457 ///
458 /// // Call potentially unsafe guest function
459 /// let result = sandbox.call::<String>("RiskyOperation", "input".to_string());
460 ///
461 /// // Check if the call failed and poisoned the sandbox
462 /// if let Err(e) = result {
463 /// eprintln!("Guest function failed: {}", e);
464 ///
465 /// if sandbox.poisoned() {
466 /// eprintln!("Sandbox was poisoned, restoring from snapshot");
467 /// sandbox.restore(&snapshot)?;
468 /// }
469 /// }
470 /// # Ok(())
471 /// # }
472 /// ```
473 #[instrument(err(Debug), skip(self, args), parent = Span::current())]
474 pub fn call<Output: SupportedReturnType>(
475 &mut self,
476 func_name: &str,
477 args: impl ParameterTuple,
478 ) -> Result<Output> {
479 if self.poisoned {
480 return Err(crate::HyperlightError::PoisonedSandbox);
481 }
482 // Reset snapshot since we are mutating the sandbox state
483 self.snapshot = None;
484 maybe_time_and_emit_guest_call(func_name, || {
485 let ret = self.call_guest_function_by_name_no_reset(
486 func_name,
487 Output::TYPE,
488 args.into_value(),
489 );
490 Output::from_value(ret?)
491 })
492 }
493
494 /// Maps a region of host memory into the sandbox address space.
495 ///
496 /// The base address and length must meet platform alignment requirements
497 /// (typically page-aligned). The `region_type` field is ignored as guest
498 /// page table entries are not created.
499 ///
500 /// ## Poisoned Sandbox
501 ///
502 /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
503 /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
504 ///
505 /// # Safety
506 ///
507 /// The caller must ensure the host memory region remains valid and unmodified
508 /// for the lifetime of `self`.
509 #[instrument(err(Debug), skip(self, rgn), parent = Span::current())]
510 pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> {
511 if self.poisoned {
512 return Err(crate::HyperlightError::PoisonedSandbox);
513 }
514 if rgn.flags.contains(MemoryRegionFlags::STACK_GUARD) {
515 // Stack guard pages are an internal implementation detail
516 // (which really should be moved into the guest)
517 log_then_return!("Cannot map host memory as a stack guard page");
518 }
519 if rgn.flags.contains(MemoryRegionFlags::WRITE) {
520 // TODO: Implement support for writable mappings, which
521 // need to be registered with the memory manager so that
522 // writes can be rolled back when necessary.
523 log_then_return!("TODO: Writable mappings not yet supported");
524 }
525 // Reset snapshot since we are mutating the sandbox state
526 self.snapshot = None;
527 unsafe { self.vm.map_region(rgn) }?;
528 self.mem_mgr.mapped_rgns += 1;
529 Ok(())
530 }
531
532 /// Map the contents of a file into the guest at a particular address
533 ///
534 /// Returns the length of the mapping in bytes.
535 ///
536 /// ## Poisoned Sandbox
537 ///
538 /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
539 /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
540 #[instrument(err(Debug), skip(self, _fp, _guest_base), parent = Span::current())]
541 pub fn map_file_cow(&mut self, _fp: &Path, _guest_base: u64) -> Result<u64> {
542 if self.poisoned {
543 return Err(crate::HyperlightError::PoisonedSandbox);
544 }
545 #[cfg(windows)]
546 log_then_return!("mmap'ing a file into the guest is not yet supported on Windows");
547 #[cfg(unix)]
548 unsafe {
549 let file = std::fs::File::options().read(true).write(true).open(_fp)?;
550 let file_size = file.metadata()?.st_size();
551 let page_size = page_size::get();
552 let size = (file_size as usize).div_ceil(page_size) * page_size;
553 let base = libc::mmap(
554 std::ptr::null_mut(),
555 size,
556 libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
557 libc::MAP_PRIVATE,
558 file.as_raw_fd(),
559 0,
560 );
561 if base == libc::MAP_FAILED {
562 log_then_return!("mmap error: {:?}", std::io::Error::last_os_error());
563 }
564
565 if let Err(err) = self.map_region(&MemoryRegion {
566 host_region: base as usize..base.wrapping_add(size) as usize,
567 guest_region: _guest_base as usize.._guest_base as usize + size,
568 flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
569 region_type: MemoryRegionType::Heap,
570 }) {
571 libc::munmap(base, size);
572 return Err(err);
573 };
574
575 Ok(size as u64)
576 }
577 }
578
579 /// Calls a guest function with type-erased parameters and return values.
580 ///
581 /// This function is used for fuzz testing parameter and return type handling.
582 ///
583 /// ## Poisoned Sandbox
584 ///
585 /// This method will return [`crate::HyperlightError::PoisonedSandbox`] if the sandbox
586 /// is currently poisoned. Use [`restore()`](Self::restore) to recover from a poisoned state.
587 #[cfg(feature = "fuzzing")]
588 #[instrument(err(Debug), skip(self, args), parent = Span::current())]
589 pub fn call_type_erased_guest_function_by_name(
590 &mut self,
591 func_name: &str,
592 ret_type: ReturnType,
593 args: Vec<ParameterValue>,
594 ) -> Result<ReturnValue> {
595 if self.poisoned {
596 return Err(crate::HyperlightError::PoisonedSandbox);
597 }
598 // Reset snapshot since we are mutating the sandbox state
599 self.snapshot = None;
600 maybe_time_and_emit_guest_call(func_name, || {
601 self.call_guest_function_by_name_no_reset(func_name, ret_type, args)
602 })
603 }
604
605 fn call_guest_function_by_name_no_reset(
606 &mut self,
607 function_name: &str,
608 return_type: ReturnType,
609 args: Vec<ParameterValue>,
610 ) -> Result<ReturnValue> {
611 if self.poisoned {
612 return Err(crate::HyperlightError::PoisonedSandbox);
613 }
614 // Mark that a guest function call is now active
615 // (This also increments the generation counter internally)
616 // The guard will automatically clear call_active when dropped
617 let _guard = CallActiveGuard::new(self.vm.interrupt_handle())?;
618
619 let res = (|| {
620 let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args);
621
622 let fc = FunctionCall::new(
623 function_name.to_string(),
624 Some(args),
625 FunctionCallType::Guest,
626 return_type,
627 );
628
629 let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
630 let buffer = fc.encode(&mut builder);
631
632 self.mem_mgr.write_guest_function_call(buffer)?;
633
634 self.vm.dispatch_call_from_host(
635 self.dispatch_ptr.clone(),
636 #[cfg(gdb)]
637 self.dbg_mem_access_fn.clone(),
638 )?;
639
640 self.mem_mgr.check_stack_guard()?;
641
642 let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner();
643
644 match guest_result {
645 Ok(val) => Ok(val),
646 Err(guest_error) => {
647 metrics::counter!(
648 METRIC_GUEST_ERROR,
649 METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string()
650 )
651 .increment(1);
652
653 Err(match guest_error.code {
654 ErrorCode::StackOverflow => HyperlightError::StackOverflow(),
655 _ => HyperlightError::GuestError(guest_error.code, guest_error.message),
656 })
657 }
658 }
659 })();
660
661 // In the happy path we do not need to clear io-buffers from the host because:
662 // - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::<FunctionCall>()`
663 // - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result`
664 // - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call`
665 // - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value`
666 if let Err(e) = &res {
667 self.mem_mgr.clear_io_buffers();
668
669 // Determine if we should poison the sandbox.
670 self.poisoned |= e.is_poison_error();
671 }
672
673 // Note: clear_call_active() is automatically called when _guard is dropped here
674
675 res
676 }
677
678 /// Returns a handle for interrupting guest execution.
679 ///
680 /// # Examples
681 ///
682 /// ```no_run
683 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
684 /// # use std::thread;
685 /// # use std::time::Duration;
686 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
687 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
688 /// GuestBinary::FilePath("guest.bin".into()),
689 /// None
690 /// )?.evolve()?;
691 ///
692 /// // Get interrupt handle before starting long-running operation
693 /// let interrupt_handle = sandbox.interrupt_handle();
694 ///
695 /// // Spawn thread to interrupt after timeout
696 /// let handle_clone = interrupt_handle.clone();
697 /// thread::spawn(move || {
698 /// thread::sleep(Duration::from_secs(5));
699 /// handle_clone.kill();
700 /// });
701 ///
702 /// // This call may be interrupted by the spawned thread
703 /// let result = sandbox.call_guest_function_by_name::<i32>("LongRunningFunction", ());
704 /// # Ok(())
705 /// # }
706 /// ```
707 pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
708 self.vm.interrupt_handle()
709 }
710
711 /// Generate a crash dump of the current state of the VM underlying this sandbox.
712 ///
713 /// Creates an ELF core dump file that can be used for debugging. The dump
714 /// captures the current state of the sandbox including registers, memory regions,
715 /// and other execution context.
716 ///
717 /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
718 /// environment variable. If not set, it defaults to the system's temporary directory.
719 ///
720 /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
721 /// is also configured to allow core dumps (which is the default behavior).
722 ///
723 /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
724 /// guest that dont cause crashes (e.g. a guest function that does not return)
725 ///
726 /// # Examples
727 ///
728 /// Attach to your running process with gdb and call this function:
729 ///
730 /// ```shell
731 /// sudo gdb -p <pid_of_your_process>
732 /// (gdb) info threads
733 /// # find the thread that is running the guest function you want to debug
734 /// (gdb) thread <thread_number>
735 /// # switch to the frame where you have access to your MultiUseSandbox instance
736 /// (gdb) backtrace
737 /// (gdb) frame <frame_number>
738 /// # get the pointer to your MultiUseSandbox instance
739 /// # Get the sandbox pointer
740 /// (gdb) print sandbox
741 /// # Call the crashdump function
742 /// call sandbox.generate_crashdump()
743 /// ```
744 /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
745 ///
746 #[cfg(crashdump)]
747 #[instrument(err(Debug), skip_all, parent = Span::current())]
748 pub fn generate_crashdump(&self) -> Result<()> {
749 crate::hypervisor::crashdump::generate_crashdump(self.vm.as_ref())
750 }
751
752 /// Returns whether the sandbox is currently poisoned.
753 ///
754 /// A poisoned sandbox is in an inconsistent state due to the guest not running to completion.
755 /// All operations will be rejected until the sandbox is restored from a non-poisoned snapshot.
756 ///
757 /// ## Causes of Poisoning
758 ///
759 /// The sandbox becomes poisoned when guest execution is interrupted:
760 /// - **Panics/Aborts** - Guest code panics or calls `abort()`
761 /// - **Invalid Memory Access** - Read/write/execute violations
762 /// - **Stack Overflow** - Guest exhausts stack space
763 /// - **Heap Exhaustion** - Guest runs out of heap memory
764 /// - **Forced Termination** - [`InterruptHandle::kill()`] called during execution
765 ///
766 /// ## Recovery
767 ///
768 /// To clear the poison state, use [`restore()`](Self::restore) with a snapshot
769 /// that was taken before the sandbox became poisoned.
770 ///
771 /// # Examples
772 ///
773 /// ```no_run
774 /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary};
775 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
776 /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new(
777 /// GuestBinary::FilePath("guest.bin".into()),
778 /// None
779 /// )?.evolve()?;
780 ///
781 /// // Check if sandbox is poisoned
782 /// if sandbox.poisoned() {
783 /// println!("Sandbox is poisoned and needs attention");
784 /// }
785 /// # Ok(())
786 /// # }
787 /// ```
788 pub fn poisoned(&self) -> bool {
789 self.poisoned
790 }
791}
792
793impl Callable for MultiUseSandbox {
794 fn call<Output: SupportedReturnType>(
795 &mut self,
796 func_name: &str,
797 args: impl ParameterTuple,
798 ) -> Result<Output> {
799 if self.poisoned {
800 return Err(crate::HyperlightError::PoisonedSandbox);
801 }
802 self.call(func_name, args)
803 }
804}
805
806impl std::fmt::Debug for MultiUseSandbox {
807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808 f.debug_struct("MultiUseSandbox")
809 .field("stack_guard", &self.mem_mgr.get_stack_cookie())
810 .finish()
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use std::sync::{Arc, Barrier};
817 use std::thread;
818
819 use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
820 use hyperlight_testing::simple_guest_as_string;
821
822 #[cfg(target_os = "linux")]
823 use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
824 #[cfg(target_os = "linux")]
825 use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
826 use crate::sandbox::SandboxConfiguration;
827 use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox};
828
829 #[test]
830 fn poison() {
831 let mut sbox: MultiUseSandbox = {
832 let path = simple_guest_as_string().unwrap();
833 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
834 u_sbox.evolve()
835 }
836 .unwrap();
837 let snapshot = sbox.snapshot().unwrap();
838
839 // poison on purpose
840 let res = sbox
841 .call::<()>("guest_panic", "hello".to_string())
842 .unwrap_err();
843 assert!(
844 matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
845 );
846 assert!(sbox.poisoned());
847
848 // guest calls should fail when poisoned
849 let res = sbox
850 .call::<()>("guest_panic", "hello2".to_string())
851 .unwrap_err();
852 assert!(matches!(res, HyperlightError::PoisonedSandbox));
853
854 // snapshot should fail when poisoned
855 if let Err(e) = sbox.snapshot() {
856 assert!(sbox.poisoned());
857 assert!(matches!(e, HyperlightError::PoisonedSandbox));
858 } else {
859 panic!("Snapshot should fail");
860 }
861
862 // map_region should fail when poisoned
863 #[cfg(target_os = "linux")]
864 {
865 let map_mem = allocate_guest_memory();
866 let guest_base = 0x0;
867 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
868 let res = unsafe { sbox.map_region(®ion) }.unwrap_err();
869 assert!(matches!(res, HyperlightError::PoisonedSandbox));
870 }
871
872 // map_file_cow should fail when poisoned
873 #[cfg(target_os = "linux")]
874 {
875 let temp_file = std::env::temp_dir().join("test_poison_map_file.bin");
876 let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err();
877 assert!(matches!(res, HyperlightError::PoisonedSandbox));
878 std::fs::remove_file(&temp_file).ok(); // Clean up
879 }
880
881 // call_guest_function_by_name (deprecated) should fail when poisoned
882 #[allow(deprecated)]
883 let res = sbox
884 .call_guest_function_by_name::<String>("Echo", "test".to_string())
885 .unwrap_err();
886 assert!(matches!(res, HyperlightError::PoisonedSandbox));
887
888 // restore to non-poisoned snapshot should work and clear poison
889 sbox.restore(&snapshot).unwrap();
890 assert!(!sbox.poisoned());
891
892 // guest calls should work again after restore
893 let res = sbox.call::<String>("Echo", "hello2".to_string()).unwrap();
894 assert_eq!(res, "hello2".to_string());
895 assert!(!sbox.poisoned());
896
897 // re-poison on purpose
898 let res = sbox
899 .call::<()>("guest_panic", "hello".to_string())
900 .unwrap_err();
901 assert!(
902 matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
903 );
904 assert!(sbox.poisoned());
905
906 // restore to non-poisoned snapshot should work again
907 sbox.restore(&snapshot).unwrap();
908 assert!(!sbox.poisoned());
909
910 // guest calls should work again
911 let res = sbox.call::<String>("Echo", "hello3".to_string()).unwrap();
912 assert_eq!(res, "hello3".to_string());
913 assert!(!sbox.poisoned());
914
915 // snapshot should work again
916 let _ = sbox.snapshot().unwrap();
917 }
918
919 /// Make sure input/output buffers are properly reset after guest call (with host call)
920 #[test]
921 fn host_func_error() {
922 let path = simple_guest_as_string().unwrap();
923 let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
924 sandbox
925 .register("HostError", || -> Result<()> {
926 Err(HyperlightError::Error("hi".to_string()))
927 })
928 .unwrap();
929 let mut sandbox = sandbox.evolve().unwrap();
930
931 // will exhaust io if leaky
932 for _ in 0..1000 {
933 let result = sandbox
934 .call::<i64>(
935 "CallGivenParamlessHostFuncThatReturnsI64",
936 "HostError".to_string(),
937 )
938 .unwrap_err();
939
940 assert!(
941 matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"),
942 );
943 }
944 }
945
946 #[test]
947 fn call_host_func_expect_error() {
948 let path = simple_guest_as_string().unwrap();
949 let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
950 let mut sandbox = sandbox.evolve().unwrap();
951 sandbox
952 .call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string())
953 .unwrap();
954 }
955
956 /// Make sure input/output buffers are properly reset after guest call (with host call)
957 #[test]
958 fn io_buffer_reset() {
959 let mut cfg = SandboxConfiguration::default();
960 cfg.set_input_data_size(4096);
961 cfg.set_output_data_size(4096);
962 let path = simple_guest_as_string().unwrap();
963 let mut sandbox =
964 UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
965 sandbox.register("HostAdd", |a: i32, b: i32| a + b).unwrap();
966 let mut sandbox = sandbox.evolve().unwrap();
967
968 // will exhaust io if leaky. Tests both success and error paths
969 for _ in 0..1000 {
970 let result = sandbox.call::<i32>("Add", (5i32, 10i32)).unwrap();
971 assert_eq!(result, 15);
972 let result = sandbox.call::<i32>("AddToStaticAndFail", ()).unwrap_err();
973 assert!(
974 matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose")
975 );
976 }
977 }
978
979 /// Tests that call_guest_function_by_name restores the state correctly
980 #[test]
981 fn test_call_guest_function_by_name() {
982 let mut sbox: MultiUseSandbox = {
983 let path = simple_guest_as_string().unwrap();
984 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
985 u_sbox.evolve()
986 }
987 .unwrap();
988
989 let snapshot = sbox.snapshot().unwrap();
990
991 let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
992 let res: i32 = sbox.call("GetStatic", ()).unwrap();
993 assert_eq!(res, 5);
994
995 sbox.restore(&snapshot).unwrap();
996 #[allow(deprecated)]
997 let _ = sbox
998 .call_guest_function_by_name::<i32>("AddToStatic", 5i32)
999 .unwrap();
1000 #[allow(deprecated)]
1001 let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap();
1002 assert_eq!(res, 0);
1003 }
1004
1005 // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (1K) and heap(14K).
1006 // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
1007 #[test]
1008 fn test_with_small_stack_and_heap() {
1009 let mut cfg = SandboxConfiguration::default();
1010 cfg.set_heap_size(20 * 1024);
1011 cfg.set_stack_size(18 * 1024);
1012
1013 let mut sbox1: MultiUseSandbox = {
1014 let path = simple_guest_as_string().unwrap();
1015 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1016 u_sbox.evolve()
1017 }
1018 .unwrap();
1019
1020 for _ in 0..1000 {
1021 sbox1.call::<String>("Echo", "hello".to_string()).unwrap();
1022 }
1023
1024 let mut sbox2: MultiUseSandbox = {
1025 let path = simple_guest_as_string().unwrap();
1026 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
1027 u_sbox.evolve()
1028 }
1029 .unwrap();
1030
1031 for i in 0..1000 {
1032 sbox2
1033 .call::<i32>(
1034 "PrintUsingPrintf",
1035 format!("Hello World {}\n", i).to_string(),
1036 )
1037 .unwrap();
1038 }
1039 }
1040
1041 /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
1042 /// and restoring a snapshot from before evolving restores the previous state
1043 #[test]
1044 fn snapshot_evolve_restore_handles_state_correctly() {
1045 let mut sbox: MultiUseSandbox = {
1046 let path = simple_guest_as_string().unwrap();
1047 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1048 u_sbox.evolve()
1049 }
1050 .unwrap();
1051
1052 let snapshot = sbox.snapshot().unwrap();
1053
1054 let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
1055
1056 let res: i32 = sbox.call("GetStatic", ()).unwrap();
1057 assert_eq!(res, 5);
1058
1059 sbox.restore(&snapshot).unwrap();
1060 let res: i32 = sbox.call("GetStatic", ()).unwrap();
1061 assert_eq!(res, 0);
1062 }
1063
1064 #[test]
1065 fn test_trigger_exception_on_guest() {
1066 let usbox = UninitializedSandbox::new(
1067 GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1068 None,
1069 )
1070 .unwrap();
1071
1072 let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap();
1073
1074 let res: Result<()> = multi_use_sandbox.call("TriggerException", ());
1075
1076 assert!(res.is_err());
1077
1078 match res.unwrap_err() {
1079 HyperlightError::GuestAborted(_, msg) => {
1080 // msg should indicate we got an invalid opcode exception
1081 assert!(msg.contains("InvalidOpcode"));
1082 }
1083 e => panic!(
1084 "Expected HyperlightError::GuestExecutionError but got {:?}",
1085 e
1086 ),
1087 }
1088 }
1089
1090 #[test]
1091 #[ignore] // this test runs by itself because it uses a lot of system resources
1092 fn create_1000_sandboxes() {
1093 let barrier = Arc::new(Barrier::new(21));
1094
1095 let mut handles = vec![];
1096
1097 for _ in 0..20 {
1098 let c = barrier.clone();
1099
1100 let handle = thread::spawn(move || {
1101 c.wait();
1102
1103 for _ in 0..50 {
1104 let usbox = UninitializedSandbox::new(
1105 GuestBinary::FilePath(
1106 simple_guest_as_string().expect("Guest Binary Missing"),
1107 ),
1108 None,
1109 )
1110 .unwrap();
1111
1112 let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap();
1113
1114 let res: i32 = multi_use_sandbox.call("GetStatic", ()).unwrap();
1115
1116 assert_eq!(res, 0);
1117 }
1118 });
1119
1120 handles.push(handle);
1121 }
1122
1123 barrier.wait();
1124
1125 for handle in handles {
1126 handle.join().unwrap();
1127 }
1128 }
1129
1130 #[cfg(target_os = "linux")]
1131 #[test]
1132 fn test_mmap() {
1133 let mut sbox = UninitializedSandbox::new(
1134 GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1135 None,
1136 )
1137 .unwrap()
1138 .evolve()
1139 .unwrap();
1140
1141 let expected = b"hello world";
1142 let map_mem = page_aligned_memory(expected);
1143 let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1144
1145 unsafe {
1146 sbox.map_region(®ion_for_memory(
1147 &map_mem,
1148 guest_base,
1149 MemoryRegionFlags::READ,
1150 ))
1151 .unwrap();
1152 }
1153
1154 let _guard = map_mem.lock.try_read().unwrap();
1155 let actual: Vec<u8> = sbox
1156 .call(
1157 "ReadMappedBuffer",
1158 (guest_base as u64, expected.len() as u64),
1159 )
1160 .unwrap();
1161
1162 assert_eq!(actual, expected);
1163 }
1164
1165 // Makes sure MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE executable but not writable
1166 #[cfg(target_os = "linux")]
1167 #[test]
1168 fn test_mmap_write_exec() {
1169 let mut sbox = UninitializedSandbox::new(
1170 GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
1171 None,
1172 )
1173 .unwrap()
1174 .evolve()
1175 .unwrap();
1176
1177 let expected = &[0x90, 0x90, 0x90, 0xC3]; // NOOP slide to RET
1178 let map_mem = page_aligned_memory(expected);
1179 let guest_base = 0x1_0000_0000; // Arbitrary guest base address
1180
1181 unsafe {
1182 sbox.map_region(®ion_for_memory(
1183 &map_mem,
1184 guest_base,
1185 MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
1186 ))
1187 .unwrap();
1188 }
1189
1190 let _guard = map_mem.lock.try_read().unwrap();
1191
1192 // Execute should pass since memory is executable
1193 let succeed = sbox
1194 .call::<bool>(
1195 "ExecMappedBuffer",
1196 (guest_base as u64, expected.len() as u64),
1197 )
1198 .unwrap();
1199 assert!(succeed, "Expected execution of mapped buffer to succeed");
1200
1201 // write should fail because the memory is mapped as read-only
1202 let err = sbox
1203 .call::<bool>(
1204 "WriteMappedBuffer",
1205 (guest_base as u64, expected.len() as u64),
1206 )
1207 .unwrap_err();
1208
1209 match err {
1210 HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {}
1211 _ => panic!("Expected MemoryAccessViolation error"),
1212 };
1213 }
1214
1215 #[cfg(target_os = "linux")]
1216 fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory {
1217 use hyperlight_common::mem::PAGE_SIZE_USIZE;
1218
1219 let len = src.len().div_ceil(PAGE_SIZE_USIZE) * PAGE_SIZE_USIZE;
1220
1221 let mut mem = ExclusiveSharedMemory::new(len).unwrap();
1222 mem.copy_from_slice(src, 0).unwrap();
1223
1224 let (_, guest_mem) = mem.build();
1225
1226 guest_mem
1227 }
1228
1229 #[cfg(target_os = "linux")]
1230 fn region_for_memory(
1231 mem: &GuestSharedMemory,
1232 guest_base: usize,
1233 flags: MemoryRegionFlags,
1234 ) -> MemoryRegion {
1235 let ptr = mem.base_addr();
1236 let len = mem.mem_size();
1237 MemoryRegion {
1238 host_region: ptr..(ptr + len),
1239 guest_region: guest_base..(guest_base + len),
1240 flags,
1241 region_type: MemoryRegionType::Heap,
1242 }
1243 }
1244
1245 #[cfg(target_os = "linux")]
1246 fn allocate_guest_memory() -> GuestSharedMemory {
1247 page_aligned_memory(b"test data for snapshot")
1248 }
1249
1250 #[test]
1251 #[cfg(target_os = "linux")]
1252 fn snapshot_restore_handles_remapping_correctly() {
1253 let mut sbox: MultiUseSandbox = {
1254 let path = simple_guest_as_string().unwrap();
1255 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1256 u_sbox.evolve().unwrap()
1257 };
1258
1259 // 1. Take snapshot 1 with no additional regions mapped
1260 let snapshot1 = sbox.snapshot().unwrap();
1261 assert_eq!(sbox.vm.get_mapped_regions().len(), 0);
1262
1263 // 2. Map a memory region
1264 let map_mem = allocate_guest_memory();
1265 let guest_base = 0x200000000_usize;
1266 let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
1267
1268 unsafe { sbox.map_region(®ion).unwrap() };
1269 assert_eq!(sbox.vm.get_mapped_regions().len(), 1);
1270
1271 // 3. Take snapshot 2 with 1 region mapped
1272 let snapshot2 = sbox.snapshot().unwrap();
1273 assert_eq!(sbox.vm.get_mapped_regions().len(), 1);
1274
1275 // 4. Restore to snapshot 1 (should unmap the region)
1276 sbox.restore(&snapshot1).unwrap();
1277 assert_eq!(sbox.vm.get_mapped_regions().len(), 0);
1278
1279 // 5. Restore forward to snapshot 2 (should remap the region)
1280 sbox.restore(&snapshot2).unwrap();
1281 assert_eq!(sbox.vm.get_mapped_regions().len(), 1);
1282
1283 // Verify the region is the same
1284 let mut restored_regions = sbox.vm.get_mapped_regions();
1285 assert_eq!(*restored_regions.next().unwrap(), region);
1286 assert!(restored_regions.next().is_none());
1287 drop(restored_regions);
1288
1289 // 6. Try map the region again (should fail since already mapped)
1290 let err = unsafe { sbox.map_region(®ion) };
1291 assert!(
1292 err.is_err(),
1293 "Expected error when remapping existing region: {:?}",
1294 err
1295 );
1296 }
1297
1298 #[test]
1299 fn snapshot_different_sandbox() {
1300 let mut sandbox = {
1301 let path = simple_guest_as_string().unwrap();
1302 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1303 u_sbox.evolve().unwrap()
1304 };
1305
1306 let mut sandbox2 = {
1307 let path = simple_guest_as_string().unwrap();
1308 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1309 u_sbox.evolve().unwrap()
1310 };
1311 assert_ne!(sandbox.id, sandbox2.id);
1312
1313 let snapshot = sandbox.snapshot().unwrap();
1314 let err = sandbox2.restore(&snapshot);
1315 assert!(matches!(err, Err(HyperlightError::SnapshotSandboxMismatch)));
1316
1317 let sandbox_id = sandbox.id;
1318 drop(sandbox);
1319 drop(sandbox2);
1320 drop(snapshot);
1321
1322 let sandbox3 = {
1323 let path = simple_guest_as_string().unwrap();
1324 let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
1325 u_sbox.evolve().unwrap()
1326 };
1327 assert_ne!(sandbox3.id, sandbox_id);
1328 }
1329}