pub use std::error::Error;
use std::{cell::Cell, panic, sync::Once};
thread_local! {
static IN_VM_RUNTIME: Cell<bool> = const { Cell::new(false) };
}
static INSTALL_VM_RUNTIME_HOOK: Once = Once::new();
fn install_vm_runtime_hook() {
INSTALL_VM_RUNTIME_HOOK.call_once(|| {
let previous_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
if IN_VM_RUNTIME.try_with(Cell::get).unwrap_or(false) {
#[cfg(debug_assertions)]
{
let trimmed = info
.to_string()
.split_ascii_whitespace()
.skip_while(|&word| word != "panicked")
.collect::<Vec<&str>>()
.join(" ");
let msg = trimmed.replacen("panicked", "VM safely halted", 1);
eprintln!("{msg}");
}
} else {
previous_hook(info);
}
}));
});
}
#[inline]
pub fn try_vm_runtime<R, F: FnOnce() -> R>(f: F) -> std::thread::Result<R> {
install_vm_runtime_hook();
let was_in_vm_runtime = IN_VM_RUNTIME.with(|is_active| is_active.replace(true));
let result = panic::catch_unwind(panic::AssertUnwindSafe(f));
IN_VM_RUNTIME.with(|is_active| is_active.set(was_in_vm_runtime));
result
}
#[cfg(test)]
mod tests {
use super::try_vm_runtime;
use std::{
env,
panic,
process::Command,
sync::{
Arc,
Barrier,
atomic::{AtomicUsize, Ordering},
},
thread,
};
const CHILD_PROCESS_ENV: &str = "SNARKVM_VM_RUNTIME_HOOK_CHILD";
const NUM_VM_THREADS: usize = 8;
#[test]
fn test_try_vm_runtime_success() {
assert_eq!(try_vm_runtime(|| 42).unwrap(), 42);
}
#[test]
fn test_try_vm_runtime_preserves_panic_payload() {
let result = try_vm_runtime(|| panic!("expected VM halt"));
let payload = result.expect_err("VM panic should be caught");
assert_eq!(payload.downcast_ref::<&str>(), Some(&"expected VM halt"));
}
#[test]
fn test_nested_try_vm_runtime() {
let result = try_vm_runtime(|| {
let inner = try_vm_runtime(|| panic!("inner VM halt"));
assert!(inner.is_err(), "Inner VM panic should be caught");
panic!("outer VM halt");
});
assert!(result.is_err(), "Outer VM panic should be caught");
}
#[test]
fn test_parallel_vm_runtime_preserves_host_hook() {
if env::var_os(CHILD_PROCESS_ENV).is_some() {
run_parallel_vm_runtime_child();
return;
}
let output = Command::new(env::current_exe().expect("Failed to locate the test executable"))
.arg("test_parallel_vm_runtime_preserves_host_hook")
.arg("--nocapture")
.env(CHILD_PROCESS_ENV, "1")
.output()
.expect("Failed to launch the child test process");
assert!(
output.status.success(),
"Child test failed.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
#[cfg(debug_assertions)]
assert_eq!(stderr.matches("VM safely halted").count(), NUM_VM_THREADS, "Unexpected stderr:\n{stderr}");
#[cfg(not(debug_assertions))]
assert!(!stderr.contains("VM safely halted"), "Unexpected stderr:\n{stderr}");
}
fn run_parallel_vm_runtime_child() {
let host_panic_count = Arc::new(AtomicUsize::new(0));
let host_panic_count_ = host_panic_count.clone();
panic::set_hook(Box::new(move |_| {
host_panic_count_.fetch_add(1, Ordering::SeqCst);
}));
let ready = Arc::new(Barrier::new(NUM_VM_THREADS + 1));
let host_panic_finished = Arc::new(Barrier::new(NUM_VM_THREADS + 1));
let mut handles = Vec::with_capacity(NUM_VM_THREADS);
for index in 0..NUM_VM_THREADS {
let ready = ready.clone();
let host_panic_finished = host_panic_finished.clone();
handles.push(thread::spawn(move || {
try_vm_runtime(|| {
ready.wait();
host_panic_finished.wait();
panic!("VM operation {index} failed");
})
}));
}
ready.wait();
let unrelated_panic = panic::catch_unwind(|| panic!("unrelated host panic"));
assert!(unrelated_panic.is_err(), "Unrelated panic should be caught by the test");
assert_eq!(host_panic_count.load(Ordering::SeqCst), 1, "Host hook should receive the unrelated panic");
host_panic_finished.wait();
for handle in handles {
let result = handle.join().expect("VM worker should not panic outside try_vm_runtime");
assert!(result.is_err(), "VM panic should be caught");
}
assert_eq!(host_panic_count.load(Ordering::SeqCst), 1, "VM panics should not reach the host hook");
let later_panic = panic::catch_unwind(|| panic!("later host panic"));
assert!(later_panic.is_err(), "Later panic should be caught by the test");
assert_eq!(host_panic_count.load(Ordering::SeqCst), 2, "Host hook should remain installed after VM calls");
}
}