Wasmtime: fix stack walking across frames from different stores (#4779)

We were previously implicitly assuming that all Wasm frames in a stack used the
same `VMRuntimeLimits` as the previous frame we walked, but this is not true
when Wasm in store A calls into the host which then calls into Wasm in store B:

    | ...             |
    | Host            |  |
    +-----------------+  | stack
    | Wasm in store A |  | grows
    +-----------------+  | down
    | Host            |  |
    +-----------------+  |
    | Wasm in store B |  V
    +-----------------+

Trying to walk this stack would previously result in a runtime panic.

The solution is to push the maintenance of our list of saved Wasm FP/SP/PC
registers that allow us to identify contiguous regions of Wasm frames on the
stack deeper into `CallThreadState`. The saved registers list is now maintained
whenever updating the `CallThreadState` linked list by making the
`CallThreadState::prev` field private and only accessible via a getter and
setter, where the setter always maintains our invariants.
This commit is contained in:
Nick Fitzgerald
2022-08-30 11:28:00 -07:00
committed by GitHub
parent 09c93c70cc
commit ff0e84ecf4
7 changed files with 492 additions and 94 deletions

View File

@@ -5,7 +5,6 @@ use std::fmt;
use std::sync::Arc;
use wasmtime_environ::{EntityRef, FilePos, TrapCode as EnvTrapCode};
use wasmtime_jit::{demangle_function_name, demangle_function_name_or_index};
use wasmtime_runtime::Backtrace;
/// A struct representing an aborted instruction execution, with a message
/// indicating the cause.
@@ -140,19 +139,24 @@ impl fmt::Display for TrapCode {
#[derive(Debug)]
pub(crate) struct TrapBacktrace {
wasm_trace: Vec<FrameInfo>,
native_trace: Backtrace,
runtime_trace: wasmtime_runtime::Backtrace,
hint_wasm_backtrace_details_env: bool,
}
impl TrapBacktrace {
pub fn new(store: &StoreOpaque, native_trace: Backtrace, trap_pc: Option<usize>) -> Self {
let mut wasm_trace = Vec::<FrameInfo>::new();
pub fn new(
store: &StoreOpaque,
runtime_trace: wasmtime_runtime::Backtrace,
trap_pc: Option<usize>,
) -> Self {
let mut wasm_trace = Vec::<FrameInfo>::with_capacity(runtime_trace.frames().len());
let mut hint_wasm_backtrace_details_env = false;
let wasm_backtrace_details_env_used =
store.engine().config().wasm_backtrace_details_env_used;
for frame in native_trace.frames() {
for frame in runtime_trace.frames() {
debug_assert!(frame.pc() != 0);
// Note that we need to be careful about the pc we pass in
// here to lookup frame information. This program counter is
// used to translate back to an original source location in
@@ -168,6 +172,31 @@ impl TrapBacktrace {
} else {
frame.pc() - 1
};
// NB: The PC we are looking up _must_ be a Wasm PC since
// `wasmtime_runtime::Backtrace` only contains Wasm frames.
//
// However, consider the case where we have multiple, nested calls
// across stores (with host code in between, by necessity, since
// only things in the same store can be linked directly together):
//
// | ... |
// | Host | |
// +-----------------+ | stack
// | Wasm in store A | | grows
// +-----------------+ | down
// | Host | |
// +-----------------+ |
// | Wasm in store B | V
// +-----------------+
//
// In this scenario, the `wasmtime_runtime::Backtrace` will contain
// two frames: Wasm in store B followed by Wasm in store A. But
// `store.modules()` will only have the module information for
// modules instantiated within this store. Therefore, we use `if let
// Some(..)` instead of the `unwrap` you might otherwise expect and
// we ignore frames from modules that were not registered in this
// store's module registry.
if let Some((info, module)) = store.modules().lookup_frame_info(pc_to_lookup) {
wasm_trace.push(info);
@@ -186,7 +215,7 @@ impl TrapBacktrace {
Self {
wasm_trace,
native_trace,
runtime_trace,
hint_wasm_backtrace_details_env,
}
}
@@ -203,7 +232,9 @@ fn _assert_trap_is_sync_and_send(t: &Trap) -> (&dyn Sync, &dyn Send) {
impl Trap {
/// Creates a new `Trap` with `message`.
///
/// # Example
///
/// ```
/// let trap = wasmtime::Trap::new("unexpected error");
/// assert!(trap.to_string().contains("unexpected error"));
@@ -343,7 +374,7 @@ impl fmt::Debug for Trap {
f.field("reason", &self.inner.reason);
if let Some(backtrace) = self.inner.backtrace.get() {
f.field("wasm_trace", &backtrace.wasm_trace)
.field("native_trace", &backtrace.native_trace);
.field("runtime_trace", &backtrace.runtime_trace);
}
f.finish()
}