Skip to content

Commit 771a3b9

Browse files
committed
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.
1 parent 93f5a56 commit 771a3b9

16 files changed

Lines changed: 619 additions & 431 deletions

File tree

Cargo.lock

Lines changed: 375 additions & 399 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ memfd = "0.6.2"
2626
paste = "1.0.3"
2727
encoding_rs = { version = "0.8.31", optional = true }
2828
sptr = "0.3.2"
29+
wasm-encoder = { workspace = true }
2930

3031
[target.'cfg(target_os = "macos")'.dependencies]
3132
mach = "0.3.2"

crates/runtime/src/traphandlers.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//! signalhandling mechanisms.
33
44
mod backtrace;
5+
mod coredump;
56

67
use crate::{Instance, VMContext, VMRuntimeLimits};
78
use anyhow::Error;
@@ -12,6 +13,7 @@ use std::ptr;
1213
use std::sync::Once;
1314

1415
pub use self::backtrace::{Backtrace, Frame};
16+
pub use self::coredump::CoreDumpStack;
1517
pub use self::tls::{tls_eager_initialize, AsyncWasmCallState, PreviousAsyncWasmCallState};
1618

1719
cfg_if::cfg_if! {
@@ -172,6 +174,8 @@ pub struct Trap {
172174
pub reason: TrapReason,
173175
/// Wasm backtrace of the trap, if any.
174176
pub backtrace: Option<Backtrace>,
177+
/// The Wasm Coredump, if any.
178+
pub coredumpstack: Option<CoreDumpStack>,
175179
}
176180

177181
/// Enumeration of different methods of raising a trap.
@@ -251,6 +255,7 @@ impl From<wasmtime_environ::Trap> for TrapReason {
251255
pub unsafe fn catch_traps<'a, F>(
252256
signal_handler: Option<*const SignalHandler<'static>>,
253257
capture_backtrace: bool,
258+
capture_coredump: bool,
254259
caller: *mut VMContext,
255260
mut closure: F,
256261
) -> Result<(), Box<Trap>>
@@ -259,19 +264,24 @@ where
259264
{
260265
let limits = Instance::from_vmctx(caller, |i| i.runtime_limits());
261266

262-
let result = CallThreadState::new(signal_handler, capture_backtrace, *limits).with(|cx| {
263-
wasmtime_setjmp(
264-
cx.jmp_buf.as_ptr(),
265-
call_closure::<F>,
266-
&mut closure as *mut F as *mut u8,
267-
caller,
268-
)
269-
});
267+
let result = CallThreadState::new(signal_handler, capture_backtrace, capture_coredump, *limits)
268+
.with(|cx| {
269+
wasmtime_setjmp(
270+
cx.jmp_buf.as_ptr(),
271+
call_closure::<F>,
272+
&mut closure as *mut F as *mut u8,
273+
caller,
274+
)
275+
});
270276

271277
return match result {
272278
Ok(x) => Ok(x),
273-
Err((UnwindReason::Trap(reason), backtrace)) => Err(Box::new(Trap { reason, backtrace })),
274-
Err((UnwindReason::Panic(panic), _)) => std::panic::resume_unwind(panic),
279+
Err((UnwindReason::Trap(reason), backtrace, coredumpstack)) => Err(Box::new(Trap {
280+
reason,
281+
backtrace,
282+
coredumpstack,
283+
})),
284+
Err((UnwindReason::Panic(panic), _, _)) => std::panic::resume_unwind(panic),
275285
};
276286

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

298310
pub(crate) limits: *const VMRuntimeLimits,
299311

@@ -327,13 +339,15 @@ mod call_thread_state {
327339
pub(super) fn new(
328340
signal_handler: Option<*const SignalHandler<'static>>,
329341
capture_backtrace: bool,
342+
capture_coredump: bool,
330343
limits: *const VMRuntimeLimits,
331344
) -> CallThreadState {
332345
CallThreadState {
333346
unwind: UnsafeCell::new(MaybeUninit::uninit()),
334347
jmp_buf: Cell::new(ptr::null()),
335348
signal_handler,
336349
capture_backtrace,
350+
capture_coredump,
337351
limits,
338352
prev: Cell::new(ptr::null()),
339353
old_last_wasm_exit_fp: Cell::new(unsafe { *(*limits).last_wasm_exit_fp.get() }),
@@ -385,7 +399,7 @@ impl CallThreadState {
385399
fn with(
386400
mut self,
387401
closure: impl FnOnce(&CallThreadState) -> i32,
388-
) -> Result<(), (UnwindReason, Option<Backtrace>)> {
402+
) -> Result<(), (UnwindReason, Option<Backtrace>, Option<CoreDumpStack>)> {
389403
let ret = tls::set(&mut self, |me| closure(me));
390404
if ret != 0 {
391405
Ok(())
@@ -395,12 +409,12 @@ impl CallThreadState {
395409
}
396410

397411
#[cold]
398-
unsafe fn read_unwind(&self) -> (UnwindReason, Option<Backtrace>) {
412+
unsafe fn read_unwind(&self) -> (UnwindReason, Option<Backtrace>, Option<CoreDumpStack>) {
399413
(*self.unwind.get()).as_ptr().read()
400414
}
401415

402416
fn unwind_with(&self, reason: UnwindReason) -> ! {
403-
let backtrace = match reason {
417+
let (backtrace, coredump) = match reason {
404418
// Panics don't need backtraces. There is nowhere to attach the
405419
// hypothetical backtrace to and it doesn't really make sense to try
406420
// in the first place since this is a Rust problem rather than a
@@ -412,11 +426,13 @@ impl CallThreadState {
412426
| UnwindReason::Trap(TrapReason::User {
413427
needs_backtrace: false,
414428
..
415-
}) => None,
416-
UnwindReason::Trap(_) => self.capture_backtrace(self.limits, None),
429+
}) => (None, None),
430+
UnwindReason::Trap(_) => (self.capture_backtrace(self.limits, None), self.capture_coredump(self.limits, None)),
417431
};
418432
unsafe {
419-
(*self.unwind.get()).as_mut_ptr().write((reason, backtrace));
433+
(*self.unwind.get())
434+
.as_mut_ptr()
435+
.write((reason, backtrace, coredump));
420436
wasmtime_longjmp(self.jmp_buf.get());
421437
}
422438
}
@@ -468,13 +484,15 @@ impl CallThreadState {
468484

469485
fn set_jit_trap(&self, pc: *const u8, fp: usize, faulting_addr: Option<usize>) {
470486
let backtrace = self.capture_backtrace(self.limits, Some((pc as usize, fp)));
487+
let coredump = self.capture_coredump(self.limits, Some((pc as usize, fp)));
471488
unsafe {
472489
(*self.unwind.get()).as_mut_ptr().write((
473490
UnwindReason::Trap(TrapReason::Jit {
474491
pc: pc as usize,
475492
faulting_addr,
476493
}),
477494
backtrace,
495+
coredump,
478496
));
479497
}
480498
}
@@ -491,6 +509,17 @@ impl CallThreadState {
491509
Some(unsafe { Backtrace::new_with_trap_state(limits, self, trap_pc_and_fp) })
492510
}
493511

512+
fn capture_coredump(
513+
&self,
514+
limits: *const VMRuntimeLimits,
515+
trap_pc_and_fp: Option<(usize, usize)>,
516+
) -> Option<CoreDumpStack> {
517+
if !self.capture_coredump {
518+
return None;
519+
}
520+
Some(CoreDumpStack::new(&self, limits, trap_pc_and_fp))
521+
}
522+
494523
pub(crate) fn iter<'a>(&'a self) -> impl Iterator<Item = &Self> + 'a {
495524
let mut state = Some(self);
496525
std::iter::from_fn(move || {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use wasm_encoder::CoreDumpValue;
2+
3+
use crate::{Backtrace, VMRuntimeLimits};
4+
5+
use super::CallThreadState;
6+
7+
/// A WebAssembly Coredump
8+
#[derive(Debug)]
9+
pub struct CoreDumpStack {
10+
/// The backtrace containing the stack frames for the CoreDump
11+
pub bt: Backtrace,
12+
13+
/// Unimplemented
14+
/// The indices of the locals and operand_stack all map to each other (ie.
15+
/// index 0 is the locals for the first frame in the backtrace, etc)
16+
pub locals: Vec<Vec<CoreDumpValue>>,
17+
18+
/// Unimplemented
19+
/// The operands for each stack frame
20+
pub operand_stack: Vec<Vec<CoreDumpValue>>,
21+
}
22+
23+
impl CoreDumpStack {
24+
/// Capture a core dump of the current wasm state
25+
pub fn new(
26+
cts: &CallThreadState,
27+
limits: *const VMRuntimeLimits,
28+
trap_pc_and_fp: Option<(usize, usize)>,
29+
) -> Self {
30+
let bt = unsafe { Backtrace::new_with_trap_state(limits, cts, trap_pc_and_fp) };
31+
32+
Self {
33+
bt,
34+
locals: vec![],
35+
operand_stack: vec![],
36+
}
37+
}
38+
}

crates/wasmtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ wasmtime-component-macro = { workspace = true, optional = true }
2929
wasmtime-component-util = { workspace = true, optional = true }
3030
target-lexicon = { workspace = true }
3131
wasmparser = { workspace = true }
32+
wasm-encoder = { workspace = true }
3233
anyhow = { workspace = true }
3334
libc = "0.2"
3435
cfg-if = { workspace = true }

crates/wasmtime/src/config.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,8 +1452,11 @@ impl Config {
14521452
self
14531453
}
14541454

1455-
/// Whether or not a coredump should be generated and attached to the Error when
1456-
/// a trap is raised.
1455+
/// Configures whether or not a coredump should be generated and attached to
1456+
/// the anyhow::Error when a trap is raised. Because a coredump is a
1457+
/// superset of a backtrace, setting this to true will override the setting
1458+
/// for `wasm_backtrace`, resulting in a WasmCoreDump being attached to the
1459+
/// raised error rather than a WasmBacktrace.
14571460
///
14581461
/// This option is disabled by default.
14591462
pub fn coredump_on_trap(&mut self, enable: bool) -> &mut Self {

crates/wasmtime/src/coredump.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use std::fmt;
2+
3+
use crate::{store::StoreOpaque, Global, Instance, Memory, Module, WasmBacktrace};
4+
5+
/// Representation of a core dump of a WebAssembly module
6+
///
7+
/// This structure is attached to the [`anyhow::Error`] returned from many
8+
/// Wasmtime functions that execute WebAssembly such as [`Instance::new`] or
9+
/// [`Func::call`]. This can be acquired with the [`anyhow::Error::downcast`]
10+
/// family of methods to programmatically inspect the coredump. Otherwise since
11+
/// it's part of the error returned this will get printed along with the rest of
12+
/// the error when the error is logged.
13+
///
14+
/// TODO: Notably absent from this structure at the moment are any locals or
15+
/// operand values for the stack frames. More work is needed to be able to
16+
/// recover those when a trap occurs, at which point they will be added here.
17+
///
18+
/// Capturing of wasm coredumps can be configured through the
19+
/// [`Config::coredump_on_trap`](crate::Config::coredump_on_trap) method.
20+
///
21+
/// For more information about errors in wasmtime see the documentation of the
22+
/// [`Trap`] type.
23+
///
24+
/// [`Func::call`]: crate::Func::call
25+
/// [`Instance::new`]: crate::Instance::new
26+
pub struct WasmCoreDump {
27+
name: String,
28+
modules: Vec<Module>,
29+
instances: Vec<Instance>,
30+
memories: Vec<Memory>,
31+
globals: Vec<Global>,
32+
backtrace: WasmBacktrace,
33+
}
34+
35+
impl WasmCoreDump {
36+
pub(crate) fn new(store: &StoreOpaque, backtrace: WasmBacktrace) -> WasmCoreDump {
37+
let modules: Vec<_> = store.modules().all_modules().cloned().collect();
38+
let instances: Vec<Instance> = store.all_instances().collect();
39+
let memories: Vec<Memory> = store.all_memories().collect();
40+
let globals: Vec<Global> = store.all_globals().collect();
41+
WasmCoreDump {
42+
name: String::from("store_name"),
43+
modules,
44+
instances,
45+
memories,
46+
globals,
47+
backtrace,
48+
}
49+
}
50+
}
51+
52+
impl fmt::Display for WasmCoreDump {
53+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54+
writeln!(f, "wasm coredump generated while executing {}:", self.name)?;
55+
writeln!(f, "modules:")?;
56+
for module in self.modules.iter() {
57+
writeln!(f, " {}", module.name().unwrap_or_default())?;
58+
}
59+
60+
writeln!(f, "instances:")?;
61+
for instance in self.instances.iter() {
62+
writeln!(f, " {:?}", instance)?;
63+
}
64+
65+
writeln!(f, "memories:")?;
66+
for memory in self.memories.iter() {
67+
writeln!(f, " {:?}", memory)?;
68+
}
69+
70+
writeln!(f, "globals:")?;
71+
for global in self.globals.iter() {
72+
writeln!(f, " {:?}", global)?;
73+
}
74+
75+
writeln!(f, "backtrace:")?;
76+
write!(f, "{}", self.backtrace)?;
77+
78+
Ok(())
79+
}
80+
}

crates/wasmtime/src/externals.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
SharedMemory, TableType, Val, ValType,
66
};
77
use anyhow::{anyhow, bail, Result};
8+
use runtime::ExportGlobal;
89
use std::mem;
910
use std::ptr;
1011
use wasmtime_runtime::{self as runtime};
@@ -248,6 +249,10 @@ impl Global {
248249
}
249250
}
250251

252+
pub(crate) fn from_stored(stored: Stored<ExportGlobal>) -> Global {
253+
Global(stored)
254+
}
255+
251256
/// Returns the underlying type of this `global`.
252257
///
253258
/// # Panics

crates/wasmtime/src/func.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,7 @@ pub(crate) fn invoke_wasm_and_catch_traps<T>(
13631363
let result = wasmtime_runtime::catch_traps(
13641364
store.0.signal_handler(),
13651365
store.0.engine().config().wasm_backtrace,
1366+
store.0.engine().config().coredump_on_trap,
13661367
store.0.default_caller(),
13671368
closure,
13681369
);

crates/wasmtime/src/instance.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ impl Instance {
329329

330330
Ok((instance, compiled_module.module().start_func))
331331
}
332+
pub(crate) fn from_stored(stored: Stored<InstanceData>) -> Instance {
333+
Instance(stored)
334+
}
332335

333336
pub(crate) fn from_wasmtime(handle: InstanceData, store: &mut StoreOpaque) -> Instance {
334337
Instance(store.store_data_mut().insert(handle))

0 commit comments

Comments
 (0)