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
This commit is contained in:
Pat Hickey
2022-05-25 12:25:50 -07:00
committed by GitHub
parent 010e028d67
commit bffce37050
20 changed files with 310 additions and 234 deletions

View File

@@ -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.

View File

@@ -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")]

View File

@@ -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

View File

@@ -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,
);

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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,95 +128,18 @@ 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,
}
fn _assert_trap_is_sync_and_send(t: &Trap) -> (&dyn Sync, &dyn Send) {
(t, t)
}
impl Trap {
/// Creates a new `Trap` with `message`.
/// # Example
/// ```
/// let trap = wasmtime::Trap::new("unexpected error");
/// assert!(trap.to_string().contains("unexpected error"));
/// ```
#[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())
}
/// 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())
}
#[cold] // see Trap::new
pub(crate) fn from_runtime_box(runtime_trap: Box<wasmtime_runtime::Trap>) -> Self {
Self::from_runtime(*runtime_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::Jit { pc, backtrace } => {
let code = GlobalModuleRegistry::with(|modules| {
modules
.lookup_trap_code(pc)
.unwrap_or(EnvTrapCode::StackOverflow)
});
Trap::new_wasm(Some(pc), code, backtrace)
}
wasmtime_runtime::Trap::Wasm {
trap_code,
backtrace,
} => Trap::new_wasm(None, trap_code, backtrace),
wasmtime_runtime::Trap::OOM { backtrace } => {
let reason = TrapReason::Message("out of memory".to_string());
Trap::new_with_trace(None, reason, backtrace)
}
}
}
#[cold] // see Trap::new
pub(crate) fn new_wasm(
trap_pc: Option<usize>,
code: EnvTrapCode,
backtrace: Backtrace,
) -> Self {
let code = TrapCode::from_non_user(code);
Trap::new_with_trace(trap_pc, 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 {
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;
#[cfg(feature = "wasm-backtrace")]
GlobalModuleRegistry::with(|registry| {
for frame in native_trace.frames() {
let pc = frame.ip() as usize;
@@ -250,15 +174,103 @@ impl Trap {
}
}
});
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)
}
impl Trap {
/// Creates a new `Trap` with `message`.
/// # Example
/// ```
/// let trap = wasmtime::Trap::new("unexpected error");
/// assert!(trap.to_string().contains("unexpected error"));
/// ```
#[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(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(TrapReason::I32Exit(status), None)
}
#[cold] // see Trap::new
pub(crate) fn from_runtime_box(runtime_trap: Box<wasmtime_runtime::Trap>) -> Self {
Self::from_runtime(*runtime_trap)
}
#[cold] // see Trap::new
pub(crate) fn from_runtime(runtime_trap: wasmtime_runtime::Trap) -> Self {
match runtime_trap {
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)
});
let backtrace = backtrace.map(|bt| TrapBacktrace::new(bt, Some(pc)));
Trap::new_wasm(code, backtrace)
}
wasmtime_runtime::Trap::Wasm {
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());
let backtrace = backtrace.map(|bt| TrapBacktrace::new(bt, None));
Trap::new_with_trace(reason, backtrace)
}
}
}
#[cold] // see Trap::new
pub(crate) fn new_wasm(code: EnvTrapCode, backtrace: Option<TrapBacktrace>) -> Self {
let code = TrapCode::from_non_user(code);
Trap::new_with_trace(TrapReason::InstructionTrap(code), backtrace)
}
/// Creates a new `Trap`.
/// * `reason` - this is the wasmtime-internal reason for why this trap is
/// being created.
///
/// * `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)
}
}
}