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