use std::path::Path;
#[cfg(crashdump)]
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use flatbuffers::FlatBufferBuilder;
use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
use hyperlight_common::flatbuffer_wrappers::function_types::{
ParameterValue, ReturnType, ReturnValue,
};
use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity;
use tracing::{Span, instrument};
use super::Callable;
use super::file_mapping::prepare_file_cow;
use super::host_funcs::FunctionRegistry;
use super::snapshot::Snapshot;
use crate::func::{ParameterTuple, SupportedReturnType};
use crate::hypervisor::InterruptHandle;
use crate::hypervisor::hyperlight_vm::{HyperlightVm, HyperlightVmError};
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
use crate::mem::mgr::SandboxMemoryManager;
use crate::mem::shared_mem::{HostSharedMemory, SharedMemory as _};
use crate::metrics::{
METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
};
use crate::{HyperlightError, Result, log_then_return};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SandboxStatus {
Ready,
Poisoned,
Unrecoverable,
}
impl SandboxStatus {
pub const fn is_ready(self) -> bool {
matches!(self, Self::Ready)
}
pub const fn is_poisoned(self) -> bool {
matches!(self, Self::Poisoned)
}
pub const fn is_unrecoverable(self) -> bool {
matches!(self, Self::Unrecoverable)
}
}
pub struct MultiUseSandbox {
status: SandboxStatus,
pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
pub(crate) mem_mgr: SandboxMemoryManager<HostSharedMemory>,
vm: HyperlightVm,
pub(crate) snapshot: Option<Arc<Snapshot>>,
pt_root_finder: Option<PtRootFinder>,
}
pub type PtRootFinder = Box<dyn Fn(&[u8], &[u8], u64) -> Vec<u64> + Send>;
impl MultiUseSandbox {
fn check_ready(&self) -> Result<()> {
match self.status {
SandboxStatus::Ready => Ok(()),
SandboxStatus::Poisoned => Err(HyperlightError::PoisonedSandbox),
SandboxStatus::Unrecoverable => Err(HyperlightError::UnrecoverableSandbox),
}
}
fn poison(&mut self) {
if self.status.is_ready() {
self.status = SandboxStatus::Poisoned;
}
}
#[instrument(skip_all, parent = Span::current(), level = "Trace")]
pub(super) fn from_uninit(
host_funcs: Arc<Mutex<FunctionRegistry>>,
mgr: SandboxMemoryManager<HostSharedMemory>,
vm: HyperlightVm,
) -> MultiUseSandbox {
Self {
status: SandboxStatus::Ready,
host_funcs,
mem_mgr: mgr,
vm,
snapshot: None,
pt_root_finder: None,
}
}
pub fn set_pt_root_finder(&mut self, finder: PtRootFinder) {
self.pt_root_finder = Some(finder);
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
pub fn from_snapshot(
snapshot: Arc<Snapshot>,
host_funcs: crate::HostFunctions,
config: Option<crate::sandbox::SandboxConfiguration>,
) -> Result<Self> {
use rand::RngExt;
use crate::mem::ptr::RawPtr;
use crate::sandbox::uninitialized_evolve::set_up_hypervisor_partition;
snapshot.validate_host_functions(host_funcs.inner())?;
let host_funcs = Arc::new(Mutex::new(host_funcs.into_inner()));
let stack_top_gva = snapshot.stack_top_gva();
let caller_supplied_config = config.is_some();
let mut config = config.unwrap_or_default();
if caller_supplied_config {
warn_on_layout_override(&config, snapshot.layout());
}
config.set_input_data_size(snapshot.layout().input_data_size());
config.set_output_data_size(snapshot.layout().output_data_size());
config.set_heap_size(snapshot.layout().heap_size() as u64);
config.set_scratch_size(snapshot.layout().get_scratch_size());
let load_info = snapshot.load_info();
let mgr = crate::mem::mgr::SandboxMemoryManager::from_snapshot(&snapshot)?;
let (mut hshm, gshm) = mgr.build()?;
let page_size = u32::try_from(page_size::get())? as usize;
#[cfg(target_os = "linux")]
crate::signal_handlers::setup_signal_handlers(&config)?;
#[cfg(any(crashdump, gdb))]
let rt_cfg = crate::sandbox::uninitialized::SandboxRuntimeConfig {
#[cfg(crashdump)]
binary_path: None,
#[cfg(gdb)]
debug_info: config.get_guest_debug_info(),
#[cfg(crashdump)]
guest_core_dump: config.get_guest_core_dump(),
#[cfg(crashdump)]
entry_point: None,
};
let mut vm = set_up_hypervisor_partition(
gshm,
&config,
stack_top_gva,
page_size,
#[cfg(any(crashdump, gdb))]
rt_cfg,
load_info,
)?;
let seed = {
let mut rng = rand::rng();
rng.random::<u64>()
};
let peb_addr = RawPtr::from(u64::try_from(hshm.layout.peb_address())?);
vm.initialise(peb_addr, seed, &mut hshm, &host_funcs, None)
.map_err(crate::hypervisor::hyperlight_vm::HyperlightVmError::Initialize)?;
if matches!(snapshot.next_action(), super::snapshot::NextAction::Call(_)) {
hshm.request_libc_rng_reseed(seed as u32)?;
}
if matches!(snapshot.next_action(), super::snapshot::NextAction::Call(_)) {
let sregs = snapshot.sregs().ok_or_else(|| {
crate::new_error!("snapshot with NextAction::Call must have captured sregs")
})?;
#[cfg(target_arch = "x86_64")]
let msrs = snapshot.msrs().ok_or_else(|| {
crate::new_error!("snapshot with NextAction::Call must have captured MSRs")
})?;
vm.apply_sregs(hshm.layout.get_pt_base_gpa(), sregs)
.map_err(|e| {
crate::HyperlightError::HyperlightVmError(
crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e.into()),
)
})?;
#[cfg(target_arch = "x86_64")]
vm.restore_msrs(msrs).map_err(|e| {
crate::HyperlightError::HyperlightVmError(
crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e),
)
})?;
}
let sbox = MultiUseSandbox::from_uninit(host_funcs, hshm, vm);
Ok(sbox)
}
#[instrument(err(Debug), skip_all, parent = Span::current())]
pub fn snapshot(&mut self) -> Result<Arc<Snapshot>> {
self.check_ready()?;
if let Some(snapshot) = &self.snapshot {
return Ok(snapshot.clone());
}
let mapped_regions_iter = self.vm.get_mapped_regions();
let mapped_regions_vec: Vec<MemoryRegion> = mapped_regions_iter.cloned().collect();
let cr3 = self
.vm
.get_root_pt()
.map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
let root_pt_gpas = if let Some(finder) = &self.pt_root_finder {
let roots = self.mem_mgr.shared_mem.with_contents(|snap| {
self.mem_mgr
.scratch_mem
.with_contents(|scratch| finder(snap, scratch, cr3))
})??;
if roots.is_empty() { vec![cr3] } else { roots }
} else {
vec![cr3]
};
let stack_top_gpa = self.vm.get_stack_top();
let sregs = self
.vm
.get_snapshot_sregs()
.map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
#[cfg(target_arch = "x86_64")]
let msrs = self
.vm
.get_msr_reset_state()
.map_err(|e| HyperlightError::HyperlightVmError(e.into()))?;
let next_action = self.vm.get_next_action();
let host_functions = (&*self.host_funcs.try_lock().map_err(|e| {
crate::new_error!("Error locking host_funcs at {}:{}: {}", file!(), line!(), e)
})?)
.into();
let memory_snapshot = self.mem_mgr.snapshot(
mapped_regions_vec,
&root_pt_gpas,
stack_top_gpa,
sregs,
#[cfg(target_arch = "x86_64")]
msrs,
next_action,
host_functions,
)?;
let snapshot = Arc::new(memory_snapshot);
self.snapshot = Some(snapshot.clone());
Ok(snapshot)
}
fn restore_memory_and_mappings(&mut self, snapshot: &Snapshot) -> Result<()> {
let (snapshot_mem, scratch_mem) = self.mem_mgr.restore_snapshot(snapshot)?;
if let Some(snapshot_mem) = snapshot_mem {
self.vm
.update_snapshot_mapping(snapshot_mem)
.map_err(HyperlightVmError::UpdateRegion)?;
}
if let Some(scratch_mem) = scratch_mem {
self.vm
.update_scratch_mapping(scratch_mem)
.map_err(HyperlightVmError::UpdateRegion)?;
}
Ok(())
}
#[instrument(err(Debug), skip_all, parent = Span::current())]
pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()> {
if self.status.is_unrecoverable() {
return Err(HyperlightError::UnrecoverableSandbox);
}
{
let host_funcs = self
.host_funcs
.try_lock()
.map_err(|e| crate::new_error!("Error locking host_funcs: {}", e))?;
snapshot.validate_host_functions(&host_funcs)?;
}
let sregs = snapshot.sregs().ok_or_else(|| {
HyperlightError::Error("snapshot from running sandbox should have sregs".to_string())
})?;
#[cfg(target_arch = "x86_64")]
let msrs = snapshot.msrs().ok_or_else(|| {
HyperlightError::Error("snapshot from running sandbox should have MSRs".to_string())
})?;
self.status = SandboxStatus::Poisoned;
self.snapshot = None;
let current_regions: Vec<MemoryRegion> = self.vm.get_mapped_regions().cloned().collect();
for region in ¤t_regions {
self.vm
.unmap_region(region)
.map_err(HyperlightVmError::UnmapRegion)?;
}
if let Err(error) = self.restore_memory_and_mappings(&snapshot) {
self.status = SandboxStatus::Unrecoverable;
return Err(error);
}
self.vm
.reset_vcpu(
snapshot.root_pt_gpa(),
sregs,
#[cfg(target_arch = "x86_64")]
msrs,
)
.map_err(HyperlightVmError::Restore)?;
self.vm.set_stack_top(snapshot.stack_top_gva());
self.vm.set_next_action(snapshot.next_action());
#[cfg(crashdump)]
{
self.vm
.set_crashdump_entry_point(snapshot.original_entrypoint());
self.vm.clear_crashdump_binary_path();
}
self.mem_mgr
.request_libc_rng_reseed(rand::random::<u32>())?;
self.snapshot = Some(snapshot.clone());
self.status = SandboxStatus::Ready;
Ok(())
}
#[doc(hidden)]
#[deprecated(
since = "0.8.0",
note = "Deprecated in favour of call and snapshot/restore."
)]
#[instrument(err(Debug), skip(self, args), parent = Span::current())]
pub fn call_guest_function_by_name<Output: SupportedReturnType>(
&mut self,
func_name: &str,
args: impl ParameterTuple,
) -> Result<Output> {
self.check_ready()?;
let snapshot = self.snapshot()?;
let res = self.call(func_name, args);
self.restore(snapshot)?;
res
}
#[instrument(err(Debug), skip(self, args), parent = Span::current())]
pub fn call<Output: SupportedReturnType>(
&mut self,
func_name: &str,
args: impl ParameterTuple,
) -> Result<Output> {
self.check_ready()?;
self.snapshot = None;
maybe_time_and_emit_guest_call(func_name, || {
let ret = self.call_guest_function_by_name_no_reset(
func_name,
Output::TYPE,
args.into_value(),
);
let ret = Output::from_value(ret?)?;
Ok(ret)
})
}
#[instrument(err(Debug), skip(self, rgn), parent = Span::current())]
pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> {
self.check_ready()?;
if rgn.flags.contains(MemoryRegionFlags::WRITE) {
log_then_return!("TODO: Writable mappings not yet supported");
}
unsafe { self.vm.map_region(rgn) }.map_err(HyperlightVmError::MapRegion)?;
self.snapshot = None;
Ok(())
}
#[instrument(err(Debug), skip(self, file_path, guest_base), parent = Span::current())]
pub fn map_file_cow(&mut self, file_path: &Path, guest_base: u64) -> Result<u64> {
self.check_ready()?;
let mut prepared = prepare_file_cow(file_path, guest_base)?;
let shared_size = self.mem_mgr.shared_mem.mem_size() as u64;
let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
let shared_end = base_addr.checked_add(shared_size).ok_or_else(|| {
crate::HyperlightError::Error("shared memory end overflow".to_string())
})?;
let mapping_end = guest_base
.checked_add(prepared.size as u64)
.ok_or_else(|| {
crate::HyperlightError::Error(format!(
"map_file_cow: guest address overflow: {:#x} + {:#x}",
guest_base, prepared.size
))
})?;
if guest_base < shared_end && mapping_end > base_addr {
return Err(crate::HyperlightError::Error(format!(
"map_file_cow: mapping [{:#x}..{:#x}) overlaps sandbox shared memory [{:#x}..{:#x})",
guest_base, mapping_end, base_addr, shared_end,
)));
}
let region = prepared.to_memory_region()?;
unsafe { self.vm.map_region(®ion) }
.map_err(HyperlightVmError::MapRegion)
.map_err(crate::HyperlightError::HyperlightVmError)?;
self.snapshot = None;
let size = prepared.size as u64;
prepared.mark_consumed();
Ok(size)
}
#[cfg(feature = "fuzzing")]
#[instrument(err(Debug), skip(self, args), parent = Span::current())]
pub fn call_type_erased_guest_function_by_name(
&mut self,
func_name: &str,
ret_type: ReturnType,
args: Vec<ParameterValue>,
) -> Result<ReturnValue> {
self.check_ready()?;
self.snapshot = None;
maybe_time_and_emit_guest_call(func_name, || {
self.call_guest_function_by_name_no_reset(func_name, ret_type, args)
})
}
fn call_guest_function_by_name_no_reset(
&mut self,
function_name: &str,
return_type: ReturnType,
args: Vec<ParameterValue>,
) -> Result<ReturnValue> {
self.check_ready()?;
self.vm.clear_cancel();
let res = (|| {
let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args);
let fc = FunctionCall::new(
function_name.to_string(),
Some(args),
FunctionCallType::Guest,
return_type,
);
let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
let buffer = fc.encode(&mut builder);
self.mem_mgr.write_guest_function_call(buffer)?;
let dispatch_res = self
.vm
.dispatch_call_from_host(&mut self.mem_mgr, &self.host_funcs);
if let Err(e) = dispatch_res {
let (error, should_poison) = e.promote();
if should_poison {
self.poison();
}
return Err(error);
}
let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner();
match guest_result {
Ok(val) => Ok(val),
Err(guest_error) => {
metrics::counter!(
METRIC_GUEST_ERROR,
METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string()
)
.increment(1);
Err(HyperlightError::GuestError(
guest_error.code,
guest_error.message,
))
}
}
})();
self.mem_mgr.abort_buffer.clear();
if let Err(e) = &res {
self.mem_mgr.clear_io_buffers();
if e.is_poison_error() {
self.poison();
}
}
res
}
pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
self.vm.interrupt_handle()
}
#[cfg(crashdump)]
#[instrument(err(Debug), skip_all, parent = Span::current())]
pub fn generate_crashdump(&mut self) -> Result<()> {
crate::hypervisor::crashdump::generate_crashdump(&self.vm, &mut self.mem_mgr, None)
}
#[cfg(crashdump)]
#[instrument(err(Debug), skip_all, parent = Span::current())]
pub fn generate_crashdump_to_dir(&mut self, dir: impl Into<PathBuf>) -> Result<()> {
crate::hypervisor::crashdump::generate_crashdump(
&self.vm,
&mut self.mem_mgr,
Some(dir.into()),
)
}
#[deprecated(since = "0.17.0", note = "use status().is_poisoned()")]
pub fn poisoned(&self) -> bool {
self.status.is_poisoned()
}
pub fn status(&self) -> SandboxStatus {
self.status
}
}
impl Callable for MultiUseSandbox {
fn call<Output: SupportedReturnType>(
&mut self,
func_name: &str,
args: impl ParameterTuple,
) -> Result<Output> {
self.check_ready()?;
self.call(func_name, args)
}
}
impl std::fmt::Debug for MultiUseSandbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MultiUseSandbox").finish()
}
}
fn warn_on_layout_override(
caller: &crate::sandbox::SandboxConfiguration,
snapshot: &crate::mem::layout::SandboxMemoryLayout,
) {
let mismatches: &[(&str, u64, u64)] = &[
(
"input_data_size",
caller.get_input_data_size() as u64,
snapshot.input_data_size() as u64,
),
(
"output_data_size",
caller.get_output_data_size() as u64,
snapshot.output_data_size() as u64,
),
(
"heap_size",
caller.get_heap_size(),
snapshot.heap_size() as u64,
),
(
"scratch_size",
caller.get_scratch_size() as u64,
snapshot.get_scratch_size() as u64,
),
];
for (name, supplied, snap) in mismatches {
if supplied != snap {
tracing::warn!(
"from_snapshot ignoring caller-supplied {} ({}); using snapshot value ({})",
name,
supplied,
snap
);
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Barrier};
use std::thread;
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE};
use hyperlight_testing::{c_simple_guest_as_pathbuf, simple_guest_as_pathbuf};
use crate::func::host_functions::Registerable;
#[cfg(not(gdb))]
use crate::hypervisor::hyperlight_vm::test_support::VmOperation;
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _};
use crate::sandbox::SandboxConfiguration;
use crate::sandbox::uninitialized::{GuestBlob, GuestEnvironment};
use crate::{
GuestBinary, HyperlightError, MultiUseSandbox, Result, SandboxBuilder, SandboxStatus,
UninitializedSandbox,
};
#[test]
fn sandbox_status_predicates() {
assert!(SandboxStatus::Ready.is_ready());
assert!(!SandboxStatus::Ready.is_poisoned());
assert!(!SandboxStatus::Ready.is_unrecoverable());
assert!(!SandboxStatus::Poisoned.is_ready());
assert!(SandboxStatus::Poisoned.is_poisoned());
assert!(!SandboxStatus::Poisoned.is_unrecoverable());
assert!(!SandboxStatus::Unrecoverable.is_ready());
assert!(!SandboxStatus::Unrecoverable.is_poisoned());
assert!(SandboxStatus::Unrecoverable.is_unrecoverable());
}
#[test]
fn poison() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sbox.snapshot().unwrap();
let res = sbox
.call::<()>("guest_panic", "hello".to_string())
.unwrap_err();
assert!(
matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
);
assert!(sbox.status().is_poisoned());
let res = sbox
.call::<()>("guest_panic", "hello2".to_string())
.unwrap_err();
assert!(matches!(res, HyperlightError::PoisonedSandbox));
if let Err(e) = sbox.snapshot() {
assert!(sbox.status().is_poisoned());
assert!(matches!(e, HyperlightError::PoisonedSandbox));
} else {
panic!("Snapshot should fail");
}
{
let map_mem = allocate_guest_memory();
let guest_base = 0x0;
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
let res = unsafe { sbox.map_region(®ion) }.unwrap_err();
assert!(matches!(res, HyperlightError::PoisonedSandbox));
}
{
let temp_file = std::env::temp_dir().join("test_poison_map_file.bin");
let res = sbox.map_file_cow(&temp_file, 0x0).unwrap_err();
assert!(matches!(res, HyperlightError::PoisonedSandbox));
std::fs::remove_file(&temp_file).ok(); }
#[allow(deprecated)]
let res = sbox
.call_guest_function_by_name::<String>("Echo", "test".to_string())
.unwrap_err();
assert!(matches!(res, HyperlightError::PoisonedSandbox));
sbox.restore(snapshot.clone()).unwrap();
assert_eq!(sbox.status(), SandboxStatus::Ready);
let res = sbox.call::<String>("Echo", "hello2".to_string()).unwrap();
assert_eq!(res, "hello2".to_string());
assert_eq!(sbox.status(), SandboxStatus::Ready);
let res = sbox
.call::<()>("guest_panic", "hello".to_string())
.unwrap_err();
assert!(
matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("hello"))
);
assert!(sbox.status().is_poisoned());
sbox.restore(snapshot.clone()).unwrap();
assert_eq!(sbox.status(), SandboxStatus::Ready);
let res = sbox.call::<String>("Echo", "hello3".to_string()).unwrap();
assert_eq!(res, "hello3".to_string());
assert_eq!(sbox.status(), SandboxStatus::Ready);
let _ = sbox.snapshot().unwrap();
}
#[test]
fn host_func_error() {
let path = simple_guest_as_pathbuf();
let mut sandbox = SandboxBuilder::from_file(path)
.host_function("HostError", || -> Result<()> {
Err(HyperlightError::Error("hi".to_string()))
})
.build()
.unwrap();
for _ in 0..1000 {
let result = sandbox
.call::<i64>(
"CallGivenParamlessHostFuncThatReturnsI64",
"HostError".to_string(),
)
.unwrap_err();
assert!(
matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"),
);
}
}
#[test]
fn call_host_func_expect_error() {
let path = simple_guest_as_pathbuf();
let mut sandbox = SandboxBuilder::from_file(path).build().unwrap();
sandbox
.call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string())
.unwrap();
}
#[test]
fn io_buffer_reset() {
let path = simple_guest_as_pathbuf();
let mut sandbox = SandboxBuilder::from_file(path)
.input_data_size(4096)
.output_data_size(4096)
.host_function("HostAdd", |a: i32, b: i32| a + b)
.build()
.unwrap();
for _ in 0..1000 {
let result = sandbox.call::<i32>("Add", (5i32, 10i32)).unwrap();
assert_eq!(result, 15);
let result = sandbox.call::<i32>("AddToStaticAndFail", ()).unwrap_err();
assert!(
matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose")
);
}
}
#[test]
fn test_call_guest_function_by_name() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sbox.snapshot().unwrap();
let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
let res: i32 = sbox.call("GetStatic", ()).unwrap();
assert_eq!(res, 5);
sbox.restore(snapshot).unwrap();
#[allow(deprecated)]
let _ = sbox
.call_guest_function_by_name::<i32>("AddToStatic", 5i32)
.unwrap();
#[allow(deprecated)]
let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap();
assert_eq!(res, 0);
}
#[test]
fn test_with_small_stack_and_heap() {
const HEAP_SIZE: u64 = 32 * 1024;
let scratch_size = {
let defaults = SandboxConfiguration::default();
hyperlight_common::layout::min_scratch_size(
defaults.get_input_data_size(),
defaults.get_output_data_size(),
)
} + 0x10000
+ 0x10000;
let mut sbox1 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.heap_size(HEAP_SIZE)
.scratch_size(scratch_size)
.build()
.unwrap();
for _ in 0..1000 {
sbox1.call::<String>("Echo", "hello".to_string()).unwrap();
}
let mut sbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.heap_size(HEAP_SIZE)
.scratch_size(scratch_size)
.build()
.unwrap();
for i in 0..1000 {
sbox2
.call::<i32>(
"PrintUsingPrintf",
format!("Hello World {}\n", i).to_string(),
)
.unwrap();
}
}
#[test]
fn snapshot_evolve_restore_handles_state_correctly() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sbox.snapshot().unwrap();
let _ = sbox.call::<i32>("AddToStatic", 5i32).unwrap();
let res: i32 = sbox.call("GetStatic", ()).unwrap();
assert_eq!(res, 5);
sbox.restore(snapshot).unwrap();
let res: i32 = sbox.call("GetStatic", ()).unwrap();
assert_eq!(res, 0);
}
#[test]
fn test_trigger_exception_on_guest() {
let mut multi_use_sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let res: Result<()> = multi_use_sandbox.call("TriggerException", ());
assert!(res.is_err());
match res.unwrap_err() {
HyperlightError::GuestAborted(_, msg) => {
#[cfg(target_arch = "x86_64")]
assert!(msg.contains("InvalidOpcode"));
#[cfg(target_arch = "aarch64")]
assert!(msg.contains("0x2000000"));
}
e => panic!("Expected HyperlightError::GuestAborted but got {:?}", e),
}
}
fn create_many_on_threads_test<const NUM_THREADS: usize, const SANDBOXES_PER_THREAD: usize>() {
let start_barrier = Arc::new(Barrier::new(NUM_THREADS + 1));
let mut thread_handles = vec![];
for _ in 0..NUM_THREADS {
let barrier = start_barrier.clone();
let handle = thread::spawn(move || {
barrier.wait();
for _ in 0..SANDBOXES_PER_THREAD {
let guest_path = simple_guest_as_pathbuf();
let mut sandbox = SandboxBuilder::from_file(guest_path).build().unwrap();
let result: i32 = sandbox.call("GetStatic", ()).unwrap();
assert_eq!(result, 0);
}
});
thread_handles.push(handle);
}
start_barrier.wait();
for handle in thread_handles {
handle.join().unwrap();
}
}
#[test]
fn create_200_sandboxes() {
create_many_on_threads_test::<20, 10>();
}
#[test]
fn create_200_threads() {
create_many_on_threads_test::<200, 1>();
}
#[test]
fn create_2000_sandboxes() {
create_many_on_threads_test::<200, 10>();
}
#[test]
fn test_mmap() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let expected = b"hello world";
let map_mem = page_aligned_memory(expected);
let guest_base = 0x1_0000_0000;
unsafe {
sbox.map_region(®ion_for_memory(
&map_mem,
guest_base,
MemoryRegionFlags::READ,
))
.unwrap();
}
let _guard = map_mem.lock.try_read().unwrap();
let actual: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base as u64, expected.len() as u64, true),
)
.unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_mmap_write_exec() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
#[cfg(target_arch = "x86_64")]
let expected = &[0x90, 0x90, 0x90, 0xC3]; #[cfg(target_arch = "aarch64")]
let expected = &[0x1f, 0x20, 0x03, 0xd5, 0xc0, 0x03, 0x5f, 0xd6];
let map_mem = page_aligned_memory(expected);
let guest_base = 0x1_0000_0000;
unsafe {
sbox.map_region(®ion_for_memory(
&map_mem,
guest_base,
MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
))
.unwrap();
}
let _guard = map_mem.lock.try_read().unwrap();
let succeed = sbox
.call::<bool>(
"ExecMappedBuffer",
(guest_base as u64, expected.len() as u64),
)
.unwrap();
assert!(succeed, "Expected execution of mapped buffer to succeed");
let err = sbox
.call::<bool>(
"WriteMappedBuffer",
(guest_base as u64, expected.len() as u64),
)
.unwrap_err();
match err {
HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {}
_ => panic!("Expected MemoryAccessViolation error"),
};
}
fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory {
let page_size = page_size::get();
let len = src.len().div_ceil(page_size) * page_size;
let mut mem = ExclusiveSharedMemory::new(len).unwrap();
mem.copy_from_slice(src, 0).unwrap();
let (_, guest_mem) = mem.build();
guest_mem
}
fn region_for_memory(
mem: &GuestSharedMemory,
guest_base: usize,
flags: MemoryRegionFlags,
) -> MemoryRegion {
let len = mem.mem_size();
MemoryRegion {
host_region: mem.host_region_base()..mem.host_region_end(),
guest_region: guest_base..(guest_base + len),
flags,
region_type: MemoryRegionType::Heap,
}
}
fn allocate_guest_memory() -> GuestSharedMemory {
page_aligned_memory(b"test data for snapshot")
}
#[test]
fn snapshot_restore_handles_remapping_correctly() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot1 = sbox.snapshot().unwrap();
assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
let map_mem = allocate_guest_memory();
let guest_base = 0x200000000_usize;
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
unsafe { sbox.map_region(®ion).unwrap() };
assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
let orig_read = sbox
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
guest_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
true,
),
)
.unwrap();
let snapshot2 = sbox.snapshot().unwrap();
assert_eq!(sbox.vm.get_mapped_regions().count(), 1);
sbox.restore(snapshot1.clone()).unwrap();
assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
let is_mapped = sbox
.call::<bool>("CheckMapped", (guest_base as u64,))
.unwrap();
assert!(!is_mapped);
sbox.restore(snapshot2.clone()).unwrap();
assert_eq!(sbox.vm.get_mapped_regions().count(), 0);
let is_mapped = sbox
.call::<bool>("CheckMapped", (guest_base as u64,))
.unwrap();
assert!(is_mapped);
let new_read = sbox
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
guest_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
false,
),
)
.unwrap();
assert_eq!(new_read, orig_read);
}
#[test]
fn snapshot_restore_across_sandboxes_preserves_mapped_region_contents() {
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let map_mem = allocate_guest_memory();
let guest_base = 0x200000000_usize;
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
unsafe { source.map_region(®ion).unwrap() };
let orig_read = source
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
guest_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
true,
),
)
.unwrap();
let snapshot = source.snapshot().unwrap();
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
assert_eq!(target.vm.get_mapped_regions().count(), 0);
target.restore(snapshot).unwrap();
assert_eq!(target.vm.get_mapped_regions().count(), 0);
let new_read = target
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
guest_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
false,
),
)
.unwrap();
assert_eq!(new_read, orig_read);
}
#[test]
fn snapshot_restore_across_sandboxes() {
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let mut sandbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
sandbox.call::<i32>("AddToStatic", 42i32).unwrap();
assert_eq!(sandbox2.call::<i32>("GetStatic", ()).unwrap(), 0);
let snapshot = sandbox.snapshot().unwrap();
sandbox2.restore(snapshot).unwrap();
assert_eq!(sandbox2.call::<i32>("GetStatic", ()).unwrap(), 42);
}
#[test]
#[cfg(not(gdb))]
fn snapshot_restore_keeps_current_base_mappings() {
let path = simple_guest_as_pathbuf();
let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let snapshot = sandbox.snapshot().unwrap();
sandbox.restore(snapshot.clone()).unwrap();
sandbox.call::<i32>("AddToStatic", 42i32).unwrap();
let mappings = sandbox.vm.base_mapping_state();
let fault_plan = sandbox
.vm
.inject_vm_faults([VmOperation::Unmap(MemoryRegionType::Snapshot)]);
sandbox.restore(snapshot).unwrap();
assert_eq!(sandbox.status(), SandboxStatus::Ready);
let new_mappings = sandbox.vm.base_mapping_state();
assert_eq!(new_mappings.0, mappings.0);
assert_eq!(new_mappings.1.map(|m| m.1), mappings.1.map(|m| m.1));
assert!(!fault_plan.is_consumed());
assert_eq!(sandbox.call::<i32>("GetStatic", ()).unwrap(), 0);
}
#[test]
#[cfg(not(gdb))]
fn snapshot_restore_mapping_failure_is_unrecoverable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 42i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let mappings = target.vm.base_mapping_state();
let fault_plan = target
.vm
.inject_vm_faults([VmOperation::Map(MemoryRegionType::Snapshot)]);
let error = target.restore(snapshot.clone()).unwrap_err();
assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
assert_eq!(target.status(), SandboxStatus::Unrecoverable);
assert_eq!(target.vm.base_mapping_state(), (None, mappings.1));
assert!(fault_plan.is_consumed());
assert!(matches!(
target.restore(snapshot),
Err(HyperlightError::UnrecoverableSandbox)
));
assert!(matches!(
target.call::<i32>("GetStatic", ()),
Err(HyperlightError::UnrecoverableSandbox)
));
assert!(matches!(
target.snapshot(),
Err(HyperlightError::UnrecoverableSandbox)
));
let map_mem = allocate_guest_memory();
let region = region_for_memory(&map_mem, 0x200000000_usize, MemoryRegionFlags::READ);
assert!(matches!(
unsafe { target.map_region(®ion) },
Err(HyperlightError::UnrecoverableSandbox)
));
}
#[test]
#[cfg(not(gdb))]
fn scratch_mapping_failure_clears_mapping_state() {
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let snapshot_mapping = target.vm.base_mapping_state().0;
let scratch = ExclusiveSharedMemory::new(target.mem_mgr.scratch_mem.mem_size()).unwrap();
let (_, scratch) = scratch.build();
let fault_plan = target
.vm
.inject_vm_faults([VmOperation::Map(MemoryRegionType::Scratch)]);
target.vm.update_scratch_mapping(scratch).unwrap_err();
assert_eq!(target.vm.base_mapping_state(), (snapshot_mapping, None));
assert!(fault_plan.is_consumed());
}
#[test]
#[cfg(not(gdb))]
fn snapshot_restore_unmapping_failure_is_unrecoverable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 42i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let mappings = target.vm.base_mapping_state();
let fault_plan = target
.vm
.inject_vm_faults([VmOperation::Unmap(MemoryRegionType::Snapshot)]);
let error = target.restore(snapshot).unwrap_err();
assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
assert_eq!(target.status(), SandboxStatus::Unrecoverable);
assert_eq!(target.vm.base_mapping_state(), mappings);
assert!(fault_plan.is_consumed());
}
#[test]
#[cfg(not(gdb))]
fn snapshot_restore_dynamic_unmapping_failure_is_recoverable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 42i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let map_mem = allocate_guest_memory();
let region = region_for_memory(&map_mem, 0x200000000_usize, MemoryRegionFlags::READ);
unsafe { target.map_region(®ion).unwrap() };
let fault_plan = target
.vm
.inject_vm_faults([VmOperation::Unmap(MemoryRegionType::Heap)]);
let error = target.restore(snapshot.clone()).unwrap_err();
assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
assert!(target.status().is_poisoned());
assert_eq!(target.vm.get_mapped_regions().count(), 1);
assert!(fault_plan.is_consumed());
target.restore(snapshot).unwrap();
assert_eq!(target.status(), SandboxStatus::Ready);
assert_eq!(target.vm.get_mapped_regions().count(), 0);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
}
#[test]
#[cfg(not(gdb))]
fn snapshot_restore_partial_dynamic_unmapping_failure_is_recoverable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 42i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let first_mem = allocate_guest_memory();
let first_region =
region_for_memory(&first_mem, 0x200000000_usize, MemoryRegionFlags::READ);
unsafe { target.map_region(&first_region).unwrap() };
let (mapped_path, _) =
create_test_file("hyperlight_test_partial_dynamic_unmapping.bin", &[0; 4096]);
target.map_file_cow(&mapped_path, 0x300000000).unwrap();
let second_region = target.vm.get_mapped_regions().last().unwrap().clone();
let fault_plan = target
.vm
.inject_vm_faults([VmOperation::Unmap(MemoryRegionType::MappedFile)]);
let error = target.restore(snapshot.clone()).unwrap_err();
assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
assert!(target.status().is_poisoned());
assert_eq!(
target.vm.get_mapped_regions().collect::<Vec<_>>(),
vec![&second_region]
);
assert!(fault_plan.is_consumed());
target.restore(snapshot).unwrap();
assert_eq!(target.status(), SandboxStatus::Ready);
assert_eq!(target.vm.get_mapped_regions().count(), 0);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
std::fs::remove_file(mapped_path).unwrap();
}
#[test]
#[cfg(not(gdb))]
fn snapshot_restore_vcpu_reset_failure_is_recoverable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 42i32).unwrap();
let snapshot = source.snapshot().unwrap();
#[cfg(target_arch = "x86_64")]
let reset_operations = [
VmOperation::SetRegs,
VmOperation::SetDebugRegs,
VmOperation::ResetXsave,
VmOperation::SetSregs,
];
#[cfg(target_arch = "aarch64")]
let reset_operations = [VmOperation::ResetVcpu];
for reset_operation in reset_operations {
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let fault_plan = target.vm.inject_vm_faults([reset_operation]);
let error = target.restore(snapshot.clone()).unwrap_err();
assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
assert!(target.status().is_poisoned());
assert!(fault_plan.is_consumed());
assert_eq!(
target.vm.base_mapping_state(),
(
Some((
target.mem_mgr.shared_mem.base_addr(),
target.mem_mgr.shared_mem.mem_size(),
)),
Some((
target.mem_mgr.scratch_mem.base_addr(),
target.mem_mgr.scratch_mem.mem_size(),
)),
)
);
target.restore(snapshot.clone()).unwrap();
assert_eq!(target.status(), SandboxStatus::Ready);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
}
}
#[test]
#[cfg(all(target_arch = "x86_64", not(gdb)))]
fn snapshot_restore_msr_failure_is_recoverable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 42i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let fault_plan = target.vm.inject_vm_faults([VmOperation::SetMsrs]);
let error = target.restore(snapshot.clone()).unwrap_err();
assert!(matches!(error, HyperlightError::HyperlightVmError(_)));
assert!(target.status().is_poisoned());
assert!(fault_plan.is_consumed());
assert_eq!(
target.vm.base_mapping_state(),
(
Some((
target.mem_mgr.shared_mem.base_addr(),
target.mem_mgr.shared_mem.mem_size(),
)),
Some((
target.mem_mgr.scratch_mem.base_addr(),
target.mem_mgr.scratch_mem.mem_size(),
)),
)
);
target.restore(snapshot).unwrap();
assert_eq!(target.status(), SandboxStatus::Ready);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
}
#[test]
fn snapshot_restore_accepts_different_configured_layout() {
type Configure = fn(&mut SandboxConfiguration);
type LayoutValue = fn(&crate::mem::layout::SandboxMemoryLayout) -> usize;
let cases: &[(&str, Configure, LayoutValue)] = &[
(
"input",
|cfg| cfg.set_input_data_size(0x8000),
|layout| layout.input_data_size(),
),
(
"output",
|cfg| cfg.set_output_data_size(0x8000),
|layout| layout.output_data_size(),
),
(
"heap",
|cfg| cfg.set_heap_size(0x40_000),
|layout| layout.heap_size(),
),
(
"scratch",
|cfg| cfg.set_scratch_size(0x90_000),
|layout| layout.get_scratch_size(),
),
];
for (name, configure, layout_value) in cases {
for incoming_is_larger in [true, false] {
let mut custom_cfg = SandboxConfiguration::default();
configure(&mut custom_cfg);
let (source_cfg, target_cfg) = if incoming_is_larger {
(custom_cfg, SandboxConfiguration::default())
} else {
(SandboxConfiguration::default(), custom_cfg)
};
let path = simple_guest_as_pathbuf();
let mut source =
UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
.unwrap()
.evolve()
.unwrap();
let path = simple_guest_as_pathbuf();
let mut target =
UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
.unwrap()
.evolve()
.unwrap();
let source_value = layout_value(&source.mem_mgr.layout);
assert_ne!(source_value, layout_value(&target.mem_mgr.layout));
source.call::<i32>("AddToStatic", 42i32).unwrap();
target
.restore(source.snapshot().unwrap())
.unwrap_or_else(|err| panic!("restore with different {name} layout: {err}"));
assert_eq!(layout_value(&target.mem_mgr.layout), source_value);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
}
}
}
#[test]
fn snapshot_restore_recovers_oom_with_larger_heap() {
let mut source_cfg = SandboxConfiguration::default();
source_cfg.set_heap_size(0x20_000);
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
.unwrap()
.evolve()
.unwrap();
let snapshot = source.snapshot().unwrap();
let mut target_cfg = SandboxConfiguration::default();
target_cfg.set_heap_size(0x8000);
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
.unwrap()
.evolve()
.unwrap();
assert!(target.call::<()>("ExhaustHeap", ()).is_err());
assert!(target.status().is_poisoned());
target.restore(snapshot).unwrap();
assert!(!target.status().is_poisoned());
assert_eq!(
target.call::<i32>("CallMalloc", 0x10_000i32).unwrap(),
0x10_000
);
}
#[test]
fn snapshot_restore_applies_smaller_heap_limit() {
let mut source_cfg = SandboxConfiguration::default();
source_cfg.set_heap_size(0x8000);
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
.unwrap()
.evolve()
.unwrap();
let snapshot = source.snapshot().unwrap();
let mut target_cfg = SandboxConfiguration::default();
target_cfg.set_heap_size(0x20_000);
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
.unwrap()
.evolve()
.unwrap();
assert_eq!(
target.call::<i32>("CallMalloc", 0x10_000i32).unwrap(),
0x10_000
);
target.restore(snapshot).unwrap();
assert_eq!(target.mem_mgr.layout.heap_size(), 0x8000);
assert!(target.call::<i32>("CallMalloc", 0x10_000i32).is_err());
assert!(target.status().is_poisoned());
}
#[test]
fn snapshot_restore_applies_smaller_io_limits() {
let mut source_cfg = SandboxConfiguration::default();
source_cfg.set_input_data_size(0x2000);
source_cfg.set_output_data_size(0x2000);
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
.unwrap()
.evolve()
.unwrap();
let snapshot = source.snapshot().unwrap();
let mut target_cfg = SandboxConfiguration::default();
target_cfg.set_input_data_size(0x8000);
target_cfg.set_output_data_size(0x8000);
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg))
.unwrap()
.evolve()
.unwrap();
let large = "x".repeat(0x3000);
assert_eq!(target.call::<String>("Echo", large.clone()).unwrap(), large);
target.restore(snapshot).unwrap();
assert_eq!(target.mem_mgr.layout.input_data_size(), 0x2000);
assert_eq!(target.mem_mgr.layout.output_data_size(), 0x2000);
assert!(target.call::<String>("Echo", large).is_err());
assert!(!target.status().is_poisoned());
assert_eq!(
target.call::<String>("Echo", "small".to_string()).unwrap(),
"small"
);
}
#[test]
fn snapshot_restore_alternates_different_layouts() {
let mut small_cfg = SandboxConfiguration::default();
small_cfg.set_input_data_size(0x2000);
small_cfg.set_output_data_size(0x2000);
small_cfg.set_heap_size(0x8000);
let path = simple_guest_as_pathbuf();
let mut small = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(small_cfg))
.unwrap()
.evolve()
.unwrap();
small.call::<i32>("AddToStatic", 11i32).unwrap();
let small_snapshot = small.snapshot().unwrap();
let mut large_cfg = SandboxConfiguration::default();
large_cfg.set_input_data_size(0x8000);
large_cfg.set_output_data_size(0x8000);
large_cfg.set_heap_size(0x40_000);
large_cfg.set_scratch_size(0x90_000);
let path = simple_guest_as_pathbuf();
let mut large = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(large_cfg))
.unwrap()
.evolve()
.unwrap();
large.call::<i32>("AddToStatic", 22i32).unwrap();
let large_snapshot = large.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
target.restore(small_snapshot.clone()).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 11);
assert_eq!(target.mem_mgr.layout.heap_size(), 0x8000);
target.restore(large_snapshot).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 22);
assert_eq!(target.mem_mgr.layout.heap_size(), 0x40_000);
target.restore(small_snapshot).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 11);
assert_eq!(target.mem_mgr.layout.heap_size(), 0x8000);
}
#[test]
fn snapshot_restore_replaces_rust_guest_with_c_guest() {
let init_data = b"cross-layout-init-data";
let source_env = GuestEnvironment {
guest_binary: GuestBinary::FilePath(c_simple_guest_as_pathbuf()),
init_data: Some(GuestBlob {
data: init_data,
permissions: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE,
}),
};
let mut source = UninitializedSandbox::new(source_env, None)
.unwrap()
.evolve()
.unwrap();
let mut target =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
assert_eq!(source.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
assert_eq!(target.call::<i32>("AddToStatic", 17i32).unwrap(), 17);
target.set_pt_root_finder(Box::new(|_, _, root| vec![root]));
assert!(target.pt_root_finder.is_some());
assert_ne!(
source.mem_mgr.layout.code_size(),
target.mem_mgr.layout.code_size()
);
assert_ne!(
source.mem_mgr.layout.init_data_size(),
target.mem_mgr.layout.init_data_size()
);
assert_ne!(
source.mem_mgr.layout.init_data_permissions(),
target.mem_mgr.layout.init_data_permissions()
);
let snapshot = source.snapshot().unwrap();
target.restore(snapshot).unwrap();
assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
assert!(matches!(
target.call::<i32>("GetStatic", ()),
Err(HyperlightError::GuestError(
ErrorCode::GuestFunctionNotFound,
name
)) if name == "GetStatic"
));
}
#[test]
fn snapshot_restore_replaces_c_guest_with_rust_guest() {
let mut source =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
assert_eq!(source.call::<i32>("AddToStatic", 42i32).unwrap(), 42);
let snapshot = source.snapshot().unwrap();
let mut target =
UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
assert_eq!(target.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
target.restore(snapshot).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
assert!(matches!(
target.call::<i32>("StackAllocate", 512i32),
Err(HyperlightError::GuestError(
ErrorCode::GuestFunctionNotFound,
name
)) if name == "StackAllocate"
));
}
#[test]
fn snapshot_restore_alternates_c_and_rust_guests() {
let mut c_source =
UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
assert_eq!(c_source.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
let c_snapshot = c_source.snapshot().unwrap();
let mut rust_source =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
rust_source.call::<i32>("AddToStatic", 42i32).unwrap();
let rust_snapshot = rust_source.snapshot().unwrap();
let mut target =
UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
assert_eq!(target.call::<i32>("StackAllocate", 256i32).unwrap(), 256);
target.restore(rust_snapshot).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 42);
assert!(matches!(
target.call::<i32>("StackAllocate", 512i32),
Err(HyperlightError::GuestError(
ErrorCode::GuestFunctionNotFound,
name
)) if name == "StackAllocate"
));
target.restore(c_snapshot).unwrap();
assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
assert!(matches!(
target.call::<i32>("GetStatic", ()),
Err(HyperlightError::GuestError(
ErrorCode::GuestFunctionNotFound,
name
)) if name == "GetStatic"
));
}
#[test]
fn snapshot_restore_keeps_target_host_function_implementation() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
source
.register_host_function("Echo42", || Ok(1i64))
.unwrap();
let mut source = source.evolve().unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
target
.register_host_function("Echo42", || Ok(42i64))
.unwrap();
let mut target = target.evolve().unwrap();
target.restore(snapshot).unwrap();
assert_eq!(
target
.call::<i64>(
"CallGivenParamlessHostFuncThatReturnsI64",
"Echo42".to_string(),
)
.unwrap(),
42
);
}
#[test]
fn snapshot_restore_recovers_poison_with_different_guest() {
let mut source =
UninitializedSandbox::new(GuestBinary::FilePath(c_simple_guest_as_pathbuf()), None)
.unwrap()
.evolve()
.unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
assert!(target.call::<()>("ExhaustHeap", ()).is_err());
assert!(target.status().is_poisoned());
target.restore(snapshot).unwrap();
assert!(!target.status().is_poisoned());
assert_eq!(target.call::<i32>("StackAllocate", 512i32).unwrap(), 512);
assert!(matches!(
target.call::<i32>("GetStatic", ()),
Err(HyperlightError::GuestError(
ErrorCode::GuestFunctionNotFound,
name
)) if name == "GetStatic"
));
}
#[test]
fn snapshot_restore_failure_leaves_target_usable() {
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
source
.register_host_function("Add", |a: i32, b: i32| Ok(a + b))
.unwrap();
let mut source = source.evolve().unwrap();
let map_mem = allocate_guest_memory();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
target.call::<i32>("AddToStatic", 5i32).unwrap();
let guest_base = 0x200000000_usize;
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
unsafe { target.map_region(®ion).unwrap() };
target
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
guest_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
true,
),
)
.unwrap();
let cached_snapshot = target.snapshot().unwrap();
let bad_snapshot = source.snapshot().unwrap();
let err = target.restore(bad_snapshot);
assert!(matches!(
err,
Err(HyperlightError::SnapshotHostFunctionMismatch { missing, .. })
if missing.iter().any(|name| name == "Add")
));
assert!(Arc::ptr_eq(&target.snapshot().unwrap(), &cached_snapshot));
assert_eq!(target.vm.get_mapped_regions().count(), 1);
assert!(
target
.call::<bool>("CheckMapped", guest_base as u64)
.unwrap()
);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 5);
target.call::<i32>("AddToStatic", 3i32).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 8);
let good_snapshot = target.snapshot().unwrap();
target.call::<i32>("AddToStatic", 100i32).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 108);
target.restore(good_snapshot).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 8);
}
#[test]
fn snapshot_restore_across_sandboxes_target_has_mapped_regions() {
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
source.call::<i32>("AddToStatic", 23i32).unwrap();
let snapshot = source.snapshot().unwrap();
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let map_mem = allocate_guest_memory();
let guest_base = 0x200000000_usize;
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
unsafe { target.map_region(®ion).unwrap() };
assert_eq!(target.vm.get_mapped_regions().count(), 1);
target.restore(snapshot).unwrap();
assert_eq!(target.vm.get_mapped_regions().count(), 0);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
}
#[test]
fn snapshot_restore_unmaps_regions_overlapping_incoming_layout() {
let mut source_cfg = SandboxConfiguration::default();
source_cfg.set_scratch_size(0x90_000);
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 23i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
assert!(snapshot.memory().mem_size() > target.mem_mgr.shared_mem.mem_size());
let map_mem = allocate_guest_memory();
let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS
+ target.mem_mgr.shared_mem.mem_size();
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
unsafe { target.map_region(®ion).unwrap() };
target.restore(snapshot).unwrap();
assert_eq!(target.vm.get_mapped_regions().count(), 0);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
}
#[test]
fn snapshot_restore_unmaps_region_overlapping_incoming_scratch() {
let incoming_scratch_size = 0x90_000;
let mut source_cfg = SandboxConfiguration::default();
source_cfg.set_scratch_size(incoming_scratch_size);
let path = simple_guest_as_pathbuf();
let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg))
.unwrap()
.evolve()
.unwrap();
source.call::<i32>("AddToStatic", 23i32).unwrap();
let snapshot = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap();
let guest_base =
hyperlight_common::layout::scratch_base_gpa(incoming_scratch_size) as usize;
let target_scratch_base =
hyperlight_common::layout::scratch_base_gpa(SandboxConfiguration::DEFAULT_SCRATCH_SIZE)
as usize;
let map_mem = allocate_guest_memory();
assert!(guest_base + map_mem.mem_size() <= target_scratch_base);
let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ);
unsafe { target.map_region(®ion).unwrap() };
target.restore(snapshot).unwrap();
assert_eq!(target.vm.get_mapped_regions().count(), 0);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 23);
}
#[test]
fn snapshot_restore_across_sandboxes_both_have_different_mapped_regions() {
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let source_mem = allocate_guest_memory();
let source_base = 0x200000000_usize;
let source_region = region_for_memory(&source_mem, source_base, MemoryRegionFlags::READ);
unsafe { source.map_region(&source_region).unwrap() };
let orig_read = source
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
source_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
true,
),
)
.unwrap();
source.call::<i32>("AddToStatic", 9i32).unwrap();
let snapshot = source.snapshot().unwrap();
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let target_mem = allocate_guest_memory();
let target_base = 0x300000000_usize;
let target_region = region_for_memory(&target_mem, target_base, MemoryRegionFlags::READ);
unsafe { target.map_region(&target_region).unwrap() };
assert_eq!(target.vm.get_mapped_regions().count(), 1);
target.restore(snapshot).unwrap();
assert_eq!(target.vm.get_mapped_regions().count(), 0);
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 9);
let new_read = target
.call::<Vec<u8>>(
"ReadMappedBuffer",
(
source_base as u64,
hyperlight_common::vmem::PAGE_SIZE as u64,
false,
),
)
.unwrap();
assert_eq!(new_read, orig_read);
}
#[test]
fn snapshot_restore_across_sandboxes_repeated() {
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
source.call::<i32>("AddToStatic", 7i32).unwrap();
let snapshot = source.snapshot().unwrap();
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
target.restore(snapshot.clone()).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 7);
target.call::<i32>("AddToStatic", 1000i32).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 1007);
target.restore(snapshot).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 7);
}
#[test]
fn snapshot_restore_resets_debug_registers() {
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sandbox.snapshot().unwrap();
let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap();
assert_eq!(dr0_initial, 0, "DR0 should initially be 0");
const DIRTY_VALUE: u64 = 0xFFFF_FEDC_7654_3210;
sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap();
#[cfg(not(hvf))]
{
let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap();
assert_eq!(
dr0_dirty, DIRTY_VALUE,
"DR0 should be dirty after SetDr0 call"
);
}
sandbox.restore(snapshot).unwrap();
let dr0_after_restore: u64 = sandbox.call("GetDr0", ()).unwrap();
assert_eq!(
dr0_after_restore, 0,
"DR0 should be 0 after restore (reset_vcpu should have been called)"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn snapshot_restore_resets_xcr0() {
let mut sandbox: MultiUseSandbox = {
let path = simple_guest_as_pathbuf();
let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
u_sbox.evolve().unwrap()
};
assert_eq!(sandbox.call::<u64>("ReadXcr0", ()).unwrap(), 1);
let snapshot = sandbox.snapshot().unwrap();
sandbox.call::<()>("WriteXcr0", 3u64).unwrap();
assert_eq!(sandbox.call::<u64>("ReadXcr0", ()).unwrap(), 3);
sandbox.restore(snapshot).unwrap();
assert_eq!(
sandbox.call::<u64>("ReadXcr0", ()).unwrap(),
1,
"restore must reset XCR0"
);
}
#[test]
fn stale_abort_buffer_does_not_leak_across_calls() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
sbox.mem_mgr.abort_buffer.extend_from_slice(&[0xAA; 1020]);
let res = sbox.call::<String>("Echo", "hello".to_string());
assert!(
res.is_ok(),
"Expected Ok after stale abort buffer, got: {:?}",
res.unwrap_err()
);
assert!(
sbox.mem_mgr.abort_buffer.is_empty(),
"abort_buffer should be empty after a guest call"
);
}
#[test]
fn test_sandbox_creation_various_sizes() {
let test_cases: [(&str, u64); 3] = [
("small (8MB heap)", SMALL_HEAP_SIZE),
("medium (64MB heap)", MEDIUM_HEAP_SIZE),
("large (256MB heap)", LARGE_HEAP_SIZE),
];
for (name, heap_size) in test_cases {
let path = simple_guest_as_pathbuf();
let sbox = SandboxBuilder::from_file(path)
.heap_size(heap_size)
.scratch_size(0x100000)
.build()
.unwrap_or_else(|e| panic!("Failed to create {} sandbox: {}", name, e));
drop(sbox);
}
}
#[cfg(feature = "trace_guest")]
fn sandbox_for_gva_tests() -> MultiUseSandbox {
let path = simple_guest_as_pathbuf();
SandboxBuilder::from_file(path).build().unwrap()
}
#[cfg(feature = "trace_guest")]
fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
let expected: Vec<u8> = sbox
.call("ReadMappedBuffer", (gva, len as u64, true))
.unwrap();
assert_eq!(expected.len(), len);
let root_pt = sbox.vm.get_root_pt().unwrap();
let actual = sbox
.mem_mgr
.read_guest_memory_by_gva(gva, len, root_pt)
.unwrap();
assert_eq!(
actual, expected,
"read_guest_memory_by_gva at GVA {:#x} (len {}) differs from guest ReadMappedBuffer",
gva, len,
);
}
#[test]
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_single_page() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
assert_gva_read_matches(&mut sbox, code_gva, 128);
}
#[test]
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_full_page() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
assert_gva_read_matches(&mut sbox, code_gva, 4096);
}
#[test]
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_unaligned_cross_page() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let start = code_gva + 4096 - 1;
println!(
"Testing unaligned cross-page read starting at {:#x} spanning 4097 bytes",
start
);
assert_gva_read_matches(&mut sbox, start, 4097);
}
#[test]
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_two_full_pages() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
}
#[test]
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_cross_page_boundary() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let start = code_gva + 4096 - 100;
assert_gva_read_matches(&mut sbox, start, 200);
}
fn create_test_file(name: &str, content: &[u8]) -> (std::path::PathBuf, Vec<u8>) {
use std::io::Write;
let page_size = page_size::get();
let padded_len = content.len().max(page_size).div_ceil(page_size) * page_size;
let mut padded = vec![0u8; padded_len];
padded[..content.len()].copy_from_slice(content);
let temp_dir = std::env::temp_dir();
let path = temp_dir.join(name);
let _ = std::fs::remove_file(&path); let mut f = std::fs::File::create(&path).unwrap();
f.write_all(&padded).unwrap();
(path, content.to_vec())
}
#[test]
fn test_map_file_cow_basic() {
let expected = b"hello world from map_file_cow";
let (path, expected_bytes) =
create_test_file("hyperlight_test_map_file_cow_basic.bin", expected);
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let guest_base: u64 = 0x1_0000_0000;
let mapped_size = sbox.map_file_cow(&path, guest_base).unwrap();
assert!(mapped_size > 0, "mapped_size should be positive");
assert!(
mapped_size >= expected.len() as u64,
"mapped_size should be >= file content length"
);
let actual: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
assert_eq!(
actual, expected_bytes,
"Guest should read back the exact file content"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_read_only_enforcement() {
let content = &[0xBB; 4096];
let (path, _) = create_test_file("hyperlight_test_map_file_cow_readonly.bin", content);
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let guest_base: u64 = 0x1_0000_0000;
sbox.map_file_cow(&path, guest_base).unwrap();
let err = sbox
.call::<bool>("WriteMappedBuffer", (guest_base, content.len() as u64))
.unwrap_err();
match err {
HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base => {}
_ => panic!(
"Expected MemoryAccessViolation at guest_base, got: {:?}",
err
),
};
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_poisoned() {
let (path, _) = create_test_file("hyperlight_test_map_file_cow_poison.bin", &[0xCC; 4096]);
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sbox.snapshot().unwrap();
let _ = sbox
.call::<()>("guest_panic", "hello".to_string())
.unwrap_err();
assert!(sbox.status().is_poisoned());
let err = sbox.map_file_cow(&path, 0x1_0000_0000).unwrap_err();
assert!(matches!(err, HyperlightError::PoisonedSandbox));
sbox.restore(snapshot).unwrap();
assert_eq!(sbox.status(), SandboxStatus::Ready);
let result = sbox.map_file_cow(&path, 0x1_0000_0000);
assert!(result.is_ok());
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_multi_vm_same_file() {
let expected = b"shared file content across VMs";
let (path, expected_bytes) =
create_test_file("hyperlight_test_map_file_cow_multi_vm.bin", expected);
let guest_base: u64 = 0x1_0000_0000;
let mut sbox1 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let mut sbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
sbox1.map_file_cow(&path, guest_base).unwrap();
sbox2.map_file_cow(&path, guest_base).unwrap();
let actual1: Vec<u8> = sbox1
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
let actual2: Vec<u8> = sbox2
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
assert_eq!(
actual1, expected_bytes,
"Sandbox 1 should read correct content"
);
assert_eq!(
actual2, expected_bytes,
"Sandbox 2 should read correct content"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_multi_vm_threaded() {
let expected = b"threaded file mapping test data";
let (path, expected_bytes) =
create_test_file("hyperlight_test_map_file_cow_threaded.bin", expected);
const NUM_THREADS: usize = 5;
let path = Arc::new(path);
let expected_bytes = Arc::new(expected_bytes);
let barrier = Arc::new(Barrier::new(NUM_THREADS));
let mut handles = vec![];
for _ in 0..NUM_THREADS {
let path = path.clone();
let expected_bytes = expected_bytes.clone();
let barrier = barrier.clone();
handles.push(thread::spawn(move || {
barrier.wait();
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let guest_base: u64 = 0x1_0000_0000;
sbox.map_file_cow(&path, guest_base).unwrap();
let actual: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
assert_eq!(actual, *expected_bytes);
}));
}
for h in handles {
h.join().unwrap();
}
let _ = std::fs::remove_file(&*path);
}
#[test]
#[cfg(target_os = "windows")]
fn test_map_file_cow_cleanup_no_handle_leak() {
let (path, _) = create_test_file("hyperlight_test_map_file_cow_cleanup.bin", &[0xDD; 4096]);
{
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
sbox.map_file_cow(&path, 0x1_0000_0000).unwrap();
}
std::fs::remove_file(&path)
.expect("File should be deletable after sandbox with map_file_cow is dropped");
}
#[test]
fn test_map_file_cow_snapshot_remapping_cycle() {
let expected = b"snapshot remapping cycle test!";
let (path, expected_bytes) =
create_test_file("hyperlight_test_map_file_cow_snapshot_remap.bin", expected);
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let guest_base: u64 = 0x1_0000_0000;
let snapshot1 = sbox.snapshot().unwrap();
sbox.map_file_cow(&path, guest_base).unwrap();
let actual: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
assert_eq!(actual, expected_bytes);
let snapshot2 = sbox.snapshot().unwrap();
sbox.restore(snapshot1.clone()).unwrap();
let is_mapped: bool = sbox.call("CheckMapped", (guest_base,)).unwrap();
assert!(
!is_mapped,
"Region should be unmapped after restoring to snapshot₁"
);
sbox.restore(snapshot2).unwrap();
let is_mapped: bool = sbox.call("CheckMapped", (guest_base,)).unwrap();
assert!(
is_mapped,
"Region should be mapped after restoring to snapshot₂"
);
let actual2: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, false),
)
.unwrap();
assert_eq!(
actual2, expected_bytes,
"Data should be intact after snapshot₂ restore"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_snapshot_restore() {
let expected = b"snapshot restore basic test!!";
let (path, expected_bytes) =
create_test_file("hyperlight_test_map_file_cow_snap_restore.bin", expected);
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let guest_base: u64 = 0x1_0000_0000;
sbox.map_file_cow(&path, guest_base).unwrap();
let actual: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
assert_eq!(actual, expected_bytes);
let snapshot = sbox.snapshot().unwrap();
sbox.restore(snapshot).unwrap();
let actual2: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, false),
)
.unwrap();
assert_eq!(
actual2, expected_bytes,
"Data should be readable after restore from snapshot"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_deferred_basic() {
let expected = b"deferred map_file_cow test data";
let (path, expected_bytes) =
create_test_file("hyperlight_test_map_file_cow_deferred.bin", expected);
let guest_base: u64 = 0x1_0000_0000;
let mut u_sbox =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap();
let mapped_size = u_sbox.map_file_cow(&path, guest_base).unwrap();
assert!(mapped_size > 0, "mapped_size should be positive");
assert!(
mapped_size >= expected.len() as u64,
"mapped_size should be >= file content length"
);
let mut sbox = u_sbox.evolve().unwrap();
let actual: Vec<u8> = sbox
.call(
"ReadMappedBuffer",
(guest_base, expected_bytes.len() as u64, true),
)
.unwrap();
assert_eq!(
actual, expected_bytes,
"Guest should read back the exact file content after deferred mapping"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_deferred_drop_without_evolve() {
let (path, _) = create_test_file(
"hyperlight_test_map_file_cow_deferred_drop.bin",
&[0xAA; 4096],
);
let guest_base: u64 = 0x1_0000_0000;
{
let mut u_sbox =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap();
u_sbox.map_file_cow(&path, guest_base).unwrap();
}
#[cfg(target_os = "windows")]
std::fs::remove_file(&path)
.expect("File should be deletable after dropping UninitializedSandbox");
#[cfg(not(target_os = "windows"))]
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_unaligned_guest_base() {
let (path, _) =
create_test_file("hyperlight_test_map_file_cow_unaligned.bin", &[0xBB; 4096]);
let mut u_sbox =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap();
let unaligned_base: u64 = (page_size::get() + 1) as u64;
let result = u_sbox.map_file_cow(&path, unaligned_base);
assert!(
result.is_err(),
"map_file_cow should reject unaligned guest_base"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_empty_file() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("hyperlight_test_map_file_cow_empty.bin");
let _ = std::fs::remove_file(&path);
std::fs::File::create(&path).unwrap();
let mut u_sbox =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap();
let guest_base: u64 = 0x1_0000_0000;
let result = u_sbox.map_file_cow(&path, guest_base);
assert!(result.is_err(), "map_file_cow should reject empty files");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_map_file_cow_overlapping_mappings() {
let (path1, _) =
create_test_file("hyperlight_test_map_file_cow_overlap1.bin", &[0xAA; 4096]);
let (path2, _) =
create_test_file("hyperlight_test_map_file_cow_overlap2.bin", &[0xBB; 4096]);
let guest_base: u64 = 0x1_0000_0000;
let mut u_sbox =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap();
u_sbox.map_file_cow(&path1, guest_base).unwrap();
let result = u_sbox.map_file_cow(&path2, guest_base);
assert!(
result.is_err(),
"map_file_cow should reject overlapping guest address ranges"
);
let _ = std::fs::remove_file(&path1);
let _ = std::fs::remove_file(&path2);
}
#[test]
fn test_map_file_cow_shared_mem_overlap() {
let (path, _) = create_test_file(
"hyperlight_test_map_file_cow_overlap_shm.bin",
&[0xCC; 4096],
);
let mut u_sbox =
UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None)
.unwrap();
let base_addr = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
let result = u_sbox.map_file_cow(&path, base_addr);
assert!(
result.is_err(),
"map_file_cow should reject guest_base inside shared memory"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn map_region_rejects_overlapping_regions() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let mem1 = allocate_guest_memory();
let mem2 = allocate_guest_memory();
let guest_base: usize = 0x200000000;
let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
unsafe { sbox.map_region(®ion1).unwrap() };
let region2 = region_for_memory(&mem2, guest_base, MemoryRegionFlags::READ);
let err = unsafe { sbox.map_region(®ion2) }.unwrap_err();
assert!(
format!("{err:?}").contains("Overlapping"),
"Expected Overlapping error, got: {err:?}"
);
}
#[test]
fn map_region_rejects_partial_overlap() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let ps = page_size::get();
let mem1 = page_aligned_memory(&vec![0xAA; ps * 2]); let mem2 = page_aligned_memory(&vec![0xBB; ps * 2]); let guest_base: usize = 0x200000000;
let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
unsafe { sbox.map_region(®ion1).unwrap() };
let overlap_base = guest_base - ps;
let region2 = region_for_memory(&mem2, overlap_base, MemoryRegionFlags::READ);
let err = unsafe { sbox.map_region(®ion2) }.unwrap_err();
assert!(
format!("{err:?}").contains("verlap"),
"Expected overlap error for partial overlap, got: {err:?}"
);
}
#[test]
fn map_region_allows_adjacent_non_overlapping() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let mem1 = allocate_guest_memory();
let mem2 = allocate_guest_memory();
let guest_base: usize = 0x200000000;
let region1 = region_for_memory(&mem1, guest_base, MemoryRegionFlags::READ);
let region_size = mem1.mem_size();
unsafe { sbox.map_region(®ion1).unwrap() };
let adjacent_base = guest_base + region_size;
let region2 = region_for_memory(&mem2, adjacent_base, MemoryRegionFlags::READ);
unsafe { sbox.map_region(®ion2).unwrap() };
}
#[test]
fn map_region_rejects_overlap_with_snapshot() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let mem = allocate_guest_memory();
let region = region_for_memory(
&mem,
crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS,
MemoryRegionFlags::READ,
);
let err = unsafe { sbox.map_region(®ion) }.unwrap_err();
assert!(
format!("{err:?}").contains("Overlapping"),
"Expected Overlapping error for snapshot overlap, got: {err:?}"
);
}
#[test]
fn map_region_rejects_overlap_with_scratch() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let scratch_addr = hyperlight_common::layout::scratch_base_gpa(
crate::sandbox::SandboxConfiguration::DEFAULT_SCRATCH_SIZE,
) as usize;
let mem = allocate_guest_memory();
let region = region_for_memory(&mem, scratch_addr, MemoryRegionFlags::READ);
let err = unsafe { sbox.map_region(®ion) }.unwrap_err();
assert!(
format!("{err:?}").contains("verlap"),
"Expected overlap error for scratch region, got: {err:?}"
);
}
#[cfg(target_arch = "x86_64")]
mod msr_tests {
use super::*;
use crate::hypervisor::hyperlight_vm::{CreateHyperlightVmError, HyperlightVmError};
use crate::hypervisor::regs::{
MSR_APERF, MSR_BNDCFGS, MSR_CSTAR, MSR_DEBUGCTL, MSR_IA32_SSP,
MSR_INTERRUPT_SSP_TABLE_ADDR, MSR_KERNEL_GS_BASE as KERNEL_GS_BASE, MSR_LSTAR,
MSR_MPERF, MSR_MTRR_DEF_TYPE, MSR_MTRR_FIX64K_00000, MSR_PAT, MSR_PL0_SSP, MSR_PL1_SSP,
MSR_PL2_SSP, MSR_PL3_SSP, MSR_S_CET, MSR_SFMASK, MSR_SPEC_CTRL, MSR_STAR,
MSR_SYSENTER_CS as SYSENTER_CS, MSR_SYSENTER_EIP, MSR_SYSENTER_ESP, MSR_TSC,
MSR_TSC_ADJUST, MSR_TSC_AUX, MSR_TSC_DEADLINE, MSR_TSX_CTRL, MSR_U_CET,
MSR_UMWAIT_CONTROL, MSR_VIRT_SPEC_CTRL, MSR_XFD, MSR_XFD_ERR, MSR_XSS,
};
use crate::hypervisor::virtual_machine::{
CreateVmError, RegisterError, ResetVcpuError, VmError,
};
use crate::sandbox::snapshot::Snapshot;
fn assert_msr_not_declarable(error: &HyperlightError, expected: u32) {
assert!(
matches!(
error,
HyperlightError::HyperlightVmError(HyperlightVmError::Create(
CreateHyperlightVmError::Vm(VmError::CreateVm(
CreateVmError::MsrNotDeclarable { msr, .. }
))
)) if *msr == expected
),
"expected MsrNotAllowable for {expected:#x}, got: {error:?}"
);
}
fn assert_snapshot_msr_index_invalid(error: &HyperlightError) {
assert!(
matches!(
error,
HyperlightError::HyperlightVmError(HyperlightVmError::Restore(
ResetVcpuError::Register(RegisterError::InvalidSnapshotMsrIndex { .. })
))
),
"expected InvalidSnapshotMsrIndex, got: {error:?}"
);
}
#[test]
fn kernel_gs_base_does_not_leak_through_swapgs() {
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let original: u64 = sandbox.call("ReadKernelGsBaseViaSwapgs", ()).unwrap();
let sentinel = if original == 0x0000_7AAA_5555_AAAA {
0x0000_6BBB_4444_BBBB
} else {
0x0000_7AAA_5555_AAAA
};
let snapshot = sandbox.snapshot().unwrap();
sandbox
.call::<()>("WriteKernelGsBaseViaSwapgs", sentinel)
.unwrap();
assert_eq!(
sandbox
.call::<u64>("ReadKernelGsBaseViaSwapgs", ())
.unwrap(),
sentinel
);
sandbox.restore(snapshot).unwrap();
assert_eq!(
sandbox
.call::<u64>("ReadKernelGsBaseViaSwapgs", ())
.unwrap(),
original,
"KERNEL_GS_BASE leaked across restore"
);
}
#[test]
fn snapshot_msr_values_survive_full_in_memory_lifecycle() {
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[KERNEL_GS_BASE])
.unwrap()
.build()
.unwrap();
let first = 0x1111;
let second = 0x2222;
let third = 0x3333;
source
.call::<()>("WriteMSR", (KERNEL_GS_BASE, first))
.unwrap();
assert_eq!(
source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
first
);
let first_snapshot = source.snapshot().unwrap();
source
.call::<()>("WriteMSR", (KERNEL_GS_BASE, second))
.unwrap();
assert_eq!(
source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
second
);
source.restore(first_snapshot.clone()).unwrap();
assert_eq!(
source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
first
);
let mut clone = SandboxBuilder::from_snapshot(first_snapshot.clone())
.guest_msrs(&[KERNEL_GS_BASE])
.unwrap()
.build()
.unwrap();
assert_eq!(clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(), first);
clone
.call::<()>("WriteMSR", (KERNEL_GS_BASE, third))
.unwrap();
assert_eq!(clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(), third);
let third_snapshot = clone.snapshot().unwrap();
source.restore(third_snapshot.clone()).unwrap();
assert_eq!(
source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
third
);
let mut second_clone = SandboxBuilder::from_snapshot(third_snapshot)
.guest_msrs(&[KERNEL_GS_BASE])
.unwrap()
.build()
.unwrap();
assert_eq!(
second_clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
third
);
second_clone.restore(first_snapshot).unwrap();
assert_eq!(
second_clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
first
);
}
#[test]
fn equivalent_msr_configs_are_order_independent_across_sandboxes() {
let source_order = [KERNEL_GS_BASE, SYSENTER_CS];
let target_order = [SYSENTER_CS, KERNEL_GS_BASE];
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&source_order)
.unwrap()
.build()
.unwrap();
source
.call::<()>("WriteMSR", (KERNEL_GS_BASE, 0x4444u64))
.unwrap();
assert_eq!(
source.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
0x4444
);
source
.call::<()>("WriteMSR", (SYSENTER_CS, 0x5555u64))
.unwrap();
assert_eq!(source.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0x5555);
let snapshot = source.snapshot().unwrap();
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&target_order)
.unwrap()
.build()
.unwrap();
target
.call::<()>("WriteMSR", (KERNEL_GS_BASE, 0xAAAAu64))
.unwrap();
assert_eq!(
target.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
0xAAAA
);
target
.call::<()>("WriteMSR", (SYSENTER_CS, 0xBBBBu64))
.unwrap();
assert_eq!(target.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0xBBBB);
target.restore(snapshot.clone()).unwrap();
assert_eq!(
target.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
0x4444
);
assert_eq!(target.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0x5555);
let mut clone = SandboxBuilder::from_snapshot(snapshot)
.guest_msrs(&target_order)
.unwrap()
.build()
.unwrap();
assert_eq!(
clone.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
0x4444
);
assert_eq!(clone.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), 0x5555);
}
#[test]
fn snapshot_restores_into_superset_guest_msrs() {
const SYSENTER_ESP: u32 = 0x175;
let sentinel: u64 = 0x1234;
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[SYSENTER_CS])
.unwrap()
.build()
.unwrap();
source
.call::<()>("WriteMSR", (SYSENTER_CS, sentinel))
.unwrap();
let snapshot = source.snapshot().unwrap();
let mut clone = SandboxBuilder::from_snapshot(snapshot.clone())
.guest_msrs(&[SYSENTER_CS, SYSENTER_ESP])
.unwrap()
.build()
.unwrap();
assert_eq!(clone.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(), sentinel);
let baseline: u64 = clone.call("ReadMSR", SYSENTER_ESP).unwrap();
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[SYSENTER_CS, SYSENTER_ESP])
.unwrap()
.build()
.unwrap();
target
.call::<()>("WriteMSR", (SYSENTER_ESP, baseline ^ 0x55))
.unwrap();
target.restore(snapshot).unwrap();
assert_eq!(
target.call::<u64>("ReadMSR", SYSENTER_CS).unwrap(),
sentinel
);
assert_eq!(
target.call::<u64>("ReadMSR", SYSENTER_ESP).unwrap(),
baseline
);
}
#[test]
fn snapshot_rejects_non_superset_guest_msrs() {
const SYSENTER_ESP: u32 = 0x175;
let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[SYSENTER_CS])
.unwrap()
.build()
.unwrap();
source
.call::<()>("WriteMSR", (SYSENTER_CS, 0x1234u64))
.unwrap();
let snapshot = source.snapshot().unwrap();
for dest in [&[][..], &[SYSENTER_ESP][..]] {
let err = SandboxBuilder::from_snapshot(snapshot.clone())
.guest_msrs(dest)
.unwrap()
.build()
.expect_err("from_snapshot must reject an unrestorable snapshot MSR");
assert_snapshot_msr_index_invalid(&err);
let mut target = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(dest)
.unwrap()
.build()
.unwrap();
let err = target
.restore(snapshot.clone())
.expect_err("restore must reject an unrestorable snapshot MSR");
assert_snapshot_msr_index_invalid(&err);
assert!(target.status().is_poisoned());
assert!(matches!(
target.call::<String>("Echo", "hi".to_string()),
Err(HyperlightError::PoisonedSandbox)
));
}
}
#[test]
fn from_pre_init_snapshot_uses_local_msr_reset_set() {
let mut config = SandboxConfiguration::default();
config.guest_msrs(&[KERNEL_GS_BASE]).unwrap();
let snapshot = Arc::new(
Snapshot::from_env(GuestBinary::FilePath(simple_guest_as_pathbuf()), config)
.unwrap(),
);
assert!(snapshot.msrs().is_none());
let mut sandbox = SandboxBuilder::from_snapshot(snapshot.clone())
.guest_msrs(&[KERNEL_GS_BASE])
.unwrap()
.build()
.unwrap();
let baseline: u64 = sandbox.call("ReadMSR", KERNEL_GS_BASE).unwrap();
sandbox
.call::<()>("WriteMSR", (KERNEL_GS_BASE, baseline ^ 0x55))
.unwrap();
assert_eq!(
sandbox.call::<u64>("ReadMSR", KERNEL_GS_BASE).unwrap(),
baseline ^ 0x55
);
}
#[test]
#[cfg(kvm)]
fn guest_cannot_enable_x2apic_through_apic_base() {
use crate::hypervisor::regs::APIC_BASE_X2APIC_ENABLE;
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
const MSR_IA32_APIC_BASE: u32 = 0x1B;
const MSR_X2APIC_BASE: u32 = 0x800;
const APIC_BASE_DEFAULT: u64 = 0xFEE0_0900;
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sandbox.snapshot().unwrap();
let x2apic_base = APIC_BASE_DEFAULT | APIC_BASE_X2APIC_ENABLE;
let result = sandbox.call::<()>("WriteMSR", (MSR_IA32_APIC_BASE, x2apic_base));
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"guest enabled x2APIC through APIC_BASE: {result:?}"
);
assert!(sandbox.status().is_poisoned());
sandbox.restore(snapshot).unwrap();
assert!(!sandbox.status().is_poisoned());
let result = sandbox.call::<()>("WriteMSR", (MSR_X2APIC_BASE, 1u64));
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"x2APIC MSR access succeeded after restore: {result:?}"
);
assert!(sandbox.status().is_poisoned());
}
#[test]
#[cfg(kvm)]
fn denied_msr_access_poisons_sandbox() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
match get_available_hypervisor() {
Some(HypervisorType::Kvm) => {}
_ => {
return;
}
}
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sbox.snapshot().unwrap();
let msr_index: u32 = 0xC000_0102;
let result = sbox.call::<u64>("ReadMSR", msr_index);
assert!(
matches!(&result, Err(HyperlightError::GuestAborted(_, _))),
"RDMSR 0x{:X}: expected direct #GP, got: {:?}",
msr_index,
result
);
assert!(sbox.status().is_poisoned());
sbox.restore(snapshot.clone()).unwrap();
let result = sbox.call::<()>("WriteMSR", (msr_index, 0x5u64));
assert!(
matches!(&result, Err(HyperlightError::GuestAborted(_, _))),
"WRMSR 0x{:X}: expected direct #GP, got: {:?}",
msr_index,
result
);
assert!(sbox.status().is_poisoned());
}
#[test]
#[cfg(target_arch = "x86_64")]
fn nested_virtualization_is_hidden_from_guest() {
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let features: u32 = sandbox.call("NestedVirtualizationCpuid", ()).unwrap();
assert_eq!(features & 0b11, 0, "guest CPUID exposes VMX or SVM");
}
#[test]
#[cfg(kvm)]
fn nested_vmx_setup_msrs_are_denied() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sandbox.snapshot().unwrap();
let vmx_basic: u32 = 0x480;
let result = sandbox.call::<u64>("ReadMSR", vmx_basic);
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"RDMSR 0x{vmx_basic:X}: expected direct #GP, got: {result:?}"
);
sandbox.restore(snapshot).unwrap();
let feature_control: u32 = 0x3A;
let result = sandbox.call::<()>("WriteMSR", (feature_control, 0x5u64));
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"WRMSR 0x{feature_control:X}: expected direct #GP, got: {result:?}"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn guest_cannot_enter_vmx_operation() {
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let result = sandbox.call::<()>("EnableVmxOperation", ());
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"guest entered VMX operation via CR4.VMXE: {result:?}"
);
assert!(sandbox.status().is_poisoned());
}
#[test]
#[cfg(target_arch = "x86_64")]
fn guest_vmlaunch_faults() {
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let result = sandbox.call::<()>("ExecuteVmlaunch", ());
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"guest executed VMLAUNCH without faulting: {result:?}"
);
assert!(sandbox.status().is_poisoned());
}
#[test]
#[cfg(kvm)]
fn x2apic_is_hidden_from_guest_cpuid() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
assert!(
!sandbox.call::<bool>("X2apicSupported", ()).unwrap(),
"guest CPUID advertises x2APIC"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_allow_non_resettable_msr_fails_creation() {
let err = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[0x49])
.unwrap()
.build()
.unwrap_err();
assert_msr_not_declarable(&err, 0x49);
}
#[test]
#[cfg(kvm)]
fn unclassified_declared_msr_rejected_at_creation() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
let err = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[0x1A0])
.unwrap()
.build()
.expect_err("an unclassified declared MSR must be rejected at creation");
assert_msr_not_declarable(&err, 0x1A0);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_multiple_guest_msrs_reset_across_restore() {
let msrs: [u32; 4] = [0x174, 0x175, 0x176, 0xC000_0102];
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&msrs)
.unwrap()
.build()
.unwrap();
let baseline_snapshot = sbox.snapshot().unwrap();
let value: u64 = 0x1000;
for &msr in &msrs {
sbox.call::<()>("WriteMSR", (msr, value)).unwrap();
let read_value: u64 = sbox.call("ReadMSR", msr).unwrap();
assert_eq!(read_value, value, "MSR 0x{msr:X} should be writable");
}
sbox.restore(baseline_snapshot).unwrap();
for &msr in &msrs {
let read_value: u64 = sbox.call("ReadMSR", msr).unwrap();
assert_ne!(
read_value, value,
"MSR 0x{msr:X} should be reset to baseline across restore"
);
}
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_declared_msr_does_not_leak_across_restore() {
let msr_index: u32 = 0xC000_0102; let sentinel: u64 = 0xCAFE_F00D;
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[msr_index])
.unwrap()
.build()
.unwrap();
let baseline = sbox.snapshot().unwrap();
let original: u64 = sbox.call("ReadMSR", msr_index).unwrap();
assert_ne!(
original, sentinel,
"test sentinel must differ from the baseline value"
);
sbox.call::<()>("WriteMSR", (msr_index, sentinel)).unwrap();
assert_eq!(
sbox.call::<u64>("ReadMSR", msr_index).unwrap(),
sentinel,
"sentinel should be observable before restore"
);
sbox.restore(baseline).unwrap();
let after: u64 = sbox.call("ReadMSR", msr_index).unwrap();
assert_ne!(after, sentinel, "sentinel leaked across restore");
assert_eq!(after, original, "MSR not reset to its baseline value");
}
#[test]
#[cfg(all(kvm, target_arch = "x86_64"))]
fn test_debugctl_and_x2apic_msr_denied_by_default() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
for msr_index in [0x1D9_u32, 0x800] {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let result = sbox.call::<()>("WriteMSR", (msr_index, 0x1u64));
assert!(
matches!(&result, Err(HyperlightError::GuestAborted(_, _))),
"WRMSR 0x{msr_index:X}: expected direct #GP, got: {result:?}"
);
assert!(
sbox.status().is_poisoned(),
"sandbox should be poisoned after a denied WRMSR to 0x{msr_index:X}"
);
}
}
#[test]
#[cfg(all(kvm, target_arch = "x86_64"))]
fn all_kvm_custom_msrs_are_denied() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
const KVM_CUSTOM_MSR_START: u32 = 0x4B56_4D00;
const KVM_CUSTOM_MSR_END: u32 = 0x4B56_4DFF;
let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let snapshot = sandbox.snapshot().unwrap();
for index in KVM_CUSTOM_MSR_START..=KVM_CUSTOM_MSR_END {
let result = sandbox.call::<u64>("ReadMSR", index);
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"RDMSR {index:#x} was not denied: {result:?}"
);
sandbox.restore(snapshot.clone()).unwrap();
let result = sandbox.call::<()>("WriteMSR", (index, 1u64));
assert!(
matches!(result, Err(HyperlightError::GuestAborted(_, _))),
"WRMSR {index:#x} was not denied: {result:?}"
);
sandbox.restore(snapshot.clone()).unwrap();
}
}
#[test]
#[cfg(target_arch = "x86_64")]
fn unresettable_msr_classes_do_not_leak() {
let cases: &[(u32, &str)] = &[
(0xC1, "PMU IA32_PMC0"),
(0x186, "PMU IA32_PERFEVTSEL0"),
(0x38F, "PMU IA32_PERF_GLOBAL_CTRL"),
(0x1C8, "LBR_SELECT"),
(0x14CE, "arch-LBR IA32_LBR_CTL"),
(0x1D4, "FRED IA32_FRED_CONFIG"),
(0xC001_0114, "AMD VM_CR"),
(0xC001_0117, "AMD VM_HSAVE_PA"),
];
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
for &(msr, _name) in cases {
assert_msr_write_does_not_survive_restore(&mut sbox, msr, 0x1);
}
}
#[test]
#[cfg(target_arch = "x86_64")]
fn misc_enable_guest_write_does_not_survive_restore() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
assert_msr_write_does_not_survive_restore(&mut sbox, 0x1A0, 1u64 << 40);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn runtime_msr_table_entries_are_justified() {
use crate::hypervisor::regs::resettable_msr_indices;
#[cfg(kvm)]
let is_kvm = matches!(
crate::hypervisor::virtual_machine::get_available_hypervisor(),
Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm)
);
#[cfg(not(kvm))]
let is_kvm = false;
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let reset_indices: Vec<u32> = sbox.vm.reset_set_indices();
for index in resettable_msr_indices() {
if !reset_indices.contains(&index) {
assert_omitted_msr_does_not_retain(&mut sbox, index);
} else if is_kvm && index == KERNEL_GS_BASE {
} else if (index == MSR_TSC && !is_kvm) || matches!(index, MSR_MPERF | MSR_APERF) {
assert_guest_counter_is_writable_and_restored(&mut sbox, index);
} else if let Some(test_value) = guest_write_test_value(index) {
assert_guest_msr_is_writable_and_restored(&mut sbox, index, test_value);
} else {
assert!(
reset_exception_reason(index).is_some(),
"MSR 0x{index:X} is in the reset set without positive guest-write coverage or an explicit reason"
);
}
}
}
fn assert_omitted_msr_does_not_retain(sbox: &mut MultiUseSandbox, index: u32) {
let baseline = sbox.snapshot().unwrap();
let original: u64 = match sbox.call("ReadMSR", index) {
Ok(value) => value,
Err(_) => {
assert!(
sbox.status().is_poisoned(),
"0x{index:X}: fault did not poison sandbox"
);
sbox.restore(baseline).unwrap();
return;
}
};
let preferred = guest_write_test_value(index).unwrap_or(original ^ 1);
let candidates = [preferred, original ^ 1, original ^ 2, 0, 1, 0x1000];
for candidate in candidates {
if candidate == original {
continue;
}
if sbox.call::<()>("WriteMSR", (index, candidate)).is_err() {
assert!(
sbox.status().is_poisoned(),
"0x{index:X}: fault did not poison sandbox"
);
sbox.restore(baseline.clone()).unwrap();
continue;
}
let written: u64 = sbox.call("ReadMSR", index).unwrap_or_else(|error| {
panic!("0x{index:X}: read after successful write failed: {error:?}")
});
if written != original {
sbox.restore(baseline).unwrap();
let after: u64 = sbox.call("ReadMSR", index).unwrap();
assert_eq!(
after, original,
"0x{index:X}: guest retained a write but the MSR is absent from the reset set"
);
return;
}
sbox.restore(baseline.clone()).unwrap();
}
}
fn guest_write_test_value(index: u32) -> Option<u64> {
match index {
SYSENTER_CS => Some(0x10),
MSR_SYSENTER_ESP | MSR_SYSENTER_EIP => Some(0x1000),
MSR_PAT => Some(0x0007_0406_0007_0406),
MSR_STAR => Some(0x001B_0008_0000_0000),
MSR_LSTAR | MSR_CSTAR => Some(0x1000),
MSR_SFMASK => Some(0x200),
KERNEL_GS_BASE => Some(0x1000),
MSR_TSC_ADJUST => Some(0x1000),
MSR_TSC_AUX => Some(0x5),
MSR_MTRR_DEF_TYPE => Some(0xC00),
0x200..=0x21F if index & 1 == 0 => Some(0x6), 0x200..=0x21F => Some(0x800), MSR_MTRR_FIX64K_00000 | 0x258 | 0x259 | 0x268..=0x26F => {
Some(0x0606_0606_0606_0606)
}
_ => None,
}
}
fn reset_exception_reason(index: u32) -> Option<&'static str> {
match index {
MSR_TSC => Some("KVM denies direct guest TSC MSR access"),
MSR_IA32_SSP => Some(
"active SSP has no architectural RDMSR/WRMSR; covered by active_ssp_does_not_leak_across_restore",
),
MSR_DEBUGCTL => Some("DEBUGCTL support depends on exposed debug features"),
MSR_SPEC_CTRL => Some("SPEC_CTRL writable bits depend on mitigation features"),
MSR_U_CET
| MSR_S_CET
| MSR_PL0_SSP
| MSR_PL1_SSP
| MSR_PL2_SSP
| MSR_PL3_SSP
| MSR_INTERRUPT_SSP_TABLE_ADDR => {
Some("CET writable state depends on exposed CET features")
}
MSR_TSX_CTRL => Some("TSX_CTRL writable bits depend on exposed TSX features"),
MSR_XFD | MSR_XFD_ERR => Some("XFD writable bits depend on exposed XSAVE features"),
MSR_UMWAIT_CONTROL => {
Some("UMWAIT_CONTROL writable bits depend on exposed WAITPKG features")
}
MSR_TSC_DEADLINE => {
Some("TSC_DEADLINE writable bits depend on exposed APIC-timer features")
}
MSR_BNDCFGS => Some("BNDCFGS writable bits depend on exposed MPX features"),
MSR_XSS => Some("XSS writable bits depend on exposed XSAVE features"),
MSR_VIRT_SPEC_CTRL => {
Some("VIRT_SPEC_CTRL writable bits depend on exposed AMD SSBD virtualization")
}
_ => None,
}
}
fn assert_guest_msr_is_writable_and_restored(
sbox: &mut MultiUseSandbox,
index: u32,
sentinel: u64,
) {
let baseline = sbox.snapshot().unwrap();
let original: u64 = sbox
.call("ReadMSR", index)
.unwrap_or_else(|error| panic!("0x{index:X}: guest RDMSR failed: {error:?}"));
let value = if original == sentinel { 0 } else { sentinel };
sbox.call::<()>("WriteMSR", (index, value))
.unwrap_or_else(|error| panic!("0x{index:X}: guest WRMSR failed: {error:?}"));
let written: u64 = sbox
.call("ReadMSR", index)
.unwrap_or_else(|error| panic!("0x{index:X}: guest read-back failed: {error:?}"));
assert_eq!(written, value, "0x{index:X}: guest write did not stick");
sbox.restore(baseline).unwrap();
let restored: u64 = sbox.call("ReadMSR", index).unwrap();
assert_eq!(
restored, original,
"0x{index:X}: restore did not recover the baseline"
);
}
fn assert_guest_counter_is_writable_and_restored(sbox: &mut MultiUseSandbox, index: u32) {
let baseline = sbox.snapshot().unwrap();
let original: u64 = sbox.call("ReadMSR", index).unwrap();
let jump = original.wrapping_add(1 << 60);
sbox.call::<()>("WriteMSR", (index, jump)).unwrap();
let written: u64 = sbox.call("ReadMSR", index).unwrap();
assert!(
written >= jump / 2,
"0x{index:X}: guest write did not stick"
);
sbox.restore(baseline).unwrap();
let restored: u64 = sbox.call("ReadMSR", index).unwrap();
assert!(
restored < jump / 2,
"0x{index:X}: restore did not pull the counter below the guest-written jump"
);
}
#[cfg(target_arch = "x86_64")]
fn assert_msr_write_does_not_survive_restore(
sbox: &mut MultiUseSandbox,
msr: u32,
sentinel: u64,
) {
let baseline = sbox.snapshot().unwrap();
let original: u64 = match sbox.call("ReadMSR", msr) {
Ok(v) => v,
Err(_) => {
assert!(
sbox.status().is_poisoned(),
"0x{msr:X}: a faulting RDMSR should poison the sandbox"
);
sbox.restore(baseline).unwrap();
return;
}
};
assert_ne!(
original, sentinel,
"0x{msr:X}: sentinel must differ from baseline"
);
if sbox.call::<()>("WriteMSR", (msr, sentinel)).is_err() {
assert!(
sbox.status().is_poisoned(),
"0x{msr:X}: a faulting WRMSR should poison the sandbox"
);
sbox.restore(baseline).unwrap();
return;
}
sbox.restore(baseline).unwrap();
let after: u64 = sbox.call("ReadMSR", msr).unwrap();
assert_eq!(
after, original,
"0x{msr:X}: MSR leaked across restore (expected 0x{original:X}, got 0x{after:X})"
);
}
#[test]
#[ignore = "slow host-dependent hardware MSR audit"]
#[cfg(target_arch = "x86_64")]
fn test_no_msr_leaks_across_restore_full_window_sweep() {
const FREE_RUNNING: &[u32] = &[
0x10, 0xE7, 0xE8, ];
#[cfg(kvm)]
if matches!(
crate::hypervisor::virtual_machine::get_available_hypervisor(),
Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm)
) {
return;
}
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
let baseline = sbox.snapshot().unwrap();
let mut readable = 0usize;
let mut exercised: Vec<u32> = Vec::new();
let mut read_only: Vec<u32> = Vec::new();
let mut masked_only: Vec<u32> = Vec::new();
let mut free_running_leaked: Vec<u32> = Vec::new();
let low = 0x0000_0000u32..=0x0000_1FFF;
let hyperv_synthetic = 0x4000_0000u32..=0x4000_1FFF;
let extended = 0xC000_0000u32..=0xC001_FFFF;
let windows = low.chain(hyperv_synthetic).chain(extended);
for msr in windows {
let original: u64 = match sbox.call("ReadMSR", msr) {
Ok(v) => v,
Err(_) => {
sbox.restore(baseline.clone()).unwrap();
continue;
}
};
readable += 1;
if FREE_RUNNING.contains(&msr) {
let jump = original.wrapping_add(1 << 60);
if sbox.call::<()>("WriteMSR", (msr, jump)).is_err() {
sbox.restore(baseline.clone()).unwrap();
read_only.push(msr);
continue;
}
let planted = match sbox.call::<u64>("ReadMSR", msr) {
Ok(v) => v,
Err(_) => {
sbox.restore(baseline.clone()).unwrap();
masked_only.push(msr);
continue;
}
};
if planted < jump / 2 {
sbox.restore(baseline.clone()).unwrap();
masked_only.push(msr);
continue;
}
sbox.restore(baseline.clone()).unwrap();
let after: u64 = sbox.call("ReadMSR", msr).unwrap();
if after < jump / 2 {
exercised.push(msr);
} else {
free_running_leaked.push(msr);
}
continue;
}
let candidates = [
original ^ 0x55,
original ^ 0x1,
original ^ (1 << 12),
original ^ (1 << 20),
original ^ (1 << 32),
original.wrapping_add(1),
0,
];
let mut planted = false;
let mut saw_write = false;
for cand in candidates {
if cand == original {
continue;
}
if sbox.call::<()>("WriteMSR", (msr, cand)).is_err() {
sbox.restore(baseline.clone()).unwrap();
continue;
}
saw_write = true;
match sbox.call::<u64>("ReadMSR", msr) {
Ok(v) if v != original => {
planted = true;
break;
}
_ => {
sbox.restore(baseline.clone()).unwrap();
}
}
}
if planted {
sbox.restore(baseline.clone()).unwrap();
match sbox.call::<u64>("ReadMSR", msr) {
Ok(after) => assert_eq!(
after, original,
"0x{msr:X}: a guest MSR write leaked across restore \
(expected 0x{original:X}, got 0x{after:X})"
),
Err(e) => panic!("0x{msr:X}: read-back after restore failed: {e:?}"),
}
exercised.push(msr);
} else if saw_write {
masked_only.push(msr);
} else {
read_only.push(msr);
}
}
let fmt = |v: &[u32]| {
v.iter()
.map(|m| format!("0x{m:X}"))
.collect::<Vec<_>>()
.join(", ")
};
eprintln!(
"full-window MSR sweep: readable={readable} exercised={} masked_only={} read_only={}",
exercised.len(),
masked_only.len(),
read_only.len()
);
eprintln!(" exercised: [{}]", fmt(&exercised));
eprintln!(" masked_only: [{}]", fmt(&masked_only));
eprintln!(" read_only: [{}]", fmt(&read_only));
eprintln!(" free_running_leaked: [{}]", fmt(&free_running_leaked));
assert!(
free_running_leaked.is_empty(),
"free-running MSRs not reset across restore on this backend: [{}]",
fmt(&free_running_leaked)
);
assert!(
!exercised.is_empty(),
"sweep was vacuous: no guest MSR write ever retained a value that restore \
then rolled back, so the rollback path was never exercised"
);
}
#[test]
#[cfg(all(any(mshv3, target_os = "windows"), target_arch = "x86_64"))]
fn active_ssp_does_not_leak_across_restore() {
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
if !sbox.call::<bool>("CetShadowStackSupported", ()).unwrap() {
return;
}
let baseline = sbox.snapshot().unwrap();
let original: u64 = sbox.call("ReadActiveSsp", ()).unwrap();
let mutated: u64 = sbox.call("IncrementActiveSsp", ()).unwrap();
assert_ne!(mutated, original, "guest did not change active SSP");
let seen: u64 = sbox.call("ReadActiveSsp", ()).unwrap();
assert_eq!(seen, mutated, "guest did not observe its own SSP mutation");
sbox.restore(baseline).unwrap();
let after: u64 = sbox.call("ReadActiveSsp", ()).unwrap();
assert_eq!(
after, original,
"active SSP leaked across restore (original=0x{original:X}, mutated=0x{mutated:X}, after=0x{after:X})"
);
}
#[test]
#[cfg(all(kvm, target_arch = "x86_64"))]
fn kvm_does_not_expose_cet_to_guest() {
use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor};
if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) {
return;
}
let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.build()
.unwrap();
assert!(
!sbox.call::<bool>("CetShadowStackSupported", ()).unwrap(),
"KVM guest CPUID exposes CET shadow stacks"
);
let err = SandboxBuilder::from_file(simple_guest_as_pathbuf())
.guest_msrs(&[MSR_S_CET])
.unwrap()
.build()
.expect_err("allowing IA32_S_CET must be rejected when CET is hidden");
assert_msr_not_declarable(&err, MSR_S_CET);
}
}
mod from_snapshot {
use std::sync::Arc;
use hyperlight_testing::simple_guest_as_pathbuf;
use crate::func::Registerable;
use crate::sandbox::SandboxConfiguration;
use crate::sandbox::snapshot::Snapshot;
use crate::{GuestBinary, HostFunctions, HyperlightError, MultiUseSandbox, SandboxBuilder};
fn make_sandbox() -> MultiUseSandbox {
let path = simple_guest_as_pathbuf();
SandboxBuilder::from_file(path).build().unwrap()
}
fn make_sandbox_with_add() -> MultiUseSandbox {
let path = simple_guest_as_pathbuf();
SandboxBuilder::from_file(path)
.host_function("Add", |a: i32, b: i32| a + b)
.build()
.unwrap()
}
fn host_funcs_with_matching_add() -> HostFunctions {
let mut hf = HostFunctions::default();
hf.register_host_function("Add", |a: i32, b: i32| Ok(a + b))
.unwrap();
hf
}
#[test]
fn round_trip_running_sandbox() {
let mut sbox = make_sandbox();
sbox.call::<i32>("AddToStatic", 11i32).unwrap();
let snapshot = sbox.snapshot().unwrap();
let mut sbox2 = SandboxBuilder::from_snapshot(snapshot).build().unwrap();
assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 11);
let echoed: String = sbox2.call("Echo", "hi".to_string()).unwrap();
assert_eq!(echoed, "hi");
}
#[test]
fn round_trip_pre_init_snapshot() {
let path = simple_guest_as_pathbuf();
let snap =
Snapshot::from_env(GuestBinary::FilePath(path), SandboxConfiguration::default())
.unwrap();
let mut sbox = SandboxBuilder::from_snapshot(Arc::new(snap))
.build()
.unwrap();
assert_eq!(sbox.call::<i32>("GetStatic", ()).unwrap(), 0);
}
#[test]
fn arc_clone_isolation_and_restore_compat() {
let mut sbox = make_sandbox();
sbox.call::<i32>("AddToStatic", 3i32).unwrap();
let snapshot = sbox.snapshot().unwrap();
let mut a = SandboxBuilder::from_snapshot(snapshot.clone())
.build()
.unwrap();
let mut b = SandboxBuilder::from_snapshot(snapshot.clone())
.build()
.unwrap();
assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 3);
assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
a.call::<i32>("AddToStatic", 7i32).unwrap();
assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 10);
assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
a.restore(snapshot.clone()).unwrap();
b.restore(snapshot).unwrap();
assert_eq!(a.call::<i32>("GetStatic", ()).unwrap(), 3);
assert_eq!(b.call::<i32>("GetStatic", ()).unwrap(), 3);
}
#[test]
fn accepts_matching_host_functions() {
let mut sbox = make_sandbox_with_add();
sbox.call::<i32>("AddToStatic", 5i32).unwrap();
let snap = sbox.snapshot().unwrap();
let mut sbox2 = SandboxBuilder::from_snapshot(snap)
.host_functions(host_funcs_with_matching_add())
.build()
.unwrap();
assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 5);
}
#[test]
fn rejects_missing_host_function() {
let mut sbox = make_sandbox_with_add();
let snap = sbox.snapshot().unwrap();
let err = SandboxBuilder::from_snapshot(snap)
.build()
.expect_err("missing `Add` must be rejected");
assert!(
matches!(
&err,
HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
if missing.iter().any(|n| n == "Add") && signature_mismatches.is_empty()
),
"got: {:?}",
err
);
}
#[test]
fn restore_rejects_missing_host_function() {
let mut sbox_with_add = make_sandbox_with_add();
let snap = sbox_with_add.snapshot().unwrap();
let mut sbox_without_add = make_sandbox();
let err = sbox_without_add
.restore(snap)
.expect_err("missing `Add` must be rejected on restore");
assert!(
matches!(
&err,
HyperlightError::SnapshotHostFunctionMismatch { missing, .. }
if missing.iter().any(|n| n == "Add")
),
"got: {:?}",
err
);
}
#[test]
fn restore_rejects_signature_mismatch() {
let mut sbox_with_add = make_sandbox_with_add();
let snap = sbox_with_add.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut sbox_wrong_add = SandboxBuilder::from_file(path)
.host_function("Add", |a: String, b: String| format!("{a}{b}"))
.build()
.unwrap();
let err = sbox_wrong_add
.restore(snap)
.expect_err("signature mismatch on `Add` must be rejected on restore");
assert!(
matches!(
&err,
HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
if missing.is_empty() && signature_mismatches.iter().any(|s| s.contains("Add"))
),
"got: {:?}",
err
);
}
#[test]
fn restore_across_sandboxes_with_superset_host_funcs() {
let mut source = make_sandbox_with_add();
source.call::<i32>("AddToStatic", 17i32).unwrap();
let snap = source.snapshot().unwrap();
let path = simple_guest_as_pathbuf();
let mut target = SandboxBuilder::from_file(path)
.host_function("Add", |a: i32, b: i32| a + b)
.host_function("Mul", |a: i32, b: i32| a * b)
.build()
.unwrap();
target.restore(snap).unwrap();
assert_eq!(target.call::<i32>("GetStatic", ()).unwrap(), 17);
}
#[test]
fn rejects_signature_mismatch() {
let mut sbox = make_sandbox_with_add();
let snap = sbox.snapshot().unwrap();
let mut hf = HostFunctions::default();
hf.register_host_function("Add", |a: String, b: String| Ok(format!("{a}{b}")))
.unwrap();
let err = SandboxBuilder::from_snapshot(snap)
.host_functions(hf)
.build()
.expect_err("signature mismatch on `Add` must be rejected");
assert!(
matches!(
&err,
HyperlightError::SnapshotHostFunctionMismatch { missing, signature_mismatches }
if missing.is_empty() && signature_mismatches.iter().any(|s| s.contains("Add"))
),
"got: {:?}",
err
);
}
#[test]
fn accepts_extra_host_functions() {
let mut sbox = make_sandbox_with_add();
sbox.call::<i32>("AddToStatic", 9i32).unwrap();
let snap = sbox.snapshot().unwrap();
let mut hf = host_funcs_with_matching_add();
hf.register_host_function("Mul", |a: i32, b: i32| Ok(a * b))
.unwrap();
let mut sbox2 = SandboxBuilder::from_snapshot(snap)
.host_functions(hf)
.build()
.unwrap();
assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 9);
}
#[test]
fn re_snapshot_after_from_snapshot() {
let mut sbox = make_sandbox();
sbox.call::<i32>("AddToStatic", 4i32).unwrap();
let snap1 = sbox.snapshot().unwrap();
let mut sbox2 = SandboxBuilder::from_snapshot(snap1).build().unwrap();
sbox2.call::<i32>("AddToStatic", 6i32).unwrap();
let snap2 = sbox2.snapshot().unwrap();
sbox2.call::<i32>("AddToStatic", 100i32).unwrap();
assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 110);
sbox2.restore(snap2.clone()).unwrap();
assert_eq!(sbox2.call::<i32>("GetStatic", ()).unwrap(), 10);
let mut sbox3 = SandboxBuilder::from_snapshot(snap2).build().unwrap();
assert_eq!(sbox3.call::<i32>("GetStatic", ()).unwrap(), 10);
}
#[test]
fn supplied_host_function_is_callable() {
let path = simple_guest_as_pathbuf();
let mut sbox = SandboxBuilder::from_file(path)
.host_function("Echo42", || 1i64)
.build()
.unwrap();
let snap = sbox.snapshot().unwrap();
let mut hf = HostFunctions::default();
hf.register_host_function("Echo42", || Ok(42i64)).unwrap();
let mut sbox2 = SandboxBuilder::from_snapshot(snap)
.host_functions(hf)
.build()
.unwrap();
let got: i64 = sbox2
.call(
"CallGivenParamlessHostFuncThatReturnsI64",
"Echo42".to_string(),
)
.unwrap();
assert_eq!(got, 42);
}
#[test]
fn pre_init_snapshot_accepts_arbitrary_host_functions() {
let path = simple_guest_as_pathbuf();
let snap =
Snapshot::from_env(GuestBinary::FilePath(path), SandboxConfiguration::default())
.unwrap();
let mut hf = HostFunctions::default();
hf.register_host_function("Unrelated", |a: i32| Ok(a + 1))
.unwrap();
let mut sbox = SandboxBuilder::from_snapshot(Arc::new(snap))
.host_functions(hf)
.build()
.unwrap();
assert_eq!(sbox.call::<i32>("GetStatic", ()).unwrap(), 0);
}
#[test]
fn snapshot_generation_propagates() {
let mut sbox = make_sandbox();
sbox.call::<i32>("AddToStatic", 1i32).unwrap();
let snap1 = sbox.snapshot().unwrap();
let gen1 = snap1.snapshot_generation();
sbox.call::<i32>("AddToStatic", 1i32).unwrap();
let snap2 = sbox.snapshot().unwrap();
let gen2 = snap2.snapshot_generation();
assert_eq!(gen2, gen1 + 1);
let mut sbox2 = SandboxBuilder::from_snapshot(snap2).build().unwrap();
sbox2.call::<i32>("AddToStatic", 1i32).unwrap();
let snap3 = sbox2.snapshot().unwrap();
assert_eq!(snap3.snapshot_generation(), gen2 + 1);
}
#[test]
fn late_register_invalidates_snapshot_cache() {
let mut sbox = make_sandbox();
let _ = sbox.snapshot().unwrap();
sbox.register_host_function("Echo42", || Ok(42i64)).unwrap();
let snap = sbox.snapshot().unwrap();
let err = SandboxBuilder::from_snapshot(snap)
.build()
.expect_err("late-registered `Echo42` must be required by the new snapshot");
let msg = format!("{}", err);
assert!(msg.contains("Echo42"), "got: {}", msg);
}
}
}