From ff39cc8e4494a47bb61b92cddf88ce9d34c67b9b Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:57:01 -0800 Subject: [PATCH 1/7] Make test_drop able to run concurrently Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- Justfile | 1 - src/hyperlight_host/src/mem/shared_mem.rs | 69 +++++++++++++---------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Justfile b/Justfile index 1848ec456..6bd3aabc9 100644 --- a/Justfile +++ b/Justfile @@ -217,7 +217,6 @@ test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_log_trace --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::initialized_multi_use::tests::create_1000_sandboxes --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::outb::tests::test_log_outb_log --exact --ignored - {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- mem::shared_mem::tests::test_drop --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --test integration_test -- log_message --exact --ignored @# metrics tests {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics,init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index cae355aae..e8bf91db3 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -1333,50 +1333,61 @@ mod tests { assert_eq!(data, ret_vec); } - /// A test to ensure that, if a `SharedMem` instance is cloned - /// and _all_ clones are dropped, the memory region will no longer - /// be valid. - /// - /// This test is ignored because it is incompatible with other tests as - /// they may be allocating memory at the same time. - /// - /// Marking this test as ignored means that running `cargo test` will not - /// run it. This feature will allow a developer who runs that command - /// from their workstation to be successful without needing to know about - /// test interdependencies. This test will, however, be run explicitly as a - /// part of the CI pipeline. + /// Test that verifies memory is properly unmapped when all SharedMemory + /// references are dropped. #[test] - #[ignore] - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", not(miri)))] fn test_drop() { - use proc_maps::maps_contain_addr; + use proc_maps::get_process_maps; + + // Use a unique size that no other test uses to avoid false positives + // from concurrent tests allocating at the same address. + // The mprotect calls split the mapping into 3 regions (guard, usable, guard), + // so we check for the usable region which has this exact size. + // + // NOTE: If this test fails intermittently, there may be a race condition + // where another test allocates memory at the same address between our + // drop and the mapping check. Ensure UNIQUE_SIZE is not used by any + // other test in the codebase to avoid this. + const UNIQUE_SIZE: usize = PAGE_SIZE_USIZE * 17; let pid = std::process::id(); - let eshm = ExclusiveSharedMemory::new(PAGE_SIZE_USIZE).unwrap(); + let eshm = ExclusiveSharedMemory::new(UNIQUE_SIZE).unwrap(); let (hshm1, gshm) = eshm.build(); let hshm2 = hshm1.clone(); - let addr = hshm1.raw_ptr() as usize; - // ensure the address is in the process's virtual memory - let maps_before_drop = proc_maps::get_process_maps(pid.try_into().unwrap()).unwrap(); + // Use the usable memory region (not raw), since mprotect splits the mapping + let base_ptr = hshm1.base_ptr() as usize; + let mem_size = hshm1.mem_size(); + + // Helper to check if exact mapping exists (matching both address and size) + let has_exact_mapping = |ptr: usize, size: usize| -> bool { + get_process_maps(pid.try_into().unwrap()) + .unwrap() + .iter() + .any(|m| m.start() == ptr && m.size() == size) + }; + + // Verify mapping exists before drop assert!( - maps_contain_addr(addr, &maps_before_drop), - "shared memory address {:#x} was not found in process map, but should be", - addr, + has_exact_mapping(base_ptr, mem_size), + "shared memory mapping not found at {:#x} with size {}", + base_ptr, + mem_size ); - // drop both shared memory instances, which should result - // in freeing the memory region + + // Drop all references drop(hshm1); drop(hshm2); drop(gshm); - let maps_after_drop = proc_maps::get_process_maps(pid.try_into().unwrap()).unwrap(); - // now, ensure the address is not in the process's virtual memory + // Verify exact mapping is gone assert!( - !maps_contain_addr(addr, &maps_after_drop), - "shared memory address {:#x} was found in the process map, but shouldn't be", - addr + !has_exact_mapping(base_ptr, mem_size), + "shared memory mapping still exists at {:#x} with size {} after drop", + base_ptr, + mem_size ); } From 663ed7a2ca92fee1ebdcbd6104e195b086efbc6c Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:03:50 -0800 Subject: [PATCH 2/7] Rename create_1000_sandboxes to create_200_sandboxes and make concurrent-safe Reduces thread count (20->10) and sandboxes per thread (50->20) so the test can run alongside other tests without exhausting system resources. Removes #[ignore] and the corresponding test-isolated entry in the Justfile. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- Justfile | 1 - .../src/sandbox/initialized_multi_use.rs | 41 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Justfile b/Justfile index 6bd3aabc9..b3f7edeb4 100644 --- a/Justfile +++ b/Justfile @@ -215,7 +215,6 @@ test-unit target=default-target features="": test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_trace_trace --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_log_trace --exact --ignored - {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::initialized_multi_use::tests::create_1000_sandboxes --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::outb::tests::test_log_outb_log --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --test integration_test -- log_message --exact --ignored @# metrics tests diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 138ebdaf6..9ea3221d2 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1119,41 +1119,38 @@ mod tests { } #[test] - #[ignore] // this test runs by itself because it uses a lot of system resources - fn create_1000_sandboxes() { - let barrier = Arc::new(Barrier::new(21)); + fn create_200_sandboxes() { + const NUM_THREADS: usize = 10; + const SANDBOXES_PER_THREAD: usize = 20; - let mut handles = vec![]; + // barrier to make sure all threads start their work simultaneously + let start_barrier = Arc::new(Barrier::new(NUM_THREADS + 1)); + let mut thread_handles = vec![]; - for _ in 0..20 { - let c = barrier.clone(); + for _ in 0..NUM_THREADS { + let barrier = start_barrier.clone(); let handle = thread::spawn(move || { - c.wait(); - - for _ in 0..50 { - let usbox = UninitializedSandbox::new( - GuestBinary::FilePath( - simple_guest_as_string().expect("Guest Binary Missing"), - ), - None, - ) - .unwrap(); + barrier.wait(); - let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap(); + for _ in 0..SANDBOXES_PER_THREAD { + let guest_path = simple_guest_as_string().expect("Guest Binary Missing"); + let uninit = + UninitializedSandbox::new(GuestBinary::FilePath(guest_path), None).unwrap(); - let res: i32 = multi_use_sandbox.call("GetStatic", ()).unwrap(); + let mut sandbox: MultiUseSandbox = uninit.evolve().unwrap(); - assert_eq!(res, 0); + let result: i32 = sandbox.call("GetStatic", ()).unwrap(); + assert_eq!(result, 0); } }); - handles.push(handle); + thread_handles.push(handle); } - barrier.wait(); + start_barrier.wait(); - for handle in handles { + for handle in thread_handles { handle.join().unwrap(); } } From 74d3c04d6930716ae0ac69eb014b5ceb9dc2a4dd Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:13:51 -0800 Subject: [PATCH 3/7] Allow execute_on_heap to execute concurrently Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- Justfile | 5 ++--- src/hyperlight_host/tests/integration_test.rs | 18 +++++++++--------- src/tests/rust_guests/simpleguest/src/main.rs | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Justfile b/Justfile index b3f7edeb4..2c7b153c0 100644 --- a/Justfile +++ b/Justfile @@ -221,9 +221,8 @@ test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics,init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact # runs integration tests. Guest can either be "rust" or "c" test-integration guest target=default-target features="": - @# run execute_on_heap test with feature "executable_heap" on and off - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {" --features executable_heap"} else {"--features executable_heap," + features} }} -- --ignored - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {""} else {"--features " + features} }} -- --ignored + @# run execute_on_heap test with feature "executable_heap" on (runs with off during normal tests) + {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {" --features executable_heap"} else {"--features executable_heap," + features} }} @# run the rest of the integration tests {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 083e690e3..ecbc747af 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -660,22 +660,22 @@ fn guard_page_check_2() { } #[test] -#[ignore] // ran from Justfile because requires feature "executable_heap" fn execute_on_heap() { let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); let result = sbox1.call::("ExecuteOnHeap", ()); - println!("{:#?}", result); #[cfg(feature = "executable_heap")] - assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + "Executed on heap successfully", + "should execute successfully" + ); #[cfg(not(feature = "executable_heap"))] - { - assert!(result.is_err()); - let err = result.unwrap_err(); - - assert!(err.to_string().contains("PageFault")); - } + assert!( + result.unwrap_err().to_string().contains("PageFault"), + "should get page fault" + ); } // checks that a recursive function with stack allocation eventually fails with stackoverflow diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 09f55212a..80f734224 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -451,7 +451,7 @@ fn execute_on_heap() -> String { black_box(heap_fn); // avoid optimization when running in release mode } // will only reach this point if heap is executable - String::from("fail") + String::from("Executed on heap successfully") } #[guest_function("TestMalloc")] From cf0bb44d771428d3e61fcaef3b5333398c0de898 Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:53:44 -0800 Subject: [PATCH 4/7] Make test_trace_trace concurrent-safe Use Interest::sometimes() in TracingSubscriber to prevent the global interest cache from permanently caching Interest::never() when another thread's no-op subscriber registers callsites first. Add a warmup call + rebuild_interest_cache() to ensure callsites are properly registered before the real test run. Also removes the now-unused build_metadata_testing module and TestLogger dependency. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- Justfile | 1 - .../src/sandbox/uninitialized.rs | 190 ++++++++++-------- src/hyperlight_host/src/testing/log_values.rs | 64 ------ .../src/tracing_subscriber.rs | 14 +- 4 files changed, 115 insertions(+), 154 deletions(-) diff --git a/Justfile b/Justfile index 2c7b153c0..0262b73fc 100644 --- a/Justfile +++ b/Justfile @@ -213,7 +213,6 @@ test-unit target=default-target features="": # runs tests that requires being run separately, for example due to global state test-isolated target=default-target features="" : - {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_trace_trace --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_log_trace --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::outb::tests::test_log_outb_log --exact --ignored {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --test integration_test -- log_message --exact --ignored diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index 4e13fa0d6..62b4315ec 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -761,115 +761,129 @@ mod tests { } } + /// Tests that tracing spans and events are properly emitted when a tracing subscriber is set. + /// + /// This test verifies: + /// 1. Spans are created with correct attributes (correlation_id) + /// 2. Nested spans from UninitializedSandbox::new are properly parented + /// 3. Error events are emitted when sandbox creation fails + /// + /// NOTE: The `#[instrument]` callsite on `UninitializedSandbox::new` uses + /// tracing's global interest cache. If another test thread registers that + /// callsite first (with the no-op subscriber), the cached `Interest::never()` + /// will suppress span creation on our thread. To work around this, we: + /// 1. Make a warmup call to force-register the callsite + /// 2. Call `rebuild_interest_cache()` to overwrite the cached interest with + /// our subscriber's `Interest::sometimes()` + /// 3. Clear recorded state and run the real test #[test] - // Tests that trace data are emitted when a trace subscriber is set - // this test is ignored because it is incompatible with other tests , specifically those which require a logger for tracing - // marking this test as ignored means that running `cargo test` will not run this test but will allow a developer who runs that command - // from their workstation to be successful without needed to know about test interdependencies - // this test will be run explicitly as a part of the CI pipeline - #[ignore] #[cfg(feature = "build-metadata")] fn test_trace_trace() { - use hyperlight_testing::logger::Logger as TestLogger; - use hyperlight_testing::tracing_subscriber::TracingSubscriber as TestSubscriber; - use serde_json::{Map, Value}; - use tracing::Level as tracing_level; + use hyperlight_testing::tracing_subscriber::TracingSubscriber; + use tracing::Level; use tracing_core::Subscriber; use tracing_core::callsite::rebuild_interest_cache; use uuid::Uuid; - use crate::testing::log_values::build_metadata_testing::try_to_strings; - use crate::testing::log_values::test_value_as_str; - - TestLogger::initialize_log_tracer(); - rebuild_interest_cache(); - let subscriber = TestSubscriber::new(tracing_level::TRACE); - tracing::subscriber::with_default(subscriber.clone(), || { - let correlation_id = Uuid::new_v4().as_hyphenated().to_string(); - let span = tracing::error_span!("test_trace_logs", correlation_id).entered(); - - // We should be in span 1 - - let current_span = subscriber.current_span(); - assert!(current_span.is_known(), "Current span is unknown"); - let current_span_metadata = current_span.into_inner().unwrap(); - assert_eq!( - current_span_metadata.0.into_u64(), - 1, - "Current span is not span 1" - ); - assert_eq!(current_span_metadata.1.name(), "test_trace_logs"); + /// Helper to extract a string value from nested JSON: obj["span"]["attributes"][key] + fn get_span_attr<'a>(span: &'a serde_json::Value, key: &str) -> Option<&'a str> { + span.get("span")?.get("attributes")?.get(key)?.as_str() + } - // Get the span data and check the correlation id + /// Helper to extract event field: obj["event"][field] + fn get_event_field<'a>(event: &'a serde_json::Value, field: &str) -> Option<&'a str> { + event.get("event")?.get(field)?.as_str() + } - let span_data = subscriber.get_span(1); - let span_attributes: &Map = span_data - .get("span") - .unwrap() - .get("attributes") - .unwrap() - .as_object() - .unwrap(); + /// Helper to extract event metadata field: obj["event"]["metadata"][field] + fn get_event_metadata<'a>(event: &'a serde_json::Value, field: &str) -> Option<&'a str> { + event.get("event")?.get("metadata")?.get(field)?.as_str() + } - test_value_as_str(span_attributes, "correlation_id", correlation_id.as_str()); + let subscriber = TracingSubscriber::new(Level::TRACE); - let mut binary_path = simple_guest_as_string().unwrap(); - binary_path.push_str("does_not_exist"); + tracing::subscriber::with_default(subscriber.clone(), || { + // Warmup: force-register the #[instrument] callsite on + // UninitializedSandbox::new by calling it once. This ensures the + // callsite exists in the global registry regardless of whether + // another thread already registered it. + let bad_path = simple_guest_as_string().unwrap() + "does_not_exist"; + let _ = UninitializedSandbox::new(GuestBinary::FilePath(bad_path.clone()), None); + + // Rebuild the interest cache. Now that the callsite is guaranteed + // to be registered, this will overwrite any cached Interest::never() + // (from another thread's no-op subscriber) with our subscriber's + // Interest::sometimes(), ensuring subsequent calls create spans. + rebuild_interest_cache(); - let sbox = UninitializedSandbox::new(GuestBinary::FilePath(binary_path), None); - assert!(sbox.is_err()); + // Clear all state from the warmup call + subscriber.clear(); - // Now we should still be in span 1 but span 2 should be created (we created entered and exited span 2 when we called UninitializedSandbox::new) + let correlation_id = Uuid::new_v4().to_string(); + let _span = tracing::error_span!("test_trace_logs", %correlation_id).entered(); + + // Verify we're in a span with correct name + let (test_span_id, span_meta) = subscriber + .current_span() + .into_inner() + .expect("Should be inside a span"); + assert_eq!(span_meta.name(), "test_trace_logs"); + + // Verify correlation_id was recorded + let span_data = subscriber.get_span(test_span_id.into_u64()); + let recorded_id = + get_span_attr(&span_data, "correlation_id").expect("correlation_id not found"); + assert_eq!(recorded_id, correlation_id); + + // Try to create a sandbox with a non-existent binary - this should fail + // and emit an error event + let result = UninitializedSandbox::new(GuestBinary::FilePath(bad_path), None); + assert!(result.is_err(), "Sandbox creation should fail"); + + // Verify we're still in our test span + let (current_id, _) = subscriber + .current_span() + .into_inner() + .expect("Should still be inside a span"); + assert_eq!( + current_id.into_u64(), + test_span_id.into_u64(), + "Should still be in the test span" + ); - let current_span = subscriber.current_span(); - assert!(current_span.is_known(), "Current span is unknown"); - let current_span_metadata = current_span.into_inner().unwrap(); + // Verify a span named "new" was created by UninitializedSandbox::new + // (look up by name rather than hardcoded ID to avoid fragility) + let all_spans = subscriber.get_all_spans(); + let new_span_entry = all_spans + .iter() + .find(|&(&id, _)| { + id != test_span_id.into_u64() + && subscriber.get_span_metadata(id).name() == "new" + }) + .expect("Expected a span named 'new' from UninitializedSandbox::new"); assert_eq!( - current_span_metadata.0.into_u64(), - 1, - "Current span is not span 1" + subscriber.get_span_metadata(*new_span_entry.0).name(), + "new" ); - let span_metadata = subscriber.get_span_metadata(2); - assert_eq!(span_metadata.name(), "new"); + // Verify the error event was emitted + let events = subscriber.get_events(); + assert_eq!(events.len(), 1, "Expected exactly one error event"); - // There should be one event for the error that the binary path does not exist plus 14 info events for the logging of the crate info + let event = &events[0]; + let level = get_event_metadata(event, "level").expect("event should have level"); + let error = get_event_field(event, "error").expect("event should have error field"); + let target = get_event_metadata(event, "target").expect("event should have target"); + let module_path = + get_event_metadata(event, "module_path").expect("event should have module_path"); - let events = subscriber.get_events(); - assert_eq!(events.len(), 1); - - let mut count_matching_events = 0; - - for json_value in events { - let event_values = json_value.as_object().unwrap().get("event").unwrap(); - let metadata_values_map = - event_values.get("metadata").unwrap().as_object().unwrap(); - let event_values_map = event_values.as_object().unwrap(); - - let expected_error_start = "Error(\"GuestBinary not found:"; - - let err_vals_res = try_to_strings([ - (metadata_values_map, "level"), - (event_values_map, "error"), - (metadata_values_map, "module_path"), - (metadata_values_map, "target"), - ]); - if let Ok(err_vals) = err_vals_res - && err_vals[0] == "ERROR" - && err_vals[1].starts_with(expected_error_start) - && err_vals[2] == "hyperlight_host::sandbox::uninitialized" - && err_vals[3] == "hyperlight_host::sandbox::uninitialized" - { - count_matching_events += 1; - } - } + assert_eq!(level, "ERROR"); assert!( - count_matching_events == 1, - "Unexpected number of matching events {}", - count_matching_events + error.contains("GuestBinary not found"), + "Error should mention 'GuestBinary not found', got: {error}" ); - span.exit(); - subscriber.clear(); + assert_eq!(target, "hyperlight_host::sandbox::uninitialized"); + assert_eq!(module_path, "hyperlight_host::sandbox::uninitialized"); }); } diff --git a/src/hyperlight_host/src/testing/log_values.rs b/src/hyperlight_host/src/testing/log_values.rs index 049010459..47f40ae0a 100644 --- a/src/hyperlight_host/src/testing/log_values.rs +++ b/src/hyperlight_host/src/testing/log_values.rs @@ -60,67 +60,3 @@ fn try_to_string<'a>(values: &'a Map, key: &'a str) -> Result<&'a Err(new_error!("value for key {} was not found", key)) } } - -#[cfg(feature = "build-metadata")] -pub(crate) mod build_metadata_testing { - use super::*; - - /// A single value in the parameter list for the `try_to_strings` - /// function. - pub(crate) type MapLookup<'a> = (&'a Map, &'a str); - - /// Given a constant-size slice of `MapLookup`s, attempt to look up the - /// string value in each `MapLookup`'s map (the first tuple element) for - /// that `MapLookup`'s key (the second tuple element). If the lookup - /// succeeded, attempt to convert the resulting value to a string. Return - /// `Ok` with all the successfully looked-up string values, or `Err` - /// if any one or more lookups or string conversions failed. - pub(crate) fn try_to_strings<'a, const NUM: usize>( - lookups: [MapLookup<'a>; NUM], - ) -> Result<[&'a str; NUM]> { - // Note (from arschles) about this code: - // - // In theory, there's a way to write this function in the functional - // programming (FP) style -- e.g. with a fold, map, flat_map, or - // something similar -- and without any mutability. - // - // In practice, however, since we're taking in a statically-sized slice, - // and we are expected to return a statically-sized slice of the same - // size, we are more limited in what we can do. There is a way to design - // a fold or flat_map to iterate over the lookups parameter and attempt to - // transform each MapLookup into the string value at that key. - // - // I wrote that code, which I'll called the "FP code" hereafter, and - // noticed two things: - // - // - It required several places where I had to explicitly deal with long - // and complex (in my opinion) types - // - It wasn't much more succinct or shorter than the code herein - // - // The FP code is functionally "pure" and maybe fun to write (if you like - // Rust or you love FP), but not fun to read. In fact, because of all the - // explicit type ceremony, I bet it'd make even the most hardcore Haskell - // programmer blush. - // - // So, I've decided to use a little bit of mutability to implement this - // function in a way I think most programmers would agree is easier to - // reason about and understand quickly. - // - // Final performance note: - // - // It's likely, but not certain, that the FP code is probably not - // significantly more memory efficient than this, since the compiler knows - // the size of both the input and output slices. Plus, this is test code, - // so even if this were 2x slower, I'd still argue the ease of - // understanding is more valuable than the (relatively small) memory - // savings. - let mut ret_slc: [&'a str; NUM] = [""; NUM]; - for (idx, lookup) in lookups.iter().enumerate() { - let map = lookup.0; - let key = lookup.1; - let val = try_to_string(map, key)?; - ret_slc[idx] = val - } - Ok(ret_slc) - } -} diff --git a/src/hyperlight_testing/src/tracing_subscriber.rs b/src/hyperlight_testing/src/tracing_subscriber.rs index 002a3c3c4..7046e4d7d 100644 --- a/src/hyperlight_testing/src/tracing_subscriber.rs +++ b/src/hyperlight_testing/src/tracing_subscriber.rs @@ -22,7 +22,7 @@ use tracing::Subscriber; use tracing_core::event::Event; use tracing_core::metadata::Metadata; use tracing_core::span::{Attributes, Current, Id, Record}; -use tracing_core::{Level, LevelFilter}; +use tracing_core::{Interest, Level, LevelFilter}; use tracing_serde::AsSerde; #[derive(Debug, Clone)] @@ -69,6 +69,11 @@ impl TracingSubscriber { EVENTS.with(|events| events.borrow().clone()) } + /// Returns all recorded spans as a HashMap + pub fn get_all_spans(&self) -> HashMap { + SPANS.with(|spans| spans.borrow().clone()) + } + pub fn test_trace_records, &Vec)>(&self, f: F) { SPANS.with(|spans| { EVENTS.with(|events| { @@ -88,6 +93,13 @@ impl TracingSubscriber { } impl Subscriber for TracingSubscriber { + fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest { + // Return Interest::sometimes() to prevent the global interest cache from caching + // our decision. This avoids race conditions when running tests in parallel, + // since each call to enabled() will be re-evaluated per-thread. + Interest::sometimes() + } + fn enabled(&self, metadata: &Metadata<'_>) -> bool { LEVEL_FILTER.with(|level_filter| metadata.level() <= &*level_filter.borrow()) } From 1e0f7122b6b9325b875cc06b886c2fab6b193829 Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:30:01 -0800 Subject: [PATCH 5/7] Remove GUEST env var and refactor test helpers Replace the GUEST environment variable approach with explicit helper functions (with_rust_sandbox, with_c_sandbox, with_all_sandboxes, etc.) that make test intent clearer and remove runtime environment coupling. Each test now explicitly declares which guest(s) it needs. Tests that work with both guests use with_all_sandboxes to run against both Rust and C guests in a single test invocation, removing the need for separate test-integration runs per guest language. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- Justfile | 11 +- src/hyperlight_host/tests/common/mod.rs | 198 ++-- src/hyperlight_host/tests/integration_test.rs | 961 +++++++++--------- .../tests/sandbox_host_tests.rs | 170 ++-- src/tests/rust_guests/dummyguest/Cargo.lock | 4 +- src/tests/rust_guests/simpleguest/Cargo.lock | 4 +- src/tests/rust_guests/witguest/Cargo.lock | 4 +- 7 files changed, 680 insertions(+), 672 deletions(-) diff --git a/Justfile b/Justfile index 0262b73fc..b4ae6c0e7 100644 --- a/Justfile +++ b/Justfile @@ -205,7 +205,7 @@ like-ci config=default-target hypervisor="kvm": just check-license-headers # runs all tests -test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration "rust" target features) (test-integration "c" target features) (test-doc target features) +test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration target features) (test-doc target features) # runs unit tests test-unit target=default-target features="": @@ -218,13 +218,14 @@ test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --test integration_test -- log_message --exact --ignored @# metrics tests {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics,init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact -# runs integration tests. Guest can either be "rust" or "c" -test-integration guest 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) - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {" --features executable_heap"} else {"--features executable_heap," + features} }} + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {" --features executable_heap"} else {"--features executable_heap," + features} }} @# run the rest of the integration tests - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' + {{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' # tests compilation with no default features on different platforms test-compilation-no-default-features target=default-target: diff --git a/src/hyperlight_host/tests/common/mod.rs b/src/hyperlight_host/tests/common/mod.rs index ab58237b8..bd1000e79 100644 --- a/src/hyperlight_host/tests/common/mod.rs +++ b/src/hyperlight_host/tests/common/mod.rs @@ -13,100 +13,136 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + use hyperlight_host::func::HostFunction; -#[cfg(gdb)] -use hyperlight_host::sandbox::config::DebugInfo; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result, UninitializedSandbox}; +use hyperlight_host::sandbox::SandboxConfiguration; +use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::{c_simple_guest_as_string, simple_guest_as_string}; -/// Returns a rust/c simpleguest depending on environment variable GUEST. -/// Uses rust guest by default. Run test with environment variable GUEST="c" to use the c version -/// If a test is only applicable to rust, use `new_uninit_rust`` instead -pub fn new_uninit() -> Result { - UninitializedSandbox::new( - GuestBinary::FilePath(get_c_or_rust_simpleguest_path()), - None, - ) +/// Returns the path to the Rust simple guest binary. +fn rust_guest_path() -> String { + simple_guest_as_string().unwrap() } -/// Use this instead of the `new_uninit` if you want your test to only run with the rust guest, not the c guest -pub fn new_uninit_rust() -> Result { - #[cfg(gdb)] - { - use hyperlight_host::sandbox::SandboxConfiguration; - let mut cfg = SandboxConfiguration::default(); - let debug_info = DebugInfo { port: 8080 }; - cfg.set_guest_debug_info(debug_info); - - UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - Some(cfg), - ) - } +/// Returns the path to the C simple guest binary. +fn c_guest_path() -> String { + c_simple_guest_as_string().unwrap() +} + +/// Creates a new Rust guest MultiUseSandbox. +pub fn new_rust_sandbox() -> MultiUseSandbox { + UninitializedSandbox::new(GuestBinary::FilePath(rust_guest_path()), None) + .unwrap() + .evolve() + .unwrap() +} + +/// Creates a new Rust guest UninitializedSandbox. +pub fn new_rust_uninit_sandbox() -> UninitializedSandbox { + UninitializedSandbox::new(GuestBinary::FilePath(rust_guest_path()), None).unwrap() +} + +// ============================================================================= +// Rust guest helpers +// ============================================================================= + +/// Runs a test with a Rust guest MultiUseSandbox. +pub fn with_rust_sandbox(f: F) +where + F: FnOnce(MultiUseSandbox), +{ + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(rust_guest_path()), None) + .unwrap() + .evolve() + .unwrap(); + f(sandbox); +} - #[cfg(not(gdb))] - UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - None, - ) +/// Runs a test with a Rust guest MultiUseSandbox using custom configuration. +pub fn with_rust_sandbox_cfg(cfg: SandboxConfiguration, f: F) +where + F: FnOnce(MultiUseSandbox), +{ + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(rust_guest_path()), Some(cfg)) + .unwrap() + .evolve() + .unwrap(); + f(sandbox); } -/// Returns a c-language simpleguest. -pub fn new_uninit_c() -> Result { - UninitializedSandbox::new( - GuestBinary::FilePath(c_simple_guest_as_string().unwrap()), - None, - ) +/// Runs a test with a Rust guest UninitializedSandbox. +pub fn with_rust_uninit_sandbox(f: F) +where + F: FnOnce(UninitializedSandbox), +{ + let sandbox = + UninitializedSandbox::new(GuestBinary::FilePath(rust_guest_path()), None).unwrap(); + f(sandbox); } -pub fn get_simpleguest_sandboxes( - writer: Option>, // An optional writer to make sure correct info is passed to the host printer -) -> Vec { - let elf_path = get_c_or_rust_simpleguest_path(); - - let sandboxes = [ - // in hypervisor elf - UninitializedSandbox::new(GuestBinary::FilePath(elf_path.clone()), None).unwrap(), - ]; - - sandboxes - .into_iter() - .map(|mut sandbox| { - if let Some(writer) = writer.clone() { - sandbox.register_print(writer).unwrap(); - } - sandbox.evolve().unwrap() - }) - .collect() +// ============================================================================= +// C guest helpers +// ============================================================================= + +/// Runs a test with a C guest MultiUseSandbox. +pub fn with_c_sandbox(f: F) +where + F: FnOnce(MultiUseSandbox), +{ + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(c_guest_path()), None) + .unwrap() + .evolve() + .unwrap(); + f(sandbox); } -pub fn get_uninit_simpleguest_sandboxes( - writer: Option>, // An optional writer to make sure correct info is passed to the host printer -) -> Vec { - let elf_path = get_c_or_rust_simpleguest_path(); - - let sandboxes = [ - // in hypervisor elf - UninitializedSandbox::new(GuestBinary::FilePath(elf_path.clone()), None).unwrap(), - ]; - - sandboxes - .into_iter() - .map(|mut sandbox| { - if let Some(writer) = writer.clone() { - sandbox.register_print(writer).unwrap(); - } - sandbox - }) - .collect() +/// Runs a test with a C guest UninitializedSandbox. +pub fn with_c_uninit_sandbox(f: F) +where + F: FnOnce(UninitializedSandbox), +{ + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(c_guest_path()), None).unwrap(); + f(sandbox); +} + +// ============================================================================= +// Both guests helpers (run test with Rust AND C guests) +// ============================================================================= + +/// Runs a test with both Rust and C guest MultiUseSandboxes. +pub fn with_all_sandboxes(f: F) +where + F: Fn(MultiUseSandbox), +{ + for path in [rust_guest_path(), c_guest_path()] { + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None) + .unwrap() + .evolve() + .unwrap(); + f(sandbox); + } +} + +/// Runs a test with both Rust and C guest UninitializedSandboxes. +pub fn with_all_uninit_sandboxes(f: F) +where + F: Fn(UninitializedSandbox), +{ + for path in [rust_guest_path(), c_guest_path()] { + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + f(sandbox); + } } -// returns the the path of simpleguest binary. Picks rust/c version depending on environment variable GUEST (or rust by default if unset) -pub(crate) fn get_c_or_rust_simpleguest_path() -> String { - let guest_type = std::env::var("GUEST").unwrap_or("rust".to_string()); - match guest_type.as_str() { - "rust" => simple_guest_as_string().unwrap(), - "c" => c_simple_guest_as_string().unwrap(), - _ => panic!("Unknown guest type '{guest_type}', use either 'rust' or 'c'"), +/// Runs a test with both Rust and C guest MultiUseSandboxes, with a print writer. +pub fn with_all_sandboxes_with_writer(writer: HostFunction, f: F) +where + F: Fn(MultiUseSandbox), +{ + for path in [rust_guest_path(), c_guest_path()] { + let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + sandbox.register_print(writer.clone()).unwrap(); + let sandbox = sandbox.evolve().unwrap(); + f(sandbox); } } diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index ecbc747af..355919d9c 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -21,130 +21,130 @@ use std::time::Duration; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_host::sandbox::SandboxConfiguration; -use hyperlight_host::{GuestBinary, HyperlightError, MultiUseSandbox, UninitializedSandbox}; +use hyperlight_host::{HyperlightError, MultiUseSandbox}; use hyperlight_testing::simplelogger::{LOGGER, SimpleLogger}; -use hyperlight_testing::{c_simple_guest_as_string, simple_guest_as_string}; use log::LevelFilter; use serial_test::serial; pub mod common; // pub to disable dead_code warning -use crate::common::{new_uninit, new_uninit_c, new_uninit_rust}; +use crate::common::{ + new_rust_sandbox, new_rust_uninit_sandbox, with_all_sandboxes, with_c_sandbox, + with_c_uninit_sandbox, with_rust_sandbox, with_rust_sandbox_cfg, with_rust_uninit_sandbox, +}; // A host function cannot be interrupted, but we can at least make sure after requesting to interrupt a host call, // we don't re-enter the guest again once the host call is done #[test] fn interrupt_host_call() { - let barrier = Arc::new(Barrier::new(2)); - let barrier2 = barrier.clone(); + with_rust_uninit_sandbox(|mut usbox| { + let barrier = Arc::new(Barrier::new(2)); + let barrier2 = barrier.clone(); - let mut usbox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), - None, - ) - .unwrap(); - - let spin = move || { - barrier2.wait(); - thread::sleep(std::time::Duration::from_secs(1)); - Ok(()) - }; + let spin = move || { + barrier2.wait(); + thread::sleep(std::time::Duration::from_secs(1)); + Ok(()) + }; - usbox.register("Spin", spin).unwrap(); + usbox.register("Spin", spin).unwrap(); - let mut sandbox: MultiUseSandbox = usbox.evolve().unwrap(); - let snapshot = sandbox.snapshot().unwrap(); - let interrupt_handle = sandbox.interrupt_handle(); - assert!(!interrupt_handle.dropped()); // not yet dropped + let mut sandbox: MultiUseSandbox = usbox.evolve().unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + let interrupt_handle = sandbox.interrupt_handle(); + assert!(!interrupt_handle.dropped()); // not yet dropped - let thread = thread::spawn({ - move || { - barrier.wait(); // wait for the host function to be entered - interrupt_handle.kill(); // send kill once host call is in progress - } - }); + let thread = thread::spawn({ + move || { + barrier.wait(); // wait for the host function to be entered + interrupt_handle.kill(); // send kill once host call is in progress + } + }); - let result = sandbox.call::("CallHostSpin", ()).unwrap_err(); - assert!(matches!(result, HyperlightError::ExecutionCanceledByHost())); - assert!(sandbox.poisoned()); + let result = sandbox.call::("CallHostSpin", ()).unwrap_err(); + assert!(matches!(result, HyperlightError::ExecutionCanceledByHost())); + assert!(sandbox.poisoned()); - // Restore from snapshot to clear poison - sandbox.restore(snapshot.clone()).unwrap(); - assert!(!sandbox.poisoned()); + // Restore from snapshot to clear poison + sandbox.restore(snapshot.clone()).unwrap(); + assert!(!sandbox.poisoned()); - thread.join().unwrap(); + thread.join().unwrap(); + }); } /// Makes sure a running guest call can be interrupted by the host #[test] fn interrupt_in_progress_guest_call() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); - let snapshot = sbox1.snapshot().unwrap(); - let barrier = Arc::new(Barrier::new(2)); - let barrier2 = barrier.clone(); - let interrupt_handle = sbox1.interrupt_handle(); - assert!(!interrupt_handle.dropped()); // not yet dropped - - // kill vm after 1 second - let thread = thread::spawn(move || { - thread::sleep(Duration::from_secs(1)); - assert!(interrupt_handle.kill()); - barrier2.wait(); // wait here until main thread has returned from the interrupted guest call - barrier2.wait(); // wait here until main thread has dropped the sandbox - assert!(interrupt_handle.dropped()); - }); + with_rust_sandbox(|mut sbox1| { + let snapshot = sbox1.snapshot().unwrap(); + let barrier = Arc::new(Barrier::new(2)); + let barrier2 = barrier.clone(); + let interrupt_handle = sbox1.interrupt_handle(); + assert!(!interrupt_handle.dropped()); // not yet dropped + + // kill vm after 1 second + let thread = thread::spawn(move || { + thread::sleep(Duration::from_secs(1)); + assert!(interrupt_handle.kill()); + barrier2.wait(); // wait here until main thread has returned from the interrupted guest call + barrier2.wait(); // wait here until main thread has dropped the sandbox + assert!(interrupt_handle.dropped()); + }); - let res = sbox1.call::("Spin", ()).unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); - assert!(sbox1.poisoned()); + let res = sbox1.call::("Spin", ()).unwrap_err(); + assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!(sbox1.poisoned()); - // Restore from snapshot to clear poison - sbox1.restore(snapshot.clone()).unwrap(); - assert!(!sbox1.poisoned()); + // Restore from snapshot to clear poison + sbox1.restore(snapshot.clone()).unwrap(); + assert!(!sbox1.poisoned()); - barrier.wait(); - // Make sure we can still call guest functions after the VM was interrupted - sbox1.call::("Echo", "hello".to_string()).unwrap(); + barrier.wait(); + // Make sure we can still call guest functions after the VM was interrupted + sbox1.call::("Echo", "hello".to_string()).unwrap(); - // drop vm to make sure other thread can detect it - drop(sbox1); - barrier.wait(); - thread.join().expect("Thread should finish"); + // drop vm to make sure other thread can detect it + drop(sbox1); + barrier.wait(); + thread.join().expect("Thread should finish"); + }); } /// Makes sure interrupting a vm before the guest call has started does not prevent the guest call from running #[test] fn interrupt_guest_call_in_advance() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); - let barrier = Arc::new(Barrier::new(2)); - let barrier2 = barrier.clone(); - let interrupt_handle = sbox1.interrupt_handle(); - assert!(!interrupt_handle.dropped()); // not yet dropped - - // kill vm before the guest call has started - let thread = thread::spawn(move || { - assert!(!interrupt_handle.kill()); // should return false since vcpu is not running yet - barrier2.wait(); - barrier2.wait(); // wait here until main thread has dropped the sandbox - assert!(interrupt_handle.dropped()); - }); + with_rust_sandbox(|mut sbox1| { + let barrier = Arc::new(Barrier::new(2)); + let barrier2 = barrier.clone(); + let interrupt_handle = sbox1.interrupt_handle(); + assert!(!interrupt_handle.dropped()); // not yet dropped + + // kill vm before the guest call has started + let thread = thread::spawn(move || { + assert!(!interrupt_handle.kill()); // should return false since vcpu is not running yet + barrier2.wait(); + barrier2.wait(); // wait here until main thread has dropped the sandbox + assert!(interrupt_handle.dropped()); + }); - barrier.wait(); // wait until `kill()` is called before starting the guest call - match sbox1.call::("Echo", "hello".to_string()) { - Ok(_) => {} - Err(HyperlightError::ExecutionCanceledByHost()) => { - panic!("Unexpected Cancellation Error"); + barrier.wait(); // wait until `kill()` is called before starting the guest call + match sbox1.call::("Echo", "hello".to_string()) { + Ok(_) => {} + Err(HyperlightError::ExecutionCanceledByHost()) => { + panic!("Unexpected Cancellation Error"); + } + Err(_) => {} } - Err(_) => {} - } - // Make sure we can still call guest functions after the VM was interrupted early - // i.e. make sure we dont kill the next iteration. - sbox1.call::("Echo", "hello".to_string()).unwrap(); + // Make sure we can still call guest functions after the VM was interrupted early + // i.e. make sure we dont kill the next iteration. + sbox1.call::("Echo", "hello".to_string()).unwrap(); - // drop vm to make sure other thread can detect it - drop(sbox1); - barrier.wait(); - thread.join().expect("Thread should finish"); + // drop vm to make sure other thread can detect it + drop(sbox1); + barrier.wait(); + thread.join().expect("Thread should finish"); + }); } /// Verifies that only the intended sandbox (`sbox2`) is interruptible, @@ -158,10 +158,10 @@ fn interrupt_guest_call_in_advance() { /// all possible interleavings, but can hopefully increases confidence somewhat. #[test] fn interrupt_same_thread() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); - let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox1: MultiUseSandbox = new_rust_sandbox(); + let mut sbox2: MultiUseSandbox = new_rust_sandbox(); let snapshot2 = sbox2.snapshot().unwrap(); - let mut sbox3: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox3: MultiUseSandbox = new_rust_sandbox(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); @@ -203,10 +203,10 @@ fn interrupt_same_thread() { /// Same test as above but with no per-iteration barrier, to get more possible interleavings. #[test] fn interrupt_same_thread_no_barrier() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); - let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox1: MultiUseSandbox = new_rust_sandbox(); + let mut sbox2: MultiUseSandbox = new_rust_sandbox(); let snapshot2 = sbox2.snapshot().unwrap(); - let mut sbox3: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox3: MultiUseSandbox = new_rust_sandbox(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); @@ -252,9 +252,9 @@ fn interrupt_same_thread_no_barrier() { // and that anther sandbox on the original thread does not get incorrectly killed #[test] fn interrupt_moved_sandbox() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox1: MultiUseSandbox = new_rust_sandbox(); let snapshot1 = sbox1.snapshot().unwrap(); - let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox2: MultiUseSandbox = new_rust_sandbox(); let interrupt_handle = sbox1.interrupt_handle(); let interrupt_handle2 = sbox2.interrupt_handle(); @@ -300,126 +300,113 @@ fn interrupt_custom_signal_no_and_retry_delay() { config.set_interrupt_vcpu_sigrtmin_offset(0).unwrap(); config.set_interrupt_retry_delay(Duration::from_secs(1)); - let mut sbox1: MultiUseSandbox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - Some(config), - ) - .unwrap() - .evolve() - .unwrap(); + with_rust_sandbox_cfg(config, |mut sbox1| { + let snapshot1 = sbox1.snapshot().unwrap(); + let interrupt_handle = sbox1.interrupt_handle(); + assert!(!interrupt_handle.dropped()); // not yet dropped - let snapshot1 = sbox1.snapshot().unwrap(); - let interrupt_handle = sbox1.interrupt_handle(); - assert!(!interrupt_handle.dropped()); // not yet dropped + const NUM_ITERS: usize = 3; - const NUM_ITERS: usize = 3; + let thread = thread::spawn(move || { + for _ in 0..NUM_ITERS { + // wait for the guest call to start + thread::sleep(Duration::from_millis(3000)); + assert!(interrupt_handle.kill()); + } + }); - let thread = thread::spawn(move || { for _ in 0..NUM_ITERS { - // wait for the guest call to start - thread::sleep(Duration::from_millis(3000)); - assert!(interrupt_handle.kill()); + let res = sbox1.call::("Spin", ()).unwrap_err(); + assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!(sbox1.poisoned()); + // immediately reenter another guest function call after having being cancelled, + // so that the vcpu is running again before the interruptor-thread has a chance to see that the vcpu is not running + sbox1.restore(snapshot1.clone()).unwrap(); + assert!(!sbox1.poisoned()); } + thread.join().expect("Thread should finish"); }); - - for _ in 0..NUM_ITERS { - let res = sbox1.call::("Spin", ()).unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); - assert!(sbox1.poisoned()); - // immediately reenter another guest function call after having being cancelled, - // so that the vcpu is running again before the interruptor-thread has a chance to see that the vcpu is not running - sbox1.restore(snapshot1.clone()).unwrap(); - assert!(!sbox1.poisoned()); - } - thread.join().expect("Thread should finish"); } #[test] fn interrupt_spamming_host_call() { - let mut uninit = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - None, - ) - .unwrap(); - - uninit - .register("HostFunc1", || { - // do nothing - }) - .unwrap(); - let mut sbox1: MultiUseSandbox = uninit.evolve().unwrap(); + with_rust_uninit_sandbox(|mut uninit| { + uninit + .register("HostFunc1", || { + // do nothing + }) + .unwrap(); + let mut sbox1: MultiUseSandbox = uninit.evolve().unwrap(); - let interrupt_handle = sbox1.interrupt_handle(); + let interrupt_handle = sbox1.interrupt_handle(); - let barrier = Arc::new(Barrier::new(2)); - let barrier2 = barrier.clone(); + let barrier = Arc::new(Barrier::new(2)); + let barrier2 = barrier.clone(); - let thread = thread::spawn(move || { - barrier2.wait(); - thread::sleep(Duration::from_secs(1)); - interrupt_handle.kill(); - }); + let thread = thread::spawn(move || { + barrier2.wait(); + thread::sleep(Duration::from_secs(1)); + interrupt_handle.kill(); + }); - barrier.wait(); - // This guest call calls "HostFunc1" in a loop - let res = sbox1 - .call::("HostCallLoop", "HostFunc1".to_string()) - .unwrap_err(); + barrier.wait(); + // This guest call calls "HostFunc1" in a loop + let res = sbox1 + .call::("HostCallLoop", "HostFunc1".to_string()) + .unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); - thread.join().expect("Thread should finish"); + thread.join().expect("Thread should finish"); + }); } #[test] fn print_four_args_c_guest() { - let path = c_simple_guest_as_string().unwrap(); - let guest_path = GuestBinary::FilePath(path); - let uninit = UninitializedSandbox::new(guest_path, None); - let mut sbox1 = uninit.unwrap().evolve().unwrap(); - - let res = sbox1.call::( - "PrintFourArgs", - ("Test4".to_string(), 3_i32, 4_i64, "Tested".to_string()), - ); - println!("{:?}", res); - assert!(matches!(res, Ok(46))); + with_c_sandbox(|mut sbox1| { + let res = sbox1.call::( + "PrintFourArgs", + ("Test4".to_string(), 3_i32, 4_i64, "Tested".to_string()), + ); + println!("{:?}", res); + assert!(matches!(res, Ok(46))); + }); } // Checks that guest can abort with a specific code. #[test] fn guest_abort() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - let error_code: u8 = 13; // this is arbitrary - let res = sbox1 - .call::<()>("GuestAbortWithCode", error_code as i32) - .unwrap_err(); - println!("{:?}", res); - assert!( - matches!(res, HyperlightError::GuestAborted(code, message) if (code == error_code && message.is_empty())) - ); + with_all_sandboxes(|mut sbox1| { + let error_code: u8 = 13; // this is arbitrary + let res = sbox1 + .call::<()>("GuestAbortWithCode", error_code as i32) + .unwrap_err(); + println!("{:?}", res); + assert!( + matches!(res, HyperlightError::GuestAborted(code, message) if (code == error_code && message.is_empty())) + ); + }); } #[test] fn guest_abort_with_context1() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - - let res = sbox1 - .call::<()>("GuestAbortWithMessage", (25_i32, "Oh no".to_string())) - .unwrap_err(); - println!("{:?}", res); - assert!( - matches!(res, HyperlightError::GuestAborted(code, context) if (code == 25 && context == "Oh no")) - ); + with_all_sandboxes(|mut sbox1| { + let res = sbox1 + .call::<()>("GuestAbortWithMessage", (25_i32, "Oh no".to_string())) + .unwrap_err(); + println!("{:?}", res); + assert!( + matches!(res, HyperlightError::GuestAborted(code, context) if (code == 25 && context == "Oh no")) + ); + }); } #[test] fn guest_abort_with_context2() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - - // The buffer size for the panic context is 1024 bytes. - // This test will see what happens if the panic message is longer than that - let abort_message = "Lorem ipsum dolor sit amet, \ + with_all_sandboxes(|mut sbox1| { + // The buffer size for the panic context is 1024 bytes. + // This test will see what happens if the panic message is longer than that + let abort_message = "Lorem ipsum dolor sit amet, \ consectetur adipiscing elit, \ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ Nec feugiat nisl pretium fusce. \ @@ -449,13 +436,14 @@ fn guest_abort_with_context2() { Arcu felis bibendum ut tristique et. \ Proin sagittis nisl rhoncus mattis rhoncus urna. Magna eget est lorem ipsum."; - let res = sbox1 - .call::<()>("GuestAbortWithMessage", (60_i32, abort_message.to_string())) - .unwrap_err(); - println!("{:?}", res); - assert!( - matches!(res, HyperlightError::GuestAborted(_, context) if context.contains("Guest abort buffer overflowed")) - ); + let res = sbox1 + .call::<()>("GuestAbortWithMessage", (60_i32, abort_message.to_string())) + .unwrap_err(); + println!("{:?}", res); + assert!( + matches!(res, HyperlightError::GuestAborted(_, context) if context.contains("Guest abort buffer overflowed")) + ); + }); } // Ensure abort with context works for c guests. @@ -463,74 +451,71 @@ fn guest_abort_with_context2() { // hopefully be removing the c guest library soon. #[test] fn guest_abort_c_guest() { - let path = c_simple_guest_as_string().unwrap(); - let guest_path = GuestBinary::FilePath(path); - let uninit = UninitializedSandbox::new(guest_path, None); - let mut sbox1 = uninit.unwrap().evolve().unwrap(); - - let res = sbox1 - .call::<()>( - "GuestAbortWithMessage", - (75_i32, "This is a test error message".to_string()), - ) - .unwrap_err(); - println!("{:?}", res); - assert!( - matches!(res, HyperlightError::GuestAborted(code, message) if (code == 75 && message == "This is a test error message") ) - ); + with_c_sandbox(|mut sbox1| { + let res = sbox1 + .call::<()>( + "GuestAbortWithMessage", + (75_i32, "This is a test error message".to_string()), + ) + .unwrap_err(); + println!("{:?}", res); + assert!( + matches!(res, HyperlightError::GuestAborted(code, message) if (code == 75 && message == "This is a test error message") ) + ); + }); } #[test] fn guest_panic() { // this test is rust-specific - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - - let res = sbox1 - .call::<()>("guest_panic", "Error... error...".to_string()) - .unwrap_err(); - println!("{:?}", res); - assert!( - matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("\nError... error...")) - ) + with_rust_sandbox(|mut sbox1| { + let res = sbox1 + .call::<()>("guest_panic", "Error... error...".to_string()) + .unwrap_err(); + println!("{:?}", res); + assert!( + matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("\nError... error...")) + ); + }); } #[test] fn guest_malloc() { // this test is rust-only - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - - let size_to_allocate = 2000_i32; - sbox1.call::("TestMalloc", size_to_allocate).unwrap(); + with_rust_sandbox(|mut sbox1| { + let size_to_allocate = 2000_i32; + sbox1.call::("TestMalloc", size_to_allocate).unwrap(); + }); } #[test] fn guest_allocate_vec() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - - let size_to_allocate = 2000_i32; - - let res = sbox1 - .call::( - "CallMalloc", // uses the rust allocator to allocate a vector on heap - size_to_allocate, - ) - .unwrap(); + with_all_sandboxes(|mut sbox1| { + let size_to_allocate = 2000_i32; + + let res = sbox1 + .call::( + "CallMalloc", // uses the rust allocator to allocate a vector on heap + size_to_allocate, + ) + .unwrap(); - assert_eq!(res, size_to_allocate); + assert_eq!(res, size_to_allocate); + }); } // checks that malloc failures are captured correctly #[test] fn guest_malloc_abort() { - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - - let size = 20000000_i32; // some big number that should fail when allocated + 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(); - println!("{:?}", res); - assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) - ); + let res = sbox1.call::("TestMalloc", size).unwrap_err(); + println!("{:?}", res); + assert!( + matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + ); + }); // allocate a vector (on heap) that is bigger than the heap let heap_size = 0x4000; @@ -539,39 +524,34 @@ fn guest_malloc_abort() { let mut cfg = SandboxConfiguration::default(); cfg.set_heap_size(heap_size); - let uninit = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - Some(cfg), - ) - .unwrap(); - let mut sbox2 = uninit.evolve().unwrap(); - - let res = sbox2.call::( - "CallMalloc", // uses the rust allocator to allocate a vector on heap - size_to_allocate as i32, - ); - println!("{:?}", res); - assert!(matches!( - res.unwrap_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 ") - )); + with_rust_sandbox_cfg(cfg, |mut sbox2| { + let res = sbox2.call::( + "CallMalloc", // uses the rust allocator to allocate a vector on heap + size_to_allocate as i32, + ); + println!("{:?}", res); + assert!(matches!( + res.unwrap_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 ") + )); + }); } /// Test that executing an OUT instruction with an invalid port causes an error and poisons the sandbox. #[test] fn guest_outb_with_invalid_port_poisons_sandbox() { - let mut sbox = new_uninit_rust().unwrap().evolve().unwrap(); - - // Port 0x1234 is not a valid hyperlight port - let res = sbox.call::<()>("OutbWithPort", (0x1234_u32, 0_u32)); - assert!(res.is_err(), "Expected error from invalid OUT port"); - - // The sandbox should be poisoned because the guest didn't complete normally - assert!( - sbox.poisoned(), - "Sandbox should be poisoned after invalid OUT" - ); + with_rust_sandbox(|mut sbox| { + // Port 0x1234 is not a valid hyperlight port + let res = sbox.call::<()>("OutbWithPort", (0x1234_u32, 0_u32)); + assert!(res.is_err(), "Expected error from invalid OUT port"); + + // The sandbox should be poisoned because the guest didn't complete normally + assert!( + sbox.poisoned(), + "Sandbox should be poisoned after invalid OUT" + ); + }); } #[test] @@ -580,117 +560,118 @@ fn guest_panic_no_alloc() { let mut cfg = SandboxConfiguration::default(); cfg.set_heap_size(heap_size); - let uninit = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - Some(cfg), - ) - .unwrap(); - let mut sbox: MultiUseSandbox = uninit.evolve().unwrap(); - - let res = sbox - .call::( - "ExhaustHeap", // uses the rust allocator to allocate small blocks on the heap until OOM - (), - ) - .unwrap_err(); - - assert!(matches!( - res, - HyperlightError::GuestAborted(code, msg) if code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") && msg.contains("bytes failed") - )); + with_rust_sandbox_cfg(cfg, |mut sbox| { + let res = sbox + .call::( + "ExhaustHeap", // uses the rust allocator to allocate small blocks on the heap until OOM + (), + ) + .unwrap_err(); + + if let HyperlightError::StackOverflow() = res { + panic!("panic on OOM caused stack overflow, this implies allocation in panic handler"); + } + + assert!(matches!( + res, + HyperlightError::GuestAborted(code, msg) if code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") && msg.contains("bytes failed") + )); + }); } // Tests libc alloca #[test] fn dynamic_stack_allocate_c_guest() { - let path = c_simple_guest_as_string().unwrap(); - let guest_path = GuestBinary::FilePath(path); - let uninit = UninitializedSandbox::new(guest_path, None); - let mut sbox1: MultiUseSandbox = uninit.unwrap().evolve().unwrap(); - - let res: i32 = sbox1.call("StackAllocate", 100_i32).unwrap(); - assert_eq!(res, 100); - - let res = sbox1 - .call::("StackAllocate", 0x800_0000_i32) - .unwrap_err(); - assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) - ); + with_c_sandbox(|mut sbox1| { + let res: i32 = sbox1.call("StackAllocate", 100_i32).unwrap(); + assert_eq!(res, 100); + + let res = sbox1 + .call::("StackAllocate", 0x800_0000_i32) + .unwrap_err(); + assert!( + matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + ); + }); } // checks that a small buffer on stack works #[test] fn static_stack_allocate() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - - let res: i32 = sbox1.call("SmallVar", ()).unwrap(); - assert_eq!(res, 1024); + with_all_sandboxes(|mut sbox1| { + let res: i32 = sbox1.call("SmallVar", ()).unwrap(); + assert_eq!(res, 1024); + }); } // checks that a huge buffer on stack fails with stackoverflow #[test] fn static_stack_allocate_overflow() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - let res = sbox1.call::("LargeVar", ()).unwrap_err(); - assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) - ); + with_all_sandboxes(|mut sbox1| { + let res = sbox1.call::("LargeVar", ()).unwrap_err(); + assert!( + matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + ); + }); } // checks that a recursive function with stack allocation works, (that chkstk can be called without overflowing) #[test] fn recursive_stack_allocate() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - - let iterations = 1_i32; - - sbox1.call::("StackOverflow", iterations).unwrap(); + with_all_sandboxes(|mut sbox1| { + let iterations = 1_i32; + sbox1.call::("StackOverflow", iterations).unwrap(); + }); } #[test] fn guard_page_check_2() { // this test is rust-guest only - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - - let res = sbox1.call::<()>("InfiniteRecursion", ()).unwrap_err(); - assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) - ); + with_rust_sandbox(|mut sbox1| { + let res = sbox1.call::<()>("InfiniteRecursion", ()).unwrap_err(); + assert!( + matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + ); + }); } #[test] fn execute_on_heap() { - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - let result = sbox1.call::("ExecuteOnHeap", ()); - - #[cfg(feature = "executable_heap")] - assert_eq!( - result.unwrap(), - "Executed on heap successfully", - "should execute successfully" - ); + 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" + ); - #[cfg(not(feature = "executable_heap"))] - assert!( - result.unwrap_err().to_string().contains("PageFault"), - "should get page fault" - ); + #[cfg(not(feature = "executable_heap"))] + assert!( + result.unwrap_err().to_string().contains("PageFault"), + "should get page fault" + ); + }); } // checks that a recursive function with stack allocation eventually fails with stackoverflow #[test] fn recursive_stack_allocate_overflow() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); + with_all_sandboxes(|mut sbox1| { + let iterations = 32_i32; - let iterations = 32_i32; - - let res = sbox1.call::<()>("StackOverflow", iterations).unwrap_err(); - assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) - ); + let res = sbox1.call::<()>("StackOverflow", iterations).unwrap_err(); + assert!( + matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + ); + }); } +// assert!( +// matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) +// ); + // Check that log messages are emitted correctly from the guest // This test is ignored as it sets a logger and therefore maybe impacted by other tests running concurrently // or it may impact other tests. @@ -761,17 +742,20 @@ fn log_test_messages(levelfilter: Option) { LOGGER.clear_log_calls(); assert_eq!(0, LOGGER.num_log_calls()); for level in log::LevelFilter::iter() { - let mut sbox = new_uninit().unwrap(); - if let Some(levelfilter) = levelfilter { - sbox.set_max_guest_log_level(levelfilter); - } + // Only use Rust guest because the C guest has a different signature for LogMessage + // (Long vs Int for the level parameter) + with_rust_uninit_sandbox(|mut sbox| { + if let Some(levelfilter) = levelfilter { + sbox.set_max_guest_log_level(levelfilter); + } - let mut sbox1 = sbox.evolve().unwrap(); + let mut sbox1 = sbox.evolve().unwrap(); - let message = format!("Hello from log_message level {}", level as i32); - sbox1 - .call::<()>("LogMessage", (message.to_string(), level as i32)) - .unwrap(); + let message = format!("Hello from log_message level {}", level as i32); + sbox1 + .call::<()>("LogMessage", (message.to_string(), level as i32)) + .unwrap(); + }); } } @@ -779,85 +763,86 @@ fn log_test_messages(levelfilter: Option) { /// or not #[test] fn test_if_guest_is_able_to_get_bool_return_values_from_host() { - let mut sbox1 = new_uninit_c().unwrap(); - - sbox1 - .register("HostBool", |a: i32, b: i32| a + b > 10) - .unwrap(); - let mut sbox3 = sbox1.evolve().unwrap(); - - for i in 1..10 { - if i < 6 { - let res = sbox3 - .call::("GuestRetrievesBoolValue", (i, i)) - .unwrap(); - println!("{:?}", res); - assert!(!res); - } else { - let res = sbox3 - .call::("GuestRetrievesBoolValue", (i, i)) - .unwrap(); - println!("{:?}", res); - assert!(res); + with_c_uninit_sandbox(|mut sbox1| { + sbox1 + .register("HostBool", |a: i32, b: i32| a + b > 10) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + + for i in 1..10 { + if i < 6 { + let res = sbox3 + .call::("GuestRetrievesBoolValue", (i, i)) + .unwrap(); + println!("{:?}", res); + assert!(!res); + } else { + let res = sbox3 + .call::("GuestRetrievesBoolValue", (i, i)) + .unwrap(); + println!("{:?}", res); + assert!(res); + } } - } + }); } /// Tests whether host is able to return Float/f32 as return type /// or not #[test] fn test_if_guest_is_able_to_get_float_return_values_from_host() { - let mut sbox1 = new_uninit_c().unwrap(); - - sbox1 - .register("HostAddFloat", |a: f32, b: f32| a + b) - .unwrap(); - let mut sbox3 = sbox1.evolve().unwrap(); - let res = sbox3 - .call::("GuestRetrievesFloatValue", (1.34_f32, 1.34_f32)) - .unwrap(); - println!("{:?}", res); - assert_eq!(res, 2.68_f32); + with_c_uninit_sandbox(|mut sbox1| { + sbox1 + .register("HostAddFloat", |a: f32, b: f32| a + b) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + let res = sbox3 + .call::("GuestRetrievesFloatValue", (1.34_f32, 1.34_f32)) + .unwrap(); + println!("{:?}", res); + assert_eq!(res, 2.68_f32); + }); } /// Tests whether host is able to return Double/f64 as return type /// or not #[test] fn test_if_guest_is_able_to_get_double_return_values_from_host() { - let mut sbox1 = new_uninit_c().unwrap(); - - sbox1 - .register("HostAddDouble", |a: f64, b: f64| a + b) - .unwrap(); - let mut sbox3 = sbox1.evolve().unwrap(); - let res = sbox3 - .call::("GuestRetrievesDoubleValue", (1.34_f64, 1.34_f64)) - .unwrap(); - println!("{:?}", res); - assert_eq!(res, 2.68_f64); + with_c_uninit_sandbox(|mut sbox1| { + sbox1 + .register("HostAddDouble", |a: f64, b: f64| a + b) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + let res = sbox3 + .call::("GuestRetrievesDoubleValue", (1.34_f64, 1.34_f64)) + .unwrap(); + println!("{:?}", res); + assert_eq!(res, 2.68_f64); + }); } /// Tests whether host is able to return String as return type /// or not #[test] fn test_if_guest_is_able_to_get_string_return_values_from_host() { - let mut sbox1 = new_uninit_c().unwrap(); - - sbox1 - .register("HostAddStrings", |a: String| { - a + ", string added by Host Function" - }) - .unwrap(); - let mut sbox3 = sbox1.evolve().unwrap(); - let res = sbox3 - .call::("GuestRetrievesStringValue", ()) - .unwrap(); - println!("{:?}", res); - assert_eq!( - res, - "Guest Function, string added by Host Function".to_string() - ); + with_c_uninit_sandbox(|mut sbox1| { + sbox1 + .register("HostAddStrings", |a: String| { + a + ", string added by Host Function" + }) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + let res = sbox3 + .call::("GuestRetrievesStringValue", ()) + .unwrap(); + println!("{:?}", res); + assert_eq!( + res, + "Guest Function, string added by Host Function".to_string() + ); + }); } + /// Test that validates interrupt behavior with random kill timing under concurrent load /// Uses a pool of 100 sandboxes, 100 threads, and 500 iterations per thread. /// Randomly decides to kill some calls at random times during execution. @@ -887,11 +872,10 @@ fn interrupt_random_kill_stress_test() { const KILL_PROBABILITY: f64 = 0.5; // 50% chance to attempt kill const GUEST_CALL_DURATION_MS: u32 = 10; // SpinForMs duration - // Create a pool of 50 sandboxes println!("Creating pool of {} sandboxes...", POOL_SIZE); let mut sandbox_pool: Vec = Vec::with_capacity(POOL_SIZE); for i in 0..POOL_SIZE { - let mut sandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sandbox = new_rust_sandbox(); // Create a snapshot for this sandbox let snapshot = sandbox.snapshot().unwrap(); if (i + 1) % 10 == 0 { @@ -1107,38 +1091,26 @@ fn interrupt_random_kill_stress_test() { ); // Create a new sandbox with snapshot - match new_uninit_rust().and_then(|uninit| uninit.evolve()) { - Ok(mut new_sandbox) => { - match new_sandbox.snapshot() { - Ok(new_snapshot) => { - // Replace the failed sandbox with the new one - sandbox_wrapper.sandbox = new_sandbox; - sandbox_wrapper.snapshot = new_snapshot; - sandbox_replaced_count_clone - .fetch_add(1, Ordering::Relaxed); - trace!( - "[THREAD-{}] Iteration {}: Successfully replaced sandbox", - thread_id, iteration - ); - } - Err(snapshot_err) => { - error!( - "CRITICAL: Thread {} iteration {}: Failed to create snapshot for new sandbox: {:?}", - thread_id, iteration, snapshot_err - ); - // Still use the new sandbox even without snapshot - sandbox_wrapper.sandbox = new_sandbox; - sandbox_replaced_count_clone - .fetch_add(1, Ordering::Relaxed); - } - } + let mut new_sandbox = new_rust_sandbox(); + match new_sandbox.snapshot() { + Ok(new_snapshot) => { + // Replace the failed sandbox with the new one + sandbox_wrapper.sandbox = new_sandbox; + sandbox_wrapper.snapshot = new_snapshot; + sandbox_replaced_count_clone.fetch_add(1, Ordering::Relaxed); + trace!( + "[THREAD-{}] Iteration {}: Successfully replaced sandbox", + thread_id, iteration + ); } - Err(create_err) => { + Err(snapshot_err) => { error!( - "CRITICAL: Thread {} iteration {}: Failed to create new sandbox: {:?}", - thread_id, iteration, create_err + "CRITICAL: Thread {} iteration {}: Failed to create snapshot for new sandbox: {:?}", + thread_id, iteration, snapshot_err ); - // Continue with the broken sandbox - it will be removed from pool eventually + // Still use the new sandbox even without snapshot + sandbox_wrapper.sandbox = new_sandbox; + sandbox_replaced_count_clone.fetch_add(1, Ordering::Relaxed); } } } @@ -1355,7 +1327,7 @@ fn interrupt_infinite_loop_stress_test() { let barrier = Arc::new(Barrier::new(2)); let barrier_for_host = barrier.clone(); - let mut uninit = new_uninit_rust().unwrap(); + let mut uninit = new_rust_uninit_sandbox(); // Register a host function that waits on the barrier uninit @@ -1440,7 +1412,7 @@ fn interrupt_infinite_moving_loop_stress_test() { let entered_guest = Arc::new(AtomicBool::new(false)); let entered_guest_clone = entered_guest.clone(); - let mut uninit = new_uninit_rust().unwrap(); + let mut uninit = new_rust_uninit_sandbox(); // Register a host function that waits on the barrier uninit .register("WaitForKill", move || { @@ -1448,7 +1420,7 @@ fn interrupt_infinite_moving_loop_stress_test() { Ok(()) }) .unwrap(); - let uninit2 = new_uninit_rust().unwrap(); + let uninit2 = new_rust_uninit_sandbox(); // These 2 sandboxes will have the same TID let sandbox = uninit.evolve().unwrap(); @@ -1606,33 +1578,33 @@ fn interrupt_infinite_moving_loop_stress_test() { #[test] fn exception_handler_installation_and_validation() { - let mut sandbox: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); - - // Verify handler count starts at 0 - let count: i32 = sandbox.call("GetExceptionHandlerCallCount", ()).unwrap(); - assert_eq!(count, 0, "Handler should not have been called yet"); + with_rust_sandbox(|mut sandbox| { + // Verify handler count starts at 0 + let count: i32 = sandbox.call("GetExceptionHandlerCallCount", ()).unwrap(); + assert_eq!(count, 0, "Handler should not have been called yet"); - // Install handler for vector - sandbox.call::<()>("InstallHandler", 3i32).unwrap(); + // Install handler for vector + sandbox.call::<()>("InstallHandler", 3i32).unwrap(); - // Try to install again - should be able to overwrite - sandbox.call::<()>("InstallHandler", 3i32).unwrap(); + // Try to install again - should be able to overwrite + sandbox.call::<()>("InstallHandler", 3i32).unwrap(); - // Trigger int3 exception - let trigger_result: i32 = sandbox.call("TriggerInt3", ()).unwrap(); - assert_eq!(trigger_result, 0, "Exception should be handled gracefully"); + // Trigger int3 exception + let trigger_result: i32 = sandbox.call("TriggerInt3", ()).unwrap(); + assert_eq!(trigger_result, 0, "Exception should be handled gracefully"); - // Verify handler was invoked - let count: i32 = sandbox.call("GetExceptionHandlerCallCount", ()).unwrap(); - assert_eq!(count, 1, "Handler should have been called once"); + // Verify handler was invoked + let count: i32 = sandbox.call("GetExceptionHandlerCallCount", ()).unwrap(); + assert_eq!(count, 1, "Handler should have been called once"); - // Trigger int3 exception - let trigger_result: i32 = sandbox.call("TriggerInt3", ()).unwrap(); - assert_eq!(trigger_result, 0, "Exception should be handled gracefully"); + // Trigger int3 exception + let trigger_result: i32 = sandbox.call("TriggerInt3", ()).unwrap(); + assert_eq!(trigger_result, 0, "Exception should be handled gracefully"); - // Verify handler was invoked a second time - let count: i32 = sandbox.call("GetExceptionHandlerCallCount", ()).unwrap(); - assert_eq!(count, 2, "Handler should have been called twice"); + // Verify handler was invoked a second time + let count: i32 = sandbox.call("GetExceptionHandlerCallCount", ()).unwrap(); + assert_eq!(count, 2, "Handler should have been called twice"); + }); } /// Tests that an exception can be properly handled even when the heap is exhausted. @@ -1640,37 +1612,38 @@ fn exception_handler_installation_and_validation() { /// This validates that the exception handling path does not require heap allocations. #[test] fn fill_heap_and_cause_exception() { - let mut sandbox: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); - 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) - assert!( - message.contains("Exception vector: 6"), - "Message should contain 'Exception vector: 6'\nFull error: {:?}", - err - ); - assert!( - message.contains("Faulting Instruction:"), - "Message should contain 'Faulting Instruction:'\nFull error: {:?}", - err - ); - assert!( - message.contains("Stack Pointer:"), - "Message should contain 'Stack Pointer:'\nFull error: {:?}", - err - ); + with_rust_sandbox(|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) + assert!( + message.contains("Exception vector: 6"), + "Message should contain 'Exception vector: 6'\nFull error: {:?}", + err + ); + assert!( + message.contains("Faulting Instruction:"), + "Message should contain 'Faulting Instruction:'\nFull error: {:?}", + err + ); + assert!( + message.contains("Stack Pointer:"), + "Message should contain 'Stack Pointer:'\nFull error: {:?}", + err + ); + } + _ => panic!("Expected GuestAborted error, got: {:?}", 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), @@ -1687,9 +1660,9 @@ fn interrupt_cancel_delete_race() { let mut handles = vec![]; for _ in 0..NUM_THREADS { - handles.push(thread::spawn(move || { + handles.push(thread::spawn(|| { for _ in 0..ITERATIONS_PER_THREAD { - let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); + let mut sandbox = new_rust_sandbox(); let interrupt_handle = sandbox.interrupt_handle(); let stop_flag = Arc::new(AtomicBool::new(false)); diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index 6d040f061..9968f0b6b 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -18,19 +18,16 @@ use core::f64; use std::sync::mpsc::channel; use std::sync::{Arc, Mutex}; -use common::{get_uninit_simpleguest_sandboxes, new_uninit}; -use hyperlight_host::sandbox::SandboxConfiguration; -use hyperlight_host::{ - GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox, new_error, -}; -use hyperlight_testing::simple_guest_as_string; +use hyperlight_host::{HyperlightError, MultiUseSandbox, Result, new_error}; pub mod common; // pub to disable dead_code warning -use crate::common::get_simpleguest_sandboxes; +use crate::common::{ + with_all_sandboxes, with_all_sandboxes_with_writer, with_all_uninit_sandboxes, +}; #[test] fn pass_byte_array() { - for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { + with_all_sandboxes(|mut sandbox| { const LEN: usize = 10; let bytes = vec![1u8; LEN]; let res: Vec = sandbox @@ -41,7 +38,7 @@ fn pass_byte_array() { sandbox .call::("SetByteArrayToZeroNoLength", bytes.clone()) .unwrap_err(); // missing length param - } + }); } #[test] @@ -78,57 +75,58 @@ fn float_roundtrip() { f32::NAN, -f32::NAN, ]; - let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); - for f in doubles.iter() { - let res: f64 = sandbox.call("EchoDouble", *f).unwrap(); + with_all_sandboxes(|mut sandbox| { + for f in doubles.iter() { + let res: f64 = sandbox.call("EchoDouble", *f).unwrap(); - // Use == for comparison (handles -0.0 == 0.0) with special case for NaN. - // Note: FlatBuffers doesn't preserve -0.0 (-0.0 round-trips to 0.0) because FlatBuffers skips - // storing values equal to the default (as an optimization), and -0.0 == 0.0 in IEEE 754. - assert!( - (res.is_nan() && f.is_nan()) || res == *f, - "Expected {:?} but got {:?}", - f, - res - ); - } - for f in floats.iter() { - let res: f32 = sandbox.call("EchoFloat", *f).unwrap(); + // Use == for comparison (handles -0.0 == 0.0) with special case for NaN. + // Note: FlatBuffers doesn't preserve -0.0 (-0.0 round-trips to 0.0) because FlatBuffers skips + // storing values equal to the default (as an optimization), and -0.0 == 0.0 in IEEE 754. + assert!( + (res.is_nan() && f.is_nan()) || res == *f, + "Expected {:?} but got {:?}", + f, + res + ); + } + for f in floats.iter() { + let res: f32 = sandbox.call("EchoFloat", *f).unwrap(); - // Use == for comparison (handles -0.0 == 0.0) with special case for NaN. - // Note: FlatBuffers doesn't preserve -0.0 (-0.0 round-trips to 0.0) because FlatBuffers skips - // storing values equal to the default (as an optimization), and -0.0 == 0.0 in IEEE 754. - assert!( - (res.is_nan() && f.is_nan()) || res == *f, - "Expected {:?} but got {:?}", - f, - res - ); - } + // Use == for comparison (handles -0.0 == 0.0) with special case for NaN. + // Note: FlatBuffers doesn't preserve -0.0 (-0.0 round-trips to 0.0) because FlatBuffers skips + // storing values equal to the default (as an optimization), and -0.0 == 0.0 in IEEE 754. + assert!( + (res.is_nan() && f.is_nan()) || res == *f, + "Expected {:?} but got {:?}", + f, + res + ); + } + }); } #[test] fn invalid_guest_function_name() { - for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { + with_all_sandboxes(|mut sandbox| { let fn_name = "FunctionDoesntExist"; let res = sandbox.call::(fn_name, ()); println!("{:?}", res); assert!( matches!(res.unwrap_err(), HyperlightError::GuestError(hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode::GuestFunctionNotFound, error_name) if error_name == fn_name) ); - } + }); } #[test] fn set_static() { - for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { + with_all_sandboxes(|mut sandbox| { let fn_name = "SetStatic"; let res = sandbox.call::(fn_name, ()); println!("{:?}", res); assert!(res.is_ok()); // the result is the size of the static array in the guest assert_eq!(res.unwrap(), 1024 * 1024); - } + }); } #[test] @@ -164,7 +162,7 @@ fn multiple_parameters() { }}; } - for mut sb in get_simpleguest_sandboxes(Some(writer.into())).into_iter() { + with_all_sandboxes_with_writer(writer.into(), |mut sb| { test_case!(sb, rx, "PrintTwoArgs", (a, b)); test_case!(sb, rx, "PrintThreeArgs", (a, b, c)); test_case!(sb, rx, "PrintFourArgs", (a, b, c, d)); @@ -175,12 +173,12 @@ fn multiple_parameters() { test_case!(sb, rx, "PrintNineArgs", (a, b, c, d, e, f, g, h, i)); test_case!(sb, rx, "PrintTenArgs", (a, b, c, d, e, f, g, h, i, j)); test_case!(sb, rx, "PrintElevenArgs", (a, b, c, d, e, f, g, h, i, j, k)); - } + }); } #[test] fn incorrect_parameter_type() { - for mut sandbox in get_simpleguest_sandboxes(None) { + with_all_sandboxes(|mut sandbox| { let res = sandbox.call::( "Echo", 2_i32, // should be string ); @@ -192,12 +190,12 @@ fn incorrect_parameter_type() { msg ) if msg == "Expected parameter type String for parameter index 0 of function Echo but got Int." )); - } + }); } #[test] fn incorrect_parameter_num() { - for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { + with_all_sandboxes(|mut sandbox| { let res = sandbox.call::("Echo", ("1".to_string(), 2_i32)); assert!(matches!( res.unwrap_err(), @@ -206,12 +204,15 @@ fn incorrect_parameter_num() { msg ) if msg == "Called function Echo with 2 parameters but it takes 1." )); - } + }); } #[test] fn max_memory_sandbox() { - let mut cfg = SandboxConfiguration::default(); + use hyperlight_host::{GuestBinary, UninitializedSandbox}; + use hyperlight_testing::simple_guest_as_string; + + let mut cfg = hyperlight_host::sandbox::SandboxConfiguration::default(); cfg.set_input_data_size(0x40000000); let a = UninitializedSandbox::new( GuestBinary::FilePath(simple_guest_as_string().unwrap()), @@ -226,15 +227,15 @@ fn max_memory_sandbox() { #[test] fn iostack_is_working() { - for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { + with_all_sandboxes(|mut sandbox| { let res: i32 = sandbox .call::("ThisIsNotARealFunctionButTheNameIsImportant", ()) .unwrap(); assert_eq!(res, 99); - } + }); } -fn simple_test_helper() -> Result<()> { +fn simple_test_helper() { let messages = Arc::new(Mutex::new(Vec::new())); let messages_clone = messages.clone(); let writer = move |msg: String| { @@ -250,7 +251,7 @@ fn simple_test_helper() -> Result<()> { let message = "hello"; let message2 = "world"; - for mut sandbox in get_simpleguest_sandboxes(Some(writer.into())).into_iter() { + with_all_sandboxes_with_writer(writer.into(), |mut sandbox| { let res: i32 = sandbox.call("PrintOutput", message.to_string()).unwrap(); assert_eq!(res, 5); @@ -262,31 +263,24 @@ fn simple_test_helper() -> Result<()> { .call("GetSizePrefixedBuffer", buffer.to_vec()) .unwrap(); assert_eq!(res, buffer); - } + }); - let expected_calls = 1; + let expected_calls = 2; // Once per guest (rust + c) - assert_eq!( - messages - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .len(), - expected_calls - ); + assert_eq!(messages.try_lock().unwrap().len(), expected_calls); assert!( messages .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? + .unwrap() .iter() .all(|msg| msg == message) ); - Ok(()) } #[test] fn simple_test() { - simple_test_helper().unwrap(); + simple_test_helper(); } #[test] @@ -294,7 +288,7 @@ fn simple_test_parallel() { let handles: Vec<_> = (0..50) .map(|_| { std::thread::spawn(|| { - simple_test_helper().unwrap(); + simple_test_helper(); }) }) .collect(); @@ -304,30 +298,33 @@ fn simple_test_parallel() { } } -fn callback_test_helper() -> Result<()> { - for mut sandbox in get_uninit_simpleguest_sandboxes(None).into_iter() { +fn callback_test_helper() { + with_all_uninit_sandboxes(|mut sandbox| { // create host function let (tx, rx) = channel(); - sandbox.register("HostMethod1", move |msg: String| { - let len = msg.len(); - tx.send(msg).unwrap(); - Ok(len as i32) - })?; + sandbox + .register("HostMethod1", move |msg: String| { + let len = msg.len(); + tx.send(msg).unwrap(); + Ok(len as i32) + }) + .unwrap(); // call guest function that calls host function - let mut init_sandbox: MultiUseSandbox = sandbox.evolve()?; + let mut init_sandbox: MultiUseSandbox = sandbox.evolve().unwrap(); let msg = "Hello world"; - init_sandbox.call::("GuestMethod1", msg.to_string())?; + init_sandbox + .call::("GuestMethod1", msg.to_string()) + .unwrap(); let messages = rx.try_iter().collect::>(); assert_eq!(messages, [format!("Hello from GuestFunction1, {msg}")]); - } - Ok(()) + }); } #[test] fn callback_test() { - callback_test_helper().unwrap(); + callback_test_helper(); } #[test] @@ -335,7 +332,7 @@ fn callback_test_parallel() { let handles: Vec<_> = (0..100) .map(|_| { std::thread::spawn(|| { - callback_test_helper().unwrap(); + callback_test_helper(); }) }) .collect(); @@ -346,17 +343,19 @@ fn callback_test_parallel() { } #[test] -fn host_function_error() -> Result<()> { - for mut sandbox in get_uninit_simpleguest_sandboxes(None).into_iter() { +fn host_function_error() { + with_all_uninit_sandboxes(|mut sandbox| { // create host function - sandbox.register("HostMethod1", |_: String| -> Result { - Err(new_error!("Host function error!")) - })?; + sandbox + .register("HostMethod1", |_: String| -> Result { + Err(new_error!("Host function error!")) + }) + .unwrap(); // call guest function that calls host function - let mut init_sandbox: MultiUseSandbox = sandbox.evolve()?; + let mut init_sandbox: MultiUseSandbox = sandbox.evolve().unwrap(); let msg = "Hello world"; - let snapshot = init_sandbox.snapshot()?; + let snapshot = init_sandbox.snapshot().unwrap(); for _ in 0..1000 { let res = init_sandbox @@ -364,15 +363,14 @@ fn host_function_error() -> Result<()> { .unwrap_err(); assert!( matches!(&res, HyperlightError::GuestError(_, msg) if msg == "Host function error!") // rust guest - || matches!(&res, HyperlightError::GuestAborted(_, msg) if msg.contains("Host function error!")), // c guest + || matches!(&res, HyperlightError::GuestAborted(_, msg) if msg.contains("Host function error!")), // c guest "expected something but got {}", res ); // C guest panics in rust guest lib when host function returns error, which will poison the sandbox if init_sandbox.poisoned() { - init_sandbox.restore(snapshot.clone())?; + init_sandbox.restore(snapshot.clone()).unwrap(); } } - } - Ok(()) + }); } diff --git a/src/tests/rust_guests/dummyguest/Cargo.lock b/src/tests/rust_guests/dummyguest/Cargo.lock index 3e2c4afef..fbf3bc62c 100644 --- a/src/tests/rust_guests/dummyguest/Cargo.lock +++ b/src/tests/rust_guests/dummyguest/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "autocfg" diff --git a/src/tests/rust_guests/simpleguest/Cargo.lock b/src/tests/rust_guests/simpleguest/Cargo.lock index 63f55ecee..0faead807 100644 --- a/src/tests/rust_guests/simpleguest/Cargo.lock +++ b/src/tests/rust_guests/simpleguest/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "bitflags" diff --git a/src/tests/rust_guests/witguest/Cargo.lock b/src/tests/rust_guests/witguest/Cargo.lock index 647cd7438..3715fc832 100644 --- a/src/tests/rust_guests/witguest/Cargo.lock +++ b/src/tests/rust_guests/witguest/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "bitflags" From 28ac106d8c069a660ac545d4e08fef36c2b4493f Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:32:32 -0800 Subject: [PATCH 6/7] Move max_memory_sandbox test to layout module Move the test from integration tests into the layout module where it belongs, and rewrite it to test SandboxMemoryLayout::get_memory_size directly instead of going through UninitializedSandbox::new. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- src/hyperlight_host/src/mem/layout.rs | 9 +++++++++ .../tests/sandbox_host_tests.rs | 18 ------------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 925067c21..783201013 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -662,4 +662,13 @@ mod tests { get_expected_memory_size(&sbox_mem_layout) ); } + + #[test] + fn test_max_memory_sandbox() { + let mut cfg = SandboxConfiguration::default(); + cfg.set_input_data_size(0x40000000); + let layout = SandboxMemoryLayout::new(cfg, 4096, 2048, 4096, 4096, None).unwrap(); + let result = layout.get_memory_size(); + assert!(matches!(result.unwrap_err(), MemoryRequestTooBig(..))); + } } diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index 9968f0b6b..a178208fa 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -207,24 +207,6 @@ fn incorrect_parameter_num() { }); } -#[test] -fn max_memory_sandbox() { - use hyperlight_host::{GuestBinary, UninitializedSandbox}; - use hyperlight_testing::simple_guest_as_string; - - let mut cfg = hyperlight_host::sandbox::SandboxConfiguration::default(); - cfg.set_input_data_size(0x40000000); - let a = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().unwrap()), - Some(cfg), - ); - - assert!(matches!( - a.unwrap_err(), - HyperlightError::MemoryRequestTooBig(..) - )); -} - #[test] fn iostack_is_working() { with_all_sandboxes(|mut sandbox| { From b28851692f211578da07e6aa3c68836655b41376 Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:23:28 -0800 Subject: [PATCH 7/7] Add error context to assert!(matches!()) and remove debug println in tests Adds descriptive error messages to pattern-matching assertions so test failures show the actual value. Removes println!("{:?}", res) calls that were only useful for manual debugging. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- src/hyperlight_host/tests/integration_test.rs | 127 +++++++++++------- .../tests/sandbox_host_tests.rs | 6 +- 2 files changed, 77 insertions(+), 56 deletions(-) diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 355919d9c..953c6b7e4 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -61,7 +61,10 @@ fn interrupt_host_call() { }); let result = sandbox.call::("CallHostSpin", ()).unwrap_err(); - assert!(matches!(result, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(&result, HyperlightError::ExecutionCanceledByHost()), + "unexpected error: {result:?}" + ); assert!(sandbox.poisoned()); // Restore from snapshot to clear poison @@ -92,7 +95,10 @@ fn interrupt_in_progress_guest_call() { }); let res = sbox1.call::("Spin", ()).unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(&res, HyperlightError::ExecutionCanceledByHost()), + "unexpected error: {res:?}" + ); assert!(sbox1.poisoned()); // Restore from snapshot to clear poison @@ -265,7 +271,10 @@ fn interrupt_moved_sandbox() { let thread = thread::spawn(move || { barrier2.wait(); let res = sbox1.call::("Spin", ()).unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(&res, HyperlightError::ExecutionCanceledByHost()), + "unexpected error: {res:?}" + ); assert!(sbox1.poisoned()); sbox1.restore(snapshot1.clone()).unwrap(); assert!(!sbox1.poisoned()); @@ -281,7 +290,10 @@ fn interrupt_moved_sandbox() { }); let res = sbox2.call::("Spin", ()).unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(&res, HyperlightError::ExecutionCanceledByHost()), + "unexpected error: {res:?}" + ); thread.join().expect("Thread should finish"); thread2.join().expect("Thread should finish"); @@ -317,7 +329,10 @@ fn interrupt_custom_signal_no_and_retry_delay() { for _ in 0..NUM_ITERS { let res = sbox1.call::("Spin", ()).unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(&res, HyperlightError::ExecutionCanceledByHost()), + "unexpected error: {res:?}" + ); assert!(sbox1.poisoned()); // immediately reenter another guest function call after having being cancelled, // so that the vcpu is running again before the interruptor-thread has a chance to see that the vcpu is not running @@ -355,7 +370,10 @@ fn interrupt_spamming_host_call() { .call::("HostCallLoop", "HostFunc1".to_string()) .unwrap_err(); - assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); + assert!( + matches!(&res, HyperlightError::ExecutionCanceledByHost()), + "unexpected error: {res:?}" + ); thread.join().expect("Thread should finish"); }); @@ -368,8 +386,7 @@ fn print_four_args_c_guest() { "PrintFourArgs", ("Test4".to_string(), 3_i32, 4_i64, "Tested".to_string()), ); - println!("{:?}", res); - assert!(matches!(res, Ok(46))); + assert!(matches!(&res, Ok(46)), "unexpected result: {res:?}"); }); } @@ -381,9 +398,9 @@ fn guest_abort() { let res = sbox1 .call::<()>("GuestAbortWithCode", error_code as i32) .unwrap_err(); - println!("{:?}", res); assert!( - matches!(res, HyperlightError::GuestAborted(code, message) if (code == error_code && message.is_empty())) + matches!(&res, HyperlightError::GuestAborted(code, message) if (*code == error_code && message.is_empty())), + "unexpected error: {res:?}" ); }); } @@ -394,9 +411,9 @@ fn guest_abort_with_context1() { let res = sbox1 .call::<()>("GuestAbortWithMessage", (25_i32, "Oh no".to_string())) .unwrap_err(); - println!("{:?}", res); assert!( - matches!(res, HyperlightError::GuestAborted(code, context) if (code == 25 && context == "Oh no")) + matches!(&res, HyperlightError::GuestAborted(code, context) if (*code == 25 && context == "Oh no")), + "unexpected error: {res:?}" ); }); } @@ -439,9 +456,9 @@ fn guest_abort_with_context2() { let res = sbox1 .call::<()>("GuestAbortWithMessage", (60_i32, abort_message.to_string())) .unwrap_err(); - println!("{:?}", res); assert!( - matches!(res, HyperlightError::GuestAborted(_, context) if context.contains("Guest abort buffer overflowed")) + matches!(&res, HyperlightError::GuestAborted(_, context) if context.contains("Guest abort buffer overflowed")), + "unexpected error: {res:?}" ); }); } @@ -458,9 +475,9 @@ fn guest_abort_c_guest() { (75_i32, "This is a test error message".to_string()), ) .unwrap_err(); - println!("{:?}", res); assert!( - matches!(res, HyperlightError::GuestAborted(code, message) if (code == 75 && message == "This is a test error message") ) + matches!(&res, HyperlightError::GuestAborted(code, message) if (*code == 75 && message == "This is a test error message")), + "unexpected error: {res:?}" ); }); } @@ -472,9 +489,9 @@ fn guest_panic() { let res = sbox1 .call::<()>("guest_panic", "Error... error...".to_string()) .unwrap_err(); - println!("{:?}", res); assert!( - matches!(res, HyperlightError::GuestAborted(code, context) if code == ErrorCode::UnknownError as u8 && context.contains("\nError... error...")) + matches!(&res, HyperlightError::GuestAborted(code, context) if *code == ErrorCode::UnknownError as u8 && context.contains("\nError... error...")), + "unexpected error: {res:?}" ); }); } @@ -511,30 +528,37 @@ fn guest_malloc_abort() { let size = 20000000_i32; // some big number that should fail when allocated let res = sbox1.call::("TestMalloc", size).unwrap_err(); - println!("{:?}", res); assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + 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 = 0x4000; let size_to_allocate = 0x10000; - assert!(size_to_allocate > heap_size); + assert!( + size_to_allocate > heap_size, + "precondition: size_to_allocate ({size_to_allocate}) must be > heap_size ({heap_size})" + ); let mut cfg = SandboxConfiguration::default(); cfg.set_heap_size(heap_size); with_rust_sandbox_cfg(cfg, |mut sbox2| { - let res = sbox2.call::( - "CallMalloc", // uses the rust allocator to allocate a vector on heap - size_to_allocate as i32, + 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:?}" ); - println!("{:?}", res); - assert!(matches!( - res.unwrap_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 ") - )); }); } @@ -572,10 +596,13 @@ fn guest_panic_no_alloc() { panic!("panic on OOM caused stack overflow, this implies allocation in panic handler"); } - assert!(matches!( - res, - HyperlightError::GuestAborted(code, msg) if code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") && msg.contains("bytes failed") - )); + assert!( + matches!( + &res, + HyperlightError::GuestAborted(code, msg) if *code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") && msg.contains("bytes failed") + ), + "unexpected error: {res:?}" + ); }); } @@ -590,7 +617,8 @@ fn dynamic_stack_allocate_c_guest() { .call::("StackAllocate", 0x800_0000_i32) .unwrap_err(); assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + matches!(&res, HyperlightError::GuestAborted(code, _) if *code == ErrorCode::MallocFailed as u8), + "unexpected error: {res:?}" ); }); } @@ -610,7 +638,8 @@ fn static_stack_allocate_overflow() { with_all_sandboxes(|mut sbox1| { let res = sbox1.call::("LargeVar", ()).unwrap_err(); assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + matches!(&res, HyperlightError::GuestAborted(code, _) if *code == ErrorCode::MallocFailed as u8), + "unexpected error: {res:?}" ); }); } @@ -630,7 +659,8 @@ fn guard_page_check_2() { with_rust_sandbox(|mut sbox1| { let res = sbox1.call::<()>("InfiniteRecursion", ()).unwrap_err(); assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + matches!(&res, HyperlightError::GuestAborted(code, _) if *code == ErrorCode::MallocFailed as u8), + "unexpected error: {res:?}" ); }); } @@ -663,15 +693,12 @@ fn recursive_stack_allocate_overflow() { let res = sbox1.call::<()>("StackOverflow", iterations).unwrap_err(); assert!( - matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) + matches!(&res, HyperlightError::GuestAborted(code, _) if *code == ErrorCode::MallocFailed as u8), + "unexpected error: {res:?}" ); }); } -// assert!( -// matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) -// ); - // Check that log messages are emitted correctly from the guest // This test is ignored as it sets a logger and therefore maybe impacted by other tests running concurrently // or it may impact other tests. @@ -774,13 +801,11 @@ fn test_if_guest_is_able_to_get_bool_return_values_from_host() { let res = sbox3 .call::("GuestRetrievesBoolValue", (i, i)) .unwrap(); - println!("{:?}", res); assert!(!res); } else { let res = sbox3 .call::("GuestRetrievesBoolValue", (i, i)) .unwrap(); - println!("{:?}", res); assert!(res); } } @@ -799,7 +824,6 @@ fn test_if_guest_is_able_to_get_float_return_values_from_host() { let res = sbox3 .call::("GuestRetrievesFloatValue", (1.34_f32, 1.34_f32)) .unwrap(); - println!("{:?}", res); assert_eq!(res, 2.68_f32); }); } @@ -816,7 +840,6 @@ fn test_if_guest_is_able_to_get_double_return_values_from_host() { let res = sbox3 .call::("GuestRetrievesDoubleValue", (1.34_f64, 1.34_f64)) .unwrap(); - println!("{:?}", res); assert_eq!(res, 2.68_f64); }); } @@ -835,7 +858,6 @@ fn test_if_guest_is_able_to_get_string_return_values_from_host() { let res = sbox3 .call::("GuestRetrievesStringValue", ()) .unwrap(); - println!("{:?}", res); assert_eq!( res, "Guest Function, string added by Host Function".to_string() @@ -1550,10 +1572,13 @@ fn interrupt_infinite_moving_loop_stress_test() { // If the guest entered before calling kill, then we know for a fact the call should have been canceled since kill() was NOT premature. if entered_before_kill { - assert!(matches!( - sandbox_res, - Err(HyperlightError::ExecutionCanceledByHost()) - )); + assert!( + matches!( + &sandbox_res, + Err(HyperlightError::ExecutionCanceledByHost()) + ), + "unexpected result: {sandbox_res:?}" + ); } // If we did NOT enter the guest before calling kill, then the call may or may not have been canceled depending on timing. diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index a178208fa..89d03728f 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -110,7 +110,6 @@ fn invalid_guest_function_name() { with_all_sandboxes(|mut sandbox| { let fn_name = "FunctionDoesntExist"; let res = sandbox.call::(fn_name, ()); - println!("{:?}", res); assert!( matches!(res.unwrap_err(), HyperlightError::GuestError(hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode::GuestFunctionNotFound, error_name) if error_name == fn_name) ); @@ -122,7 +121,6 @@ fn set_static() { with_all_sandboxes(|mut sandbox| { let fn_name = "SetStatic"; let res = sandbox.call::(fn_name, ()); - println!("{:?}", res); assert!(res.is_ok()); // the result is the size of the static array in the guest assert_eq!(res.unwrap(), 1024 * 1024); @@ -154,10 +152,8 @@ fn multiple_parameters() { macro_rules! test_case { ($sandbox:ident, $rx:ident, $name:literal, ($($p:ident),+)) => {{ let ($($p),+, ..) = args.clone(); - let res: i32 = $sandbox.call($name, ($($p.0,)+)).unwrap(); - println!("{res:?}"); + let _res: i32 = $sandbox.call($name, ($($p.0,)+)).unwrap(); let output = $rx.try_recv().unwrap(); - println!("{output:?}"); assert_eq!(output, format!("Message: {}.", [$($p.1),+].join(" "))); }}; }