diff --git a/Justfile b/Justfile index e11c16ec3..2d352826e 100644 --- a/Justfile +++ b/Justfile @@ -155,7 +155,7 @@ like-ci config=default-target hypervisor="kvm": {{ if config == "release" { "just bench-ci main " + if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }} # 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="": @@ -163,24 +163,21 @@ 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::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 -# 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 + +# 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) + {{ 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 @# skip interrupt_random_kill_stress_test and then run it explicitly so we can see the output more - {{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 '*' -- --skip interrupt_random_kill_stress_test - {{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 integration_test interrupt_random_kill_stress_test -- --nocapture --exact + {{ 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 '*' -- --skip interrupt_random_kill_stress_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 integration_test interrupt_random_kill_stress_test -- --nocapture --exact # tests compilation with no default features on different platforms test-compilation-no-default-features target=default-target: diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index e353f25b1..c83ed1935 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -839,4 +839,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, 0x3000, 0, None).unwrap(); + let result = layout.get_memory_size(); + assert!(matches!(result.unwrap_err(), MemoryRequestTooBig(..))); + } } diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index a69394c23..daed0697e 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -1331,50 +1331,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 ); } diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 8673c6e9c..352bd028a 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1071,41 +1071,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(); } } diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index ea40a7f43..b21cf05db 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -758,115 +758,90 @@ 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 #[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"); - - // Get the span data and check the correlation id - - let span_data = subscriber.get_span(1); - let span_attributes: &Map = span_data - .get("span") - .unwrap() - .get("attributes") - .unwrap() - .as_object() - .unwrap(); - - test_value_as_str(span_attributes, "correlation_id", correlation_id.as_str()); - - let mut binary_path = simple_guest_as_string().unwrap(); - binary_path.push_str("does_not_exist"); + /// 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() + } - let sbox = UninitializedSandbox::new(GuestBinary::FilePath(binary_path), None); - assert!(sbox.is_err()); + /// 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() + } - // 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) + /// 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() + } - 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" - ); + let subscriber = TracingSubscriber::new(Level::TRACE); - let span_metadata = subscriber.get_span_metadata(2); - assert_eq!(span_metadata.name(), "new"); + tracing::subscriber::with_default(subscriber.clone(), || { + let correlation_id = Uuid::new_v4().to_string(); + let _span = tracing::error_span!("test_trace_logs", %correlation_id).entered(); + + // Verify we're in span 1 with correct name + let (span_id, span_meta) = subscriber + .current_span() + .into_inner() + .expect("Should be inside a span"); + assert_eq!(span_id.into_u64(), 1, "Should be in span 1"); + assert_eq!(span_meta.name(), "test_trace_logs"); + + // Verify correlation_id was recorded + let span_data = subscriber.get_span(1); + 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 bad_path = simple_guest_as_string().unwrap() + "does_not_exist"; + let result = UninitializedSandbox::new(GuestBinary::FilePath(bad_path), None); + assert!(result.is_err(), "Sandbox creation should fail"); + + // Verify we're still in span 1 (our test span) + let (span_id, _) = subscriber + .current_span() + .into_inner() + .expect("Should still be inside a span"); + assert_eq!(span_id.into_u64(), 1, "Should still be in span 1"); + + // Verify span 2 was created by UninitializedSandbox::new + let inner_span_meta = subscriber.get_span_metadata(2); + assert_eq!(inner_span_meta.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_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 9a395ee81..506521962 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -22,129 +22,129 @@ use std::time::Duration; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::mem::PAGE_SIZE; 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; 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(); @@ -299,126 +299,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(1000)); + 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(1000)); - 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. \ @@ -448,13 +435,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. @@ -462,74 +450,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; @@ -538,39 +523,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] @@ -579,72 +559,64 @@ 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(); - - if let HyperlightError::StackOverflow() = res { - panic!("panic on OOM caused stack overflow, this implies allocation in panic handler"); - } + 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") - )); + 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::StackOverflow())); + 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::StackOverflow())); + }); } // 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::StackOverflow())); + with_all_sandboxes(|mut sbox1| { + let res = sbox1.call::("LargeVar", ()).unwrap_err(); + assert!(matches!(res, HyperlightError::StackOverflow())); + }); } // 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(); + }); } // checks stack guard page (between guest stack and heap) @@ -667,72 +639,73 @@ fn guard_page_check() { for offset in offsets_from_page_guard_start { // we have to create a sandbox each iteration because can't reuse after MMIO error in release mode - - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - let result = sbox1.call::("test_write_raw_ptr", offset); - if guard_range.contains(&offset) { - // should have failed - assert!(matches!( - result.unwrap_err(), - HyperlightError::StackOverflow() - )); - } else { - assert!(result.is_ok(), "offset {} should pass", offset) - } + with_rust_sandbox(|mut sbox1| { + let result = sbox1.call::("test_write_raw_ptr", offset); + if guard_range.contains(&offset) { + // should have failed + assert!(matches!( + result.unwrap_err(), + HyperlightError::StackOverflow() + )); + } else { + assert!(result.is_ok(), "offset {} should pass", offset) + } + }); } } #[test] fn guard_page_check_2() { // this test is rust-guest only - let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - - let result = sbox1.call::<()>("InfiniteRecursion", ()).unwrap_err(); - assert!(matches!(result, HyperlightError::StackOverflow())); + with_rust_sandbox(|mut sbox1| { + let result = sbox1.call::<()>("InfiniteRecursion", ()).unwrap_err(); + assert!(matches!(result, HyperlightError::StackOverflow())); + }); } #[test] fn execute_on_stack() { - let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); + with_all_sandboxes(|mut sbox1| { + let result = sbox1.call::("ExecuteOnStack", ()).unwrap_err(); - let result = sbox1.call::("ExecuteOnStack", ()).unwrap_err(); - - let err = result.to_string(); - assert!( - // exception that indicates a page fault - err.contains("PageFault") - ); + let err = result.to_string(); + assert!( + // exception that indicates a page fault + err.contains("PageFault") + ); + }); } #[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()); - - #[cfg(not(feature = "executable_heap"))] - { - assert!(result.is_err()); - let err = result.unwrap_err(); + 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" + ); - assert!(err.to_string().contains("PageFault")); - } + #[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(); - - let iterations = 10_i32; + with_all_sandboxes(|mut sbox1| { + let iterations = 10_i32; - let res = sbox1.call::<()>("StackOverflow", iterations).unwrap_err(); - println!("{:?}", res); - assert!(matches!(res, HyperlightError::StackOverflow())); + let res = sbox1.call::<()>("StackOverflow", iterations).unwrap_err(); + println!("{:?}", res); + assert!(matches!(res, HyperlightError::StackOverflow())); + }); } // Check that log messages are emitted correctly from the guest @@ -805,17 +778,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(); + }); } } @@ -823,85 +799,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. @@ -930,11 +907,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 { @@ -1150,38 +1126,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); } } } @@ -1397,7 +1361,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 @@ -1481,7 +1445,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 || { @@ -1489,7 +1453,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(); @@ -1647,33 +1611,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. @@ -1681,37 +1645,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), @@ -1728,9 +1693,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..a178208fa 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,35 +204,20 @@ 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(); - 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() { - 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 +233,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 +245,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 +270,7 @@ fn simple_test_parallel() { let handles: Vec<_> = (0..50) .map(|_| { std::thread::spawn(|| { - simple_test_helper().unwrap(); + simple_test_helper(); }) }) .collect(); @@ -304,30 +280,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 +314,7 @@ fn callback_test_parallel() { let handles: Vec<_> = (0..100) .map(|_| { std::thread::spawn(|| { - callback_test_helper().unwrap(); + callback_test_helper(); }) }) .collect(); @@ -346,17 +325,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 +345,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/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index df7de9469..586d1bcb6 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -489,7 +489,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")]