From a05f227f9ecb7e7c9803282e769231c48495b53a Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:23:15 -0800 Subject: [PATCH 1/2] Move the executable heap flag into the PEB The host wrote the flag to scratch-top metadata on every restore. A snapshot restored by a host with a different executable_heap feature mixed executable and non-executable heap pages. The guest copies the flag from the PEB at init, and snapshots keep it. Bump the snapshot ABI to 10. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- CHANGELOG.md | 3 +++ docs/snapshot-versioning.md | 3 +++ src/hyperlight_common/src/layout.rs | 7 ++----- src/hyperlight_common/src/mem.rs | 3 +++ src/hyperlight_guest/src/layout.rs | 3 --- .../src/arch/aarch64/exception/handle.rs | 4 +--- .../src/arch/amd64/exception/handle.rs | 4 +--- src/hyperlight_guest_bin/src/lib.rs | 9 +++++++++ src/hyperlight_host/src/mem/layout.rs | 1 + src/hyperlight_host/src/mem/mgr.rs | 4 ---- src/hyperlight_host/src/sandbox/snapshot/file/config.rs | 4 ++-- .../src/sandbox/snapshot/file/media_types.rs | 2 +- src/hyperlight_host/src/sandbox/snapshot/file_tests.rs | 4 ++-- src/hyperlight_host/src/sandbox/snapshot/tripwires.rs | 2 +- .../tests/snapshot_goldens/goldens_version.rs | 2 +- 15 files changed, 30 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d430b0b5..e1f72cb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Prerelease] - Unreleased +### Changed +* **Breaking:** Snapshots use ABI 10. Existing snapshots must be regenerated. + ## [v0.19.0] - 2026-11-04 ### Added diff --git a/docs/snapshot-versioning.md b/docs/snapshot-versioning.md index 1a92a423b..f103e22c8 100644 --- a/docs/snapshot-versioning.md +++ b/docs/snapshot-versioning.md @@ -52,6 +52,9 @@ regenerated. ABI 9 removes the heap region from snapshot memory. Earlier snapshots must be regenerated. +ABI 10 moves the executable heap flag from the scratch-top metadata into the +PEB. 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/src/hyperlight_common/src/layout.rs b/src/hyperlight_common/src/layout.rs index 731f73fe9..01e4c60e9 100644 --- a/src/hyperlight_common/src/layout.rs +++ b/src/hyperlight_common/src/layout.rs @@ -25,8 +25,8 @@ pub const SCRATCH_TOP_RESERVED_PAGES: usize = 12; // down from the top of scratch memory. #[repr(C)] struct ScratchTopMetadata { - /// Nonzero if guest heap pages are mapped executable. - heap_executable: u64, + /// Padding that keeps the exception stack 16-byte aligned. + _alignment_padding: [u8; 8], /// Host-published capacity of each H2G buffer. h2g_buffer_size: u64, /// Number of pages reserved for the H2G pool. @@ -85,8 +85,6 @@ pub const SCRATCH_TOP_LIBC_RNG_SEED_OFFSET: u64 = scratch_top_offset(offset_of!(ScratchTopMetadata, libc_rng_seed)); pub const SCRATCH_TOP_GUEST_LOG_LEVEL_OFFSET: u64 = scratch_top_offset(offset_of!(ScratchTopMetadata, guest_log_level)); -pub const SCRATCH_TOP_HEAP_EXECUTABLE_OFFSET: u64 = - scratch_top_offset(offset_of!(ScratchTopMetadata, heap_executable)); pub const SCRATCH_TOP_EXN_STACK_OFFSET: u64 = size_of::() as u64; const _: () = { @@ -107,7 +105,6 @@ const _: () = { assert!(SCRATCH_TOP_H2G_QUEUE_SIZE_OFFSET == 0x58); assert!(SCRATCH_TOP_H2G_POOL_PAGES_OFFSET == 0x60); assert!(SCRATCH_TOP_H2G_BUFFER_SIZE_OFFSET == 0x68); - assert!(SCRATCH_TOP_HEAP_EXECUTABLE_OFFSET == 0x70); assert!(SCRATCH_TOP_EXN_STACK_OFFSET == 0x70); assert!(VIRTQ_BUFFER_GVA_START < VIRTQ_BUFFER_GVA_END); diff --git a/src/hyperlight_common/src/mem.rs b/src/hyperlight_common/src/mem.rs index b5b61209a..c41742e69 100644 --- a/src/hyperlight_common/src/mem.rs +++ b/src/hyperlight_common/src/mem.rs @@ -15,6 +15,8 @@ pub struct GuestMemoryRegion { #[repr(C)] pub struct HyperlightPEB { pub init_data: GuestMemoryRegion, + /// Nonzero if guest heap pages are mapped executable. + pub heap_executable: u64, } #[cfg(test)] @@ -28,6 +30,7 @@ mod tests { size: 0x1111, ptr: 0x2222, }, + heap_executable: 1, }; let bytes = bytemuck::bytes_of(&peb); let peb2 = *bytemuck::from_bytes::(bytes); diff --git a/src/hyperlight_guest/src/layout.rs b/src/hyperlight_guest/src/layout.rs index 2f8ea911c..a110568e2 100644 --- a/src/hyperlight_guest/src/layout.rs +++ b/src/hyperlight_guest/src/layout.rs @@ -58,9 +58,6 @@ pub fn guest_log_level_gva() -> *mut u64 { use hyperlight_common::layout::{SCRATCH_TOP_GUEST_LOG_LEVEL_OFFSET, SCRATCH_TOP_GVA}; (SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_GUEST_LOG_LEVEL_OFFSET + 1) as *mut u64 } -pub fn heap_executable_gva() -> *mut u64 { - scratch_top_gva(hyperlight_common::layout::SCRATCH_TOP_HEAP_EXECUTABLE_OFFSET) -} pub use arch::{scratch_base_gpa, scratch_base_gva}; /// Exclusive upper GPA for the scratch page allocator: the transport arena, 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 58656d02c..a9df10de8 100644 --- a/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs @@ -106,9 +106,7 @@ fn handle_internal_fault(exn: Exception, far: u64, orig_mapping: Option unsafe { handle_stack_or_heap_fault(far, false) }; true } else if (HEAP_GVA_START..HEAP_GVA_END).contains(&far) { - // SAFETY: the host writes the scratch-top metadata before entry. - let executable = - unsafe { hyperlight_guest::layout::heap_executable_gva().read_volatile() } != 0; + 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. 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 2e7a3479a..b6ce9ef91 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/exception/handle.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/exception/handle.rs @@ -133,9 +133,7 @@ fn try_handle_internal_pagefault( return true; } if (HEAP_GVA_START..HEAP_GVA_END).contains(&gva) { - // SAFETY: the host writes the scratch-top metadata before entry. - let executable = - unsafe { hyperlight_guest::layout::heap_executable_gva().read_volatile() } != 0; + 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) }; diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index 90793db86..c4fe87d69 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, compiler_fence}; use arch::dispatch::dispatch_function; use guest_function::register::GuestFunctionRegister; @@ -116,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(); @@ -265,6 +269,11 @@ pub(crate) extern "C" fn generic_init( #[allow(static_mut_refs)] let peb_ptr = GUEST_HANDLE.peb().unwrap(); + // SAFETY: the host writes the PEB at `peb_address` before entry. + HEAP_EXECUTABLE.store((*peb_ptr).heap_executable != 0, Ordering::Relaxed); + // The heap accesses below fault into the handler that reads this flag. + compiler_fence(Ordering::SeqCst); + #[cfg(not(all(feature = "mem_profile", target_arch = "x86_64")))] let heap_allocator = &HEAP_ALLOCATOR; #[cfg(all(feature = "mem_profile", target_arch = "x86_64"))] diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 0cf9a5b3d..346a89f7f 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -461,6 +461,7 @@ impl SandboxMemoryLayout { size: (self.get_unaligned_memory_size() - self.init_data_offset()) as u64, ptr: guest_base + self.init_data_offset() as u64, }, + heap_executable: cfg!(feature = "executable_heap") as u64, }; let offset = self.peb_offset(); diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 80b2ee797..435118aec 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -1160,10 +1160,6 @@ impl SandboxMemoryManager { SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET, self.snapshot_count, )?; - self.update_scratch_bookkeeping_item( - SCRATCH_TOP_HEAP_EXECUTABLE_OFFSET, - cfg!(feature = "executable_heap") as u64, - )?; // Record the G2H and H2G queue sizes, pool page counts, and buffer sizes. self.update_scratch_bookkeeping_item( SCRATCH_TOP_G2H_QUEUE_SIZE_OFFSET, diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 1dbade579..e4aa4d3ae 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -926,7 +926,7 @@ mod schema_pin { const PINNED_CALL: &str = r#"{ "hyperlight_version": "x.y.z", "arch": "x86_64", - "abi_version": 9, + "abi_version": 10, "hypervisor": "mshv", "cpu_vendor": "intel", "layer_alignment": 4096, @@ -1122,7 +1122,7 @@ mod schema_pin { const PINNED_CALL: &str = r#"{ "hyperlight_version": "x.y.z", "arch": "aarch64", - "abi_version": 9, + "abi_version": 10, "hypervisor": "mshv", "cpu_vendor": "intel", "layer_alignment": 16384, 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 2a98cc3e9..b43c06e74 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs @@ -25,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 = 9; +pub(in crate::sandbox::snapshot) const SNAPSHOT_ABI_VERSION: u32 = 10; /// 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_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 58bfa19aa..c3cb64c2e 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -291,7 +291,7 @@ fn layered_writer_preserves_blobs_and_transport_geometry() { manifest["artifactType"], "application/vnd.hyperlight.snapshot.config.v5+json" ); - assert_eq!(config["abi_version"], 9); + assert_eq!(config["abi_version"], 10); let blobs = snapshot.snapshot_memory().blobs().collect::>(); assert_eq!(layers.len(), blobs.len() + 1); for (index, blob) in blobs.iter().enumerate() { @@ -2398,7 +2398,7 @@ fn legacy_config_versions_rejected() { &path, OciTag::new("latest").unwrap(), )); - assert_err_contains(err, "incompatible with snapshot ABI 9"); + assert_err_contains(err, "incompatible with snapshot ABI 10"); } } diff --git a/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs b/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs index f34933d21..ef2e334d1 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs @@ -16,7 +16,7 @@ use super::file::{ SNAPSHOT_ABI_VERSION, }; -const EXPECTED_ABI_VERSION: u32 = 9; +const EXPECTED_ABI_VERSION: u32 = 10; 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"; diff --git a/src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs b/src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs index 549feb1b0..924e35125 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 = "v9.0"; +pub(crate) const GOLDENS_VERSION: &str = "v10.0"; /// Old majors kept loadable through a compatibility path, verified /// alongside `GOLDENS_VERSION`. A backwards-compatible break (Option 2) From 682e2cb2bbc34c1d961543166c0097bb7ff95034 Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:23:38 -0800 Subject: [PATCH 2/2] Add a runtime executable heap setting Add SandboxConfiguration::set_executable_heap and SandboxBuilder::executable_heap. The executable_heap feature sets the default. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- CHANGELOG.md | 3 + Justfile | 2 +- .../src/hypervisor/hyperlight_vm/x86_64.rs | 4 +- src/hyperlight_host/src/mem/layout.rs | 4 +- src/hyperlight_host/src/sandbox/builder.rs | 11 +++- src/hyperlight_host/src/sandbox/config.rs | 22 ++++++++ .../src/sandbox/snapshot/mod.rs | 2 +- src/hyperlight_host/tests/integration_test.rs | 55 ++++++++++++------- 8 files changed, 78 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f72cb59..f43717ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Prerelease] - Unreleased +### Added +* Add `SandboxConfiguration::set_executable_heap` and `SandboxBuilder::executable_heap` to choose per sandbox whether guest heap pages are executable. The `executable_heap` cargo feature sets the default. + ### Changed * **Breaking:** Snapshots use ABI 10. Existing snapshots must be regenerated. diff --git a/Justfile b/Justfile index a05dd8f3d..ef2d435ec 100644 --- a/Justfile +++ b/Justfile @@ -297,7 +297,7 @@ test-isolated target=default-target features="" : # runs integration tests test-integration target=default-target features="": - @# run execute_on_heap test with feature "executable_heap" on (runs with off during normal tests) + @# run execute_on_heap with feature "executable_heap" on, which sets the default heap permission {{ cargo-cmd }} test {{ if features =="" {"--features executable_heap"} else if features=="no-default-features" {"--no-default-features --features executable_heap"} else {"--no-default-features -F executable_heap," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap @# run component-util unit tests and the integration tests that exercise WIT input codegen diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index f6037240a..4535b8bfd 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1467,7 +1467,9 @@ mod tests { let mut snapshot_contents = vec![0u8; mem_size]; snapshot_contents[layout.guest_code_offset()..layout.guest_code_offset() + code.len()] .copy_from_slice(code); - layout.write_peb(&mut snapshot_contents).unwrap(); + layout + .write_peb(&mut snapshot_contents, config.get_executable_heap()) + .unwrap(); let snapshot_memory = crate::sandbox::snapshot::memory::tests::flat_memory( &snapshot_contents, &pt_bytes, diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 346a89f7f..7766edd5f 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -451,7 +451,7 @@ impl SandboxMemoryLayout { /// Note: `mem` may have been modified, even if `Err` was returned /// from this function. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn write_peb(&self, mem: &mut [u8]) -> Result<()> { + pub(crate) fn write_peb(&self, mem: &mut [u8], heap_executable: bool) -> Result<()> { use hyperlight_common::mem::GuestMemoryRegion; let guest_base = Self::BASE_ADDRESS as u64; @@ -461,7 +461,7 @@ impl SandboxMemoryLayout { size: (self.get_unaligned_memory_size() - self.init_data_offset()) as u64, ptr: guest_base + self.init_data_offset() as u64, }, - heap_executable: cfg!(feature = "executable_heap") as u64, + heap_executable: heap_executable as u64, }; let offset = self.peb_offset(); diff --git a/src/hyperlight_host/src/sandbox/builder.rs b/src/hyperlight_host/src/sandbox/builder.rs index f00ab6099..504ae3fb9 100644 --- a/src/hyperlight_host/src/sandbox/builder.rs +++ b/src/hyperlight_host/src/sandbox/builder.rs @@ -120,7 +120,8 @@ impl SandboxBuilder { /// Build a sandbox restoring the guest from `snapshot`. /// - /// The snapshot's layout overrides scratch and transport settings. + /// The snapshot's layout overrides scratch and transport settings. The + /// snapshot keeps its executable heap setting. pub fn from_snapshot(snapshot: Arc) -> Self { Self::with_source(Source::Snapshot(snapshot)) } @@ -316,6 +317,14 @@ impl SandboxBuilder { self.cfg.get_scratch_size() } + /// Set whether guest heap pages are mapped executable. + /// + /// See [`SandboxConfiguration::set_executable_heap`]. + pub fn executable_heap(mut self, executable: bool) -> Self { + self.cfg.set_executable_heap(executable); + self + } + /// Set the G2H virtqueue descriptor count. /// /// See [`SandboxConfiguration::set_g2h_queue_size`] for normalization. diff --git a/src/hyperlight_host/src/sandbox/config.rs b/src/hyperlight_host/src/sandbox/config.rs index 0f70e2f34..13e097ff1 100644 --- a/src/hyperlight_host/src/sandbox/config.rs +++ b/src/hyperlight_host/src/sandbox/config.rs @@ -83,6 +83,8 @@ pub struct SandboxConfiguration { g2h_pool_pages: usize, /// Number of pages in the H2G buffer pool. h2g_pool_pages: usize, + /// Whether guest heap pages are mapped executable. + executable_heap: bool, /// Declared guest MSRs, stored inline to keep this type `Copy`. #[cfg(target_arch = "x86_64")] guest_msrs: [u32; Self::MAX_GUEST_MSRS], @@ -113,6 +115,9 @@ impl SandboxConfiguration { pub const DEFAULT_G2H_POOL_PAGES: usize = 12; /// The default total number of H2G pool pages. pub const DEFAULT_H2G_POOL_PAGES: usize = 8; + /// Whether guest heap pages are executable by default. The + /// `executable_heap` feature sets this. + pub const DEFAULT_EXECUTABLE_HEAP: bool = cfg!(feature = "executable_heap"); /// The minimum G2H virtqueue descriptor count. const MIN_QUEUE_SIZE: usize = 2; /// The maximum G2H virtqueue descriptor count. @@ -146,6 +151,7 @@ impl SandboxConfiguration { h2g_buffer_size: Self::DEFAULT_H2G_BUFFER_SIZE, g2h_pool_pages: Self::DEFAULT_G2H_POOL_PAGES, h2g_pool_pages: Self::DEFAULT_H2G_POOL_PAGES, + executable_heap: Self::DEFAULT_EXECUTABLE_HEAP, interrupt_retry_delay, interrupt_vcpu_sigrtmin_offset, #[cfg(gdb)] @@ -281,6 +287,22 @@ impl SandboxConfiguration { self.scratch_size = scratch_size; } + /// Set whether guest heap pages are mapped executable. + /// + /// The guest sets this permission in its own page tables, so it protects + /// a well-behaved guest from running injected heap data but cannot stop a + /// malicious guest. Sandboxes built from a snapshot keep the value of the + /// sandbox built from the guest binary. + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub fn set_executable_heap(&mut self, executable: bool) { + self.executable_heap = executable; + } + + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_executable_heap(&self) -> bool { + self.executable_heap + } + /// Sets the maximum log level for guest code execution. /// /// If not set, the level is determined from the `RUST_LOG` environment diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index aaed10e2d..0d42da25d 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -675,7 +675,7 @@ impl Snapshot { &mut data[layout.guest_code_offset()..], )?; - layout.write_peb(data)?; + layout.write_peb(data, cfg.get_executable_heap())?; blob.map(|x| layout.write_init_data(data, x.data)) .transpose()?; diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index eca999e32..60eaebd77 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -633,27 +633,44 @@ fn guard_page_check_2() { #[test] fn execute_on_heap() { - with_rust_sandbox(|mut sbox1| { - let result = sbox1.call::("ExecuteOnHeap", ()); - - #[cfg(feature = "executable_heap")] - assert_eq!( - result.unwrap(), - "Executed on heap successfully", - "should execute successfully" + for executable in [true, false] { + with_rust_sandbox_from( + |builder| builder.executable_heap(executable), + |mut sbox| { + let result = sbox.call::("ExecuteOnHeap", ()); + if executable { + assert_eq!(result.unwrap(), "Executed on heap successfully"); + } else { + let err = result.unwrap_err().to_string(); + assert!( + err.contains( + #[cfg(target_arch = "x86_64")] + "PageFault", + #[cfg(target_arch = "aarch64")] + "Exception Syndrome: 0x86", + ), + "should get page fault: {err}" + ); + } + }, ); + } +} - #[cfg(not(feature = "executable_heap"))] - assert!( - result.unwrap_err().to_string().contains( - #[cfg(target_arch = "x86_64")] - "PageFault", - #[cfg(target_arch = "aarch64")] - "Exception Syndrome: 0x86", - ), - "should get page fault" - ); - }); +#[test] +fn snapshot_keeps_executable_heap_setting() { + let mut sbox = SandboxBuilder::from_file(hyperlight_testing::simple_guest_as_pathbuf()) + .executable_heap(true) + .build() + .unwrap(); + let snapshot = sbox.snapshot().unwrap(); + + let mut restored = SandboxBuilder::from_snapshot(snapshot) + .executable_heap(false) + .build() + .unwrap(); + let result: String = restored.call("ExecuteOnHeap", ()).unwrap(); + assert_eq!(result, "Executed on heap successfully"); } // checks that a recursive function with stack allocation eventually fails with stackoverflow