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