diff --git a/Cargo.lock b/Cargo.lock index 706c9d3fb..0874f69c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.8.11" @@ -74,6 +89,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -530,6 +560,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "group" version = "0.13.0" @@ -782,6 +818,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "more-asserts" version = "0.3.1" @@ -901,6 +946,15 @@ dependencies = [ "syn", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1129,6 +1183,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1369,6 +1429,7 @@ name = "soroban-env-host" version = "22.0.0" dependencies = [ "arbitrary", + "backtrace", "bytes-lit", "curve25519-dalek", "ecdsa", diff --git a/cackle.toml b/cackle.toml index f682c436c..6a4bb6214 100644 --- a/cackle.toml +++ b/cackle.toml @@ -109,6 +109,14 @@ allow_apis = [ [pkg.rand] allow_unsafe = true +[pkg.backtrace] +allow_apis = [ + "env", + "fs", + "thread", +] +allow_unsafe = true + [pkg.rand_chacha] allow_apis = [ "rand", diff --git a/soroban-env-common/src/vmcaller_env.rs b/soroban-env-common/src/vmcaller_env.rs index 251d7797b..1b97d8bb4 100644 --- a/soroban-env-common/src/vmcaller_env.rs +++ b/soroban-env-common/src/vmcaller_env.rs @@ -174,9 +174,9 @@ macro_rules! vmcaller_none_function_helper { => { // We call `augment_err_result` here to give the Env a chance to attach - // context to any error that was generated by code that didn't have an - // Env on hand when creating the error. This will at least localize the - // error to a given Env call. + // context (eg. a backtrace) to any error that was generated by code + // that didn't have an Env on hand when creating the error. This will at + // least localize the error to a given Env call. fn $fn_id(&self, $($arg:$type),*) -> Result<$ret, Self::Error> { // Check the ledger protocol version against the function-specified // boundaries, this prevents calling a host function using the host diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml index 1a82ddc3f..66c3a6167 100644 --- a/soroban-env-host/Cargo.toml +++ b/soroban-env-host/Cargo.toml @@ -32,6 +32,7 @@ rand_chacha = "0.3.1" num-traits = "0.2.17" num-integer = "0.1.45" num-derive = "0.4.1" +backtrace = { version = "0.3.69", optional = true } k256 = {version = "0.13.1", default-features = false, features = ["ecdsa", "arithmetic"]} p256 = {version = "0.13.2", default-features = false, features = ["ecdsa", "arithmetic"]} ecdsa = {version = "0.16.7", default-features = false} @@ -85,6 +86,7 @@ wasmprinter = "0.2.72" expect-test = "1.4.1" more-asserts = "0.3.1" pretty_assertions = "1.4.0" +backtrace = "0.3.69" serde_json = "1.0.108" arbitrary = "1.3.2" lstsq = "0.5.0" @@ -103,7 +105,7 @@ default-features = false features = ["arbitrary"] [features] -testutils = ["soroban-env-common/testutils", "recording_mode"] +testutils = ["soroban-env-common/testutils", "recording_mode", "dep:backtrace"] next = ["soroban-env-common/next", "stellar-xdr/next"] tracy = ["dep:tracy-client", "soroban-env-common/tracy"] recording_mode = [] diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index a362550b7..549c0aeb3 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -820,7 +820,7 @@ impl EnvBase for Host { // will display a relatively ugly message like "thread panicked at Box" to stderr, when it is much more useful to the user if we have it // print the result of HostError::Debug, with its glorious Error, - // site-of-origin debug log. + // site-of-origin backtrace and debug log. // // To get it to do that, we have to call `panic!()`, not `panic_any`. // Personally I think this is a glaring weakness of `panic_any` but we are diff --git a/soroban-env-host/src/host/error.rs b/soroban-env-host/src/host/error.rs index d5df8d9fc..d2bfcfca4 100644 --- a/soroban-env-host/src/host/error.rs +++ b/soroban-env-host/src/host/error.rs @@ -5,6 +5,8 @@ use crate::{ ConversionError, EnvBase, Error, Host, TryFromVal, U32Val, Val, }; +#[cfg(any(test, feature = "testutils"))] +use backtrace::{Backtrace, BacktraceFrame}; use core::fmt::Debug; use std::{ cell::{Ref, RefCell, RefMut}, @@ -16,6 +18,8 @@ use super::metered_clone::MeteredClone; #[derive(Clone)] pub(crate) struct DebugInfo { events: Events, + #[cfg(any(test, feature = "testutils"))] + backtrace: Backtrace, } #[derive(Clone)] @@ -62,13 +66,68 @@ impl DebugInfo { } Ok(()) } + + #[cfg(not(any(test, feature = "testutils")))] + fn write_backtrace(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } + + #[cfg(any(test, feature = "testutils"))] + fn write_backtrace(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // We do a little trimming here, skipping the first two frames (which + // are always into, from, and one or more Host::err_foo calls) and all + // the frames _after_ the short-backtrace marker that rust compiles-in. + + fn frame_name_matches(frame: &BacktraceFrame, pat: &str) -> bool { + for sym in frame.symbols() { + match sym.name() { + Some(sn) if format!("{:}", sn).contains(pat) => { + return true; + } + _ => (), + } + } + false + } + + fn frame_is_short_backtrace_start(frame: &BacktraceFrame) -> bool { + frame_name_matches(frame, "__rust_begin_short_backtrace") + } + + fn frame_is_initial_error_plumbing(frame: &BacktraceFrame) -> bool { + frame_name_matches(frame, "::from") + || frame_name_matches(frame, "::into") + || frame_name_matches(frame, "host::err") + || frame_name_matches(frame, "Host::err") + || frame_name_matches(frame, "Host>::err") + || frame_name_matches(frame, "::augment_err_result") + || frame_name_matches(frame, "::with_shadow_mode") + || frame_name_matches(frame, "::with_debug_mode") + || frame_name_matches(frame, "::maybe_get_debug_info") + || frame_name_matches(frame, "::map_err") + } + let mut bt = self.backtrace.clone(); + bt.resolve(); + let frames: Vec = bt + .frames() + .iter() + .skip_while(|f| frame_is_initial_error_plumbing(f)) + .take_while(|f| !frame_is_short_backtrace_start(f)) + .cloned() + .collect(); + let bt: Backtrace = frames.into(); + writeln!(f)?; + writeln!(f, "Backtrace (newest first):")?; + writeln!(f, "{:?}", bt) + } } impl Debug for HostError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "HostError: {:?}", self.error)?; if let Some(info) = &self.info { - info.write_events(f) + info.write_events(f)?; + info.write_backtrace(f) } else { writeln!(f, "DebugInfo not available") } @@ -202,7 +261,7 @@ impl Host { /// [Error], and when running in [DiagnosticMode::Debug] additionally /// records a diagnostic event with the provided `msg` and `args` and then /// enriches the returned [Error] with [DebugInfo] in the form of a - /// snapshot of the [Events] buffer. + /// [Backtrace] and snapshot of the [Events] buffer. pub(crate) fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError { let mut he = HostError::from(error); self.with_debug_mode(|| { @@ -237,7 +296,8 @@ impl Host { self.with_debug_mode(|| { if let Ok(events_ref) = self.0.events.try_borrow() { let events = events_ref.externalize(self)?; - res = Some(Box::new(DebugInfo { events })); + let backtrace = Backtrace::new_unresolved(); + res = Some(Box::new(DebugInfo { backtrace, events })); } Ok(()) });