Skip to content

Commit 2494339

Browse files
committed
Add test that makes sure kill() never fails
Signed-off-by: Ludvig Liljenberg <[email protected]>
1 parent dfe1dd9 commit 2494339

File tree

3 files changed

+125
-6
lines changed

3 files changed

+125
-6
lines changed

src/hyperlight_host/tests/integration_test.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,3 +1346,97 @@ fn interrupt_random_kill_stress_test() {
13461346

13471347
println!("\n✅ All validations passed!");
13481348
}
1349+
1350+
/// Ensures that `kill()` reliably interrupts a running guest
1351+
///
1352+
/// The test works by:
1353+
/// 1. Guest calls a host function which waits on a barrier, ensuring the guest is "in-progress" and that `kill()` is not called prematurely to be ignored.
1354+
/// 2. Once the guest has passed that host function barrier, the host calls `kill()`. The `kill()` could be delivered at any time after this point, for example while guest is still in the host func, or returning into guest vm.
1355+
/// 3. The guest enters an infinite loop, so `kill()` is the only way to stop it.
1356+
///
1357+
/// This is repeated across multiple threads and iterations to stress test the cancellation mechanism.
1358+
///
1359+
/// **Failure Condition:** If this test hangs, it means `kill()` failed to stop the guest, leaving it spinning forever.
1360+
#[test]
1361+
fn interrupt_infinite_loop_stress_test() {
1362+
use std::sync::{Arc, Barrier};
1363+
use std::thread;
1364+
1365+
const NUM_THREADS: usize = 50;
1366+
const ITERATIONS_PER_THREAD: usize = 500;
1367+
1368+
let mut handles = vec![];
1369+
1370+
for i in 0..NUM_THREADS {
1371+
handles.push(thread::spawn(move || {
1372+
// Create a barrier for 2 threads:
1373+
// 1. The guest (executing a host function)
1374+
// 2. The killer thread
1375+
let barrier = Arc::new(Barrier::new(2));
1376+
let barrier_for_host = barrier.clone();
1377+
1378+
let mut uninit = new_uninit_rust().unwrap();
1379+
1380+
// Register a host function that waits on the barrier
1381+
uninit
1382+
.register("WaitForKill", move || {
1383+
barrier_for_host.wait();
1384+
Ok(())
1385+
})
1386+
.unwrap();
1387+
1388+
let mut sandbox = uninit.evolve().unwrap();
1389+
// Take a snapshot to restore after each kill
1390+
let snapshot = sandbox.snapshot().unwrap();
1391+
1392+
for j in 0..ITERATIONS_PER_THREAD {
1393+
let barrier_for_killer = barrier.clone();
1394+
let interrupt_handle = sandbox.interrupt_handle();
1395+
1396+
// Spawn the killer thread
1397+
let killer_thread = std::thread::spawn(move || {
1398+
// Wait for the guest to call WaitForKill
1399+
barrier_for_killer.wait();
1400+
1401+
// The guest is now waiting on the barrier (or just finished waiting).
1402+
// We kill it immediately.
1403+
interrupt_handle.kill();
1404+
});
1405+
1406+
// Call the guest function "CallHostThenSpin" which calls "WaitForKill" once then spins
1407+
// NOTE: If this test hangs, it means the guest was not successfully killed and is spinning forever.
1408+
// This indicates a bug in the cancellation mechanism.
1409+
let res = sandbox.call::<()>("CallHostThenSpin", "WaitForKill".to_string());
1410+
1411+
// Wait for killer thread to finish
1412+
killer_thread.join().unwrap();
1413+
1414+
// We expect the execution to be canceled
1415+
match res {
1416+
Err(HyperlightError::ExecutionCanceledByHost()) => {
1417+
// Success!
1418+
}
1419+
Ok(_) => {
1420+
panic!(
1421+
"Thread {} Iteration {}: Guest finished successfully but should have been killed!",
1422+
i, j
1423+
);
1424+
}
1425+
Err(e) => {
1426+
panic!(
1427+
"Thread {} Iteration {}: Guest failed with unexpected error: {:?}",
1428+
i, j, e
1429+
);
1430+
}
1431+
}
1432+
1433+
// Restore the sandbox for the next iteration
1434+
sandbox.restore(&snapshot).unwrap();
1435+
}
1436+
}));
1437+
}
1438+
1439+
for handle in handles {
1440+
handle.join().unwrap();
1441+
}
1442+
}

src/tests/rust_guests/simpleguest/src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,14 @@ pub extern "C" fn hyperlight_main() {
10971097
);
10981098
register_function(host_call_loop_def);
10991099

1100+
let call_host_then_spin_def = GuestFunctionDefinition::new(
1101+
"CallHostThenSpin".to_string(),
1102+
Vec::from(&[ParameterType::String]),
1103+
ReturnType::Void,
1104+
call_host_then_spin as usize,
1105+
);
1106+
register_function(call_host_then_spin_def);
1107+
11001108
let print_using_printf_def = GuestFunctionDefinition::new(
11011109
"PrintUsingPrintf".to_string(),
11021110
Vec::from(&[ParameterType::String]),
@@ -1639,6 +1647,23 @@ fn host_call_loop(function_call: &FunctionCall) -> Result<Vec<u8>> {
16391647
}
16401648
}
16411649

1650+
// Calls the given host function (no param, no return value) and then spins indefinitely.
1651+
fn call_host_then_spin(function_call: &FunctionCall) -> Result<Vec<u8>> {
1652+
if let ParameterValue::String(host_func_name) = &function_call.parameters.as_ref().unwrap()[0] {
1653+
call_host_function::<()>(host_func_name, None, ReturnType::Void)?;
1654+
#[expect(
1655+
clippy::empty_loop,
1656+
reason = "This function is used to keep the CPU busy"
1657+
)]
1658+
loop {}
1659+
} else {
1660+
Err(HyperlightGuestError::new(
1661+
ErrorCode::GuestFunctionParameterTypeMismatch,
1662+
"Invalid parameters passed to call_host_then_spin".to_string(),
1663+
))
1664+
}
1665+
}
1666+
16421667
// Interprets the given guest function call as a host function call and dispatches it to the host.
16431668
fn fuzz_host_function(func: FunctionCall) -> Result<Vec<u8>> {
16441669
let mut params = func.parameters.unwrap();

src/tests/rust_guests/witguest/Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)