Skip to content

Commit

Permalink
Add core dump support to the runtime (bytecodealliance#6513)
Browse files Browse the repository at this point in the history
* Add config method for whether or not to capture coredumps on trap

* Add core dump support to the runtime

This adds the machinery to capture a core dump when a trap occurs
and attach it to the resulting anyhow::Error that gets bubbled up to
the caller. I've created a CoreDumpStack structure in the runtime, which is
currently just a backtrace until we design a way to recover the locals
stack values when a trap occurs. When that CoreDumpStack gets converted to
a wasmtime::WasmCoreDump, we add additional information from the Store such
as globals, memories, and instance information.

A lot of this is mechanistically similar to how backtraces
are captured and attached to errors. Given that they both are attached as
context to anyhow::Errors, setting coredump_on_trap to true will supercede
any setting for wasm_backtrace.

* Address some PR feedback

* Fix docs

* Switch WasmCoreDump to have vec of module names rather than Modules

* Add smoketests for WasmCoreDump

* clean up tests

* Update memories and globals field to be store_

* add debug impl for wasmcoredump

* Remove duplicate imports

* ignore miri for wasm tests
  • Loading branch information
itsrainy authored and eduardomourar committed Aug 18, 2023
1 parent 2c9a742 commit 31639dd
Show file tree
Hide file tree
Showing 18 changed files with 436 additions and 32 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ memfd = "0.6.2"
paste = "1.0.3"
encoding_rs = { version = "0.8.31", optional = true }
sptr = "0.3.2"
wasm-encoder = { workspace = true }

[target.'cfg(target_os = "macos")'.dependencies]
mach = "0.3.2"
Expand Down
63 changes: 46 additions & 17 deletions crates/runtime/src/traphandlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! signalhandling mechanisms.
mod backtrace;
mod coredump;

use crate::{Instance, VMContext, VMRuntimeLimits};
use anyhow::Error;
Expand All @@ -12,6 +13,7 @@ use std::ptr;
use std::sync::Once;

pub use self::backtrace::{Backtrace, Frame};
pub use self::coredump::CoreDumpStack;
pub use self::tls::{tls_eager_initialize, AsyncWasmCallState, PreviousAsyncWasmCallState};

cfg_if::cfg_if! {
Expand Down Expand Up @@ -176,6 +178,8 @@ pub struct Trap {
pub reason: TrapReason,
/// Wasm backtrace of the trap, if any.
pub backtrace: Option<Backtrace>,
/// The Wasm Coredump, if any.
pub coredumpstack: Option<CoreDumpStack>,
}

/// Enumeration of different methods of raising a trap.
Expand Down Expand Up @@ -255,6 +259,7 @@ impl From<wasmtime_environ::Trap> for TrapReason {
pub unsafe fn catch_traps<'a, F>(
signal_handler: Option<*const SignalHandler<'static>>,
capture_backtrace: bool,
capture_coredump: bool,
caller: *mut VMContext,
mut closure: F,
) -> Result<(), Box<Trap>>
Expand All @@ -263,19 +268,24 @@ where
{
let limits = Instance::from_vmctx(caller, |i| i.runtime_limits());

let result = CallThreadState::new(signal_handler, capture_backtrace, *limits).with(|cx| {
wasmtime_setjmp(
cx.jmp_buf.as_ptr(),
call_closure::<F>,
&mut closure as *mut F as *mut u8,
caller,
)
});
let result = CallThreadState::new(signal_handler, capture_backtrace, capture_coredump, *limits)
.with(|cx| {
wasmtime_setjmp(
cx.jmp_buf.as_ptr(),
call_closure::<F>,
&mut closure as *mut F as *mut u8,
caller,
)
});

return match result {
Ok(x) => Ok(x),
Err((UnwindReason::Trap(reason), backtrace)) => Err(Box::new(Trap { reason, backtrace })),
Err((UnwindReason::Panic(panic), _)) => std::panic::resume_unwind(panic),
Err((UnwindReason::Trap(reason), backtrace, coredumpstack)) => Err(Box::new(Trap {
reason,
backtrace,
coredumpstack,
})),
Err((UnwindReason::Panic(panic), _, _)) => std::panic::resume_unwind(panic),
};

extern "C" fn call_closure<F>(payload: *mut u8, caller: *mut VMContext)
Expand All @@ -294,10 +304,12 @@ mod call_thread_state {
/// Temporary state stored on the stack which is registered in the `tls` module
/// below for calls into wasm.
pub struct CallThreadState {
pub(super) unwind: UnsafeCell<MaybeUninit<(UnwindReason, Option<Backtrace>)>>,
pub(super) unwind:
UnsafeCell<MaybeUninit<(UnwindReason, Option<Backtrace>, Option<CoreDumpStack>)>>,
pub(super) jmp_buf: Cell<*const u8>,
pub(super) signal_handler: Option<*const SignalHandler<'static>>,
pub(super) capture_backtrace: bool,
pub(super) capture_coredump: bool,

pub(crate) limits: *const VMRuntimeLimits,

Expand Down Expand Up @@ -331,13 +343,15 @@ mod call_thread_state {
pub(super) fn new(
signal_handler: Option<*const SignalHandler<'static>>,
capture_backtrace: bool,
capture_coredump: bool,
limits: *const VMRuntimeLimits,
) -> CallThreadState {
CallThreadState {
unwind: UnsafeCell::new(MaybeUninit::uninit()),
jmp_buf: Cell::new(ptr::null()),
signal_handler,
capture_backtrace,
capture_coredump,
limits,
prev: Cell::new(ptr::null()),
old_last_wasm_exit_fp: Cell::new(unsafe { *(*limits).last_wasm_exit_fp.get() }),
Expand Down Expand Up @@ -389,7 +403,7 @@ impl CallThreadState {
fn with(
mut self,
closure: impl FnOnce(&CallThreadState) -> i32,
) -> Result<(), (UnwindReason, Option<Backtrace>)> {
) -> Result<(), (UnwindReason, Option<Backtrace>, Option<CoreDumpStack>)> {
let ret = tls::set(&mut self, |me| closure(me));
if ret != 0 {
Ok(())
Expand All @@ -399,12 +413,12 @@ impl CallThreadState {
}

#[cold]
unsafe fn read_unwind(&self) -> (UnwindReason, Option<Backtrace>) {
unsafe fn read_unwind(&self) -> (UnwindReason, Option<Backtrace>, Option<CoreDumpStack>) {
(*self.unwind.get()).as_ptr().read()
}

fn unwind_with(&self, reason: UnwindReason) -> ! {
let backtrace = match reason {
let (backtrace, coredump) = match reason {
// Panics don't need backtraces. There is nowhere to attach the
// hypothetical backtrace to and it doesn't really make sense to try
// in the first place since this is a Rust problem rather than a
Expand All @@ -416,11 +430,13 @@ impl CallThreadState {
| UnwindReason::Trap(TrapReason::User {
needs_backtrace: false,
..
}) => None,
UnwindReason::Trap(_) => self.capture_backtrace(self.limits, None),
}) => (None, None),
UnwindReason::Trap(_) => (self.capture_backtrace(self.limits, None), self.capture_coredump(self.limits, None)),
};
unsafe {
(*self.unwind.get()).as_mut_ptr().write((reason, backtrace));
(*self.unwind.get())
.as_mut_ptr()
.write((reason, backtrace, coredump));
wasmtime_longjmp(self.jmp_buf.get());
}
}
Expand Down Expand Up @@ -472,13 +488,15 @@ impl CallThreadState {

fn set_jit_trap(&self, pc: *const u8, fp: usize, faulting_addr: Option<usize>) {
let backtrace = self.capture_backtrace(self.limits, Some((pc as usize, fp)));
let coredump = self.capture_coredump(self.limits, Some((pc as usize, fp)));
unsafe {
(*self.unwind.get()).as_mut_ptr().write((
UnwindReason::Trap(TrapReason::Jit {
pc: pc as usize,
faulting_addr,
}),
backtrace,
coredump,
));
}
}
Expand All @@ -495,6 +513,17 @@ impl CallThreadState {
Some(unsafe { Backtrace::new_with_trap_state(limits, self, trap_pc_and_fp) })
}

fn capture_coredump(
&self,
limits: *const VMRuntimeLimits,
trap_pc_and_fp: Option<(usize, usize)>,
) -> Option<CoreDumpStack> {
if !self.capture_coredump {
return None;
}
Some(CoreDumpStack::new(&self, limits, trap_pc_and_fp))
}

pub(crate) fn iter<'a>(&'a self) -> impl Iterator<Item = &Self> + 'a {
let mut state = Some(self);
std::iter::from_fn(move || {
Expand Down
38 changes: 38 additions & 0 deletions crates/runtime/src/traphandlers/coredump.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use wasm_encoder::CoreDumpValue;

use crate::{Backtrace, VMRuntimeLimits};

use super::CallThreadState;

/// A WebAssembly Coredump
#[derive(Debug)]
pub struct CoreDumpStack {
/// The backtrace containing the stack frames for the CoreDump
pub bt: Backtrace,

/// Unimplemented
/// The indices of the locals and operand_stack all map to each other (ie.
/// index 0 is the locals for the first frame in the backtrace, etc)
pub locals: Vec<Vec<CoreDumpValue>>,

/// Unimplemented
/// The operands for each stack frame
pub operand_stack: Vec<Vec<CoreDumpValue>>,
}

impl CoreDumpStack {
/// Capture a core dump of the current wasm state
pub fn new(
cts: &CallThreadState,
limits: *const VMRuntimeLimits,
trap_pc_and_fp: Option<(usize, usize)>,
) -> Self {
let bt = unsafe { Backtrace::new_with_trap_state(limits, cts, trap_pc_and_fp) };

Self {
bt,
locals: vec![],
operand_stack: vec![],
}
}
}
1 change: 1 addition & 0 deletions crates/wasmtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ wasmtime-component-macro = { workspace = true, optional = true }
wasmtime-component-util = { workspace = true, optional = true }
target-lexicon = { workspace = true }
wasmparser = { workspace = true }
wasm-encoder = { workspace = true }
anyhow = { workspace = true }
libc = "0.2"
cfg-if = { workspace = true }
Expand Down
11 changes: 11 additions & 0 deletions crates/wasmtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub struct Config {
pub(crate) memory_init_cow: bool,
pub(crate) memory_guaranteed_dense_image_size: u64,
pub(crate) force_memory_init_memfd: bool,
pub(crate) coredump_on_trap: bool,
}

/// User-provided configuration for the compiler.
Expand Down Expand Up @@ -199,6 +200,7 @@ impl Config {
memory_init_cow: true,
memory_guaranteed_dense_image_size: 16 << 20,
force_memory_init_memfd: false,
coredump_on_trap: false,
};
#[cfg(any(feature = "cranelift", feature = "winch"))]
{
Expand Down Expand Up @@ -1467,6 +1469,15 @@ impl Config {
self
}

/// Configures whether or not a coredump should be generated and attached to
/// the anyhow::Error when a trap is raised.
///
/// This option is disabled by default.
pub fn coredump_on_trap(&mut self, enable: bool) -> &mut Self {
self.coredump_on_trap = enable;
self
}

/// Configures the "guaranteed dense image size" for copy-on-write
/// initialized memories.
///
Expand Down
115 changes: 115 additions & 0 deletions crates/wasmtime/src/coredump.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::fmt;

use crate::{store::StoreOpaque, FrameInfo, Global, Instance, Memory, Module, WasmBacktrace};

/// Representation of a core dump of a WebAssembly module
///
/// When the Config::coredump_on_trap option is enabled this structure is
/// attached to the [`anyhow::Error`] returned from many Wasmtime functions that
/// execute WebAssembly such as [`Instance::new`] or [`Func::call`]. This can be
/// acquired with the [`anyhow::Error::downcast`] family of methods to
/// programmatically inspect the coredump. Otherwise since it's part of the
/// error returned this will get printed along with the rest of the error when
/// the error is logged.
///
/// Note that some state, such as Wasm locals or values on the operand stack,
/// may be optimized away by the compiler or otherwise not recovered in the
/// coredump.
///
/// Capturing of wasm coredumps can be configured through the
/// [`Config::coredump_on_trap`][crate::Config::coredump_on_trap] method.
///
/// For more information about errors in wasmtime see the documentation of the
/// [`Trap`][crate::Trap] type.
///
/// [`Func::call`]: crate::Func::call
/// [`Instance::new`]: crate::Instance::new
pub struct WasmCoreDump {
name: String,
modules: Vec<Module>,
instances: Vec<Instance>,
store_memories: Vec<Memory>,
store_globals: Vec<Global>,
backtrace: WasmBacktrace,
}

impl WasmCoreDump {
pub(crate) fn new(store: &StoreOpaque, backtrace: WasmBacktrace) -> WasmCoreDump {
let modules: Vec<_> = store.modules().all_modules().cloned().collect();
let instances: Vec<Instance> = store.all_instances().collect();
let store_memories: Vec<Memory> = store.all_memories().collect();
let store_globals: Vec<Global> = store.all_globals().collect();

WasmCoreDump {
name: String::from("store_name"),
modules,
instances,
store_memories,
store_globals,
backtrace,
}
}

/// The stack frames for the CoreDump
pub fn frames(&self) -> &[FrameInfo] {
self.backtrace.frames()
}

/// The names of the modules involved in the CoreDump
pub fn modules(&self) -> &[Module] {
self.modules.as_ref()
}

/// The instances involved in the CoreDump
pub fn instances(&self) -> &[Instance] {
self.instances.as_ref()
}

/// The imported globals that belong to the store, rather than a specific
/// instance
pub fn store_globals(&self) -> &[Global] {
self.store_globals.as_ref()
}

/// The imported memories that belong to the store, rather than a specific
/// instance.
pub fn store_memories(&self) -> &[Memory] {
self.store_memories.as_ref()
}
}

impl fmt::Display for WasmCoreDump {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "wasm coredump generated while executing {}:", self.name)?;
writeln!(f, "modules:")?;
for module in self.modules.iter() {
writeln!(f, " {}", module.name().unwrap_or("<module>"))?;
}

writeln!(f, "instances:")?;
for instance in self.instances.iter() {
writeln!(f, " {:?}", instance)?;
}

writeln!(f, "memories:")?;
for memory in self.store_memories.iter() {
writeln!(f, " {:?}", memory)?;
}

writeln!(f, "globals:")?;
for global in self.store_globals.iter() {
writeln!(f, " {:?}", global)?;
}

writeln!(f, "backtrace:")?;
write!(f, "{}", self.backtrace)?;

Ok(())
}
}

impl fmt::Debug for WasmCoreDump {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<wasm core dump>")
}
}
Loading

0 comments on commit 31639dd

Please sign in to comment.