Browse Source

make backtrace collection a Config field rather than a cargo feature (#4183)

* sorta working in runtime

* wasmtime-runtime: get rid of wasm-backtrace feature

* wasmtime: factor to make backtraces recording optional. not configurable yet

* get rid of wasm-backtrace features

* trap tests: now a Trap optionally contains backtrace

* eliminate wasm-backtrace feature

* code review fixes

* ci: no more wasm-backtrace feature

* c_api: backtraces always enabled

* config: unwind required by backtraces and ref types

* plumbed

* test that disabling backtraces works

* code review comments

* fuzzing generator: wasm_backtrace is a runtime config now

* doc fix
pull/4188/head
Pat Hickey 2 years ago
committed by GitHub
parent
commit
bffce37050
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .github/workflows/main.yml
  2. 4
      Cargo.lock
  3. 2
      Cargo.toml
  4. 2
      crates/c-api/Cargo.toml
  5. 24
      crates/c-api/src/trap.rs
  6. 1
      crates/cli-flags/Cargo.toml
  7. 2
      crates/cli-flags/src/lib.rs
  8. 2
      crates/fuzzing/src/generators.rs
  9. 3
      crates/runtime/Cargo.toml
  10. 4
      crates/runtime/src/externref.rs
  11. 6
      crates/runtime/src/libcalls.rs
  12. 115
      crates/runtime/src/traphandlers.rs
  13. 11
      crates/wasmtime/Cargo.toml
  14. 69
      crates/wasmtime/src/config.rs
  15. 2
      crates/wasmtime/src/engine.rs
  16. 1
      crates/wasmtime/src/func.rs
  17. 8
      crates/wasmtime/src/lib.rs
  18. 6
      crates/wasmtime/src/store.rs
  19. 200
      crates/wasmtime/src/trap.rs
  20. 41
      tests/all/traps.rs

3
.github/workflows/main.yml

@ -136,9 +136,8 @@ jobs:
- run: cargo check -p wasmtime --no-default-features --features async
- run: cargo check -p wasmtime --no-default-features --features pooling-allocator
- run: cargo check -p wasmtime --no-default-features --features cranelift
- run: cargo check -p wasmtime --no-default-features --features wasm-backtrace
- run: cargo check -p wasmtime --no-default-features --features component-model
- run: cargo check -p wasmtime --no-default-features --features cranelift,wat,async,cache,wasm-backtrace
- run: cargo check -p wasmtime --no-default-features --features cranelift,wat,async,cache
- run: cargo check --features component-model
# Check that benchmarks of the cranelift project build

4
Cargo.lock

@ -1871,9 +1871,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.10.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "oorandom"

2
Cargo.toml

@ -98,7 +98,6 @@ default = [
"wasi-nn",
"pooling-allocator",
"memory-init-cow",
"wasm-backtrace",
]
jitdump = ["wasmtime/jitdump"]
vtune = ["wasmtime/vtune"]
@ -108,7 +107,6 @@ memory-init-cow = ["wasmtime/memory-init-cow", "wasmtime-cli-flags/memory-init-c
pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"]
all-arch = ["wasmtime/all-arch"]
posix-signals-on-macos = ["wasmtime/posix-signals-on-macos"]
wasm-backtrace = ["wasmtime/wasm-backtrace", "wasmtime-cli-flags/wasm-backtrace"]
component-model = ["wasmtime/component-model", "wasmtime-wast/component-model", "wasmtime-cli-flags/component-model"]
# Stub feature that does nothing, for Cargo-features compatibility: the new

2
crates/c-api/Cargo.toml

@ -20,7 +20,7 @@ doctest = false
env_logger = "0.9"
anyhow = "1.0"
once_cell = "1.3"
wasmtime = { path = "../wasmtime", default-features = false, features = ['cranelift', 'wasm-backtrace'] }
wasmtime = { path = "../wasmtime", default-features = false, features = ['cranelift'] }
wasmtime-c-api-macros = { path = "macros" }
# Optional dependency for the `wat2wasm` API

24
crates/c-api/src/trap.rs

@ -64,7 +64,13 @@ pub extern "C" fn wasm_trap_message(trap: &wasm_trap_t, out: &mut wasm_message_t
#[no_mangle]
pub extern "C" fn wasm_trap_origin(raw: &wasm_trap_t) -> Option<Box<wasm_frame_t>> {
if raw.trap.trace().len() > 0 {
if raw
.trap
.trace()
.expect("backtraces are always enabled")
.len()
> 0
{
Some(Box::new(wasm_frame_t {
trap: raw.trap.clone(),
idx: 0,
@ -78,7 +84,11 @@ pub extern "C" fn wasm_trap_origin(raw: &wasm_trap_t) -> Option<Box<wasm_frame_t
#[no_mangle]
pub extern "C" fn wasm_trap_trace(raw: &wasm_trap_t, out: &mut wasm_frame_vec_t) {
let vec = (0..raw.trap.trace().len())
let vec = (0..raw
.trap
.trace()
.expect("backtraces are always enabled")
.len())
.map(|idx| {
Some(Box::new(wasm_frame_t {
trap: raw.trap.clone(),
@ -128,7 +138,7 @@ pub extern "C" fn wasmtime_trap_exit_status(raw: &wasm_trap_t, status: &mut i32)
#[no_mangle]
pub extern "C" fn wasm_frame_func_index(frame: &wasm_frame_t) -> u32 {
frame.trap.trace()[frame.idx].func_index()
frame.trap.trace().expect("backtraces are always enabled")[frame.idx].func_index()
}
#[no_mangle]
@ -136,7 +146,7 @@ pub extern "C" fn wasmtime_frame_func_name(frame: &wasm_frame_t) -> Option<&wasm
frame
.func_name
.get_or_init(|| {
frame.trap.trace()[frame.idx]
frame.trap.trace().expect("backtraces are always enabled")[frame.idx]
.func_name()
.map(|s| wasm_name_t::from(s.to_string().into_bytes()))
})
@ -148,7 +158,7 @@ pub extern "C" fn wasmtime_frame_module_name(frame: &wasm_frame_t) -> Option<&wa
frame
.module_name
.get_or_init(|| {
frame.trap.trace()[frame.idx]
frame.trap.trace().expect("backtraces are always enabled")[frame.idx]
.module_name()
.map(|s| wasm_name_t::from(s.to_string().into_bytes()))
})
@ -157,7 +167,7 @@ pub extern "C" fn wasmtime_frame_module_name(frame: &wasm_frame_t) -> Option<&wa
#[no_mangle]
pub extern "C" fn wasm_frame_func_offset(frame: &wasm_frame_t) -> usize {
frame.trap.trace()[frame.idx]
frame.trap.trace().expect("backtraces are always enabled")[frame.idx]
.func_offset()
.unwrap_or(usize::MAX)
}
@ -169,7 +179,7 @@ pub extern "C" fn wasm_frame_instance(_arg1: *const wasm_frame_t) -> *mut wasm_i
#[no_mangle]
pub extern "C" fn wasm_frame_module_offset(frame: &wasm_frame_t) -> usize {
frame.trap.trace()[frame.idx]
frame.trap.trace().expect("backtraces are always enabled")[frame.idx]
.module_offset()
.unwrap_or(usize::MAX)
}

1
crates/cli-flags/Cargo.toml

@ -25,5 +25,4 @@ default = [
]
pooling-allocator = []
memory-init-cow = []
wasm-backtrace = []
component-model = []

2
crates/cli-flags/src/lib.rs

@ -346,9 +346,7 @@ impl CommonOptions {
config.wasm_bulk_memory(enable);
}
if let Some(enable) = reference_types {
#[cfg(feature = "wasm-backtrace")]
config.wasm_reference_types(enable);
drop(enable); // suppress unused warnings
}
if let Some(enable) = multi_value {
config.wasm_multi_value(enable);

2
crates/fuzzing/src/generators.rs

@ -213,6 +213,7 @@ pub struct WasmtimeConfig {
codegen: CodegenSettings,
padding_between_functions: Option<u16>,
generate_address_map: bool,
wasm_backtraces: bool,
}
/// Configuration for linear memories in Wasmtime.
@ -387,6 +388,7 @@ impl Config {
.wasm_multi_memory(self.module_config.config.max_memories > 1)
.wasm_simd(self.module_config.config.simd_enabled)
.wasm_memory64(self.module_config.config.memory64_enabled)
.wasm_backtrace(self.wasmtime.wasm_backtraces)
.cranelift_nan_canonicalization(self.wasmtime.canonicalize_nans)
.cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime())
.consume_fuel(self.wasmtime.consume_fuel)

3
crates/runtime/Cargo.toml

@ -22,7 +22,7 @@ indexmap = "1.0.2"
thiserror = "1.0.4"
more-asserts = "0.2.1"
cfg-if = "1.0"
backtrace = { version = "0.3.61", optional = true }
backtrace = { version = "0.3.61" }
rand = "0.8.3"
anyhow = "1.0.38"
memfd = { version = "0.4.1", optional = true }
@ -44,7 +44,6 @@ maintenance = { status = "actively-developed" }
[features]
memory-init-cow = ['memfd']
wasm-backtrace = ["backtrace"]
async = ["wasmtime-fiber"]

4
crates/runtime/src/externref.rs

@ -704,7 +704,6 @@ impl VMExternRefActivationsTable {
}
}
#[cfg_attr(not(feature = "wasm-backtrace"), allow(dead_code))]
fn insert_precise_stack_root(
precise_stack_roots: &mut HashSet<VMExternRefWithTraits>,
root: NonNull<VMExternData>,
@ -867,7 +866,6 @@ impl<T> std::ops::DerefMut for DebugOnly<T> {
///
/// Additionally, you must have registered the stack maps for every Wasm module
/// that has frames on the stack with the given `stack_maps_registry`.
#[cfg_attr(not(feature = "wasm-backtrace"), allow(unused_mut, unused_variables))]
pub unsafe fn gc(
module_info_lookup: &dyn ModuleInfoLookup,
externref_activations_table: &mut VMExternRefActivationsTable,
@ -895,7 +893,6 @@ pub unsafe fn gc(
None => {
if cfg!(debug_assertions) {
// Assert that there aren't any Wasm frames on the stack.
#[cfg(feature = "wasm-backtrace")]
backtrace::trace(|frame| {
assert!(module_info_lookup.lookup(frame.ip() as usize).is_none());
true
@ -937,7 +934,6 @@ pub unsafe fn gc(
});
}
#[cfg(feature = "wasm-backtrace")]
backtrace::trace(|frame| {
let pc = frame.ip() as usize;
let sp = frame.sp() as usize;

6
crates/runtime/src/libcalls.rs

@ -507,7 +507,7 @@ pub unsafe extern "C" fn memory_atomic_notify(
// just to be sure.
let addr_to_check = addr.checked_add(4).unwrap();
validate_atomic_addr(instance, memory, addr_to_check).and_then(|()| {
Err(Trap::User(anyhow::anyhow!(
Err(Trap::user(anyhow::anyhow!(
"unimplemented: wasm atomics (fn memory_atomic_notify) unsupported",
)))
})
@ -534,7 +534,7 @@ pub unsafe extern "C" fn memory_atomic_wait32(
// but we still double-check
let addr_to_check = addr.checked_add(4).unwrap();
validate_atomic_addr(instance, memory, addr_to_check).and_then(|()| {
Err(Trap::User(anyhow::anyhow!(
Err(Trap::user(anyhow::anyhow!(
"unimplemented: wasm atomics (fn memory_atomic_wait32) unsupported",
)))
})
@ -561,7 +561,7 @@ pub unsafe extern "C" fn memory_atomic_wait64(
// but we still double-check
let addr_to_check = addr.checked_add(8).unwrap();
validate_atomic_addr(instance, memory, addr_to_check).and_then(|()| {
Err(Trap::User(anyhow::anyhow!(
Err(Trap::user(anyhow::anyhow!(
"unimplemented: wasm atomics (fn memory_atomic_wait64) unsupported",
)))
})

115
crates/runtime/src/traphandlers.rs

@ -11,6 +11,7 @@ use std::sync::Once;
use wasmtime_environ::TrapCode;
pub use self::tls::{tls_eager_initialize, TlsRestore};
pub use backtrace::Backtrace;
#[link(name = "wasmtime-helpers")]
extern "C" {
@ -112,14 +113,19 @@ pub unsafe fn resume_panic(payload: Box<dyn Any + Send>) -> ! {
#[derive(Debug)]
pub enum Trap {
/// A user-raised trap through `raise_user_trap`.
User(Error),
User {
/// The user-provided error
error: Error,
/// Native stack backtrace at the time the trap occurred
backtrace: Option<Backtrace>,
},
/// A trap raised from jit code
Jit {
/// The program counter in JIT code where this trap happened.
pc: usize,
/// Native stack backtrace at the time the trap occurred
backtrace: Backtrace,
backtrace: Option<Backtrace>,
},
/// A trap raised from a wasm libcall
@ -127,63 +133,53 @@ pub enum Trap {
/// Code of the trap.
trap_code: TrapCode,
/// Native stack backtrace at the time the trap occurred
backtrace: Backtrace,
backtrace: Option<Backtrace>,
},
/// A trap indicating that the runtime was unable to allocate sufficient memory.
OOM {
/// Native stack backtrace at the time the OOM occurred
backtrace: Backtrace,
backtrace: Option<Backtrace>,
},
}
impl Trap {
/// Construct a new Wasm trap with the given source location and trap code.
/// Construct a new Wasm trap with the given trap code.
///
/// Internally saves a backtrace when constructed.
/// Internally saves a backtrace when passed across a setjmp boundary, if the
/// engine is configured to save backtraces.
pub fn wasm(trap_code: TrapCode) -> Self {
Trap::Wasm {
trap_code,
backtrace: Backtrace::new(),
backtrace: None,
}
}
/// Construct a new OOM trap with the given source location and trap code.
/// Construct a new Wasm trap from a user Error.
///
/// Internally saves a backtrace when constructed.
pub fn oom() -> Self {
Trap::OOM {
backtrace: Backtrace::new(),
/// Internally saves a backtrace when passed across a setjmp boundary, if the
/// engine is configured to save backtraces.
pub fn user(error: Error) -> Self {
Trap::User {
error,
backtrace: None,
}
}
}
/// A crate-local backtrace type which conditionally, at compile time, actually
/// contains a backtrace from the `backtrace` crate or nothing.
#[derive(Debug)]
pub struct Backtrace {
#[cfg(feature = "wasm-backtrace")]
trace: backtrace::Backtrace,
}
impl Backtrace {
/// Captures a new backtrace
/// Construct a new OOM trap.
///
/// Note that this function does nothing if the `wasm-backtrace` feature is
/// disabled.
pub fn new() -> Backtrace {
Backtrace {
#[cfg(feature = "wasm-backtrace")]
trace: backtrace::Backtrace::new_unresolved(),
}
/// Internally saves a backtrace when passed across a setjmp boundary, if the
/// engine is configured to save backtraces.
pub fn oom() -> Self {
Trap::OOM { backtrace: None }
}
/// Returns the backtrace frames associated with this backtrace. Note that
/// this is conditionally defined and not present when `wasm-backtrace` is
/// not present.
#[cfg(feature = "wasm-backtrace")]
pub fn frames(&self) -> &[backtrace::BacktraceFrame] {
self.trace.frames()
fn insert_backtrace(&mut self, bt: Backtrace) {
match self {
Trap::User { backtrace, .. } => *backtrace = Some(bt),
Trap::Jit { backtrace, .. } => *backtrace = Some(bt),
Trap::Wasm { backtrace, .. } => *backtrace = Some(bt),
Trap::OOM { backtrace, .. } => *backtrace = Some(bt),
}
}
}
@ -193,13 +189,14 @@ impl Backtrace {
/// Highly unsafe since `closure` won't have any dtors run.
pub unsafe fn catch_traps<'a, F>(
signal_handler: Option<*const SignalHandler<'static>>,
capture_backtrace: bool,
callee: *mut VMContext,
mut closure: F,
) -> Result<(), Box<Trap>>
where
F: FnMut(*mut VMContext),
{
return CallThreadState::new(signal_handler).with(|cx| {
return CallThreadState::new(signal_handler, capture_backtrace).with(|cx| {
wasmtime_setjmp(
cx.jmp_buf.as_ptr(),
call_closure::<F>,
@ -219,29 +216,34 @@ where
/// Temporary state stored on the stack which is registered in the `tls` module
/// below for calls into wasm.
pub struct CallThreadState {
unwind: UnsafeCell<MaybeUninit<UnwindReason>>,
unwind: UnsafeCell<MaybeUninit<(UnwindReason, Option<Backtrace>)>>,
jmp_buf: Cell<*const u8>,
handling_trap: Cell<bool>,
signal_handler: Option<*const SignalHandler<'static>>,
prev: Cell<tls::Ptr>,
capture_backtrace: bool,
}
enum UnwindReason {
Panic(Box<dyn Any + Send>),
UserTrap(Error),
LibTrap(Trap),
JitTrap { backtrace: Backtrace, pc: usize },
JitTrap { pc: usize }, // Removed a backtrace here
}
impl CallThreadState {
#[inline]
fn new(signal_handler: Option<*const SignalHandler<'static>>) -> CallThreadState {
fn new(
signal_handler: Option<*const SignalHandler<'static>>,
capture_backtrace: bool,
) -> CallThreadState {
CallThreadState {
unwind: UnsafeCell::new(MaybeUninit::uninit()),
jmp_buf: Cell::new(ptr::null()),
handling_trap: Cell::new(false),
signal_handler,
prev: Cell::new(ptr::null()),
capture_backtrace,
}
}
@ -257,16 +259,26 @@ impl CallThreadState {
#[cold]
unsafe fn read_trap(&self) -> Box<Trap> {
Box::new(match (*self.unwind.get()).as_ptr().read() {
UnwindReason::UserTrap(data) => Trap::User(data),
UnwindReason::LibTrap(trap) => trap,
UnwindReason::JitTrap { backtrace, pc } => Trap::Jit { pc, backtrace },
UnwindReason::Panic(panic) => std::panic::resume_unwind(panic),
(UnwindReason::UserTrap(error), backtrace) => Trap::User { error, backtrace },
(UnwindReason::LibTrap(mut trap), backtrace) => {
if let Some(backtrace) = backtrace {
trap.insert_backtrace(backtrace);
}
trap
}
(UnwindReason::JitTrap { pc }, backtrace) => Trap::Jit { pc, backtrace },
(UnwindReason::Panic(panic), _) => std::panic::resume_unwind(panic),
})
}
fn unwind_with(&self, reason: UnwindReason) -> ! {
let backtrace = if self.capture_backtrace {
Some(Backtrace::new())
} else {
None
};
unsafe {
(*self.unwind.get()).as_mut_ptr().write(reason);
(*self.unwind.get()).as_mut_ptr().write((reason, backtrace));
wasmtime_longjmp(self.jmp_buf.get());
}
}
@ -327,14 +339,15 @@ impl CallThreadState {
}
fn capture_backtrace(&self, pc: *const u8) {
let backtrace = Backtrace::new();
let backtrace = if self.capture_backtrace {
Some(Backtrace::new())
} else {
None
};
unsafe {
(*self.unwind.get())
.as_mut_ptr()
.write(UnwindReason::JitTrap {
backtrace,
pc: pc as usize,
});
.write((UnwindReason::JitTrap { pc: pc as usize }, backtrace));
}
}
}

11
crates/wasmtime/Cargo.toml

@ -25,7 +25,7 @@ anyhow = "1.0.19"
region = "2.2.0"
libc = "0.2"
cfg-if = "1.0"
backtrace = { version = "0.3.61", optional = true }
backtrace = { version = "0.3.61" }
log = "0.4.8"
wat = { version = "1.0.43", optional = true }
serde = { version = "1.0.94", features = ["derive"] }
@ -37,7 +37,7 @@ lazy_static = "1.4"
rayon = { version = "1.0", optional = true }
object = { version = "0.28", default-features = false, features = ['read_core', 'elf'] }
async-trait = { version = "0.1.51", optional = true }
once_cell = "1.9"
once_cell = "1.12"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = "0.3.7"
@ -61,7 +61,6 @@ default = [
'pooling-allocator',
'memory-init-cow',
'vtune',
'wasm-backtrace',
]
# An on-by-default feature enabling runtime compilation of WebAssembly modules
@ -106,12 +105,6 @@ posix-signals-on-macos = ["wasmtime-runtime/posix-signals-on-macos"]
# Enabling this feature has no effect on unsupported platforms.
memory-init-cow = ["wasmtime-runtime/memory-init-cow"]
# Enables runtime support necessary to capture backtraces of WebAssembly code
# that is running.
#
# This is enabled by default.
wasm-backtrace = ["wasmtime-runtime/wasm-backtrace", "backtrace"]
# Enables in-progress support for the component model. Note that this feature is
# in-progress, buggy, and incomplete. This is primarily here for internal
# testing purposes.

69
crates/wasmtime/src/config.rs

@ -91,6 +91,7 @@ pub struct Config {
pub(crate) allocation_strategy: InstanceAllocationStrategy,
pub(crate) max_wasm_stack: usize,
pub(crate) features: WasmFeatures,
pub(crate) wasm_backtrace: bool,
pub(crate) wasm_backtrace_details_env_used: bool,
#[cfg(feature = "async")]
pub(crate) async_stack_size: usize,
@ -124,6 +125,7 @@ impl Config {
// 1` forces this), or at least it passed when this change was
// committed.
max_wasm_stack: 512 * 1024,
wasm_backtrace: true,
wasm_backtrace_details_env_used: false,
features: WasmFeatures::default(),
#[cfg(feature = "async")]
@ -140,9 +142,7 @@ impl Config {
ret.cranelift_debug_verifier(false);
ret.cranelift_opt_level(OptLevel::Speed);
}
#[cfg(feature = "wasm-backtrace")]
ret.wasm_reference_types(true);
ret.features.reference_types = cfg!(feature = "wasm-backtrace");
ret.wasm_multi_value(true);
ret.wasm_bulk_memory(true);
ret.wasm_simd(true);
@ -279,6 +279,35 @@ impl Config {
self
}
/// Configures whether backtraces exist in a `Trap`.
///
/// Enabled by default, this feature builds in support to
/// generate backtraces at runtime for WebAssembly modules. This means that
/// unwinding information is compiled into wasm modules and necessary runtime
/// dependencies are enabled as well.
///
/// When disabled, wasm backtrace details are ignored, and [`crate::Trap::trace()`]
/// will always return `None`.
pub fn wasm_backtrace(&mut self, enable: bool) -> &mut Self {
self.wasm_backtrace = enable;
#[cfg(compiler)]
{
// unwind_info must be enabled when either backtraces or reference types are enabled:
self.compiler
.set(
"unwind_info",
if enable || self.features.reference_types {
"true"
} else {
"false"
},
)
.unwrap();
}
self
}
/// Configures whether backtraces in `Trap` will parse debug info in the wasm file to
/// have filename/line number information.
///
@ -508,14 +537,9 @@ impl Config {
/// Note that enabling the reference types feature will also enable the bulk
/// memory feature.
///
/// This feature is `true` by default. If the `wasm-backtrace` feature is
/// disabled at compile time, however, then this is `false` by default and
/// it cannot be turned on since GC currently requires backtraces to work.
/// Note that the `wasm-backtrace` feature is on by default, however.
/// This feature is `true` by default.
///
/// [proposal]: https://github.com/webassembly/reference-types
#[cfg(feature = "wasm-backtrace")]
#[cfg_attr(nightlydoc, doc(cfg(feature = "wasm-backtrace")))]
pub fn wasm_reference_types(&mut self, enable: bool) -> &mut Self {
self.features.reference_types = enable;
@ -524,6 +548,17 @@ impl Config {
self.compiler
.set("enable_safepoints", if enable { "true" } else { "false" })
.unwrap();
// unwind_info must be enabled when either backtraces or reference types are enabled:
self.compiler
.set(
"unwind_info",
if enable || self.wasm_backtrace {
"true"
} else {
"false"
},
)
.unwrap();
}
// The reference types proposal depends on the bulk memory proposal.
@ -1288,20 +1323,9 @@ impl Config {
#[cfg(compiler)]
fn compiler_builder(strategy: Strategy) -> Result<Box<dyn CompilerBuilder>> {
let mut builder = match strategy {
Strategy::Auto | Strategy::Cranelift => wasmtime_cranelift::builder(),
};
builder
.set(
"unwind_info",
if cfg!(feature = "wasm-backtrace") {
"true"
} else {
"false"
},
)
.unwrap();
Ok(builder)
match strategy {
Strategy::Auto | Strategy::Cranelift => Ok(wasmtime_cranelift::builder()),
}
}
fn round_up_to_pages(val: u64) -> u64 {
@ -1331,6 +1355,7 @@ impl Clone for Config {
mem_creator: self.mem_creator.clone(),
allocation_strategy: self.allocation_strategy.clone(),
max_wasm_stack: self.max_wasm_stack,
wasm_backtrace: self.wasm_backtrace,
wasm_backtrace_details_env_used: self.wasm_backtrace_details_env_used,
async_support: self.async_support,
#[cfg(feature = "async")]

2
crates/wasmtime/src/engine.rs

@ -303,7 +303,7 @@ impl Engine {
// can affect the way the generated code performs or behaves at
// runtime.
"avoid_div_traps" => *value == FlagValue::Bool(true),
"unwind_info" => *value == FlagValue::Bool(cfg!(feature = "wasm-backtrace")),
"unwind_info" => *value == FlagValue::Bool(true),
"libcall_call_conv" => *value == FlagValue::Enum("isa_default".into()),
// Features wasmtime doesn't use should all be disabled, since

1
crates/wasmtime/src/func.rs

@ -1235,6 +1235,7 @@ pub(crate) fn invoke_wasm_and_catch_traps<T>(
}
let result = wasmtime_runtime::catch_traps(
store.0.signal_handler(),
store.0.engine().config().wasm_backtrace,
store.0.default_callee(),
closure,
);

8
crates/wasmtime/src/lib.rs

@ -290,14 +290,6 @@
//! run-time via [`Config::memory_init_cow`] (which is also enabled by
//! default).
//!
//! * `wasm-backtrace` - Enabled by default, this feature builds in support to
//! generate backtraces at runtime for WebAssembly modules. This means that
//! unwinding information is compiled into wasm modules and necessary runtime
//! dependencies are enabled as well. If this is turned off then some methods
//! to look at trap frames will not be available. Additionally at this time
//! disabling this feature means that the reference types feature is always
//! disabled as well.
//!
//! ## Examples
//!
//! In addition to the examples below be sure to check out the [online embedding

6
crates/wasmtime/src/store.rs

@ -1914,11 +1914,7 @@ unsafe impl<T> wasmtime_runtime::Store for StoreInner<T> {
fn new_epoch(&mut self) -> Result<u64, anyhow::Error> {
return match &mut self.epoch_deadline_behavior {
EpochDeadline::Trap => {
let trap = Trap::new_wasm(
None,
wasmtime_environ::TrapCode::Interrupt,
wasmtime_runtime::Backtrace::new(),
);
let trap = Trap::new_wasm(wasmtime_environ::TrapCode::Interrupt, None);
Err(anyhow::Error::from(trap))
}
EpochDeadline::Callback(callback) => {

200
crates/wasmtime/src/trap.rs

@ -1,5 +1,6 @@
use crate::module::GlobalModuleRegistry;
use crate::FrameInfo;
use once_cell::sync::OnceCell;
use std::fmt;
use std::sync::Arc;
use wasmtime_environ::TrapCode as EnvTrapCode;
@ -127,15 +128,65 @@ impl fmt::Display for TrapCode {
}
}
struct TrapInner {
reason: TrapReason,
#[cfg(feature = "wasm-backtrace")]
#[derive(Debug)]
pub(crate) struct TrapBacktrace {
wasm_trace: Vec<FrameInfo>,
native_trace: Backtrace,
#[cfg(feature = "wasm-backtrace")]
hint_wasm_backtrace_details_env: bool,
}
impl TrapBacktrace {
pub fn new(native_trace: Backtrace, trap_pc: Option<usize>) -> Self {
let mut wasm_trace = Vec::<FrameInfo>::new();
let mut hint_wasm_backtrace_details_env = false;
GlobalModuleRegistry::with(|registry| {
for frame in native_trace.frames() {
let pc = frame.ip() as usize;
if pc == 0 {
continue;
}
// 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
// the origin wasm module. If this pc is the exact pc that
// the trap happened at, then we look up that pc precisely.
// Otherwise backtrace information typically points at the
// pc *after* the call instruction (because otherwise it's
// likely a call instruction on the stack). In that case we
// want to lookup information for the previous instruction
// (the call instruction) so we subtract one as the lookup.
let pc_to_lookup = if Some(pc) == trap_pc { pc } else { pc - 1 };
if let Some((info, has_unparsed_debuginfo, wasm_backtrace_details_env_used)) =
registry.lookup_frame_info(pc_to_lookup)
{
wasm_trace.push(info);
// If this frame has unparsed debug information and the
// store's configuration indicates that we were
// respecting the environment variable of whether to
// do this then we will print out a helpful note in
// `Display` to indicate that more detailed information
// in a trap may be available.
if has_unparsed_debuginfo && wasm_backtrace_details_env_used {
hint_wasm_backtrace_details_env = true;
}
}
}
});
Self {
wasm_trace,
native_trace,
hint_wasm_backtrace_details_env,
}
}
}
struct TrapInner {
reason: TrapReason,
backtrace: OnceCell<TrapBacktrace>,
}
fn _assert_trap_is_sync_and_send(t: &Trap) -> (&dyn Sync, &dyn Send) {
(t, t)
}
@ -150,14 +201,14 @@ impl Trap {
#[cold] // traps are exceptional, this helps move handling off the main path
pub fn new<I: Into<String>>(message: I) -> Self {
let reason = TrapReason::Message(message.into());
Trap::new_with_trace(None, reason, Backtrace::new())
Trap::new_with_trace(reason, None)
}
/// Creates a new `Trap` representing an explicit program exit with a classic `i32`
/// exit status value.
#[cold] // see Trap::new
pub fn i32_exit(status: i32) -> Self {
Trap::new_with_trace(None, TrapReason::I32Exit(status), Backtrace::new())
Trap::new_with_trace(TrapReason::I32Exit(status), None)
}
#[cold] // see Trap::new
@ -168,97 +219,58 @@ impl Trap {
#[cold] // see Trap::new
pub(crate) fn from_runtime(runtime_trap: wasmtime_runtime::Trap) -> Self {
match runtime_trap {
wasmtime_runtime::Trap::User(error) => Trap::from(error),
wasmtime_runtime::Trap::User { error, backtrace } => {
let trap = Trap::from(error);
if let Some(backtrace) = backtrace {
trap.record_backtrace(TrapBacktrace::new(backtrace, None));
}
trap
}
wasmtime_runtime::Trap::Jit { pc, backtrace } => {
let code = GlobalModuleRegistry::with(|modules| {
modules
.lookup_trap_code(pc)
.unwrap_or(EnvTrapCode::StackOverflow)
});
Trap::new_wasm(Some(pc), code, backtrace)
let backtrace = backtrace.map(|bt| TrapBacktrace::new(bt, Some(pc)));
Trap::new_wasm(code, backtrace)
}
wasmtime_runtime::Trap::Wasm {
trap_code,
backtrace,
} => Trap::new_wasm(None, trap_code, backtrace),
} => {
let backtrace = backtrace.map(|bt| TrapBacktrace::new(bt, None));
Trap::new_wasm(trap_code, backtrace)
}
wasmtime_runtime::Trap::OOM { backtrace } => {
let reason = TrapReason::Message("out of memory".to_string());
Trap::new_with_trace(None, reason, backtrace)
let backtrace = backtrace.map(|bt| TrapBacktrace::new(bt, None));
Trap::new_with_trace(reason, backtrace)
}
}
}
#[cold] // see Trap::new
pub(crate) fn new_wasm(
trap_pc: Option<usize>,
code: EnvTrapCode,
backtrace: Backtrace,
) -> Self {
pub(crate) fn new_wasm(code: EnvTrapCode, backtrace: Option<TrapBacktrace>) -> Self {
let code = TrapCode::from_non_user(code);
Trap::new_with_trace(trap_pc, TrapReason::InstructionTrap(code), backtrace)
Trap::new_with_trace(TrapReason::InstructionTrap(code), backtrace)
}
/// Creates a new `Trap`.
///
/// * `trap_pc` - this is the precise program counter, if available, that
/// wasm trapped at. This is used when learning about the wasm stack trace
/// to ensure we assign the correct source to every frame.
///
/// * `reason` - this is the wasmtime-internal reason for why this trap is
/// being created.
///
/// * `native_trace` - this is a captured backtrace from when the trap
/// occurred, and this will iterate over the frames to find frames that
/// lie in wasm jit code.
#[cfg_attr(not(feature = "wasm-backtrace"), allow(unused_mut, unused_variables))]
fn new_with_trace(trap_pc: Option<usize>, reason: TrapReason, native_trace: Backtrace) -> Self {
let mut wasm_trace = Vec::<FrameInfo>::new();
let mut hint_wasm_backtrace_details_env = false;
#[cfg(feature = "wasm-backtrace")]
GlobalModuleRegistry::with(|registry| {
for frame in native_trace.frames() {
let pc = frame.ip() as usize;
if pc == 0 {
continue;
}
// 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
// the origin wasm module. If this pc is the exact pc that
// the trap happened at, then we look up that pc precisely.
// Otherwise backtrace information typically points at the
// pc *after* the call instruction (because otherwise it's
// likely a call instruction on the stack). In that case we
// want to lookup information for the previous instruction
// (the call instruction) so we subtract one as the lookup.
let pc_to_lookup = if Some(pc) == trap_pc { pc } else { pc - 1 };
if let Some((info, has_unparsed_debuginfo, wasm_backtrace_details_env_used)) =
registry.lookup_frame_info(pc_to_lookup)
{
wasm_trace.push(info);
// If this frame has unparsed debug information and the
// store's configuration indicates that we were
// respecting the environment variable of whether to
// do this then we will print out a helpful note in
// `Display` to indicate that more detailed information
// in a trap may be available.
if has_unparsed_debuginfo && wasm_backtrace_details_env_used {
hint_wasm_backtrace_details_env = true;
}
}
}
});
/// * `backtrace` - this is a captured backtrace from when the trap
/// occurred. Contains the native backtrace, and the backtrace of
/// WebAssembly frames.
fn new_with_trace(reason: TrapReason, backtrace: Option<TrapBacktrace>) -> Self {
let backtrace = if let Some(bt) = backtrace {
OnceCell::with_value(bt)
} else {
OnceCell::new()
};
Trap {
inner: Arc::new(TrapInner {
reason,
native_trace,
#[cfg(feature = "wasm-backtrace")]
wasm_trace,
#[cfg(feature = "wasm-backtrace")]
hint_wasm_backtrace_details_env,
}),
inner: Arc::new(TrapInner { reason, backtrace }),
}
}
@ -283,10 +295,12 @@ impl Trap {
/// Returns a list of function frames in WebAssembly code that led to this
/// trap happening.
#[cfg(feature = "wasm-backtrace")]
#[cfg_attr(nightlydoc, doc(cfg(feature = "wasm-backtrace")))]
pub fn trace(&self) -> &[FrameInfo] {
&self.inner.wasm_trace
pub fn trace(&self) -> Option<&[FrameInfo]> {
self.inner
.backtrace
.get()
.as_ref()
.map(|bt| bt.wasm_trace.as_slice())
}
/// Code of a trap that happened while executing a WASM instruction.
@ -297,16 +311,26 @@ impl Trap {
_ => None,
}
}
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);
}
}
impl fmt::Debug for Trap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut f = f.debug_struct("Trap");
f.field("reason", &self.inner.reason);
#[cfg(feature = "wasm-backtrace")]
{
f.field("wasm_trace", &self.inner.wasm_trace)
.field("native_trace", &self.inner.native_trace);
if let Some(backtrace) = self.inner.backtrace.get() {
f.field("wasm_trace", &backtrace.wasm_trace)
.field("native_trace", &backtrace.native_trace);
}
f.finish()
}
@ -316,15 +340,13 @@ impl fmt::Display for Trap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner.reason)?;
#[cfg(feature = "wasm-backtrace")]
{
let trace = self.trace();
if let Some(trace) = self.trace() {
if trace.is_empty() {
return Ok(());
}
writeln!(f, "\nwasm backtrace:")?;
for (i, frame) in self.trace().iter().enumerate() {
for (i, frame) in trace.iter().enumerate() {
let name = frame.module_name().unwrap_or("<unknown>");
write!(f, " {:>3}: ", i)?;
@ -369,7 +391,13 @@ impl fmt::Display for Trap {
}
}
}
if self.inner.hint_wasm_backtrace_details_env {
if self
.inner
.backtrace
.get()
.map(|t| t.hint_wasm_backtrace_details_env)
.unwrap_or(false)
{
writeln!(f, "note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable to may show more debugging information")?;
}
}
@ -404,7 +432,7 @@ impl From<Box<dyn std::error::Error + Send + Sync>> for Trap {
trap.clone()
} else {
let reason = TrapReason::Error(e.into());
Trap::new_with_trace(None, reason, Backtrace::new())
Trap::new_with_trace(reason, None)
}
}
}

41
tests/all/traps.rs

@ -49,7 +49,7 @@ fn test_trap_trace() -> Result<()> {
.err()
.expect("error calling function");
let trace = e.trace();
let trace = e.trace().expect("backtrace is available");
assert_eq!(trace.len(), 2);
assert_eq!(trace[0].module_name().unwrap(), "hello_mod");
assert_eq!(trace[0].func_index(), 1);
@ -70,6 +70,32 @@ fn test_trap_trace() -> Result<()> {
Ok(())
}
#[test]
fn test_trap_backtrace_disabled() -> Result<()> {
let mut config = Config::default();
config.wasm_backtrace(false);
let engine = Engine::new(&config).unwrap();
let mut store = Store::<()>::new(&engine, ());
let wat = r#"
(module $hello_mod
(func (export "run") (call $hello))
(func $hello (unreachable))
)
"#;
let module = Module::new(store.engine(), wat)?;
let instance = Instance::new(&mut store, &module, &[])?;
let run_func = instance.get_typed_func::<(), (), _>(&mut store, "run")?;
let e = run_func
.call(&mut store, ())
.err()
.expect("error calling function");
assert!(e.trace().is_none(), "backtraces should be disabled");
Ok(())
}
#[test]
#[cfg_attr(all(target_os = "macos", target_arch = "aarch64"), ignore)] // TODO #2808 system libunwind is broken on aarch64
fn test_trap_trace_cb() -> Result<()> {
@ -94,7 +120,7 @@ fn test_trap_trace_cb() -> Result<()> {
.err()
.expect("error calling function");
let trace = e.trace();
let trace = e.trace().expect("backtrace is available");
assert_eq!(trace.len(), 2);
assert_eq!(trace[0].module_name().unwrap(), "hello_mod");
assert_eq!(trace[0].func_index(), 2);
@ -124,7 +150,7 @@ fn test_trap_stack_overflow() -> Result<()> {
.err()
.expect("error calling function");
let trace = e.trace();
let trace = e.trace().expect("backtrace is available");
assert!(trace.len() >= 32);
for i in 0..trace.len() {
assert_eq!(trace[i].module_name().unwrap(), "rec_mod");
@ -429,8 +455,9 @@ fn present_after_module_drop() -> Result<()> {
fn assert_trap(t: Trap) {
println!("{}", t);
assert_eq!(t.trace().len(), 1);
assert_eq!(t.trace()[0].func_index(), 0);
let trace = t.trace().expect("backtrace is available");
assert_eq!(trace.len(), 1);
assert_eq!(trace[0].func_index(), 0);
}
}
@ -525,7 +552,7 @@ fn parse_dwarf_info() -> Result<()> {
.downcast::<Trap>()?;
let mut found = false;
for frame in trap.trace() {
for frame in trap.trace().expect("backtrace is available") {
for symbol in frame.symbols() {
if let Some(file) = symbol.file() {
if file.ends_with("input.rs") {
@ -661,7 +688,7 @@ fn traps_without_address_map() -> Result<()> {
.err()
.expect("error calling function");
let trace = e.trace();
let trace = e.trace().expect("backtrace is available");
assert_eq!(trace.len(), 2);
assert_eq!(trace[0].func_name(), Some("hello"));
assert_eq!(trace[0].func_index(), 1);

Loading…
Cancel
Save