//! This module defines the interface used by minidump-unwind to symbolize stack traces. //! //! minidump-unwind uses a series of traits to represent symbolizing functionality and interfaces: //! //! * [SymbolProvider][] - provides symbolication, cfi evaluation, and debug statistics //! * Implemented by [Symbolizer][] and [debuginfo::DebugInfoSymbolProvider][] (requires the //! `debuginfo` feature to be enabled). //! //! * [SymbolSupplier][] - maps a [Module][] to a [SymbolFile][] //! * minidump-unwind does not directly use this, it's just there so the Symbolizer can //! generically handle different symbol fetching strategies. //! //! * [FrameSymbolizer][] - callbacks that symbolication uses to return its results. //! * Implemented by [StackFrame][crate::StackFrame] //! * Implemented by DummyFrame (private, for a stack scanning heuristic) //! * [FrameWalker][] - callbacks that cfi eval uses to read callee state and write caller state. //! * Implemented by CfiStackWalker (private) //! //! //! The following concrete functions are provided to allow configuration of the symbol fetching //! strategy: //! //! * [http_symbol_supplier][] - a [SymbolSupplier][] that can find symbols over HTTP (and cache). //! Requires the `http` feature to be enabled. //! * [simple_symbol_supplier][] - a [SymbolSupplier][] that can find symbols on disk. //! * [string_symbol_supplier][] - a mock [SymbolSupplier][] for tests. //! //! //! The following concrete types are provided: //! //! * [Symbolizer][] - the main interface of the symbolizer, implementing [SymbolProvider][]. //! * Wraps the [SymbolSupplier][] implementation that is selected. //! * Queries the [SymbolSupplier] and manages the SymbolFiles however it pleases. //! * [SymbolStats][] - debug statistic output. //! * [SymbolFile][] - part of [LocateSymbolsResult][] that a [SymbolProvider][] returns to the //! Symbolizer. //! * Never handled by minidump-unwind, public for the trait. //! * [SymbolError][] - possible errors a [SymbolProvider][] can yield. //! * Never handled by minidump-unwind, public for the trait. //! * [FillSymbolError][] - possible errors for `fill_symbol`. //! * While this *is* handled by minidump-unwind, it doesn't actually look at the value. It's //! just there to be an Error type for the sake of API design. //! * [LocateSymbolsResult][] - a payload that a [SymbolProvider][] returns to the Symbolizer. //! * Never handled by minidump-unwind, public for the trait. //! * [DebugInfoResult][] - part of [LocateSymbolsResult][] that a [SymbolProvider][] returns to //! the Symbolizer. //! * Never handled by minidump-unwind, public for the trait. use std::collections::HashMap; use std::path::PathBuf; use async_trait::async_trait; use minidump::Module; pub use breakpad_symbols::{ DebugInfoResult, FileError, FileKind, FillSymbolError, FrameSymbolizer, FrameWalker, LocateSymbolsResult, PendingSymbolStats, SymbolError, SymbolFile, SymbolStats, SymbolSupplier, Symbolizer, }; #[cfg(feature = "debuginfo-unwind")] pub mod debuginfo; /// The [`SymbolProvider`] is the main extension point for minidump processing. /// /// It is primarily used by the `process_minidump` function to do stack /// unwinding via CFI (call frame information) of a [`Module`] using the /// `walk_frame` function. /// /// The `fill_symbol` function is responsible for filling in the source location /// (function, file, line triple) corresponding to an instruction address, as /// well as a dual purpose of informing the stack scanning heuristic whether a /// given instruction address might be valid inside of a [`Module`]. /// /// All the asynchronous trait methods can be called concurrently and need to /// handle synchronization and request coalescing (based on the [`Module`]). #[async_trait] pub trait SymbolProvider { /// Fill symbol information in [`FrameSymbolizer`] using the instruction /// address from `frame`, and the module information from [`Module`]. /// /// An Error indicates that no symbols could be found for the relevant /// module. /// /// This is used for filling in the resulting source location of the /// frame as a (function, file, line) triple, as well as providing the /// `parameter_size` which is used during CFI evaluation and stack walking. /// /// This function also serves a dual purpose in informing the stack scanning /// heuristic whether a potential instruction address points to a valid /// function or not. async fn fill_symbol( &self, module: &(dyn Module + Sync), frame: &mut (dyn FrameSymbolizer + Send), ) -> Result<(), FillSymbolError>; /// Tries to use CFI to walk the stack frame of the [`FrameWalker`] /// using the symbols of the given [`Module`]. /// /// Output should be written using the [`FrameWalker`]'s `set_caller_*` APIs. async fn walk_frame( &self, module: &(dyn Module + Sync), walker: &mut (dyn FrameWalker + Send), ) -> Option<()>; /// Gets the path to the binary code file for a given module (or an Error). /// /// This might be used later on to inspect the assembly instructions of /// a module. async fn get_file_path( &self, module: &(dyn Module + Sync), file_kind: FileKind, ) -> Result; /// Collect various statistics on the symbols. /// /// Keys are implementation dependent. /// For example the file name of the module (code_file's file name). /// /// This is only really intended to be queried after processing an /// entire minidump, and may have non-trivial overhead to compute. /// It's als possible we'd want it to also be able to contain stats /// that don't really make sense in intermediate states. /// /// In a world where you might want to have one SymbolSupplier shared /// by multiple instances of `process` running in parallel, it's unclear /// if this is the right abstraction. Perhaps we should have some kind /// of "session" abstraction so you can get stats about each individual /// processing task? Of course all pooling/caching between the tasks /// muddies things too. fn stats(&self) -> HashMap { HashMap::new() } /// Collect various pending statistics on the symbols. /// /// This is intended to be queried during processing to give some /// interactive feedback to the user, and so is fine to poll as /// much as you want, whenever you want. fn pending_stats(&self) -> PendingSymbolStats { PendingSymbolStats::default() } } #[async_trait] impl SymbolProvider for &(dyn SymbolProvider + Sync) { async fn fill_symbol( &self, module: &(dyn Module + Sync), frame: &mut (dyn FrameSymbolizer + Send), ) -> Result<(), FillSymbolError> { (*self).fill_symbol(module, frame).await } async fn walk_frame( &self, module: &(dyn Module + Sync), walker: &mut (dyn FrameWalker + Send), ) -> Option<()> { (*self).walk_frame(module, walker).await } async fn get_file_path( &self, module: &(dyn Module + Sync), file_kind: FileKind, ) -> Result { (*self).get_file_path(module, file_kind).await } fn stats(&self) -> HashMap { (*self).stats() } fn pending_stats(&self) -> PendingSymbolStats { (*self).pending_stats() } } #[derive(Default)] pub struct MultiSymbolProvider { providers: Vec>, } impl MultiSymbolProvider { pub fn new() -> MultiSymbolProvider { Default::default() } pub fn add(&mut self, provider: Box) { self.providers.push(provider); } } #[async_trait] impl SymbolProvider for MultiSymbolProvider { async fn fill_symbol( &self, module: &(dyn Module + Sync), frame: &mut (dyn FrameSymbolizer + Send), ) -> Result<(), FillSymbolError> { // Return Ok if *any* symbol provider came back with Ok, so that the user can // distinguish between having no symbols at all and just not being able to // symbolize this particular frame. let mut best_result = Err(FillSymbolError {}); for p in self.providers.iter() { let new_result = p.fill_symbol(module, frame).await; best_result = best_result.or(new_result); } best_result } async fn walk_frame( &self, module: &(dyn Module + Sync), walker: &mut (dyn FrameWalker + Send), ) -> Option<()> { for p in self.providers.iter() { let result = p.walk_frame(module, walker).await; if result.is_some() { return result; } } None } async fn get_file_path( &self, module: &(dyn Module + Sync), file_kind: FileKind, ) -> Result { // Return Ok if *any* symbol provider came back with Ok let mut best_result = Err(FileError::NotFound); for p in self.providers.iter() { let new_result = p.get_file_path(module, file_kind).await; best_result = best_result.or(new_result); } best_result } fn stats(&self) -> HashMap { let mut result = HashMap::new(); for p in self.providers.iter() { // FIXME: do more intelligent merging of the stats // (currently doesn't matter as only one provider reports non-empty stats). result.extend(p.stats()); } result } fn pending_stats(&self) -> PendingSymbolStats { let mut result = PendingSymbolStats::default(); for p in self.providers.iter() { // FIXME: do more intelligent merging of the stats // (currently doesn't matter as only one provider reports non-empty stats). result = p.pending_stats(); } result } } #[async_trait] impl SymbolProvider for Symbolizer { async fn fill_symbol( &self, module: &(dyn Module + Sync), frame: &mut (dyn FrameSymbolizer + Send), ) -> Result<(), FillSymbolError> { self.fill_symbol(module, frame).await } async fn walk_frame( &self, module: &(dyn Module + Sync), walker: &mut (dyn FrameWalker + Send), ) -> Option<()> { self.walk_frame(module, walker).await } async fn get_file_path( &self, module: &(dyn Module + Sync), file_kind: FileKind, ) -> Result { self.get_file_path(module, file_kind).await } fn stats(&self) -> HashMap { self.stats() } fn pending_stats(&self) -> PendingSymbolStats { self.pending_stats() } } /// Gets a SymbolSupplier that looks up symbols by path or with urls. /// /// * `symbols_paths` is a list of paths to check for symbol files. Paths /// are searched in order until one returns a payload. If none do, then /// urls are used. /// /// * `symbols_urls` is a list of "base urls" that should all point to Tecken /// servers. urls are queried in order until one returns a payload. If none /// do, then it's an error. /// /// * `symbols_cache` is a directory where an on-disk cache should be located. /// This should be assumed to be a "temp" directory that another process /// you don't control is garbage-collecting old files from (to provide an LRU cache). /// The cache is queried before paths and urls (otherwise it wouldn't be much of a cache). /// /// * `symbols_tmp` is a directory where symbol files should be downloaded to /// before atomically swapping them into the cache. Has the same "temp" /// assumptions as symbols_cache. /// /// * `timeout` a maximum time limit for a symbol file download. This /// is primarily defined to avoid getting stuck on buggy infinite downloads. /// As of this writing, minidump-stackwalk defaults this to 1000 seconds. In /// the event of a timeout, the supplier may still try to parse the truncated /// download. #[cfg(feature = "http")] pub fn http_symbol_supplier( symbol_paths: Vec, symbol_urls: Vec, symbols_cache: PathBuf, symbols_tmp: PathBuf, timeout: std::time::Duration, ) -> impl SymbolSupplier { breakpad_symbols::HttpSymbolSupplier::new( symbol_urls, symbols_cache, symbols_tmp, symbol_paths, timeout, ) } /// Gets a SymbolSupplier that looks up symbols by path. /// /// Paths are queried in order until one returns a payload. pub fn simple_symbol_supplier(symbol_paths: Vec) -> impl SymbolSupplier { breakpad_symbols::SimpleSymbolSupplier::new(symbol_paths) } /// Gets a mock SymbolSupplier that just maps module names /// to a string containing an entire breakpad .sym file, for tests. pub fn string_symbol_supplier(modules: HashMap) -> impl SymbolSupplier { breakpad_symbols::StringSymbolSupplier::new(modules) }