Don't re-capture backtraces when propagating traps through host frames (#5049)
* Add a benchmark for traps with many Wasm<-->host calls on the stack * Add a test for expected Wasm stack traces with Wasm<--host calls on the stack when we trap * Don't re-capture backtraces when propagating traps through host frames This fixes some accidentally quadratic code where we would re-capture a Wasm stack trace (takes `O(n)` time) every time we propagated a trap through a host frame back to Wasm (can happen `O(n)` times). And `O(n) * O(n) = O(n^2)`, of course. Whoops. After this commit, it trapping with a call stack that is `n` frames deep of Wasm-to-host-to-Wasm calls just captures a single backtrace and is therefore just a proper `O(n)` time operation, as it is intended to be. Now we explicitly track whether we need to capture a Wasm backtrace or not when raising a trap. This unfortunately isn't as straightforward as one might hope, however, because of the split between `wasmtime::Trap` and `wasmtime_runtime::Trap`. We need to decide whether or not to capture a Wasm backtrace inside `wasmtime_runtime` but in order to determine whether to do that or not we need to reflect on the `anyhow::Error` and see if it is a `wasmtime::Trap` that already has a backtrace or not. This can't be done the straightforward way because it would introduce a cyclic dependency between the `wasmtime` and `wasmtime-runtime` crates. We can't merge those two `Trap` types-- at least not without effectively merging the whole `wasmtime` and `wasmtime-runtime` crates together, which would be a good idea in a perfect world but would be a *ton* of ocean boiling from where we currently are -- because `wasmtime::Trap` does symbolication of stack traces which relies on module registration information data that resides inside the `wasmtime` crate and therefore can't be moved into `wasmtime-runtime`. We resolve this problem by adding a boolean to `wasmtime_runtime::raise_user_trap` that controls whether we should capture a Wasm backtrace or not, and then determine whether we need a backtrace or not at each of that function's call sites, which are in `wasmtime` and therefore can do the reflection to determine whether the user trap already has a backtrace or not. Phew! Fixes #5037 * debug assert that we don't record unnecessary backtraces for traps * Add assertions around `needs_backtrace` Unfortunately we can't do debug_assert_eq!(needs_backtrace, trap.inner.backtrace.get().is_some()); because `needs_backtrace` doesn't consider whether Wasm backtraces have been disabled via config. * Consolidate `needs_backtrace` calculation followed by calling `raise_user_trap` into one place
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use crate::component::func::{Memory, MemoryMut, Options};
|
||||
use crate::component::storage::slice_to_storage_mut;
|
||||
use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Type, Val};
|
||||
use crate::{AsContextMut, StoreContextMut, ValRaw};
|
||||
use crate::{AsContextMut, StoreContextMut, Trap, ValRaw};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::any::Any;
|
||||
use std::mem::{self, MaybeUninit};
|
||||
@@ -265,7 +265,7 @@ fn validate_inbounds<T: ComponentType>(memory: &[u8], ptr: &ValRaw) -> Result<us
|
||||
unsafe fn handle_result(func: impl FnOnce() -> Result<()>) {
|
||||
match panic::catch_unwind(AssertUnwindSafe(func)) {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => wasmtime_runtime::raise_user_trap(e),
|
||||
Ok(Err(e)) => Trap::raise(e),
|
||||
Err(e) => wasmtime_runtime::resume_panic(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ use std::pin::Pin;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Arc;
|
||||
use wasmtime_runtime::{
|
||||
raise_user_trap, ExportFunction, InstanceHandle, VMCallerCheckedAnyfunc, VMContext,
|
||||
VMFunctionBody, VMFunctionImport, VMHostFuncContext, VMOpaqueContext, VMSharedSignatureIndex,
|
||||
VMTrampoline,
|
||||
ExportFunction, InstanceHandle, VMCallerCheckedAnyfunc, VMContext, VMFunctionBody,
|
||||
VMFunctionImport, VMHostFuncContext, VMOpaqueContext, VMSharedSignatureIndex, VMTrampoline,
|
||||
};
|
||||
|
||||
/// A WebAssembly function which can be called.
|
||||
@@ -1887,7 +1886,7 @@ macro_rules! impl_into_func {
|
||||
|
||||
match result {
|
||||
CallResult::Ok(val) => val,
|
||||
CallResult::Trap(trap) => raise_user_trap(trap),
|
||||
CallResult::Trap(err) => Trap::raise(err),
|
||||
CallResult::Panic(panic) => wasmtime_runtime::resume_panic(panic),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ unsafe extern "C" fn stub_fn<F>(
|
||||
// call-site, which gets unwrapped in `Trap::from_runtime` later on as we
|
||||
// convert from the internal `Trap` type to our own `Trap` type in this
|
||||
// crate.
|
||||
Ok(Err(trap)) => wasmtime_runtime::raise_user_trap(trap.into()),
|
||||
Ok(Err(trap)) => Trap::raise(trap.into()),
|
||||
|
||||
// And finally if the imported function panicked, then we trigger the
|
||||
// form of unwinding that's safe to jump over wasm code on all
|
||||
|
||||
@@ -252,6 +252,15 @@ impl Trap {
|
||||
Trap::new_with_trace(TrapReason::I32Exit(status), None)
|
||||
}
|
||||
|
||||
// Same safety requirements and caveats as
|
||||
// `wasmtime_runtime::raise_user_trap`.
|
||||
pub(crate) unsafe fn raise(error: anyhow::Error) -> ! {
|
||||
let needs_backtrace = error
|
||||
.downcast_ref::<Trap>()
|
||||
.map_or(true, |trap| trap.trace().is_none());
|
||||
wasmtime_runtime::raise_user_trap(error, needs_backtrace)
|
||||
}
|
||||
|
||||
#[cold] // see Trap::new
|
||||
pub(crate) fn from_runtime_box(
|
||||
store: &StoreOpaque,
|
||||
@@ -264,9 +273,14 @@ impl Trap {
|
||||
pub(crate) fn from_runtime(store: &StoreOpaque, runtime_trap: wasmtime_runtime::Trap) -> Self {
|
||||
let wasmtime_runtime::Trap { reason, backtrace } = runtime_trap;
|
||||
match reason {
|
||||
wasmtime_runtime::TrapReason::User(error) => {
|
||||
wasmtime_runtime::TrapReason::User {
|
||||
error,
|
||||
needs_backtrace,
|
||||
} => {
|
||||
let trap = Trap::from(error);
|
||||
if let Some(backtrace) = backtrace {
|
||||
debug_assert!(needs_backtrace);
|
||||
debug_assert!(trap.inner.backtrace.get().is_none());
|
||||
trap.record_backtrace(TrapBacktrace::new(store, backtrace, None));
|
||||
}
|
||||
trap
|
||||
@@ -359,12 +373,15 @@ impl Trap {
|
||||
fn record_backtrace(&self, backtrace: TrapBacktrace) {
|
||||
// When a trap is created on top of the wasm stack, the trampoline will
|
||||
// re-raise it via
|
||||
// `wasmtime_runtime::raise_user_trap(trap.into::<Box<dyn Error>>())`
|
||||
// after panic::catch_unwind. We don't want to overwrite the first
|
||||
// backtrace recorded, as it is most precise.
|
||||
// FIXME: make sure backtraces are only created once per trap! they are
|
||||
// actually kinda expensive to create.
|
||||
let _ = self.inner.backtrace.try_insert(backtrace);
|
||||
// `wasmtime_runtime::raise_user_trap(trap.into::<Box<dyn Error>>(),
|
||||
// ..)` after `panic::catch_unwind`. We don't want to overwrite the
|
||||
// first backtrace recorded, as it is most precise. However, this should
|
||||
// never happen in the first place because we thread `needs_backtrace`
|
||||
// booleans throuch all calls to `raise_user_trap` to avoid capturing
|
||||
// unnecessary backtraces! So debug assert that we don't ever capture
|
||||
// unnecessary backtraces.
|
||||
let result = self.inner.backtrace.try_insert(backtrace);
|
||||
debug_assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user