* Add a `WasmBacktrace::new()` constructor This commit adds a method of manually capturing a backtrace of WebAssembly frames within a `Store`. The new constructor can be called with any `AsContext` values, primarily `&Store` and `&Caller`, during host functions to inspect the calling state. For now this does not respect the `Config::wasm_backtrace` option and instead unconditionally captures the backtrace. It's hoped that this can continue to adapt to needs of embedders by making it more configurable int he future if necessary. Closes #5339 * Split `new` into `capture` and `force_capture`
1163 lines
34 KiB
Rust
1163 lines
34 KiB
Rust
use anyhow::{bail, Error, Result};
|
|
use std::panic::{self, AssertUnwindSafe};
|
|
use std::process::Command;
|
|
use wasmtime::*;
|
|
|
|
#[test]
|
|
fn test_trap_return() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module
|
|
(func $hello (import "" "hello"))
|
|
(func (export "run") (call $hello))
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let hello_type = FuncType::new(None, None);
|
|
let hello_func = Func::new(&mut store, hello_type, |_, _, _| bail!("test 123"));
|
|
|
|
let instance = Instance::new(&mut store, &module, &[hello_func.into()])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
assert!(format!("{e:?}").contains("test 123"));
|
|
|
|
assert!(
|
|
e.downcast_ref::<WasmBacktrace>().is_some(),
|
|
"error should contain a WasmBacktrace"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_anyhow_error_return() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module
|
|
(func $hello (import "" "hello"))
|
|
(func (export "run") (call $hello))
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let hello_type = FuncType::new(None, None);
|
|
let hello_func = Func::new(&mut store, hello_type, |_, _, _| {
|
|
Err(anyhow::Error::msg("test 1234"))
|
|
});
|
|
|
|
let instance = Instance::new(&mut store, &module, &[hello_func.into()])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
assert!(!e.to_string().contains("test 1234"));
|
|
assert!(format!("{:?}", e).contains("Caused by:\n test 1234"));
|
|
|
|
assert!(e.downcast_ref::<Trap>().is_none());
|
|
assert!(e.downcast_ref::<WasmBacktrace>().is_some());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trap_return_downcast() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module
|
|
(func $hello (import "" "hello"))
|
|
(func (export "run") (call $hello))
|
|
)
|
|
"#;
|
|
|
|
#[derive(Debug)]
|
|
struct MyTrap;
|
|
impl std::fmt::Display for MyTrap {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "my trap")
|
|
}
|
|
}
|
|
impl std::error::Error for MyTrap {}
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let hello_type = FuncType::new(None, None);
|
|
let hello_func = Func::new(&mut store, hello_type, |_, _, _| {
|
|
Err(anyhow::Error::from(MyTrap))
|
|
});
|
|
|
|
let instance = Instance::new(&mut store, &module, &[hello_func.into()])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func
|
|
.call(&mut store, ())
|
|
.err()
|
|
.expect("error calling function");
|
|
let dbg = format!("{:?}", e);
|
|
println!("{}", dbg);
|
|
|
|
assert!(!e.to_string().contains("my trap"));
|
|
assert!(dbg.contains("Caused by:\n my trap"));
|
|
|
|
e.downcast_ref::<MyTrap>()
|
|
.expect("error downcasts to MyTrap");
|
|
let bt = e
|
|
.downcast_ref::<WasmBacktrace>()
|
|
.expect("error downcasts to WasmBacktrace");
|
|
assert_eq!(bt.frames().len(), 1);
|
|
println!("{:?}", bt);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trap_trace() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module $hello_mod
|
|
(func (export "run") (call $hello))
|
|
(func $hello (unreachable))
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
|
|
let trace = e.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert_eq!(trace.len(), 2);
|
|
assert_eq!(trace[0].module_name().unwrap(), "hello_mod");
|
|
assert_eq!(trace[0].func_index(), 1);
|
|
assert_eq!(trace[0].func_name(), Some("hello"));
|
|
assert_eq!(trace[0].func_offset(), Some(1));
|
|
assert_eq!(trace[0].module_offset(), Some(0x26));
|
|
assert_eq!(trace[1].module_name().unwrap(), "hello_mod");
|
|
assert_eq!(trace[1].func_index(), 0);
|
|
assert_eq!(trace[1].func_name(), None);
|
|
assert_eq!(trace[1].func_offset(), Some(1));
|
|
assert_eq!(trace[1].module_offset(), Some(0x21));
|
|
assert_eq!(e.downcast::<Trap>()?, Trap::UnreachableCodeReached);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trap_through_host() -> Result<()> {
|
|
let wat = r#"
|
|
(module $hello_mod
|
|
(import "" "" (func $host_func_a))
|
|
(import "" "" (func $host_func_b))
|
|
(func $a (export "a")
|
|
call $host_func_a
|
|
)
|
|
(func $b (export "b")
|
|
call $host_func_b
|
|
)
|
|
(func $c (export "c")
|
|
unreachable
|
|
)
|
|
)
|
|
"#;
|
|
|
|
let engine = Engine::default();
|
|
let module = Module::new(&engine, wat)?;
|
|
let mut store = Store::<()>::new(&engine, ());
|
|
|
|
let host_func_a = Func::new(
|
|
&mut store,
|
|
FuncType::new(vec![], vec![]),
|
|
|mut caller, _args, _results| {
|
|
caller
|
|
.get_export("b")
|
|
.unwrap()
|
|
.into_func()
|
|
.unwrap()
|
|
.call(caller, &[], &mut [])?;
|
|
Ok(())
|
|
},
|
|
);
|
|
let host_func_b = Func::new(
|
|
&mut store,
|
|
FuncType::new(vec![], vec![]),
|
|
|mut caller, _args, _results| {
|
|
caller
|
|
.get_export("c")
|
|
.unwrap()
|
|
.into_func()
|
|
.unwrap()
|
|
.call(caller, &[], &mut [])?;
|
|
Ok(())
|
|
},
|
|
);
|
|
|
|
let instance = Instance::new(
|
|
&mut store,
|
|
&module,
|
|
&[host_func_a.into(), host_func_b.into()],
|
|
)?;
|
|
let a = instance.get_typed_func::<(), ()>(&mut store, "a")?;
|
|
let err = a.call(&mut store, ()).unwrap_err();
|
|
let trace = err.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert_eq!(trace.len(), 3);
|
|
assert_eq!(trace[0].func_name(), Some("c"));
|
|
assert_eq!(trace[1].func_name(), Some("b"));
|
|
assert_eq!(trace[2].func_name(), Some("a"));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[allow(deprecated)]
|
|
fn test_trap_backtrace_disabled() -> Result<()> {
|
|
let mut config = Config::default();
|
|
config.wasm_backtrace(false);
|
|
let engine = Engine::new(&config).unwrap();
|
|
let mut store = Store::<()>::new(&engine, ());
|
|
let wat = r#"
|
|
(module $hello_mod
|
|
(func (export "run") (call $hello))
|
|
(func $hello (unreachable))
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
assert!(e.downcast_ref::<WasmBacktrace>().is_none());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trap_trace_cb() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module $hello_mod
|
|
(import "" "throw" (func $throw))
|
|
(func (export "run") (call $hello))
|
|
(func $hello (call $throw))
|
|
)
|
|
"#;
|
|
|
|
let fn_type = FuncType::new(None, None);
|
|
let fn_func = Func::new(&mut store, fn_type, |_, _, _| bail!("cb throw"));
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[fn_func.into()])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
|
|
let trace = e.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert_eq!(trace.len(), 2);
|
|
assert_eq!(trace[0].module_name().unwrap(), "hello_mod");
|
|
assert_eq!(trace[0].func_index(), 2);
|
|
assert_eq!(trace[1].module_name().unwrap(), "hello_mod");
|
|
assert_eq!(trace[1].func_index(), 1);
|
|
assert!(format!("{e:?}").contains("cb throw"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trap_stack_overflow() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module $rec_mod
|
|
(func $run (export "run") (call $run))
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
|
|
let trace = e.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert!(trace.len() >= 32);
|
|
for i in 0..trace.len() {
|
|
assert_eq!(trace[i].module_name().unwrap(), "rec_mod");
|
|
assert_eq!(trace[i].func_index(), 0);
|
|
assert_eq!(trace[i].func_name(), Some("run"));
|
|
}
|
|
assert_eq!(e.downcast::<Trap>()?, Trap::StackOverflow);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn trap_display_pretty() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module $m
|
|
(func $die unreachable)
|
|
(func call $die)
|
|
(func $foo call 1)
|
|
(func (export "bar") call $foo)
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "bar")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
assert_eq!(
|
|
format!("{:?}", e),
|
|
"\
|
|
error while executing at wasm backtrace:
|
|
0: 0x23 - m!die
|
|
1: 0x27 - m!<wasm function 1>
|
|
2: 0x2c - m!foo
|
|
3: 0x31 - m!<wasm function 3>
|
|
|
|
Caused by:
|
|
wasm trap: wasm `unreachable` instruction executed\
|
|
"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn trap_display_multi_module() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module $a
|
|
(func $die unreachable)
|
|
(func call $die)
|
|
(func $foo call 1)
|
|
(func (export "bar") call $foo)
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let bar = instance.get_export(&mut store, "bar").unwrap();
|
|
|
|
let wat = r#"
|
|
(module $b
|
|
(import "" "" (func $bar))
|
|
(func $middle call $bar)
|
|
(func (export "bar2") call $middle)
|
|
)
|
|
"#;
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[bar])?;
|
|
let bar2 = instance.get_typed_func::<(), ()>(&mut store, "bar2")?;
|
|
|
|
let e = bar2.call(&mut store, ()).unwrap_err();
|
|
assert_eq!(
|
|
format!("{e:?}"),
|
|
"\
|
|
error while executing at wasm backtrace:
|
|
0: 0x23 - a!die
|
|
1: 0x27 - a!<wasm function 1>
|
|
2: 0x2c - a!foo
|
|
3: 0x31 - a!<wasm function 3>
|
|
4: 0x29 - b!middle
|
|
5: 0x2e - b!<wasm function 2>
|
|
|
|
Caused by:
|
|
wasm trap: wasm `unreachable` instruction executed\
|
|
"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn trap_start_function_import() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let binary = wat::parse_str(
|
|
r#"
|
|
(module $a
|
|
(import "" "" (func $foo))
|
|
(start $foo)
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let module = Module::new(store.engine(), &binary)?;
|
|
let sig = FuncType::new(None, None);
|
|
let func = Func::new(&mut store, sig, |_, _, _| bail!("user trap"));
|
|
let err = Instance::new(&mut store, &module, &[func.into()]).unwrap_err();
|
|
assert!(format!("{err:?}").contains("user trap"));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn rust_panic_import() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let binary = wat::parse_str(
|
|
r#"
|
|
(module $a
|
|
(import "" "" (func $foo))
|
|
(import "" "" (func $bar))
|
|
(func (export "foo") call $foo)
|
|
(func (export "bar") call $bar)
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let module = Module::new(store.engine(), &binary)?;
|
|
let sig = FuncType::new(None, None);
|
|
let func = Func::new(&mut store, sig, |_, _, _| panic!("this is a panic"));
|
|
let func2 = Func::wrap(&mut store, || panic!("this is another panic"));
|
|
let instance = Instance::new(&mut store, &module, &[func.into(), func2.into()])?;
|
|
let func = instance.get_typed_func::<(), ()>(&mut store, "foo")?;
|
|
let err =
|
|
panic::catch_unwind(AssertUnwindSafe(|| drop(func.call(&mut store, ())))).unwrap_err();
|
|
assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic"));
|
|
|
|
let func = instance.get_typed_func::<(), ()>(&mut store, "bar")?;
|
|
let err = panic::catch_unwind(AssertUnwindSafe(|| {
|
|
drop(func.call(&mut store, ()));
|
|
}))
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
err.downcast_ref::<&'static str>(),
|
|
Some(&"this is another panic")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// Test that we properly save/restore our trampolines' saved Wasm registers
|
|
// (used when capturing backtraces) before we resume panics.
|
|
#[test]
|
|
fn rust_catch_panic_import() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
|
|
let binary = wat::parse_str(
|
|
r#"
|
|
(module $a
|
|
(import "" "panic" (func $panic))
|
|
(import "" "catch panic" (func $catch_panic))
|
|
(func (export "panic") call $panic)
|
|
(func (export "run")
|
|
call $catch_panic
|
|
call $catch_panic
|
|
unreachable
|
|
)
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let module = Module::new(store.engine(), &binary)?;
|
|
let num_panics = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
|
|
let sig = FuncType::new(None, None);
|
|
let panic = Func::new(&mut store, sig, {
|
|
let num_panics = num_panics.clone();
|
|
move |_, _, _| {
|
|
num_panics.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
panic!("this is a panic");
|
|
}
|
|
});
|
|
let catch_panic = Func::wrap(&mut store, |mut caller: Caller<'_, _>| {
|
|
panic::catch_unwind(AssertUnwindSafe(|| {
|
|
drop(
|
|
caller
|
|
.get_export("panic")
|
|
.unwrap()
|
|
.into_func()
|
|
.unwrap()
|
|
.call(&mut caller, &[], &mut []),
|
|
);
|
|
}))
|
|
.unwrap_err();
|
|
});
|
|
|
|
let instance = Instance::new(&mut store, &module, &[panic.into(), catch_panic.into()])?;
|
|
let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
let trap = run.call(&mut store, ()).unwrap_err();
|
|
let trace = trap.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert_eq!(trace.len(), 1);
|
|
assert_eq!(trace[0].func_index(), 3);
|
|
assert_eq!(num_panics.load(std::sync::atomic::Ordering::SeqCst), 2);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn rust_panic_start_function() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let binary = wat::parse_str(
|
|
r#"
|
|
(module $a
|
|
(import "" "" (func $foo))
|
|
(start $foo)
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let module = Module::new(store.engine(), &binary)?;
|
|
let sig = FuncType::new(None, None);
|
|
let func = Func::new(&mut store, sig, |_, _, _| panic!("this is a panic"));
|
|
let err = panic::catch_unwind(AssertUnwindSafe(|| {
|
|
drop(Instance::new(&mut store, &module, &[func.into()]));
|
|
}))
|
|
.unwrap_err();
|
|
assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic"));
|
|
|
|
let func = Func::wrap(&mut store, || panic!("this is another panic"));
|
|
let err = panic::catch_unwind(AssertUnwindSafe(|| {
|
|
drop(Instance::new(&mut store, &module, &[func.into()]));
|
|
}))
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
err.downcast_ref::<&'static str>(),
|
|
Some(&"this is another panic")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn mismatched_arguments() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let binary = wat::parse_str(
|
|
r#"
|
|
(module $a
|
|
(func (export "foo") (param i32))
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let module = Module::new(store.engine(), &binary)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let func = instance.get_func(&mut store, "foo").unwrap();
|
|
assert_eq!(
|
|
func.call(&mut store, &[], &mut []).unwrap_err().to_string(),
|
|
"expected 1 arguments, got 0"
|
|
);
|
|
assert_eq!(
|
|
func.call(&mut store, &[Val::F32(0)], &mut [])
|
|
.unwrap_err()
|
|
.to_string(),
|
|
"argument type mismatch: found f32 but expected i32",
|
|
);
|
|
assert_eq!(
|
|
func.call(&mut store, &[Val::I32(0), Val::I32(1)], &mut [])
|
|
.unwrap_err()
|
|
.to_string(),
|
|
"expected 1 arguments, got 2"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn call_signature_mismatch() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let binary = wat::parse_str(
|
|
r#"
|
|
(module $a
|
|
(func $foo
|
|
i32.const 0
|
|
call_indirect)
|
|
(func $bar (param i32))
|
|
(start $foo)
|
|
|
|
(table 1 anyfunc)
|
|
(elem (i32.const 0) 1)
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let module = Module::new(store.engine(), &binary)?;
|
|
let err = Instance::new(&mut store, &module, &[])
|
|
.err()
|
|
.unwrap()
|
|
.downcast::<Trap>()
|
|
.unwrap();
|
|
assert!(err
|
|
.to_string()
|
|
.contains("wasm trap: indirect call type mismatch"));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn start_trap_pretty() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let wat = r#"
|
|
(module $m
|
|
(func $die unreachable)
|
|
(func call $die)
|
|
(func $foo call 1)
|
|
(func $start call $foo)
|
|
(start $start)
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let e = match Instance::new(&mut store, &module, &[]) {
|
|
Ok(_) => panic!("expected failure"),
|
|
Err(e) => e,
|
|
};
|
|
|
|
assert_eq!(
|
|
format!("{e:?}"),
|
|
"\
|
|
error while executing at wasm backtrace:
|
|
0: 0x1d - m!die
|
|
1: 0x21 - m!<wasm function 1>
|
|
2: 0x26 - m!foo
|
|
3: 0x2b - m!start
|
|
|
|
Caused by:
|
|
wasm trap: wasm `unreachable` instruction executed\
|
|
"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn present_after_module_drop() -> Result<()> {
|
|
let mut store = Store::<()>::default();
|
|
let module = Module::new(store.engine(), r#"(func (export "foo") unreachable)"#)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let func = instance.get_typed_func::<(), ()>(&mut store, "foo")?;
|
|
|
|
println!("asserting before we drop modules");
|
|
assert_trap(func.call(&mut store, ()).unwrap_err());
|
|
drop((instance, module));
|
|
|
|
println!("asserting after drop");
|
|
assert_trap(func.call(&mut store, ()).unwrap_err());
|
|
return Ok(());
|
|
|
|
fn assert_trap(t: Error) {
|
|
println!("{:?}", t);
|
|
let trace = t.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert_eq!(trace.len(), 1);
|
|
assert_eq!(trace[0].func_index(), 0);
|
|
}
|
|
}
|
|
|
|
fn assert_trap_code(wat: &str, code: wasmtime::Trap) {
|
|
let mut store = Store::<()>::default();
|
|
let module = Module::new(store.engine(), wat).unwrap();
|
|
|
|
let err = match Instance::new(&mut store, &module, &[]) {
|
|
Ok(_) => unreachable!(),
|
|
Err(e) => e,
|
|
};
|
|
let trap = err.downcast_ref::<Trap>().unwrap();
|
|
assert_eq!(*trap, code);
|
|
}
|
|
|
|
#[test]
|
|
fn heap_out_of_bounds_trap() {
|
|
assert_trap_code(
|
|
r#"
|
|
(module
|
|
(memory 0)
|
|
(func $start (drop (i32.load (i32.const 1000000))))
|
|
(start $start)
|
|
)
|
|
"#,
|
|
Trap::MemoryOutOfBounds,
|
|
);
|
|
|
|
assert_trap_code(
|
|
r#"
|
|
(module
|
|
(memory 0)
|
|
(func $start (drop (i32.load memory.size)))
|
|
(start $start)
|
|
)
|
|
"#,
|
|
Trap::MemoryOutOfBounds,
|
|
);
|
|
}
|
|
|
|
fn rustc(src: &str) -> Vec<u8> {
|
|
let td = tempfile::TempDir::new().unwrap();
|
|
let output = td.path().join("foo.wasm");
|
|
let input = td.path().join("input.rs");
|
|
std::fs::write(&input, src).unwrap();
|
|
let result = Command::new("rustc")
|
|
.arg(&input)
|
|
.arg("-o")
|
|
.arg(&output)
|
|
.arg("--target")
|
|
.arg("wasm32-wasi")
|
|
.arg("-g")
|
|
.output()
|
|
.unwrap();
|
|
if result.status.success() {
|
|
return std::fs::read(&output).unwrap();
|
|
}
|
|
panic!(
|
|
"rustc failed: {}\n{}",
|
|
result.status,
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_dwarf_info() -> Result<()> {
|
|
let wasm = rustc(
|
|
"
|
|
fn main() {
|
|
panic!();
|
|
}
|
|
",
|
|
);
|
|
let mut config = Config::new();
|
|
config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
|
|
let engine = Engine::new(&config)?;
|
|
let module = Module::new(&engine, &wasm)?;
|
|
let mut linker = Linker::new(&engine);
|
|
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
|
|
let mut store = Store::new(
|
|
&engine,
|
|
wasmtime_wasi::sync::WasiCtxBuilder::new()
|
|
.inherit_stdio()
|
|
.build(),
|
|
);
|
|
linker.module(&mut store, "", &module)?;
|
|
let run = linker.get_default(&mut store, "")?;
|
|
let trap = run.call(&mut store, &[], &mut []).unwrap_err();
|
|
|
|
let mut found = false;
|
|
let frames = trap.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
for frame in frames {
|
|
for symbol in frame.symbols() {
|
|
if let Some(file) = symbol.file() {
|
|
if file.ends_with("input.rs") {
|
|
found = true;
|
|
assert!(symbol.name().unwrap().contains("main"));
|
|
assert_eq!(symbol.line(), Some(3));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(found);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn no_hint_even_with_dwarf_info() -> Result<()> {
|
|
let mut config = Config::new();
|
|
config.wasm_backtrace_details(WasmBacktraceDetails::Disable);
|
|
let engine = Engine::new(&config)?;
|
|
let mut store = Store::new(&engine, ());
|
|
let module = Module::new(
|
|
&engine,
|
|
r#"
|
|
(module
|
|
(@custom ".debug_info" (after last) "")
|
|
(func $start
|
|
unreachable)
|
|
(start $start)
|
|
)
|
|
"#,
|
|
)?;
|
|
let trap = Instance::new(&mut store, &module, &[]).unwrap_err();
|
|
assert_eq!(
|
|
format!("{trap:?}"),
|
|
"\
|
|
error while executing at wasm backtrace:
|
|
0: 0x1a - <unknown>!start
|
|
|
|
Caused by:
|
|
wasm trap: wasm `unreachable` instruction executed\
|
|
"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hint_with_dwarf_info() -> Result<()> {
|
|
// Skip this test if the env var is already configure, but in CI we're sure
|
|
// to run tests without this env var configured.
|
|
if std::env::var("WASMTIME_BACKTRACE_DETAILS").is_ok() {
|
|
return Ok(());
|
|
}
|
|
let mut store = Store::<()>::default();
|
|
let module = Module::new(
|
|
store.engine(),
|
|
r#"
|
|
(module
|
|
(@custom ".debug_info" (after last) "")
|
|
(func $start
|
|
unreachable)
|
|
(start $start)
|
|
)
|
|
"#,
|
|
)?;
|
|
let trap = Instance::new(&mut store, &module, &[]).unwrap_err();
|
|
assert_eq!(
|
|
format!("{trap:?}"),
|
|
"\
|
|
error while executing at wasm backtrace:
|
|
0: 0x1a - <unknown>!start
|
|
note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable to may show more debugging information
|
|
|
|
Caused by:
|
|
wasm trap: wasm `unreachable` instruction executed\
|
|
"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn multithreaded_traps() -> Result<()> {
|
|
// Compile and run unreachable on a thread, then moves over the whole store to another thread,
|
|
// and make sure traps are still correctly caught after notifying the store of the move.
|
|
let mut store = Store::<()>::default();
|
|
let module = Module::new(
|
|
store.engine(),
|
|
r#"(module (func (export "run") unreachable))"#,
|
|
)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
|
|
assert!(instance
|
|
.get_typed_func::<(), ()>(&mut store, "run")?
|
|
.call(&mut store, ())
|
|
.is_err());
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
assert!(instance
|
|
.get_typed_func::<(), ()>(&mut store, "run")
|
|
.unwrap()
|
|
.call(&mut store, ())
|
|
.is_err());
|
|
});
|
|
|
|
handle.join().expect("couldn't join thread");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn traps_without_address_map() -> Result<()> {
|
|
let mut config = Config::new();
|
|
config.generate_address_map(false);
|
|
let engine = Engine::new(&config)?;
|
|
let mut store = Store::new(&engine, ());
|
|
let wat = r#"
|
|
(module $hello_mod
|
|
(func (export "run") (call $hello))
|
|
(func $hello (unreachable))
|
|
)
|
|
"#;
|
|
|
|
let module = Module::new(store.engine(), wat)?;
|
|
let instance = Instance::new(&mut store, &module, &[])?;
|
|
let run_func = instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
|
|
let e = run_func.call(&mut store, ()).unwrap_err();
|
|
|
|
let trace = e.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
assert_eq!(trace.len(), 2);
|
|
assert_eq!(trace[0].func_name(), Some("hello"));
|
|
assert_eq!(trace[0].func_index(), 1);
|
|
assert_eq!(trace[0].module_offset(), None);
|
|
assert_eq!(trace[1].func_name(), None);
|
|
assert_eq!(trace[1].func_index(), 0);
|
|
assert_eq!(trace[1].module_offset(), None);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn catch_trap_calling_across_stores() -> Result<()> {
|
|
let _ = env_logger::try_init();
|
|
|
|
let engine = Engine::default();
|
|
|
|
let mut child_store = Store::new(&engine, ());
|
|
let child_module = Module::new(
|
|
child_store.engine(),
|
|
r#"
|
|
(module $child
|
|
(func $trap (export "trap")
|
|
unreachable
|
|
)
|
|
)
|
|
"#,
|
|
)?;
|
|
let child_instance = Instance::new(&mut child_store, &child_module, &[])?;
|
|
|
|
struct ParentCtx {
|
|
child_store: Store<()>,
|
|
child_instance: Instance,
|
|
}
|
|
|
|
let mut linker = Linker::new(&engine);
|
|
linker.func_wrap(
|
|
"host",
|
|
"catch_child_trap",
|
|
move |mut caller: Caller<'_, ParentCtx>| {
|
|
let mut ctx = caller.as_context_mut();
|
|
let data = ctx.data_mut();
|
|
let func = data
|
|
.child_instance
|
|
.get_typed_func::<(), ()>(&mut data.child_store, "trap")
|
|
.expect("trap function should be exported");
|
|
|
|
let trap = func.call(&mut data.child_store, ()).unwrap_err();
|
|
assert!(
|
|
format!("{trap:?}").contains("unreachable"),
|
|
"trap should contain 'unreachable', got: {trap:?}"
|
|
);
|
|
|
|
let trace = trap.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
|
|
assert_eq!(trace.len(), 1);
|
|
assert_eq!(trace[0].func_name(), Some("trap"));
|
|
// For now, we only get stack frames for Wasm in this store, not
|
|
// across all stores.
|
|
//
|
|
// assert_eq!(trace[1].func_name(), Some("run"));
|
|
|
|
Ok(())
|
|
},
|
|
)?;
|
|
|
|
let mut store = Store::new(
|
|
&engine,
|
|
ParentCtx {
|
|
child_store,
|
|
child_instance,
|
|
},
|
|
);
|
|
|
|
let parent_module = Module::new(
|
|
store.engine(),
|
|
r#"
|
|
(module $parent
|
|
(func $host.catch_child_trap (import "host" "catch_child_trap"))
|
|
(func $run (export "run")
|
|
call $host.catch_child_trap
|
|
)
|
|
)
|
|
"#,
|
|
)?;
|
|
|
|
let parent_instance = linker.instantiate(&mut store, &parent_module)?;
|
|
|
|
let func = parent_instance.get_typed_func::<(), ()>(&mut store, "run")?;
|
|
func.call(store, ())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn async_then_sync_trap() -> Result<()> {
|
|
// Test the trapping and capturing the stack with the following sequence of
|
|
// calls:
|
|
//
|
|
// a[async] ---> b[host] ---> c[sync]
|
|
|
|
drop(env_logger::try_init());
|
|
|
|
let wat = r#"
|
|
(module
|
|
(import "" "b" (func $b))
|
|
(func $a (export "a")
|
|
call $b
|
|
)
|
|
(func $c (export "c")
|
|
unreachable
|
|
)
|
|
)
|
|
"#;
|
|
|
|
let mut sync_store = Store::new(&Engine::default(), ());
|
|
|
|
let sync_module = Module::new(sync_store.engine(), wat)?;
|
|
|
|
let mut sync_linker = Linker::new(sync_store.engine());
|
|
sync_linker.func_wrap("", "b", |_caller: Caller<_>| unreachable!())?;
|
|
|
|
let sync_instance = sync_linker.instantiate(&mut sync_store, &sync_module)?;
|
|
|
|
struct AsyncCtx {
|
|
sync_instance: Instance,
|
|
sync_store: Store<()>,
|
|
}
|
|
|
|
let mut async_store = Store::new(
|
|
&Engine::new(Config::new().async_support(true)).unwrap(),
|
|
AsyncCtx {
|
|
sync_instance,
|
|
sync_store,
|
|
},
|
|
);
|
|
|
|
let async_module = Module::new(async_store.engine(), wat)?;
|
|
|
|
let mut async_linker = Linker::new(async_store.engine());
|
|
async_linker.func_wrap("", "b", move |mut caller: Caller<AsyncCtx>| {
|
|
log::info!("Called `b`...");
|
|
let sync_instance = caller.data().sync_instance;
|
|
let sync_store = &mut caller.data_mut().sync_store;
|
|
|
|
log::info!("Calling `c`...");
|
|
let c = sync_instance
|
|
.get_typed_func::<(), ()>(&mut *sync_store, "c")
|
|
.unwrap();
|
|
c.call(sync_store, ())?;
|
|
Ok(())
|
|
})?;
|
|
|
|
let async_instance = async_linker
|
|
.instantiate_async(&mut async_store, &async_module)
|
|
.await?;
|
|
|
|
log::info!("Calling `a`...");
|
|
let a = async_instance
|
|
.get_typed_func::<(), ()>(&mut async_store, "a")
|
|
.unwrap();
|
|
let trap = a.call_async(&mut async_store, ()).await.unwrap_err();
|
|
|
|
let trace = trap.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
// We don't support cross-store or cross-engine symbolication currently, so
|
|
// the other frames are ignored.
|
|
assert_eq!(trace.len(), 1);
|
|
assert_eq!(trace[0].func_name(), Some("c"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn sync_then_async_trap() -> Result<()> {
|
|
// Test the trapping and capturing the stack with the following sequence of
|
|
// calls:
|
|
//
|
|
// a[sync] ---> b[host] ---> c[async]
|
|
|
|
drop(env_logger::try_init());
|
|
|
|
let wat = r#"
|
|
(module
|
|
(import "" "b" (func $b))
|
|
(func $a (export "a")
|
|
call $b
|
|
)
|
|
(func $c (export "c")
|
|
unreachable
|
|
)
|
|
)
|
|
"#;
|
|
|
|
let mut async_store = Store::new(&Engine::new(Config::new().async_support(true)).unwrap(), ());
|
|
|
|
let async_module = Module::new(async_store.engine(), wat)?;
|
|
|
|
let mut async_linker = Linker::new(async_store.engine());
|
|
async_linker.func_wrap("", "b", |_caller: Caller<_>| unreachable!())?;
|
|
|
|
let async_instance = async_linker
|
|
.instantiate_async(&mut async_store, &async_module)
|
|
.await?;
|
|
|
|
struct SyncCtx {
|
|
async_instance: Instance,
|
|
async_store: Store<()>,
|
|
}
|
|
|
|
let mut sync_store = Store::new(
|
|
&Engine::default(),
|
|
SyncCtx {
|
|
async_instance,
|
|
async_store,
|
|
},
|
|
);
|
|
|
|
let sync_module = Module::new(sync_store.engine(), wat)?;
|
|
|
|
let mut sync_linker = Linker::new(sync_store.engine());
|
|
sync_linker.func_wrap("", "b", move |mut caller: Caller<SyncCtx>| -> Result<()> {
|
|
log::info!("Called `b`...");
|
|
let async_instance = caller.data().async_instance;
|
|
let async_store = &mut caller.data_mut().async_store;
|
|
|
|
log::info!("Calling `c`...");
|
|
let c = async_instance
|
|
.get_typed_func::<(), ()>(&mut *async_store, "c")
|
|
.unwrap();
|
|
tokio::task::block_in_place(|| {
|
|
tokio::runtime::Handle::current()
|
|
.block_on(async move { c.call_async(async_store, ()).await })
|
|
})?;
|
|
Ok(())
|
|
})?;
|
|
|
|
let sync_instance = sync_linker.instantiate(&mut sync_store, &sync_module)?;
|
|
|
|
log::info!("Calling `a`...");
|
|
let a = sync_instance
|
|
.get_typed_func::<(), ()>(&mut sync_store, "a")
|
|
.unwrap();
|
|
let trap = a.call(&mut sync_store, ()).unwrap_err();
|
|
|
|
let trace = trap.downcast_ref::<WasmBacktrace>().unwrap().frames();
|
|
// We don't support cross-store or cross-engine symbolication currently, so
|
|
// the other frames are ignored.
|
|
assert_eq!(trace.len(), 1);
|
|
assert_eq!(trace[0].func_name(), Some("c"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn standalone_backtrace() -> Result<()> {
|
|
let engine = Engine::default();
|
|
let mut store = Store::new(&engine, ());
|
|
let trace = WasmBacktrace::capture(&store);
|
|
assert!(trace.frames().is_empty());
|
|
let module = Module::new(
|
|
&engine,
|
|
r#"
|
|
(module
|
|
(import "" "" (func $host))
|
|
(func $foo (export "f") call $bar)
|
|
(func $bar call $host)
|
|
)
|
|
"#,
|
|
)?;
|
|
let func = Func::wrap(&mut store, |cx: Caller<'_, ()>| {
|
|
let trace = WasmBacktrace::capture(&cx);
|
|
assert_eq!(trace.frames().len(), 2);
|
|
let frame1 = &trace.frames()[0];
|
|
let frame2 = &trace.frames()[1];
|
|
assert_eq!(frame1.func_index(), 2);
|
|
assert_eq!(frame1.func_name(), Some("bar"));
|
|
assert_eq!(frame2.func_index(), 1);
|
|
assert_eq!(frame2.func_name(), Some("foo"));
|
|
});
|
|
let instance = Instance::new(&mut store, &module, &[func.into()])?;
|
|
let f = instance.get_typed_func::<(), ()>(&mut store, "f")?;
|
|
f.call(&mut store, ())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[allow(deprecated)]
|
|
fn standalone_backtrace_disabled() -> Result<()> {
|
|
let mut config = Config::new();
|
|
config.wasm_backtrace(false);
|
|
let engine = Engine::new(&config)?;
|
|
let mut store = Store::new(&engine, ());
|
|
let module = Module::new(
|
|
&engine,
|
|
r#"
|
|
(module
|
|
(import "" "" (func $host))
|
|
(func $foo (export "f") call $bar)
|
|
(func $bar call $host)
|
|
)
|
|
"#,
|
|
)?;
|
|
let func = Func::wrap(&mut store, |cx: Caller<'_, ()>| {
|
|
let trace = WasmBacktrace::capture(&cx);
|
|
assert_eq!(trace.frames().len(), 0);
|
|
let trace = WasmBacktrace::force_capture(&cx);
|
|
assert_eq!(trace.frames().len(), 2);
|
|
});
|
|
let instance = Instance::new(&mut store, &module, &[func.into()])?;
|
|
let f = instance.get_typed_func::<(), ()>(&mut store, "f")?;
|
|
f.call(&mut store, ())?;
|
|
Ok(())
|
|
}
|