diff --git a/RELEASES.md b/RELEASES.md index 73e936eb973d..0a86aab2f8de 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,19 @@ +## 25.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) + +-------------------------------------------------------------------------------- + ## 25.0.1 Released 2024-09-24. 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/traps.rs b/tests/all/traps.rs index bfd39e0a5451..f5368a48f4a7 100644 --- a/tests/all/traps.rs +++ b/tests/all/traps.rs @@ -5,6 +5,7 @@ use std::panic::{self, AssertUnwindSafe}; use std::process::Command; use std::sync::{Arc, Mutex}; use wasmtime::*; +use wasmtime_test_macros::wasmtime_test; #[test] fn test_trap_return() -> Result<()> { @@ -1679,3 +1680,113 @@ fn async_stack_size_ignored_if_disabled() -> Result<()> { Ok(()) } + +#[wasmtime_test(wasm_features(tail_call))] +fn tail_call_to_imported_function(config: &mut Config) -> Result<()> { + 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(()) +} + +#[wasmtime_test(wasm_features(tail_call))] +fn tail_call_to_imported_function_in_start_function(config: &mut Config) -> Result<()> { + 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(()) +} + +#[wasmtime_test(wasm_features(tail_call, function_references))] +fn return_call_ref_to_imported_function(config: &mut Config) -> Result<()> { + 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(()) +} + +#[wasmtime_test(wasm_features(tail_call, function_references))] +fn return_call_indirect_to_imported_function(config: &mut Config) -> Result<()> { + 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(()) +}