diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7bfe13064a..53ed6a9f02 100644 --- a/.github/workflows/main.yml +++ b/.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 diff --git a/Cargo.lock b/Cargo.lock index e23d2f39a2..7f81b5bd08 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index 8518ac5da6..8f47c827d1 100644 --- a/Cargo.toml +++ b/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 diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index a765987796..de269f3486 100644 --- a/crates/c-api/Cargo.toml +++ b/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 diff --git a/crates/c-api/src/trap.rs b/crates/c-api/src/trap.rs index 6a1da6b3f7..213bb9740f 100644 --- a/crates/c-api/src/trap.rs +++ b/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> { - 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 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) } diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index a8183e34ea..39e285db15 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -25,5 +25,4 @@ default = [ ] pooling-allocator = [] memory-init-cow = [] -wasm-backtrace = [] component-model = [] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 13904ccf22..532fd3213b 100644 --- a/crates/cli-flags/src/lib.rs +++ b/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); diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index 00f386d5d3..74bdd7f423 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -213,6 +213,7 @@ pub struct WasmtimeConfig { codegen: CodegenSettings, padding_between_functions: Option, 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) diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 84d355eb69..bb1c337377 100644 --- a/crates/runtime/Cargo.toml +++ b/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"] diff --git a/crates/runtime/src/externref.rs b/crates/runtime/src/externref.rs index 7b5b3ef06d..354e4fde9b 100644 --- a/crates/runtime/src/externref.rs +++ b/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, root: NonNull, @@ -867,7 +866,6 @@ impl std::ops::DerefMut for DebugOnly { /// /// 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; diff --git a/crates/runtime/src/libcalls.rs b/crates/runtime/src/libcalls.rs index 8375d9c9e1..d1a75286c9 100644 --- a/crates/runtime/src/libcalls.rs +++ b/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", ))) }) diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs index c3b323832b..caf1553f0a 100644 --- a/crates/runtime/src/traphandlers.rs +++ b/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) -> ! { #[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, + }, /// 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, }, /// 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, }, /// 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, }, } 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> 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::, @@ -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>, + unwind: UnsafeCell)>>, jmp_buf: Cell<*const u8>, handling_trap: Cell, signal_handler: Option<*const SignalHandler<'static>>, prev: Cell, + capture_backtrace: bool, } enum UnwindReason { Panic(Box), 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 { 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)); } } } diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 8517fbf991..4e78397dcb 100644 --- a/crates/wasmtime/Cargo.toml +++ b/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. diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 16e3272bba..7b70f27a88 100644 --- a/crates/wasmtime/src/config.rs +++ b/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> { - 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")] diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index c81ae6e67d..4886d78910 100644 --- a/crates/wasmtime/src/engine.rs +++ b/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 diff --git a/crates/wasmtime/src/func.rs b/crates/wasmtime/src/func.rs index 1b019c2310..fff3389aa4 100644 --- a/crates/wasmtime/src/func.rs +++ b/crates/wasmtime/src/func.rs @@ -1235,6 +1235,7 @@ pub(crate) fn invoke_wasm_and_catch_traps( } let result = wasmtime_runtime::catch_traps( store.0.signal_handler(), + store.0.engine().config().wasm_backtrace, store.0.default_callee(), closure, ); diff --git a/crates/wasmtime/src/lib.rs b/crates/wasmtime/src/lib.rs index c71943a794..64cf5f84de 100644 --- a/crates/wasmtime/src/lib.rs +++ b/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 diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 6a5ec7f3c3..afa35b4754 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -1914,11 +1914,7 @@ unsafe impl wasmtime_runtime::Store for StoreInner { fn new_epoch(&mut self) -> Result { 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) => { diff --git a/crates/wasmtime/src/trap.rs b/crates/wasmtime/src/trap.rs index 1f81f788a4..977e8bd050 100644 --- a/crates/wasmtime/src/trap.rs +++ b/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, native_trace: Backtrace, - #[cfg(feature = "wasm-backtrace")] hint_wasm_backtrace_details_env: bool, } +impl TrapBacktrace { + pub fn new(native_trace: Backtrace, trap_pc: Option) -> Self { + let mut wasm_trace = Vec::::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, +} + 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>(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, - code: EnvTrapCode, - backtrace: Backtrace, - ) -> Self { + pub(crate) fn new_wasm(code: EnvTrapCode, backtrace: Option) -> 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, reason: TrapReason, native_trace: Backtrace) -> Self { - let mut wasm_trace = Vec::::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) -> 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::>())` + // 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(""); 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> 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) } } } diff --git a/tests/all/traps.rs b/tests/all/traps.rs index efe192ede7..02eec0c784 100644 --- a/tests/all/traps.rs +++ b/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::()?; 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);