diff --git a/RELEASES.md b/RELEASES.md index b1b260ad3085..24d48b88cdb0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,21 @@ -------------------------------------------------------------------------------- +## 21.0.2 + +Released 2024-10-09. + +### Fixed + +* Fix a runtime crash when combining tail-calls with host imports that capture a + stack trace or trap. + [GHSA-q8hx-mm92-4wvg](https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-q8hx-mm92-4wvg) + +* Fix a race condition could lead to WebAssembly control-flow integrity and type + safety violations. + [GHSA-7qmx-3fpx-r45m](https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-7qmx-3fpx-r45m) + +-------------------------------------------------------------------------------- + ## 21.0.1 Released 2024-05-22. diff --git a/crates/wasmtime/src/runtime/vm/traphandlers/backtrace.rs b/crates/wasmtime/src/runtime/vm/traphandlers/backtrace.rs index cf589aaf3813..0c610a35252c 100644 --- a/crates/wasmtime/src/runtime/vm/traphandlers/backtrace.rs +++ b/crates/wasmtime/src/runtime/vm/traphandlers/backtrace.rs @@ -178,6 +178,26 @@ impl Backtrace { arch::assert_entry_sp_is_aligned(trampoline_sp); + // It is possible that the contiguous sequence of Wasm frames is + // empty. This is rare, but can happen if: + // + // * Host calls into Wasm, pushing the entry trampoline frame + // + // * Entry trampoline calls the actual Wasm function, pushing a Wasm frame + // + // * Wasm function tail calls to an imported host function, *replacing* + // the Wasm frame with the exit trampoline's frame + // + // Now we have a stack like `[host, entry trampoline, exit trampoline]` + // which has a contiguous sequence of Wasm frames that are empty. + // + // Therefore, check if we've reached the entry trampoline's SP as the + // first thing we do. + if arch::reached_entry_sp(fp, trampoline_sp) { + log::trace!("=== Empty contiguous sequence of Wasm frames ==="); + return ControlFlow::Continue(()); + } + loop { // At the start of each iteration of the loop, we know that `fp` is // a frame pointer from Wasm code. Therefore, we know it is not diff --git a/tests/all/module.rs b/tests/all/module.rs index db11101275fd..48ff46af48d9 100644 --- a/tests/all/module.rs +++ b/tests/all/module.rs @@ -459,7 +459,6 @@ fn concurrent_type_modifications_and_checks() -> Result<()> { let mut config = Config::new(); config.wasm_function_references(true); - let engine = Engine::new(&config)?; let mut threads = Vec::new(); diff --git a/tests/all/traps.rs b/tests/all/traps.rs index 6679704e0b00..810615e71231 100644 --- a/tests/all/traps.rs +++ b/tests/all/traps.rs @@ -1679,3 +1679,127 @@ fn async_stack_size_ignored_if_disabled() -> Result<()> { Ok(()) } + +#[test] +#[cfg(not(target_arch = "s390x"))] +fn tail_call_to_imported_function() -> Result<()> { + let mut config = Config::new(); + config.wasm_tail_call(true); + let engine = Engine::new(&config)?; + + let module = Module::new( + &engine, + r#" + (module + (import "" "" (func (result i32))) + + (func (export "run") (result i32) + return_call 0 + ) + ) + "#, + )?; + + let mut store = Store::new(&engine, ()); + let host_func = Func::wrap(&mut store, || -> Result { bail!("whoopsie") }); + let instance = Instance::new(&mut store, &module, &[host_func.into()])?; + + let run = instance.get_typed_func::<(), i32>(&mut store, "run")?; + let err = run.call(&mut store, ()).unwrap_err(); + assert!(err.to_string().contains("whoopsie")); + + Ok(()) +} + +#[test] +#[cfg(not(target_arch = "s390x"))] +fn tail_call_to_imported_function_in_start_function() -> Result<()> { + let mut config = Config::new(); + config.wasm_tail_call(true); + let engine = Engine::new(&config)?; + + let module = Module::new( + &engine, + r#" + (module + (import "" "" (func)) + + (func $f + return_call 0 + ) + + (start $f) + ) + "#, + )?; + + let mut store = Store::new(&engine, ()); + let host_func = Func::wrap(&mut store, || -> Result<()> { bail!("whoopsie") }); + let err = Instance::new(&mut store, &module, &[host_func.into()]).unwrap_err(); + assert!(err.to_string().contains("whoopsie")); + + Ok(()) +} + +#[test] +#[cfg(not(target_arch = "s390x"))] +fn return_call_ref_to_imported_function() -> Result<()> { + let mut config = Config::new(); + config.wasm_tail_call(true); + config.wasm_function_references(true); + let engine = Engine::new(&config)?; + + let module = Module::new( + &engine, + r#" + (module + (type (func (result i32))) + (func (export "run") (param (ref 0)) (result i32) + (return_call_ref 0 (local.get 0)) + ) + ) + "#, + )?; + + let mut store = Store::new(&engine, ()); + let host_func = Func::wrap(&mut store, || -> Result { bail!("whoopsie") }); + let instance = Instance::new(&mut store, &module, &[])?; + + let run = instance.get_typed_func::(&mut store, "run")?; + let err = run.call(&mut store, host_func).unwrap_err(); + assert!(err.to_string().contains("whoopsie")); + + Ok(()) +} + +#[test] +#[cfg(not(target_arch = "s390x"))] +fn return_call_indirect_to_imported_function() -> Result<()> { + let mut config = Config::new(); + config.wasm_tail_call(true); + config.wasm_function_references(true); + let engine = Engine::new(&config)?; + + let module = Module::new( + &engine, + r#" + (module + (import "" "" (func (result i32))) + (table 1 funcref (ref.func 0)) + (func (export "run") (result i32) + (return_call_indirect (result i32) (i32.const 0)) + ) + ) + "#, + )?; + + let mut store = Store::new(&engine, ()); + let host_func = Func::wrap(&mut store, || -> Result { bail!("whoopsie") }); + let instance = Instance::new(&mut store, &module, &[host_func.into()])?; + + let run = instance.get_typed_func::<(), i32>(&mut store, "run")?; + let err = run.call(&mut store, ()).unwrap_err(); + assert!(err.to_string().contains("whoopsie")); + + Ok(()) +}