diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2c768d8..e9f22cfc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added ### Changed -* **Breaking:** Snapshots use ABI 7 and config schema v4. Existing snapshots +* **Breaking:** Snapshots use ABI 8 and config schema v5. Existing snapshots must be regenerated. * **Breaking:** `PtRootFinder` receives a `SnapshotMemoryReader` and returns a `Result>`. Read snapshot bytes with `snapshot.read_exact(gpa, buffer)?` @@ -28,8 +28,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). creation. * Use `talc` as the guest heap allocator. Guest init writes only the start and end of the heap, which speeds up sandbox creation. +* **Breaking:** The guest heap has no fixed size. The guest maps heap pages + from scratch on first touch. Scratch backs heap pages written since the last + snapshot, shared with the stack, page tables, and transport. Allocations + succeed beyond the scratch size, and the guest aborts with `MallocFailed` + when a write needs a page and scratch is full. Snapshots no longer contain a + zero-filled heap region, which makes them smaller and faster to create, + save, and load. ### Removed +* **Breaking:** `SandboxConfiguration::set_heap_size`, + `SandboxConfiguration::DEFAULT_HEAP_SIZE`, `SandboxBuilder::heap_size`, and + `SandboxBuilder::get_heap_size`. ### Fixed diff --git a/docs/hyperlight-execution-details.md b/docs/hyperlight-execution-details.md index 2c96a2483..3bf391a21 100644 --- a/docs/hyperlight-execution-details.md +++ b/docs/hyperlight-execution-details.md @@ -27,7 +27,7 @@ With this background in mind, it's well worth focusing on the "lifecycle" of a V At the highest level, Hyperlight takes roughly the following steps to create and run arbitrary code inside a VM: 1. Loads a specially built, statically linked binary (currently, the [PE](https://en.wikipedia.org/wiki/Portable_Executable) and [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) executable formats are supported) into memory. This is the code that is executed inside a virtual machine. -2. Allocates additional memory regions, for example stack and heap for the guest, as well as some regions used for communication between the host and the guest. +2. Allocates a scratch memory region. The guest maps its stack and heap from scratch on first touch. Scratch also holds the regions used for communication between the host and the guest. 3. Creates a Virtual Machine and maps shared memory into it 5. Create one virtual CPU (vCPU) within the newly created VM 6. Write appropriate values to the new vCPUs registers. diff --git a/docs/snapshot-oci-format.md b/docs/snapshot-oci-format.md index 479e44341..279eb3b39 100644 --- a/docs/snapshot-oci-format.md +++ b/docs/snapshot-oci-format.md @@ -32,7 +32,7 @@ Four blob kinds per tag: * **manifest** (`application/vnd.oci.image.manifest.v1+json`). Tiny JSON pointer record selected via `index.json`. References one config, the snapshot blobs, and one transport layer by digest. -* **config** (`application/vnd.hyperlight.snapshot.config.v4+json`). The +* **config** (`application/vnd.hyperlight.snapshot.config.v5+json`). The snapshot descriptor: arch, hypervisor, CPU vendor, ABI version, resume address and captured registers, memory and transport layout, registered host functions, the start address, data length, and page-table diff --git a/docs/snapshot-versioning.md b/docs/snapshot-versioning.md index c704c311c..74f7232fc 100644 --- a/docs/snapshot-versioning.md +++ b/docs/snapshot-versioning.md @@ -27,8 +27,8 @@ A snapshot carries four independently evolvable version markers: (`application/vnd.hyperlight.snapshot.transport.v1`), aliased as `MT_TRANSPORT_CURRENT`. This is the binary encoding of canonical virtqueue state stored outside the memory layer. -* **Config schema**, `MT_CONFIG_V4` - (`application/vnd.hyperlight.snapshot.config.v4+json`), aliased as +* **Config schema**, `MT_CONFIG_V5` + (`application/vnd.hyperlight.snapshot.config.v5+json`), aliased as `MT_CONFIG_CURRENT`. This is the JSON shape of the config blob: field names, types, required vs optional, the descriptors the loader needs in order to reconstruct the sandbox (memory sizes, buffer @@ -36,11 +36,13 @@ A snapshot carries four independently evolvable version markers: changing its type, or adding a required field is a schema change and bumps this constant. Version 4 describes snapshot blobs, the blob with the page tables, and transport geometry, and requires a transport - layer. Config v1, v2, and v3 are incompatible with the current ABI. + layer. Version 5 drops the heap size. Config v1 to v4 are incompatible + with the current ABI. ABI 6 requires checkpoint readiness after guest initialization and permits H2G prefill to omit retained slots. ABI 7 stores snapshot memory as -layers. Earlier snapshots must be regenerated. +layers. ABI 8 removes the heap region from snapshot memory. Earlier +snapshots must be regenerated. The `OCI_LAYOUT_VERSION` constant is pinned by the OCI image-layout spec at `1.0.0`. diff --git a/fuzz/fuzz_targets/host_call.rs b/fuzz/fuzz_targets/host_call.rs index f7aa8d30b..01bfe8ad3 100644 --- a/fuzz/fuzz_targets/host_call.rs +++ b/fuzz/fuzz_targets/host_call.rs @@ -19,7 +19,6 @@ static SANDBOX: OnceLock> = OnceLock::new(); fuzz_target!( init: { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(512 * 1024); cfg.set_g2h_pool_pages(16); cfg.set_h2g_pool_pages(16); cfg.set_scratch_size(512 * 1024); diff --git a/src/hyperlight_common/src/mem.rs b/src/hyperlight_common/src/mem.rs index 2f89ba2b7..c41742e69 100644 --- a/src/hyperlight_common/src/mem.rs +++ b/src/hyperlight_common/src/mem.rs @@ -15,7 +15,8 @@ pub struct GuestMemoryRegion { #[repr(C)] pub struct HyperlightPEB { pub init_data: GuestMemoryRegion, - pub guest_heap: GuestMemoryRegion, + /// Nonzero if guest heap pages are mapped executable. + pub heap_executable: u64, } #[cfg(test)] @@ -29,10 +30,7 @@ mod tests { size: 0x1111, ptr: 0x2222, }, - guest_heap: GuestMemoryRegion { - size: 0x3333, - ptr: 0x4444, - }, + heap_executable: 1, }; let bytes = bytemuck::bytes_of(&peb); let peb2 = *bytemuck::from_bytes::(bytes); diff --git a/src/hyperlight_guest/src/arch/aarch64/layout.rs b/src/hyperlight_guest/src/arch/aarch64/layout.rs index 87bea9356..5530b6f6a 100644 --- a/src/hyperlight_guest/src/arch/aarch64/layout.rs +++ b/src/hyperlight_guest/src/arch/aarch64/layout.rs @@ -5,6 +5,11 @@ pub const MAIN_STACK_TOP_GVA: u64 = 0x0000_ff00_0000_0000; pub const MAIN_STACK_LIMIT_GVA: u64 = 0x0000_fe00_0000_0000; +/// Guest heap. Pages are mapped from scratch on first touch. The range +/// is 512 GiB aligned so it sits under one level 0 table entry. +pub const HEAP_GVA_START: u64 = 0x0000_fd80_0000_0000; +pub const HEAP_GVA_END: u64 = 0x0000_fe00_0000_0000; + pub fn scratch_size() -> u64 { let addr = crate::layout::scratch_size_gva(); unsafe { addr.read_volatile() } diff --git a/src/hyperlight_guest/src/arch/amd64/layout.rs b/src/hyperlight_guest/src/arch/amd64/layout.rs index 146c124c6..81577ca59 100644 --- a/src/hyperlight_guest/src/arch/amd64/layout.rs +++ b/src/hyperlight_guest/src/arch/amd64/layout.rs @@ -13,6 +13,11 @@ pub const MAIN_STACK_TOP_GVA: u64 = 0xffff_ff00_0000_0000; pub const MAIN_STACK_LIMIT_GVA: u64 = 0xffff_fe00_0000_0000; +/// Guest heap. Pages are mapped from scratch on first touch. The range +/// is 512 GiB aligned so it sits under one PML4 entry. +pub const HEAP_GVA_START: u64 = 0xffff_fd80_0000_0000; +pub const HEAP_GVA_END: u64 = 0xffff_fe00_0000_0000; + pub fn scratch_size() -> u64 { let addr = crate::layout::scratch_size_gva(); let x: u64; diff --git a/src/hyperlight_guest/src/layout.rs b/src/hyperlight_guest/src/layout.rs index 0b340e211..9bc42ea6f 100644 --- a/src/hyperlight_guest/src/layout.rs +++ b/src/hyperlight_guest/src/layout.rs @@ -5,7 +5,14 @@ #[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/layout.rs")] mod arch; -pub use arch::{MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA}; +pub use arch::{HEAP_GVA_END, HEAP_GVA_START, MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA}; + +const _: () = assert!( + hyperlight_common::layout::VIRTQ_BUFFER_GVA_END <= HEAP_GVA_START + && HEAP_GVA_START < HEAP_GVA_END + && HEAP_GVA_END <= MAIN_STACK_LIMIT_GVA, + "heap overlaps the transport aliases or the stack" +); fn scratch_top_gva(offset: u64) -> *mut u64 { (hyperlight_common::layout::SCRATCH_TOP_GVA as u64 - offset + 1) as *mut u64 diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs index 83d9802df..a9df10de8 100644 --- a/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs @@ -9,16 +9,23 @@ use hyperlight_common::arch::exn::{ use hyperlight_common::vmem::{BasicMapping, Mapping, MappingKind, PAGE_SIZE, PhysAddr, VirtAddr}; use hyperlight_guest::error::ErrorCode; use hyperlight_guest::exit::write_abort; -use hyperlight_guest::layout::{MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA}; +use hyperlight_guest::layout::{ + HEAP_GVA_END, HEAP_GVA_START, MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA, +}; use super::super::{mrs, msr}; use super::types::*; use crate::HyperlightAbortWriter; -fn handle_stack_fault(far: u64) { - // TODO: perhaps we should have a sanity check that the - // stack grows only one page at a time, which should be - // ensured by our stack probing discipline? +/// Maps a fresh scratch page at `far` in the stack or heap range. +/// +/// # Safety +/// `far` must be in the stack or heap range and must not be mapped. No +/// other page table operation may run concurrently. +unsafe fn handle_stack_or_heap_fault(far: u64, executable: bool) { + // SAFETY: the scratch allocator is set up before any exception can + // occur, and the masked address is page aligned. The caller upholds + // the rest of the `map_region` contract. unsafe { let new_page = hyperlight_guest::prim_alloc::alloc_phys_pages(1); crate::paging::map_region( @@ -28,7 +35,7 @@ fn handle_stack_fault(far: u64) { MappingKind::Basic(BasicMapping { readable: true, writable: true, - executable: false, + executable, }), ); // We don't use crate::barrier::first_valid_same_ctx, because @@ -90,7 +97,20 @@ fn handle_internal_fault(exn: Exception, far: u64, orig_mapping: Option .. }) => { if (MAIN_STACK_LIMIT_GVA..MAIN_STACK_TOP_GVA).contains(&far) { - handle_stack_fault(far); + // TODO: perhaps we should have a sanity check that the + // stack grows only one page at a time, which should be + // ensured by our stack probing discipline? + // SAFETY: a translation fault in the stack region means + // the page is unmapped, and exception handling is the + // only page table writer. + unsafe { handle_stack_or_heap_fault(far, false) }; + true + } else if (HEAP_GVA_START..HEAP_GVA_END).contains(&far) { + let executable = crate::HEAP_EXECUTABLE.load(Ordering::Relaxed); + // SAFETY: a translation fault in the heap region means + // the page is unmapped, and exception handling is the + // only page table writer. + unsafe { handle_stack_or_heap_fault(far, executable) }; true } else if let Some( m @ Mapping { diff --git a/src/hyperlight_guest_bin/src/arch/amd64/exception/handle.rs b/src/hyperlight_guest_bin/src/arch/amd64/exception/handle.rs index 78765bf4d..b6ce9ef91 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/exception/handle.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/exception/handle.rs @@ -7,7 +7,9 @@ use core::sync::atomic::Ordering; use hyperlight_common::outb::Exception; use hyperlight_common::vmem::{BasicMapping, MappingKind, PAGE_SIZE, PhysAddr, VirtAddr}; use hyperlight_guest::exit::write_abort; -use hyperlight_guest::layout::{MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA}; +use hyperlight_guest::layout::{ + HEAP_GVA_END, HEAP_GVA_START, MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA, +}; use super::super::context::Context; use super::super::machine::ExceptionInfo; @@ -46,10 +48,15 @@ pub type ExceptionHandler = fn( page_fault_address: u64, ) -> bool; -fn handle_stack_pagefault(gva: u64) { - // TODO: perhaps we should have a sanity check that the - // stack grows only one page at a time, which should be - // ensured by our stack probing discipline? +/// Maps a fresh scratch page at `gva` in the stack or heap range. +/// +/// # Safety +/// `gva` must be in the stack or heap range and must not be mapped. No +/// other page table operation may run concurrently. +unsafe fn handle_stack_or_heap_pagefault(gva: u64, executable: bool) { + // SAFETY: the scratch allocator is set up before any exception can + // occur, and the masked address is page aligned. The caller upholds + // the rest of the `map_region` contract. unsafe { let new_page = hyperlight_guest::prim_alloc::alloc_phys_pages(1); crate::paging::map_region( @@ -59,7 +66,7 @@ fn handle_stack_pagefault(gva: u64) { MappingKind::Basic(BasicMapping { readable: true, writable: true, - executable: false, + executable, }), ); // This is mapping an entry for the first time, so we don't @@ -113,18 +120,32 @@ fn try_handle_internal_pagefault( ) -> bool { let error_code = unsafe { (&raw const (*exn_info).error_code).read_volatile() }; - let mut orig_mappings = crate::paging::virt_to_phys(gva); - let orig_mapping = orig_mappings.next(); - let present = (error_code & (1 << 0)) != 0; // bit 0 is P if !present { - // If the fault was caused by a not-present page, check if we - // should populate it with a stack page + // The stack and heap ranges need no page table walk. if (MAIN_STACK_LIMIT_GVA..MAIN_STACK_TOP_GVA).contains(&gva) { - handle_stack_pagefault(gva); + // TODO: perhaps we should have a sanity check that the + // stack grows only one page at a time, which should be + // ensured by our stack probing discipline? + // SAFETY: the fault is not-present in the stack region, + // and exception handling is the only page table writer. + unsafe { handle_stack_or_heap_pagefault(gva, false) }; + return true; + } + if (HEAP_GVA_START..HEAP_GVA_END).contains(&gva) { + let executable = crate::HEAP_EXECUTABLE.load(Ordering::Relaxed); + // SAFETY: the fault is not-present in the heap region, and + // exception handling is the only page table writer. + unsafe { handle_stack_or_heap_pagefault(gva, executable) }; return true; } - // Or, check if it is in any zero-init region + } + + let mut orig_mappings = crate::paging::virt_to_phys(gva); + let orig_mapping = orig_mappings.next(); + + if !present { + // Check if it is in any zero-init region if let Some(mapping) = orig_mapping && let MappingKind::ZeroInit(bm) = mapping.kind { diff --git a/src/hyperlight_guest_bin/src/arch/amd64/layout.rs b/src/hyperlight_guest_bin/src/arch/amd64/layout.rs index 03c90043c..9a1a6d0a0 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/layout.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/layout.rs @@ -15,3 +15,8 @@ const _: () = assert!( hyperlight_common::layout::VIRTQ_BUFFER_GVA_END <= PROC_CONTROL_GVA, "transport aliases overlap processor control" ); +const _: () = assert!( + PROC_CONTROL_GVA + 2 * hyperlight_common::vmem::PAGE_SIZE as u64 + <= hyperlight_guest::layout::HEAP_GVA_START, + "processor control overlaps the heap" +); diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index c253fac20..22b064346 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -6,6 +6,7 @@ extern crate alloc; use core::fmt::Write; +use core::sync::atomic::{AtomicBool, Ordering}; use arch::dispatch::dispatch_function; use guest_function::register::GuestFunctionRegister; @@ -17,6 +18,7 @@ use hyperlight_common::mem::HyperlightPEB; use hyperlight_common::outb::OutBAction; use hyperlight_guest::exit::write_abort; use hyperlight_guest::guest_handle::handle::GuestHandle; +use hyperlight_guest::layout::{HEAP_GVA_END, HEAP_GVA_START}; use talc::TalcLock; use talc::source::Manual; @@ -115,6 +117,9 @@ pub(crate) static HEAP_ALLOCATOR: GuestHeap = GuestHeap::new(Manual); #[global_allocator] pub(crate) static HEAP_ALLOCATOR: ProfiledLockedHeap = ProfiledLockedHeap(GuestHeap::new(Manual)); +/// Whether heap pages are mapped executable on first touch. +pub(crate) static HEAP_EXECUTABLE: AtomicBool = AtomicBool::new(false); + pub static mut GUEST_HANDLE: GuestHandle = GuestHandle::new(); pub(crate) static mut REGISTERED_GUEST_FUNCTIONS: GuestFunctionRegister = GuestFunctionRegister::new(); @@ -264,18 +269,21 @@ pub(crate) extern "C" fn generic_init( #[allow(static_mut_refs)] let peb_ptr = GUEST_HANDLE.peb().unwrap(); - let heap_start = (*peb_ptr).guest_heap.ptr as usize; - let heap_size = (*peb_ptr).guest_heap.size as usize; + HEAP_EXECUTABLE.store((*peb_ptr).heap_executable != 0, Ordering::Relaxed); #[cfg(not(all(feature = "mem_profile", target_arch = "x86_64")))] let heap_allocator = &HEAP_ALLOCATOR; #[cfg(all(feature = "mem_profile", target_arch = "x86_64"))] let heap_allocator = &HEAP_ALLOCATOR.0; - // SAFETY: the host reserves the PEB heap range for the guest heap, and - // init claims it once. Only this allocator writes to it. + // SAFETY: nothing else uses the heap range, and init claims it + // once. The fault handlers, installed before this point, map + // each page from scratch on first touch. heap_allocator .try_lock() .expect("Failed to access HEAP_ALLOCATOR") - .claim(heap_start as *mut u8, heap_size) + .claim( + HEAP_GVA_START as *mut u8, + (HEAP_GVA_END - HEAP_GVA_START) as usize, + ) .expect("Failed to claim guest heap"); peb_ptr }; diff --git a/src/hyperlight_host/benches/benchmarks.rs b/src/hyperlight_host/benches/benchmarks.rs index 6d161158a..c25dd6fcc 100644 --- a/src/hyperlight_host/benches/benchmarks.rs +++ b/src/hyperlight_host/benches/benchmarks.rs @@ -18,32 +18,36 @@ use hyperlight_host::func::Registerable; use hyperlight_host::mem::shared_mem::ExclusiveSharedMemory; use hyperlight_host::sandbox::{Sandbox, SandboxConfiguration, UninitializedSandbox}; use hyperlight_host::{GuestBinary, HostFunctions, SandboxBuilder}; -use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE}; +use hyperlight_testing::sandbox_sizes::{ + LARGE_SCRATCH_SIZE, MEDIUM_SCRATCH_SIZE, SMALL_SCRATCH_SIZE, +}; use hyperlight_testing::{c_simple_guest_as_pathbuf, simple_guest_as_pathbuf}; -/// Sandbox heap sizes and scratch budgets for benchmarking. -/// Larger heaps need extra scratch for page tables. +/// Sandbox scratch sizes for benchmarking. #[derive(Clone, Copy)] enum SandboxSize { /// Default configuration (uses hyperlight defaults) Default, - /// Small heap: 8 MB + /// Small scratch: 8 MiB Small, - /// Medium heap: 64 MB + /// Medium scratch: 64 MiB Medium, - /// Large heap: 256 MB + /// Large scratch: 256 MiB Large, } impl SandboxSize { /// Returns a builder for the simple guest, configured for this sandbox size. fn builder(&self) -> SandboxBuilder { - let builder = SandboxBuilder::from_file(simple_guest_as_pathbuf()); + SandboxBuilder::from_file(simple_guest_as_pathbuf()).scratch_size(self.scratch_size()) + } + + fn scratch_size(&self) -> usize { match self { - Self::Default => builder, - Self::Small => builder.heap_size(SMALL_HEAP_SIZE), - Self::Medium => builder.heap_size(MEDIUM_HEAP_SIZE).scratch_size(0x80000), - Self::Large => builder.heap_size(LARGE_HEAP_SIZE).scratch_size(0x100000), + Self::Default => SandboxConfiguration::DEFAULT_SCRATCH_SIZE, + Self::Small => SMALL_SCRATCH_SIZE, + Self::Medium => MEDIUM_SCRATCH_SIZE, + Self::Large => LARGE_SCRATCH_SIZE, } } @@ -223,6 +227,24 @@ fn bench_guest_call_interrupt_latency(b: &mut criterion::Bencher, size: SandboxS }); } +/// Writes half of scratch to heap the guest has not touched since the last restore. +fn bench_guest_call_heap_first_touch(b: &mut criterion::Bencher, size: SandboxSize) { + let touch_size = (size.scratch_size() / 2) as u64; + let mut sbox = create_multiuse_sandbox_with_size(size); + let snapshot = sbox.snapshot().unwrap(); + + b.iter_custom(|iters| { + let mut total_duration = Duration::ZERO; + for _ in 0..iters { + sbox.restore(snapshot.clone()).unwrap(); + let start = Instant::now(); + sbox.call::<()>("AllocAndWritePattern", touch_size).unwrap(); + total_duration += start.elapsed(); + } + total_duration + }); +} + fn guest_calls_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("guest_calls"); @@ -252,6 +274,13 @@ fn guest_calls_benchmark(c: &mut Criterion) { bench_guest_call_interrupt_latency(b, SandboxSize::Default) }); + // Default scratch is mostly fixed reservations, so half of it does not fit. + for size in [SandboxSize::Small, SandboxSize::Medium, SandboxSize::Large] { + group.bench_function(format!("heap_first_touch/{}", size.name()), |b| { + bench_guest_call_heap_first_touch(b, size) + }); + } + group.finish(); } @@ -480,7 +509,6 @@ fn guest_call_benchmark_large_param(c: &mut Criterion) { let mut config = SandboxConfiguration::default(); config.set_h2g_buffer_size(4 * MIB); config.set_h2g_pool_pages((2 * SIZE + 8 * MIB).div_ceil(PAGE_SIZE)); - config.set_heap_size(SIZE as u64 * 15); config.set_scratch_size(9 * SIZE); let sandbox = UninitializedSandbox::new( diff --git a/src/hyperlight_host/examples/map-file-cow-test/main.rs b/src/hyperlight_host/examples/map-file-cow-test/main.rs index 7a7930acb..d37b9deff 100644 --- a/src/hyperlight_host/examples/map-file-cow-test/main.rs +++ b/src/hyperlight_host/examples/map-file-cow-test/main.rs @@ -21,7 +21,6 @@ use hyperlight_host::SandboxBuilder; fn run_once(test_file: &Path, label: &str) -> hyperlight_host::Result<()> { let mut sandbox = SandboxBuilder::from_file(hyperlight_testing::simple_guest_as_pathbuf()) - .heap_size(4 * 1024 * 1024) .scratch_size(64 * 1024 * 1024) .mapped_file_cow(test_file, 0xC000_0000) .build()?; diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index aee561b47..c24fdbb8a 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -508,8 +508,9 @@ pub(crate) mod tests { )?; // Set up required parameters for initialise - let peb_addr = RawPtr::from(0x1000u64); // Dummy PEB address - let seed = 12345u64; // Random seed + // The dummy guest halts only when it receives these values. + let peb_addr = RawPtr::from(0x230000u64); + let seed = 1234567890u64; let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default())); let guest_max_log_level = Some(tracing_core::LevelFilter::ERROR); diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 443c9c4b3..5ab9fb084 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -12,8 +12,6 @@ //! +-------------------------------------------+ //! | Init Data | (GuestBlob size) //! +-------------------------------------------+ -//! | Guest Heap | -//! +-------------------------------------------+ //! | PEB Struct | (HyperlightPEB size) //! +-------------------------------------------+ //! | Guest Code | @@ -29,8 +27,8 @@ //! - `InitData` - some extra data that can be loaded onto the sandbox during //! initialization. //! -//! - `GuestHeap` - this is a buffer that is used for heap data in the guest. the length -//! of this field is returned by the `heap_size()` method of this struct +//! The guest heap is outside this layout. The guest maps heap pages +//! from scratch on first touch. //! //! There is also a scratch region at the top of physical memory, //! which is mostly laid out as a large undifferentiated blob of @@ -55,7 +53,7 @@ use hyperlight_common::mem::HyperlightPEB; use hyperlight_common::vmem::PAGE_SIZE; use tracing::{Span, instrument}; -use super::memory_region::MemoryRegionType::{Code, Heap, InitData, Peb}; +use super::memory_region::MemoryRegionType::{Code, InitData, Peb}; use super::memory_region::{ DEFAULT_GUEST_BLOB_MEM_FLAGS, GuestMemoryRegion, MemoryRegion_, MemoryRegionFlags, MemoryRegionVecBuilder, @@ -66,8 +64,6 @@ use crate::{Result, new_error}; #[derive(Copy, Clone)] pub(crate) struct SandboxMemoryLayout { - /// The heap size of this sandbox. - heap_size: usize, /// The size of the guest code section. code_size: usize, /// Guest virtual address of the code section. @@ -99,7 +95,6 @@ impl Debug for SandboxMemoryLayout { ) .field("Code Size", &format_args!("{:#x}", self.code_size)) .field("Code GVA", &format_args!("{:#x}", self.code_gva)) - .field("Heap Size", &format_args!("{:#x}", self.heap_size)) .field( "Init Data Size", &format_args!("{:#x}", self.init_data_size), @@ -116,11 +111,7 @@ impl Debug for SandboxMemoryLayout { &format_args!("{:#x}", self.guest_code_offset()), ) .field("PEB Offset", &format_args!("{:#x}", self.peb_offset())) - .field("PEB Address", &format_args!("{:#x}", self.peb_address())); - ff.field( - "Guest Heap Buffer Offset", - &format_args!("{:#x}", self.guest_heap_buffer_offset()), - ) + .field("PEB Address", &format_args!("{:#x}", self.peb_address())) .field( "Init Data Offset", &format_args!("{:#x}", self.init_data_offset()), @@ -141,7 +132,7 @@ impl SandboxMemoryLayout { pub(crate) const BASE_ADDRESS: usize = 0x4000; /// Create a new `SandboxMemoryLayout` with the given - /// `SandboxConfiguration`, code size and stack/heap size. + /// `SandboxConfiguration` and code size. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn new( cfg: SandboxConfiguration, @@ -149,7 +140,6 @@ impl SandboxMemoryLayout { init_data_size: usize, init_data_permissions: Option, ) -> Result { - let heap_size = usize::try_from(cfg.get_heap_size())?; let scratch_size = cfg.get_scratch_size(); if scratch_size > Self::MAX_MEMORY_SIZE { return Err(MemoryRequestTooBig(scratch_size, Self::MAX_MEMORY_SIZE)); @@ -179,7 +169,6 @@ impl SandboxMemoryLayout { } let ret = Self { - heap_size, code_size, code_gva: Self::BASE_ADDRESS, init_data_size, @@ -195,10 +184,6 @@ impl SandboxMemoryLayout { Ok(ret) } - pub(crate) fn heap_size(&self) -> usize { - self.heap_size - } - pub(crate) fn code_size(&self) -> usize { self.code_size } @@ -280,33 +265,9 @@ impl SandboxMemoryLayout { } // PEB - let heap_offset = + let init_data_offset = builder.push_page_aligned(size_of::(), MemoryRegionFlags::READ, Peb); - let expected_heap_offset = TryInto::::try_into(self.guest_heap_buffer_offset())?; - - if heap_offset != expected_heap_offset { - return Err(new_error!( - "Guest Heap offset does not match expected Guest Heap offset expected: {}, actual: {}", - expected_heap_offset, - heap_offset - )); - } - - // heap - #[cfg(feature = "executable_heap")] - let init_data_offset = builder.push_page_aligned( - self.heap_size, - MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE, - Heap, - ); - #[cfg(not(feature = "executable_heap"))] - let init_data_offset = builder.push_page_aligned( - self.heap_size, - MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, - Heap, - ); - let expected_init_data_offset = TryInto::::try_into(self.init_data_offset())?; if init_data_offset != expected_init_data_offset { @@ -423,10 +384,7 @@ impl SandboxMemoryLayout { size: (self.get_unaligned_memory_size() - self.init_data_offset()) as u64, ptr: guest_base + self.init_data_offset() as u64, }, - guest_heap: GuestMemoryRegion { - size: self.heap_size as u64, - ptr: guest_base + self.guest_heap_buffer_offset() as u64, - }, + heap_executable: cfg!(feature = "executable_heap") as u64, }; let offset = self.peb_offset(); @@ -463,14 +421,9 @@ impl SandboxMemoryLayout { Self::BASE_ADDRESS + self.peb_offset() } - /// Offset of the guest heap buffer within the snapshot region. - pub(crate) fn guest_heap_buffer_offset(&self) -> usize { - (self.peb_offset() + size_of::()).next_multiple_of(PAGE_SIZE) - } - /// Offset of the init data section within the snapshot region. pub(crate) fn init_data_offset(&self) -> usize { - (self.guest_heap_buffer_offset() + self.heap_size).next_multiple_of(PAGE_SIZE) + (self.peb_offset() + size_of::()).next_multiple_of(PAGE_SIZE) } /// The code offset is always 0. @@ -544,8 +497,6 @@ mod tests { let peb_and_array = size_of::(); expected_size += peb_and_array.next_multiple_of(PAGE_SIZE); - expected_size += layout.heap_size.next_multiple_of(PAGE_SIZE); - expected_size.next_multiple_of(page_size::get()) } @@ -717,16 +668,17 @@ mod tests { ); let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(0x2000); cfg.set_scratch_size(0x30000); let layout = SandboxMemoryLayout::new(cfg, 0x1000, 0, None).unwrap(); pin_eq!(layout.guest_code_offset(), 0); pin_eq!(layout.peb_offset(), 0x1000); pin_eq!(layout.peb_address(), 0x5000); - pin_eq!(layout.guest_heap_buffer_offset(), 0x2000); - pin_eq!(layout.init_data_offset(), 0x4000); - pin_eq!(layout.get_memory_size().unwrap(), 0x4000); + pin_eq!(layout.init_data_offset(), 0x2000); + pin_eq!( + layout.get_memory_size().unwrap(), + 0x2000_usize.next_multiple_of(page_size::get()) + ); pin_eq!(layout.get_scratch_size(), 0x30000); pin_eq!(layout.get_pt_base_scratch_offset(), 0x15000); @@ -750,18 +702,16 @@ mod tests { // A second snapshot layout keeps the transport prefix fixed // relative to its scratch base. let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(0x5000); cfg.set_scratch_size(0x40000); let layout = SandboxMemoryLayout::new(cfg, 0x3000, 0, None).unwrap(); pin_eq!(layout.guest_code_offset(), 0); pin_eq!(layout.peb_offset(), 0x3000); pin_eq!(layout.peb_address(), 0x7000); - pin_eq!(layout.guest_heap_buffer_offset(), 0x4000); - pin_eq!(layout.init_data_offset(), 0x9000); + pin_eq!(layout.init_data_offset(), 0x4000); pin_eq!( layout.get_memory_size().unwrap(), - 0x9000_usize.next_multiple_of(page_size::get()) + 0x4000_usize.next_multiple_of(page_size::get()) ); pin_eq!(layout.get_scratch_size(), 0x40000); diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 50a5c759f..a962174f6 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -1237,7 +1237,9 @@ mod tests { use hyperlight_common::virtq::DescFlags; use hyperlight_common::vmem::{self, BasicMapping, Mapping, MappingKind, PAGE_SIZE}; #[cfg(target_arch = "x86_64")] - use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE}; + use hyperlight_testing::sandbox_sizes::{ + LARGE_SCRATCH_SIZE, MEDIUM_SCRATCH_SIZE, SMALL_SCRATCH_SIZE, + }; #[cfg(target_arch = "x86_64")] use hyperlight_testing::simple_guest_as_pathbuf; @@ -1540,20 +1542,19 @@ mod tests { fn test_page_tables_for_various_configurations() { let test_cases: [(&str, SandboxConfiguration); 4] = [ ("default", { SandboxConfiguration::default() }), - ("small (8MB heap)", { + ("small (8MiB scratch)", { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(SMALL_HEAP_SIZE); + cfg.set_scratch_size(SMALL_SCRATCH_SIZE); cfg }), - ("medium (64MB heap)", { + ("medium (64MiB scratch)", { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(MEDIUM_HEAP_SIZE); + cfg.set_scratch_size(MEDIUM_SCRATCH_SIZE); cfg }), - ("large (256MB heap)", { + ("large (256MiB scratch)", { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(LARGE_HEAP_SIZE); - cfg.set_scratch_size(0x100000); + cfg.set_scratch_size(LARGE_SCRATCH_SIZE); cfg }), ]; diff --git a/src/hyperlight_host/src/sandbox/builder.rs b/src/hyperlight_host/src/sandbox/builder.rs index 144aa5eec..f00ab6099 100644 --- a/src/hyperlight_host/src/sandbox/builder.rs +++ b/src/hyperlight_host/src/sandbox/builder.rs @@ -55,7 +55,7 @@ impl Source { /// # use hyperlight_host::{Result, SandboxBuilder}; /// # fn example() -> Result<()> { /// let mut sandbox = SandboxBuilder::from_file("guest.bin") -/// .heap_size(1024 * 1024) +/// .scratch_size(1024 * 1024) /// .host_function("Add", |a: i32, b: i32| a + b) /// .build()?; /// @@ -120,7 +120,7 @@ impl SandboxBuilder { /// Build a sandbox restoring the guest from `snapshot`. /// - /// The snapshot's layout overrides heap, scratch, and transport settings. + /// The snapshot's layout overrides scratch and transport settings. pub fn from_snapshot(snapshot: Arc) -> Self { Self::with_source(Source::Snapshot(snapshot)) } @@ -304,20 +304,8 @@ impl SandboxBuilder { } impl SandboxBuilder { - /// Set the guest heap size. A size of 0 selects - /// [`SandboxConfiguration::DEFAULT_HEAP_SIZE`]. - pub fn heap_size(mut self, size: u64) -> Self { - self.cfg.set_heap_size(size); - self - } - - /// The guest heap size, defaulting to - /// [`SandboxConfiguration::DEFAULT_HEAP_SIZE`] when no override is set. - pub fn get_heap_size(&self) -> u64 { - self.cfg.get_heap_size() - } - - /// Set how much writable memory to offer the guest. + /// Set how much writable memory to offer the guest. Scratch backs the + /// guest heap and stack. pub fn scratch_size(mut self, size: usize) -> Self { self.cfg.set_scratch_size(size); self diff --git a/src/hyperlight_host/src/sandbox/config.rs b/src/hyperlight_host/src/sandbox/config.rs index 5ae49a5e4..0f70e2f34 100644 --- a/src/hyperlight_host/src/sandbox/config.rs +++ b/src/hyperlight_host/src/sandbox/config.rs @@ -47,13 +47,6 @@ pub struct SandboxConfiguration { /// Guest gdb debug port #[cfg(gdb)] guest_debug_info: Option, - /// The heap size to use in the guest sandbox. If set to 0, the heap - /// size will be determined from the PE file header - /// - /// Note: this is a C-compatible struct, so even though this optional - /// field should be represented as an `Option`, that type is not - /// FFI-safe, so it cannot be. - heap_size_override: u64, /// Delay between interrupt retries. This duration specifies how long to wait /// between attempts to send signals to the thread running the sandbox's VCPU. /// Multiple retries may be necessary because signals only interrupt the VCPU @@ -103,9 +96,8 @@ impl SandboxConfiguration { pub const DEFAULT_INTERRUPT_RETRY_DELAY: Duration = Duration::from_micros(500); /// The default signal offset from `SIGRTMIN` used to determine the signal number for interrupting pub const INTERRUPT_VCPU_SIGRTMIN_OFFSET: u8 = 0; - /// The default heap size of a hyperlight sandbox - pub const DEFAULT_HEAP_SIZE: u64 = 131072; - /// Scratch backs the default heap, transport pools, page tables, and exception stack. + /// Scratch backs the guest heap and stack, transport pools, page + /// tables, and exception stack. /// The size is 16 KiB aligned for macOS hosts. /// TODO: revisit when https://github.com/hyperlight-dev/hyperlight/issues/1925 is fixed pub const DEFAULT_SCRATCH_SIZE: usize = 0x64000; @@ -139,7 +131,6 @@ impl SandboxConfiguration { /// Create a new configuration for a sandbox with the given sizes. #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn new( - heap_size_override: Option, scratch_size: usize, interrupt_retry_delay: Duration, interrupt_vcpu_sigrtmin_offset: u8, @@ -147,7 +138,6 @@ impl SandboxConfiguration { #[cfg(crashdump)] guest_core_dump: bool, ) -> Self { Self { - heap_size_override: heap_size_override.unwrap_or(0), scratch_size, max_guest_log_level: Self::MAX_GUEST_LOG_LEVEL_UNSET, g2h_queue_size: Self::DEFAULT_G2H_QUEUE_SIZE, @@ -169,12 +159,6 @@ impl SandboxConfiguration { } } - /// Set the heap size to use in the guest sandbox. If set to 0, the heap size will be determined from the PE file header - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub fn set_heap_size(&mut self, heap_size: u64) { - self.heap_size_override = heap_size; - } - /// Sets the interrupt retry delay #[cfg(any(kvm, mshv3, hvf))] pub fn set_interrupt_retry_delay(&mut self, delay: Duration) { @@ -290,7 +274,8 @@ impl SandboxConfiguration { self.scratch_size } - /// Set the size of the scratch regiong + /// Set how much writable memory to offer the guest. Scratch backs the + /// guest heap and stack. #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub fn set_scratch_size(&mut self, scratch_size: usize) { self.scratch_size = scratch_size; @@ -419,19 +404,6 @@ impl SandboxConfiguration { self.guest_debug_info } - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn heap_size_override_opt(&self) -> Option { - (self.heap_size_override > 0).then_some(self.heap_size_override) - } - - /// If self.heap_size_override is non-zero, return it. Otherwise, - /// return exe_info.heap_reserve() - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn get_heap_size(&self) -> u64 { - self.heap_size_override_opt() - .unwrap_or(Self::DEFAULT_HEAP_SIZE) - } - fn normalize_queue_size(size: usize) -> usize { size.clamp(Self::MIN_QUEUE_SIZE, Self::MAX_QUEUE_SIZE) .next_power_of_two() @@ -450,7 +422,6 @@ impl Default for SandboxConfiguration { #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn default() -> Self { Self::new( - None, Self::DEFAULT_SCRATCH_SIZE, Self::DEFAULT_INTERRUPT_RETRY_DELAY, Self::INTERRUPT_VCPU_SIGRTMIN_OFFSET, @@ -553,10 +524,8 @@ mod tests { #[test] fn overrides() { - const HEAP_SIZE_OVERRIDE: u64 = 0x50000; const SCRATCH_SIZE_OVERRIDE: usize = 0x60000; let mut cfg = SandboxConfiguration::new( - Some(HEAP_SIZE_OVERRIDE), SCRATCH_SIZE_OVERRIDE, SandboxConfiguration::DEFAULT_INTERRUPT_RETRY_DELAY, SandboxConfiguration::INTERRUPT_VCPU_SIGRTMIN_OFFSET, @@ -566,14 +535,10 @@ mod tests { true, ); - let heap_size = cfg.get_heap_size(); let scratch_size = cfg.get_scratch_size(); - assert_eq!(HEAP_SIZE_OVERRIDE, heap_size); assert_eq!(SCRATCH_SIZE_OVERRIDE, scratch_size); - cfg.heap_size_override = 2048; cfg.scratch_size = 0x40000; - assert_eq!(2048, cfg.heap_size_override); assert_eq!(0x40000, cfg.scratch_size); assert_eq!( SandboxConfiguration::DEFAULT_G2H_QUEUE_SIZE, @@ -666,23 +631,15 @@ mod tests { assert_eq!(3, cfg.get_h2g_pool_pages()); } + #[cfg(gdb)] mod proptests { use proptest::prelude::*; use super::SandboxConfiguration; - #[cfg(gdb)] use crate::sandbox::config::DebugInfo; proptest! { #[test] - fn heap_size_override(size in 0x1000..=0x10000u64) { - let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(size); - prop_assert_eq!(size, cfg.heap_size_override); - } - - #[test] - #[cfg(gdb)] fn guest_debug_info(port in 9000..=u16::MAX) { let mut cfg = SandboxConfiguration::default(); let debug_info = DebugInfo { port }; diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index ca128d811..0efc56f96 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -289,7 +289,6 @@ impl Sandbox { if caller_supplied_config { warn_on_layout_override(&config, snapshot.layout()); } - config.set_heap_size(snapshot.layout().heap_size() as u64); config.set_scratch_size(snapshot.layout().get_scratch_size()); config.set_g2h_queue_size(snapshot.layout().get_g2h_queue_size()); config.set_h2g_queue_size(snapshot.layout().get_h2g_queue_size()); @@ -1283,11 +1282,6 @@ fn warn_on_layout_override( snapshot: &crate::mem::layout::SandboxMemoryLayout, ) { let mismatches: &[(&str, u64, u64)] = &[ - ( - "heap_size", - caller.get_heap_size(), - snapshot.heap_size() as u64, - ), ( "scratch_size", caller.get_scratch_size() as u64, @@ -1345,7 +1339,9 @@ mod tests { use hyperlight_common::flatbuffer_wrappers::function_types::ParameterValue; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::func::{Bytes, SupportedReturnType}; - use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE}; + use hyperlight_testing::sandbox_sizes::{ + LARGE_SCRATCH_SIZE, MEDIUM_SCRATCH_SIZE, SMALL_SCRATCH_SIZE, + }; use hyperlight_testing::{c_simple_guest_as_pathbuf, simple_guest_as_pathbuf}; use super::normalize_page_table_roots; @@ -1548,8 +1544,7 @@ mod tests { // Checks that 1,000 calls work with constrained guest memory. // This catches guest stack reset and heap leaks. #[test] - fn test_with_small_stack_and_heap() { - const HEAP_SIZE: u64 = 128 * 1024; + fn test_with_small_scratch() { // Leave room for runtime allocations and eagerly copied page tables. let scratch_size = { let defaults = SandboxConfiguration::default(); @@ -1560,7 +1555,6 @@ mod tests { } + 0x40000; let mut sbox1 = SandboxBuilder::from_file(simple_guest_as_pathbuf()) - .heap_size(HEAP_SIZE) .scratch_size(scratch_size) .build() .unwrap(); @@ -1570,7 +1564,6 @@ mod tests { } let mut sbox2 = SandboxBuilder::from_file(simple_guest_as_pathbuf()) - .heap_size(HEAP_SIZE) .scratch_size(scratch_size) .build() .unwrap(); @@ -2763,11 +2756,6 @@ mod tests { |cfg| cfg.set_h2g_pool_pages(16), |layout| layout.get_h2g_pool_pages(), ), - ( - "heap", - |cfg| cfg.set_heap_size(0x40_000), - |layout| layout.heap_size(), - ), ( "scratch", |cfg| cfg.set_scratch_size(0x90_000), @@ -2813,9 +2801,9 @@ mod tests { } #[test] - fn snapshot_restore_recovers_oom_with_larger_heap() { + fn snapshot_restore_recovers_oom_with_larger_scratch() { let mut source_cfg = SandboxConfiguration::default(); - source_cfg.set_heap_size(0x20_000); + source_cfg.set_scratch_size(0x200_000); let path = simple_guest_as_pathbuf(); let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg)) .unwrap() @@ -2823,10 +2811,8 @@ mod tests { .unwrap(); let snapshot = source.snapshot().unwrap(); - let mut target_cfg = SandboxConfiguration::default(); - target_cfg.set_heap_size(40 * 1024); let path = simple_guest_as_pathbuf(); - let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg)) + let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), None) .unwrap() .evolve() .unwrap(); @@ -2836,38 +2822,41 @@ mod tests { target.restore(snapshot).unwrap(); assert!(!target.status().is_poisoned()); - assert_eq!( - target.call::("CallMalloc", 0x10_000i32).unwrap(), - 0x10_000 - ); + target + .call::<()>("AllocAndWritePattern", 0x100_000u64) + .unwrap(); } #[test] - fn snapshot_restore_applies_smaller_heap_limit() { - let mut source_cfg = SandboxConfiguration::default(); - source_cfg.set_heap_size(40 * 1024); + fn snapshot_restore_applies_smaller_scratch_limit() { let path = simple_guest_as_pathbuf(); - let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(source_cfg)) + let mut source = UninitializedSandbox::new(GuestBinary::FilePath(path), None) .unwrap() .evolve() .unwrap(); let snapshot = source.snapshot().unwrap(); let mut target_cfg = SandboxConfiguration::default(); - target_cfg.set_heap_size(0x20_000); + target_cfg.set_scratch_size(0x200_000); let path = simple_guest_as_pathbuf(); let mut target = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(target_cfg)) .unwrap() .evolve() .unwrap(); + target + .call::<()>("AllocAndWritePattern", 0x100_000u64) + .unwrap(); + target.restore(snapshot).unwrap(); assert_eq!( - target.call::("CallMalloc", 0x10_000i32).unwrap(), - 0x10_000 + target.mem_mgr.layout.get_scratch_size(), + SandboxConfiguration::DEFAULT_SCRATCH_SIZE + ); + assert!( + target + .call::<()>("AllocAndWritePattern", 0x100_000u64) + .is_err() ); - target.restore(snapshot).unwrap(); - assert_eq!(target.mem_mgr.layout.heap_size(), 40 * 1024); - assert!(target.call::("CallMalloc", 0x10_000i32).is_err()); assert!(target.status().is_poisoned()); } @@ -2907,7 +2896,7 @@ mod tests { #[test] fn snapshot_restore_alternates_different_layouts() { let mut small_cfg = SandboxConfiguration::default(); - small_cfg.set_heap_size(40 * 1024); + small_cfg.set_scratch_size(0x70_000); let path = simple_guest_as_pathbuf(); let mut small = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(small_cfg)) .unwrap() @@ -2917,7 +2906,6 @@ mod tests { let small_snapshot = small.snapshot().unwrap(); let mut large_cfg = SandboxConfiguration::default(); - 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)) @@ -2935,15 +2923,15 @@ mod tests { target.restore(small_snapshot.clone()).unwrap(); assert_eq!(target.call::("GetStatic", ()).unwrap(), 11); - assert_eq!(target.mem_mgr.layout.heap_size(), 40 * 1024); + assert_eq!(target.mem_mgr.layout.get_scratch_size(), 0x70_000); target.restore(large_snapshot).unwrap(); assert_eq!(target.call::("GetStatic", ()).unwrap(), 22); - assert_eq!(target.mem_mgr.layout.heap_size(), 0x40_000); + assert_eq!(target.mem_mgr.layout.get_scratch_size(), 0x90_000); target.restore(small_snapshot).unwrap(); assert_eq!(target.call::("GetStatic", ()).unwrap(), 11); - assert_eq!(target.mem_mgr.layout.heap_size(), 40 * 1024); + assert_eq!(target.mem_mgr.layout.get_scratch_size(), 0x70_000); } #[test] @@ -3521,20 +3509,18 @@ mod tests { ); } - /// Test that sandboxes can be created and evolved with different heap sizes #[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), + let test_cases: [(&str, usize); 3] = [ + ("small (8MiB scratch)", SMALL_SCRATCH_SIZE), + ("medium (64MiB scratch)", MEDIUM_SCRATCH_SIZE), + ("large (256MiB scratch)", LARGE_SCRATCH_SIZE), ]; - for (name, heap_size) in test_cases { + for (name, scratch_size) in test_cases { let path = simple_guest_as_pathbuf(); let sbox = SandboxBuilder::from_file(path) - .heap_size(heap_size) - .scratch_size(0x100000) + .scratch_size(scratch_size) .build() .unwrap_or_else(|e| panic!("Failed to create {} sandbox: {}", name, e)); diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 4012b01b4..cbbe9f893 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -163,7 +163,7 @@ impl CpuVendor { /// Top-level Hyperlight snapshot config JSON. Lives at /// `blobs/sha256/` with media type -/// `application/vnd.hyperlight.snapshot.config.v4+json`. +/// `application/vnd.hyperlight.snapshot.config.v5+json`. /// /// In OCI terms this is the "image config" blob that the manifest's /// `config` descriptor points to. It describes the accompanying @@ -222,7 +222,6 @@ pub(super) struct OciSnapshotConfig { #[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub(super) struct MemoryLayout { - pub(super) heap_size: usize, pub(super) code_size: usize, /// Virtual base address of the code region. A value of zero means the /// code region is identity mapped. @@ -486,7 +485,6 @@ impl OciSnapshotConfig { // checked against the snapshot GPA span in `load_inner`. let max_region = SandboxMemoryLayout::MAX_MEMORY_SIZE; for (name, value) in [ - ("heap_size", self.layout.heap_size), ("code_size", self.layout.code_size), ("init_data_size", self.layout.init_data_size), ("scratch_size", self.layout.scratch_size), @@ -823,7 +821,6 @@ mod tests { #[cfg(target_arch = "x86_64")] msrs: Vec::new(), layout: MemoryLayout { - heap_size: 0, code_size: 0, code_virt_base: 0, init_data_size: 0, @@ -916,7 +913,7 @@ mod schema_pin { const PINNED_CALL: &str = r#"{ "hyperlight_version": "x.y.z", "arch": "x86_64", - "abi_version": 7, + "abi_version": 8, "hypervisor": "mshv", "cpu_vendor": "intel", "host_page_size": 4096, @@ -1076,7 +1073,6 @@ mod schema_pin { } ], "layout": { - "heap_size": 3, "code_size": 4, "code_virt_base": 0, "init_data_size": 5, @@ -1113,7 +1109,7 @@ mod schema_pin { const PINNED_CALL: &str = r#"{ "hyperlight_version": "x.y.z", "arch": "aarch64", - "abi_version": 7, + "abi_version": 8, "hypervisor": "mshv", "cpu_vendor": "intel", "host_page_size": 16384, @@ -1129,7 +1125,6 @@ mod schema_pin { "sp_el1": 6 }, "layout": { - "heap_size": 3, "code_size": 4, "code_virt_base": 0, "init_data_size": 5, diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs b/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs index 9eedbb6e8..9da32d8a3 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs @@ -12,7 +12,9 @@ pub(in crate::sandbox::snapshot) const MT_CONFIG_V3: &str = "application/vnd.hyperlight.snapshot.config.v3+json"; pub(in crate::sandbox::snapshot) const MT_CONFIG_V4: &str = "application/vnd.hyperlight.snapshot.config.v4+json"; -pub(in crate::sandbox::snapshot) const MT_CONFIG_CURRENT: &str = MT_CONFIG_V4; +pub(in crate::sandbox::snapshot) const MT_CONFIG_V5: &str = + "application/vnd.hyperlight.snapshot.config.v5+json"; +pub(in crate::sandbox::snapshot) const MT_CONFIG_CURRENT: &str = MT_CONFIG_V5; pub(in crate::sandbox::snapshot) const MT_SNAPSHOT_V2: &str = "application/vnd.hyperlight.snapshot.memory.v2"; pub(in crate::sandbox::snapshot) const MT_SNAPSHOT_CURRENT: &str = MT_SNAPSHOT_V2; @@ -23,7 +25,7 @@ pub(in crate::sandbox::snapshot) const MT_TRANSPORT_CURRENT: &str = MT_TRANSPORT /// ABI version for the snapshot memory blob. Bumped when the /// host-guest contract for the snapshot bytes changes. See /// docs/snapshot-versioning.md. -pub(in crate::sandbox::snapshot) const SNAPSHOT_ABI_VERSION: u32 = 7; +pub(in crate::sandbox::snapshot) const SNAPSHOT_ABI_VERSION: u32 = 8; /// OCI standard annotation key for a manifest's tag inside an image /// index. Set on the manifest descriptor in `index.json`, not on the diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index 3fa493fab..38d51f8d3 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -29,8 +29,9 @@ use self::media_types::{ ANNOTATION_ARCH, ANNOTATION_CPU, ANNOTATION_HYPERVISOR, ANNOTATION_REF_NAME, }; pub(super) use self::media_types::{ - MT_CONFIG_CURRENT, MT_CONFIG_V1, MT_CONFIG_V2, MT_CONFIG_V3, MT_CONFIG_V4, MT_SNAPSHOT_CURRENT, - MT_SNAPSHOT_V2, MT_TRANSPORT_CURRENT, MT_TRANSPORT_V1, SNAPSHOT_ABI_VERSION, + MT_CONFIG_CURRENT, MT_CONFIG_V1, MT_CONFIG_V2, MT_CONFIG_V3, MT_CONFIG_V4, MT_CONFIG_V5, + MT_SNAPSHOT_CURRENT, MT_SNAPSHOT_V2, MT_TRANSPORT_CURRENT, MT_TRANSPORT_V1, + SNAPSHOT_ABI_VERSION, }; use self::reference::{OciDigest, OciReference, OciTag}; use super::memory::{MAX_SNAPSHOT_LAYERS, host_page_size}; @@ -642,7 +643,6 @@ impl Snapshot { .ok_or_else(|| crate::new_error!("snapshot has no MSR state"))? .clone(), layout: MemoryLayout { - heap_size: l.heap_size(), code_size: l.code_size(), code_virt_base: l.get_guest_code_gva() as u64, init_data_size: l.init_data_size(), @@ -789,8 +789,8 @@ impl Snapshot { // Loader dispatch on config media type. let cfg_media = cfg_desc.media_type().to_string(); match cfg_media.as_str() { - MT_CONFIG_V4 => {} - MT_CONFIG_V1 | MT_CONFIG_V2 | MT_CONFIG_V3 => { + MT_CONFIG_V5 => {} + MT_CONFIG_V1 | MT_CONFIG_V2 | MT_CONFIG_V3 | MT_CONFIG_V4 => { return Err(crate::new_error!( "snapshot config media type {:?} is incompatible with snapshot ABI {}", cfg_media, @@ -801,7 +801,7 @@ impl Snapshot { return Err(crate::new_error!( "unexpected config media type {:?} (supported: {:?})", other, - MT_CONFIG_V4 + MT_CONFIG_V5 )); } } @@ -936,7 +936,6 @@ impl Snapshot { // 6. Reconstruct layout. let mut sbox_cfg = crate::sandbox::SandboxConfiguration::default(); - sbox_cfg.set_heap_size(cfg.layout.heap_size as u64); sbox_cfg.set_scratch_size(cfg.layout.scratch_size); sbox_cfg.set_g2h_queue_size(cfg.layout.g2h_queue_size); sbox_cfg.set_h2g_queue_size(cfg.layout.h2g_queue_size); @@ -1009,7 +1008,7 @@ impl Snapshot { } // The snapshot GPA span must cover at least the regions the - // layout fields describe (code, PEB, heap, init data), + // layout fields describe (code, PEB, init data), // otherwise the guest mapping is too short to back them. let required_memory_size = layout.get_memory_size()?; if memory.gpa_span_len() < required_memory_size { diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 7b40c45e1..cbda0e23a 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -282,16 +282,16 @@ fn layered_writer_preserves_blobs_and_transport_geometry() { let layers = manifest["layers"].as_array().unwrap(); assert_eq!( manifest["config"]["mediaType"], - "application/vnd.hyperlight.snapshot.config.v4+json" + "application/vnd.hyperlight.snapshot.config.v5+json" ); // `artifactType` mirrors `config.mediaType` so registries that surface // the distribution-spec referrers API report a useful type, and tooling // that falls back to `config.mediaType` sees the same value. assert_eq!( manifest["artifactType"], - "application/vnd.hyperlight.snapshot.config.v4+json" + "application/vnd.hyperlight.snapshot.config.v5+json" ); - assert_eq!(config["abi_version"], 7); + assert_eq!(config["abi_version"], 8); let blobs = snapshot.snapshot_memory().blobs().collect::>(); assert_eq!(layers.len(), blobs.len() + 1); for (index, blob) in blobs.iter().enumerate() { @@ -2374,6 +2374,7 @@ fn legacy_config_versions_rejected() { "application/vnd.hyperlight.snapshot.config.v1+json", "application/vnd.hyperlight.snapshot.config.v2+json", "application/vnd.hyperlight.snapshot.config.v3+json", + "application/vnd.hyperlight.snapshot.config.v4+json", ] { let (_dir, path) = save_for_mutation(); rewrite_manifest(&path, |m| { @@ -2383,7 +2384,7 @@ fn legacy_config_versions_rejected() { &path, OciTag::new("latest").unwrap(), )); - assert_err_contains(err, "incompatible with snapshot ABI 7"); + assert_err_contains(err, "incompatible with snapshot ABI 8"); } } @@ -3172,13 +3173,13 @@ fn layout_field_too_large_rejected() { let (_dir, path) = save_for_mutation(); rewrite_config(&path, |cfg| { // 64 GiB exceeds MAX_MEMORY_SIZE for an individual region. - cfg["layout"]["heap_size"] = Value::from(64u64 * 1024 * 1024 * 1024); + cfg["layout"]["code_size"] = Value::from(64u64 * 1024 * 1024 * 1024); }); let err = unwrap_err_snapshot(Snapshot::checked_load( &path, OciTag::new("latest").unwrap(), )); - assert_err_contains(err, "heap_size"); + assert_err_contains(err, "code_size"); } #[test] @@ -3354,7 +3355,6 @@ fn persisted_non_default_layout_loads_and_runs() { let mut config = SandboxConfiguration::default(); config.set_g2h_pool_pages(16); config.set_h2g_pool_pages(16); - config.set_heap_size(0x40_000); config.set_scratch_size(0x90_000); let mut source = UninitializedSandbox::new( GuestBinary::FilePath(simple_guest_as_pathbuf()), @@ -3374,7 +3374,6 @@ fn persisted_non_default_layout_loads_and_runs() { let loaded = Arc::new(Snapshot::checked_load(&path, OciTag::new("latest").unwrap()).unwrap()); assert_eq!(loaded.layout().get_g2h_pool_pages(), 16); assert_eq!(loaded.layout().get_h2g_pool_pages(), 16); - assert_eq!(loaded.layout().heap_size(), 0x40_000); assert_eq!(loaded.layout().get_scratch_size(), 0x90_000); let mut restored = Sandbox::from_snapshot(loaded, HostFunctions::default(), None).unwrap(); @@ -3394,7 +3393,6 @@ fn persisted_non_default_layout_loads_and_runs() { fn round_trip_preserves_transport_layout() { let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) .scratch_size(512 * 1024) - .heap_size(512 * 1024) .g2h_queue_size(128) .h2g_queue_size(16) .g2h_buffer_size(8192) @@ -3818,7 +3816,6 @@ fn from_snapshot_silently_ignores_layout_overrides() { let original = snapshot.layout(); let mut sbox2 = SandboxBuilder::from_snapshot(snapshot.clone()) - .heap_size((original.heap_size() as u64) * 2) .scratch_size(original.get_scratch_size() * 2) .g2h_queue_size(128) .h2g_queue_size(16) @@ -3833,7 +3830,6 @@ fn from_snapshot_silently_ignores_layout_overrides() { let new_snap = sbox2.snapshot().unwrap(); let restored = new_snap.layout(); - assert_eq!(restored.heap_size(), original.heap_size()); assert_eq!(restored.get_scratch_size(), original.get_scratch_size()); assert_eq!(restored.get_g2h_queue_size(), original.get_g2h_queue_size()); assert_eq!(restored.get_h2g_queue_size(), original.get_h2g_queue_size()); diff --git a/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs b/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs index bc5e2f937..548a8c96a 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs @@ -16,8 +16,8 @@ use super::file::{ SNAPSHOT_ABI_VERSION, }; -const EXPECTED_ABI_VERSION: u32 = 7; -const EXPECTED_MT_CONFIG: &str = "application/vnd.hyperlight.snapshot.config.v4+json"; +const EXPECTED_ABI_VERSION: u32 = 8; +const EXPECTED_MT_CONFIG: &str = "application/vnd.hyperlight.snapshot.config.v5+json"; const EXPECTED_MT_SNAPSHOT: &str = "application/vnd.hyperlight.snapshot.memory.v2"; const EXPECTED_MT_TRANSPORT: &str = "application/vnd.hyperlight.snapshot.transport.v1"; const EXPECTED_OCI_LAYOUT_VERSION: &str = "1.0.0"; @@ -46,7 +46,7 @@ const _: () = { const _: () = { use hyperlight_common::mem::HyperlightPEB; use hyperlight_common::vmem::PAGE_SIZE; - // The loading host derives `guest_heap_buffer_offset`, and every + // The loading host derives `init_data_offset`, and every // offset after it, from the PEB's page-rounded size. Existing // snapshots place the PEB in a single page, so the only thing that // must hold is that the PEB keeps fitting in one page. Field layout diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index 785e00a78..7fc2f3df2 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -462,7 +462,7 @@ mod tests { // Non default memory configuration let cfg = { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(0x1000); + cfg.set_scratch_size(SandboxConfiguration::DEFAULT_SCRATCH_SIZE * 2); Some(cfg) }; @@ -1154,30 +1154,7 @@ mod tests { let _evolved2 = sandbox2.evolve().expect("Failed to evolve sandbox2"); } - // Test 2: Create snapshot with custom heap size - { - let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(16 * 1024 * 1024); // 16MB heap - - let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None); - - let snapshot = Arc::new( - Snapshot::from_env(env, cfg) - .expect("Failed to create snapshot with custom heap size"), - ); - - let sandbox = UninitializedSandbox::from_snapshot( - snapshot, - None, - #[cfg(crashdump)] - Some(binary_path.clone()), - ) - .expect("Failed to create sandbox from snapshot with custom heap"); - - let _evolved = sandbox.evolve().expect("Failed to evolve sandbox"); - } - - // Test 3: Create snapshot with custom scratch size + // Test 2: Create snapshot with custom scratch size { let mut cfg = SandboxConfiguration::default(); cfg.set_scratch_size(SandboxConfiguration::DEFAULT_SCRATCH_SIZE + 64 * 1024); @@ -1200,10 +1177,9 @@ mod tests { let _evolved = sandbox.evolve().expect("Failed to evolve sandbox"); } - // Test 4: Create snapshot with custom heap and scratch sizes + // Test 3: Create several sandboxes from a snapshot with a custom scratch size { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(32 * 1024 * 1024); // 32MB heap cfg.set_scratch_size(512 * 1024); // 512KB scratch let env = GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), None); diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index dd98517a0..eca999e32 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -531,44 +531,18 @@ fn guest_allocate_vec() { }); } -// checks that malloc failures are captured correctly #[test] fn guest_malloc_abort() { - with_rust_sandbox(|mut sbox1| { - let size = 20000000_i32; // some big number that should fail when allocated - - let res = sbox1.call::("TestMalloc", size).unwrap_err(); + with_rust_sandbox(|mut sbox| { + // Writing more heap memory than scratch holds exhausts scratch. + let res = sbox + .call::<()>("AllocAndWritePattern", 20_000_000_u64) + .unwrap_err(); assert!( matches!(&res, HyperlightError::GuestAborted(code, _) if *code == ErrorCode::MallocFailed as u8), "unexpected error: {res:?}" ); }); - - // allocate a vector (on heap) that is bigger than the heap - let heap_size = 40 * 1024; - let size_to_allocate = 0x10000; - assert!( - size_to_allocate > heap_size, - "precondition: size_to_allocate ({size_to_allocate}) must be > heap_size ({heap_size})" - ); - - let configure = |builder: SandboxBuilder| builder.heap_size(heap_size); - with_rust_sandbox_from(configure, |mut sbox2| { - let err = sbox2 - .call::( - "CallMalloc", // uses the rust allocator to allocate a vector on heap - size_to_allocate as i32, - ) - .unwrap_err(); - assert!( - matches!( - &err, - // OOM memory errors in rust allocator are panics. Our panic handler returns ErrorCode::UnknownError on panic - HyperlightError::GuestAborted(code, msg) if *code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") - ), - "unexpected error: {err:?}" - ); - }); } /// Test that executing an OUT instruction with an invalid port causes an error and poisons the sandbox. @@ -588,23 +562,11 @@ fn guest_outb_with_invalid_port_poisons_sandbox() { } #[test] -fn guest_panic_no_alloc() { - let heap_size = 40 * 1024; - - let configure = |builder: SandboxBuilder| builder.heap_size(heap_size); - with_rust_sandbox_from(configure, |mut sbox| { - let res = sbox - .call::( - "ExhaustHeap", // uses the rust allocator to allocate small blocks on the heap until OOM - (), - ) - .unwrap_err(); - +fn guest_heap_exhaustion_aborts() { + with_rust_sandbox(|mut sbox| { + let res = sbox.call::<()>("ExhaustHeap", ()).unwrap_err(); assert!( - matches!( - &res, - HyperlightError::GuestAborted(code, msg) if *code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") && msg.contains("bytes failed") - ), + matches!(&res, HyperlightError::GuestAborted(code, _) if *code == ErrorCode::MallocFailed as u8), "unexpected error: {res:?}" ); }); @@ -1890,60 +1852,6 @@ fn page_fault_handler() { }); } -/// Tests that an exception can be properly handled even when the heap is exhausted. -/// The guest function fills the heap completely, then triggers a ud2 exception. -/// This validates that the exception handling path does not require heap allocations. -#[test] -fn fill_heap_and_cause_exception() { - // The heap must run out before scratch backing its CoW pages. - let configure = |builder: SandboxBuilder| builder.heap_size(40 * 1024); - - with_rust_sandbox_from(configure, |mut sandbox| { - let result = sandbox.call::<()>("FillHeapAndCauseException", ()); - - // The call should fail with an exception error since there's no handler installed - assert!(result.is_err(), "Expected an error from ud2 exception"); - - let err = result.unwrap_err(); - match &err { - HyperlightError::GuestAborted(code, message) => { - assert_eq!(*code, ErrorCode::GuestError as u8, "Full error: {:?}", err); - - // Verify the message was properly formatted (proves no-allocation path worked) - // Exception vector 6 is #UD (Invalid Opcode from ud2 instruction) - #[cfg(target_arch = "x86_64")] - let vector = "Exception vector: 6"; - #[cfg(target_arch = "aarch64")] - let vector = "Exception vector: CurrentSP0 Synchronous"; - assert!( - message.contains(vector), - "Message should contain '{}'\nFull error: {:?}", - vector, - err - ); - assert!( - message.contains("Faulting Instruction:"), - "Message should contain 'Faulting Instruction:'\nFull error: {:?}", - err - ); - #[cfg(target_arch = "x86_64")] - assert!( - message.contains("Stack Pointer:"), - "Message should contain 'Stack Pointer:'\nFull error: {:?}", - err - ); - #[cfg(target_arch = "aarch64")] - assert!( - message.contains("Exception Syndrome:"), - "Message should contain 'Exception Syndrome:'\nFull error: {:?}", - err - ); - } - _ => panic!("Expected GuestAborted error, got: {:?}", err), - } - }); -} - /// This test is "likely" to catch a race condition where WHvCancelRunVirtualProcessor runs halfway, then the partition is deleted (by drop calling WHvDeletePartition), /// and WHvCancelRunVirtualProcessor continues, and tries to access freed memory. /// diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index 6421fa5b3..039ff5c4d 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -40,7 +40,6 @@ fn fragmented_control_round_trip_releases_buffers() { with_all_guests(|path| { let mut sbox = SandboxBuilder::from_file(path) - .heap_size(256 * 1024) .scratch_size(512 * 1024) .build() .unwrap(); @@ -319,13 +318,16 @@ fn c_fallback_error_overrides_null() { }); } +/// Scratch small enough that leaked call results exhaust it. +const LEAK_TEST_SCRATCH_SIZE: usize = 400 * 1024; + #[test] fn c_registered_error_drops_returned_value() { with_c_sandbox_from( - |builder| builder.heap_size(64 * 1024), + |builder| builder.scratch_size(LEAK_TEST_SCRATCH_SIZE), |mut sbox| { - // Eight ignored 16 KiB results exceed this heap if their payloads leak. - for _ in 0..8 { + // 32 leaked 16 KiB results would exceed `LEAK_TEST_SCRATCH_SIZE`. + for _ in 0..32 { let error = sbox .call::>("ReturnValueWithError", ()) .unwrap_err(); @@ -346,10 +348,10 @@ fn c_registered_error_drops_returned_value() { #[test] fn c_fallback_error_drops_returned_value() { with_c_sandbox_from( - |builder| builder.heap_size(64 * 1024), + |builder| builder.scratch_size(LEAK_TEST_SCRATCH_SIZE), |mut sbox| { - // Eight ignored 16 KiB results exceed this heap if their payloads leak. - for _ in 0..8 { + // 32 leaked 16 KiB results would exceed `LEAK_TEST_SCRATCH_SIZE`. + for _ in 0..32 { let error = sbox .call::>("FallbackValueWithError", ()) .unwrap_err(); diff --git a/src/hyperlight_host/tests/snapshot_goldens/fixtures.rs b/src/hyperlight_host/tests/snapshot_goldens/fixtures.rs index 45a792bee..567d67b9d 100644 --- a/src/hyperlight_host/tests/snapshot_goldens/fixtures.rs +++ b/src/hyperlight_host/tests/snapshot_goldens/fixtures.rs @@ -28,7 +28,6 @@ pub(crate) const CALL_COUNTER_BUMP: i32 = 42; /// least one region between generate-time and load-time. fn golden_config() -> SandboxConfiguration { let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(256 * 1024); cfg.set_scratch_size(512 * 1024); cfg.set_g2h_pool_pages(16); cfg.set_h2g_pool_pages(16); diff --git a/src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs b/src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs index 91ce709f9..96bf2d600 100644 --- a/src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs +++ b/src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs @@ -8,7 +8,7 @@ //! publish. See `docs/snapshot-versioning.md`. /// Goldens version, a `vMAJOR.MINOR` string. -pub(crate) const GOLDENS_VERSION: &str = "v7.0"; +pub(crate) const GOLDENS_VERSION: &str = "v8.0"; /// Old majors kept loadable through a compatibility path, verified /// alongside `GOLDENS_VERSION`. A backwards-compatible break (Option 2) diff --git a/src/hyperlight_testing/src/lib.rs b/src/hyperlight_testing/src/lib.rs index a9a058bf9..b324a7b88 100644 --- a/src/hyperlight_testing/src/lib.rs +++ b/src/hyperlight_testing/src/lib.rs @@ -176,14 +176,12 @@ pub fn simple_guest_for_fuzzing_as_string() -> Result { .to_string()) } -/// Standard sandbox heap sizes for benchmarking and testing. -/// These constants define common heap sizes used across benchmarks and tests -/// to ensure consistency. +/// Standard sandbox scratch sizes for benchmarks and tests. pub mod sandbox_sizes { - /// Small heap size: 8 MB - pub const SMALL_HEAP_SIZE: u64 = 8 * 1024 * 1024; - /// Medium heap size: 64 MB - pub const MEDIUM_HEAP_SIZE: u64 = 64 * 1024 * 1024; - /// Large heap size: 256 MB - pub const LARGE_HEAP_SIZE: u64 = 256 * 1024 * 1024; + /// Small scratch size: 8 MiB + pub const SMALL_SCRATCH_SIZE: usize = 8 * 1024 * 1024; + /// Medium scratch size: 64 MiB + pub const MEDIUM_SCRATCH_SIZE: usize = 64 * 1024 * 1024; + /// Large scratch size: 256 MiB + pub const LARGE_SCRATCH_SIZE: usize = 256 * 1024 * 1024; } diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 66fa2709e..e1c80d831 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -384,34 +384,13 @@ fn call_malloc(size: i32) -> i32 { size } -#[guest_function("FillHeapAndCauseException")] -fn fill_heap_and_cause_exception() { - let layout: Layout = Layout::new::(); - let mut ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; - while !ptr.is_null() { - black_box(ptr); - ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; - } - - // trigger an undefined instruction exception - trigger_exception(); -} - +/// Allocates until scratch runs out and the guest aborts. #[guest_function("ExhaustHeap")] fn exhaust_heap() { let layout: Layout = Layout::new::(); - let mut ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; - while !ptr.is_null() { - black_box(ptr); - ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; + loop { + black_box(unsafe { alloc::alloc::alloc_zeroed(layout) }); } - - // after alloc::alloc_zeroed failure (null return when called in loop above) - // allocate a Vec to ensure OOM panic - let vec = Vec::::with_capacity(1); - black_box(vec); - - panic!("function should have panicked before due to OOM") } #[guest_function("MallocAndFree")]