Files
wasmtime/tests/all/traps.rs
Peter Huene 54c07d8f16 Implement shared host functions. (#2625)
* Implement defining host functions at the Config level.

This commit introduces defining host functions at the `Config` rather than with
`Func` tied to a `Store`.

The intention here is to enable a host to define all of the functions once
with a `Config` and then use a `Linker` (or directly with
`Store::get_host_func`) to use the functions when instantiating a module.

This should help improve the performance of use cases where a `Store` is
short-lived and redefining the functions at every module instantiation is a
noticeable performance hit.

This commit adds `add_to_config` to the code generation for Wasmtime's `Wasi`
type.

The new method adds the WASI functions to the given config as host functions.

This commit adds context functions to `Store`: `get` to get a context of a
particular type and `set` to set the context on the store.

For safety, `set` cannot replace an existing context value of the same type.

`Wasi::set_context` was added to set the WASI context for a `Store` when using
`Wasi::add_to_config`.

* Add `Config::define_host_func_async`.

* Make config "async" rather than store.

This commit moves the concept of "async-ness" to `Config` rather than `Store`.

Note: this is a breaking API change for anyone that's already adopted the new
async support in Wasmtime.

Now `Config::new_async` is used to create an "async" config and any `Store`
associated with that config is inherently "async".

This is needed for async shared host functions to have some sanity check during their
execution (async host functions, like "async" `Func`, need to be called with
the "async" variants).

* Update async function tests to smoke async shared host functions.

This commit updates the async function tests to also smoke the shared host
functions, plus `Func::wrap0_async`.

This also changes the "wrap async" method names on `Config` to
`wrap$N_host_func_async` to slightly better match what is on `Func`.

* Move the instance allocator into `Engine`.

This commit moves the instantiated instance allocator from `Config` into
`Engine`.

This makes certain settings in `Config` no longer order-dependent, which is how
`Config` should ideally be.

This also removes the confusing concept of the "default" instance allocator,
instead opting to construct the on-demand instance allocator when needed.

This does alter the semantics of the instance allocator as now each `Engine`
gets its own instance allocator rather than sharing a single one between all
engines created from a configuration.

* Make `Engine::new` return `Result`.

This is a breaking API change for anyone using `Engine::new`.

As creating the pooling instance allocator may fail (likely cause is not enough
memory for the provided limits), instead of panicking when creating an
`Engine`, `Engine::new` now returns a `Result`.

* Remove `Config::new_async`.

This commit removes `Config::new_async` in favor of treating "async support" as
any other setting on `Config`.

The setting is `Config::async_support`.

* Remove order dependency when defining async host functions in `Config`.

This commit removes the order dependency where async support must be enabled on
the `Config` prior to defining async host functions.

The check is now delayed to when an `Engine` is created from the config.

* Update WASI example to use shared `Wasi::add_to_config`.

This commit updates the WASI example to use `Wasi::add_to_config`.

As only a single store and instance are used in the example, it has no semantic
difference from the previous example, but the intention is to steer users
towards defining WASI on the config and only using `Wasi::add_to_linker` when
more explicit scoping of the WASI context is required.
2021-03-11 10:14:03 -06:00

626 lines
18 KiB
Rust

use anyhow::Result;
use std::panic::{self, AssertUnwindSafe};
use std::process::Command;
use wasmtime::*;
#[test]
fn test_trap_return() -> Result<()> {
let 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(&store, hello_type, |_, _, _| Err(Trap::new("test 123")));
let instance = Instance::new(&store, &module, &[hello_func.into()])?;
let run_func = instance.get_func("run").expect("expected function export");
let e = run_func
.call(&[])
.err()
.expect("error calling function")
.downcast::<Trap>()?;
assert!(e.to_string().contains("test 123"));
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn test_trap_trace() -> Result<()> {
let 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(&store, &module, &[])?;
let run_func = instance.get_func("run").expect("expected function export");
let e = run_func
.call(&[])
.err()
.expect("error calling function")
.downcast::<Trap>()?;
let trace = e.trace();
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(), 1);
assert_eq!(trace[0].module_offset(), 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(), 1);
assert_eq!(trace[1].module_offset(), 0x21);
assert!(
e.to_string().contains("unreachable"),
"wrong message: {}",
e.to_string()
);
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn test_trap_trace_cb() -> Result<()> {
let 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(&store, fn_type, |_, _, _| Err(Trap::new("cb throw")));
let module = Module::new(store.engine(), wat)?;
let instance = Instance::new(&store, &module, &[fn_func.into()])?;
let run_func = instance.get_func("run").expect("expected function export");
let e = run_func
.call(&[])
.err()
.expect("error calling function")
.downcast::<Trap>()?;
let trace = e.trace();
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!(e.to_string().contains("cb throw"));
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn test_trap_stack_overflow() -> Result<()> {
let 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(&store, &module, &[])?;
let run_func = instance.get_func("run").expect("expected function export");
let e = run_func
.call(&[])
.err()
.expect("error calling function")
.downcast::<Trap>()?;
let trace = e.trace();
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!(e.to_string().contains("call stack exhausted"));
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn trap_display_pretty() -> Result<()> {
let 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(&store, &module, &[])?;
let run_func = instance.get_func("bar").expect("expected function export");
let e = run_func.call(&[]).err().expect("error calling function");
assert_eq!(
e.to_string(),
"\
wasm trap: unreachable
wasm backtrace:
0: 0x23 - m!die
1: 0x27 - m!<wasm function 1>
2: 0x2c - m!foo
3: 0x31 - m!<wasm function 3>
"
);
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn trap_display_multi_module() -> Result<()> {
let 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(&store, &module, &[])?;
let bar = instance.get_export("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(&store, &module, &[bar])?;
let bar2 = instance.get_func("bar2").expect("expected function export");
let e = bar2.call(&[]).err().expect("error calling function");
assert_eq!(
e.to_string(),
"\
wasm trap: unreachable
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>
"
);
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn trap_start_function_import() -> Result<()> {
let 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(&store, sig, |_, _, _| Err(Trap::new("user trap")));
let err = Instance::new(&store, &module, &[func.into()])
.err()
.unwrap();
assert!(err
.downcast_ref::<Trap>()
.unwrap()
.to_string()
.contains("user trap"));
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn rust_panic_import() -> Result<()> {
let 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(&store, sig, |_, _, _| panic!("this is a panic"));
let instance = Instance::new(
&store,
&module,
&[
func.into(),
Func::wrap(&store, || panic!("this is another panic")).into(),
],
)?;
let func = instance.get_func("foo").unwrap();
let err = panic::catch_unwind(AssertUnwindSafe(|| {
drop(func.call(&[]));
}))
.unwrap_err();
assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic"));
let func = instance.get_func("bar").unwrap();
let err = panic::catch_unwind(AssertUnwindSafe(|| {
drop(func.call(&[]));
}))
.unwrap_err();
assert_eq!(
err.downcast_ref::<&'static str>(),
Some(&"this is another panic")
);
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn rust_panic_start_function() -> Result<()> {
let 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(&store, sig, |_, _, _| panic!("this is a panic"));
let err = panic::catch_unwind(AssertUnwindSafe(|| {
drop(Instance::new(&store, &module, &[func.into()]));
}))
.unwrap_err();
assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic"));
let func = Func::wrap(&store, || panic!("this is another panic"));
let err = panic::catch_unwind(AssertUnwindSafe(|| {
drop(Instance::new(&store, &module, &[func.into()]));
}))
.unwrap_err();
assert_eq!(
err.downcast_ref::<&'static str>(),
Some(&"this is another panic")
);
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn mismatched_arguments() -> Result<()> {
let 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(&store, &module, &[])?;
let func = instance.get_func("foo").unwrap();
assert_eq!(
func.call(&[]).unwrap_err().to_string(),
"expected 1 arguments, got 0"
);
assert_eq!(
func.call(&[Val::F32(0)]).unwrap_err().to_string(),
"argument type mismatch: found f32 but expected i32",
);
assert_eq!(
func.call(&[Val::I32(0), Val::I32(1)])
.unwrap_err()
.to_string(),
"expected 1 arguments, got 2"
);
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn call_signature_mismatch() -> Result<()> {
let 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(&store, &module, &[])
.err()
.unwrap()
.downcast::<Trap>()
.unwrap();
assert!(err
.to_string()
.contains("wasm trap: indirect call type mismatch"));
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "windows", target_arch = "aarch64"), ignore)] // FIXME(#1642)
#[cfg_attr(all(target_os = "windows", feature = "experimental_x64"), ignore)] // FIXME(#2079)
fn start_trap_pretty() -> Result<()> {
let 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(&store, &module, &[]) {
Ok(_) => panic!("expected failure"),
Err(e) => e.downcast::<Trap>()?,
};
assert_eq!(
e.to_string(),
"\
wasm trap: unreachable
wasm backtrace:
0: 0x1d - m!die
1: 0x21 - m!<wasm function 1>
2: 0x26 - m!foo
3: 0x2b - m!start
"
);
Ok(())
}
#[test]
fn present_after_module_drop() -> Result<()> {
let store = Store::default();
let module = Module::new(store.engine(), r#"(func (export "foo") unreachable)"#)?;
let instance = Instance::new(&store, &module, &[])?;
let func = instance.get_func("foo").unwrap();
println!("asserting before we drop modules");
assert_trap(func.call(&[]).unwrap_err().downcast()?);
drop((instance, module));
println!("asserting after drop");
assert_trap(func.call(&[]).unwrap_err().downcast()?);
return Ok(());
fn assert_trap(t: Trap) {
println!("{}", t);
assert_eq!(t.trace().len(), 1);
assert_eq!(t.trace()[0].func_index(), 0);
}
}
fn assert_trap_code(wat: &str, code: wasmtime::TrapCode) {
let store = Store::default();
let module = Module::new(store.engine(), wat).unwrap();
let err = match Instance::new(&store, &module, &[]) {
Ok(_) => unreachable!(),
Err(e) => e,
};
let trap = err.downcast_ref::<Trap>().unwrap();
assert_eq!(trap.trap_code(), Some(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)
)
"#,
TrapCode::MemoryOutOfBounds,
);
assert_trap_code(
r#"
(module
(memory 0)
(func $start (drop (i32.load memory.size)))
(start $start)
)
"#,
TrapCode::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 store = Store::new(&engine);
let module = Module::new(&engine, &wasm)?;
let mut linker = Linker::new(&store);
wasmtime_wasi::Wasi::new(
&store,
wasi_cap_std_sync::WasiCtxBuilder::new()
.inherit_stdio()
.build()?,
)
.add_to_linker(&mut linker)?;
linker.module("", &module)?;
let run = linker.get_default("")?;
let trap = run.call(&[]).unwrap_err().downcast::<Trap>()?;
let mut found = false;
for frame in trap.trace() {
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 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(&store, &module, &[])
.err()
.unwrap()
.downcast::<Trap>()?;
assert_eq!(
trap.to_string(),
"\
wasm trap: unreachable
wasm backtrace:
0: 0x1a - <unknown>!start
"
);
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 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(&store, &module, &[])
.err()
.unwrap()
.downcast::<Trap>()?;
assert_eq!(
trap.to_string(),
"\
wasm trap: unreachable
wasm backtrace:
0: 0x1a - <unknown>!start
note: run with `WASMTIME_BACKTRACE_DETAILS=1` environment variable to display more information
"
);
Ok(())
}