backtrace_ext/
lib.rs

1//! Minor conveniences on top of the backtrace crate
2//!
3//! See [`short_frames_strict`][] for details.
4use backtrace::*;
5use std::ops::Range;
6
7#[cfg(test)]
8mod test;
9
10/// Gets an iterator over the frames that are part of Rust's "short backtrace" range.
11/// If no such range is found, the full stack is yielded.
12///
13/// Rust generally tries to include special frames on the stack called `rust_end_short_backtrace`
14/// and `rust_begin_short_backtrace` which delimit the "real" stackframes from "gunk" stackframes
15/// like setting up main and invoking the panic runtime. This yields all the "real" frames between
16/// those two (which theoretically can be nothing with enough optimization, although that's unlikely
17/// for any non-trivial program).
18///
19/// If only one of the special frames is present we will only clamp one side of the stack
20/// (similar to `a..` or `..a`). If the special frames are in the wrong order we will discard
21/// them and produce the full stack. If multiple versions of a special frame are found
22/// (I've seen it in the wild), we will pick the "innermost" ones, producing the smallest
23/// possible backtrace (and excluding all special frames from the output).
24///
25/// Each element of the iterator includes a Range which you should use to slice
26/// the frame's `symbols()` array. This handles the theoretical situation where "real" frames
27/// got inlined together with the special marker frames. I want to believe this can't happen
28/// but you can never trust backtraces to be reasonable! We will never yield a Frame to you
29/// with an empty Range.
30///
31/// Note that some "gunk" frames may still be found within the short backtrace, as there is still some
32/// platform-specific and optimization-specific glue around the edges because compilers are
33/// complicated and nothing's perfect. This can include:
34///
35/// * `core::ops::function::FnOnce::call_once`
36/// * `std::panicking::begin_panic_handler`
37/// * `core::panicking::panic_fmt`
38/// * `rust_begin_unwind`
39///
40/// In the future we may introduce a non-strict short_frames which heuristically filters
41/// those frames out too. Until then, the strict approach is safe.
42///
43/// # Example
44///
45/// Here's an example simple "short backtrace" implementation.
46/// Note the use of `sub_frames` for the inner loop to restrict `symbols`!
47///
48/// This example is based off of code found in `miette` (Apache-2.0), which itself
49/// copied the logic from `human-panic` (MIT/Apache-2.0).
50///
51/// FIXME: it would be nice if this example consulted `RUST_BACKTRACE=full`,
52/// and maybe other vars used by rust's builtin panic handler..?
53///
54/// ```
55/// fn backtrace() -> String {
56///     use std::fmt::Write;
57///     if let Ok(var) = std::env::var("RUST_BACKTRACE") {
58///         if !var.is_empty() && var != "0" {
59///             const HEX_WIDTH: usize = std::mem::size_of::<usize>() + 2;
60///             // Padding for next lines after frame's address
61///             const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6;
62///             let mut backtrace = String::new();
63///             let trace = backtrace::Backtrace::new();
64///             let frames = backtrace_ext::short_frames_strict(&trace).enumerate();
65///             for (idx, (frame, subframes)) in frames {
66///                 let ip = frame.ip();
67///                 let _ = write!(backtrace, "\n{:4}: {:2$?}", idx, ip, HEX_WIDTH);
68///     
69///                 let symbols = frame.symbols();
70///                 if symbols.is_empty() {
71///                     let _ = write!(backtrace, " - <unresolved>");
72///                     continue;
73///                 }
74///     
75///                 for (idx, symbol) in symbols[subframes].iter().enumerate() {
76///                     // Print symbols from this address,
77///                     // if there are several addresses
78///                     // we need to put it on next line
79///                     if idx != 0 {
80///                         let _ = write!(backtrace, "\n{:1$}", "", NEXT_SYMBOL_PADDING);
81///                     }
82///     
83///                     if let Some(name) = symbol.name() {
84///                         let _ = write!(backtrace, " - {}", name);
85///                     } else {
86///                         let _ = write!(backtrace, " - <unknown>");
87///                     }
88///     
89///                     // See if there is debug information with file name and line
90///                     if let (Some(file), Some(line)) = (symbol.filename(), symbol.lineno()) {
91///                         let _ = write!(
92///                             backtrace,
93///                             "\n{:3$}at {}:{}",
94///                             "",
95///                             file.display(),
96///                             line,
97///                             NEXT_SYMBOL_PADDING
98///                         );
99///                     }
100///                 }
101///             }
102///             return backtrace;
103///         }
104///     }
105///     "".into()
106/// }
107/// ```
108pub fn short_frames_strict(
109    backtrace: &Backtrace,
110) -> impl Iterator<Item = (&BacktraceFrame, Range<usize>)> {
111    short_frames_strict_impl(backtrace)
112}
113
114pub(crate) fn short_frames_strict_impl<B: Backtraceish>(
115    backtrace: &B,
116) -> impl Iterator<Item = (&B::Frame, Range<usize>)> {
117    // Search for the special frames
118    let mut short_start = None;
119    let mut short_end = None;
120    let frames = backtrace.frames();
121    for (frame_idx, frame) in frames.iter().enumerate() {
122        let symbols = frame.symbols();
123        for (subframe_idx, frame) in symbols.iter().enumerate() {
124            if let Some(name) = frame.name_str() {
125                // Yes these ARE backwards, and that's intentional! We want to print the frames from
126                // "newest to oldest" (show what panicked first), and that's the order that Backtrace
127                // gives us, but these magic labels view the stack in the opposite order. So we just
128                // swap it once here and forget about that weirdness.
129                //
130                // Note that due to platform/optimization wobblyness you can end up with multiple frames
131                // that contain these names in sequence. If that happens we just want to pick the two
132                // that are closest together. For the start that means just using the last one we found,
133                // and for the end that means taking the first one we find.
134                if name.contains("rust_end_short_backtrace") {
135                    short_start = Some((frame_idx, subframe_idx));
136                }
137                if name.contains("rust_begin_short_backtrace") && short_end.is_none() {
138                    short_end = Some((frame_idx, subframe_idx));
139                }
140            }
141        }
142    }
143
144    // Check if these are in the right order, if they aren't, discard them
145    // This also handles the mega-cursed case of "someone made a symbol with both names
146    // so actually they're the exact same subframe".
147    if let (Some(start), Some(end)) = (short_start, short_end) {
148        if start >= end {
149            short_start = None;
150            short_end = None;
151        }
152    }
153
154    // By default we want to produce a full stack trace and now we'll try to clamp it.
155    let mut first_frame = 0usize;
156    let mut first_subframe = 0usize;
157    // NOTE: this is INCLUSIVE
158    let mut last_frame = frames.len().saturating_sub(1);
159    // NOTE: this is EXCLUSIVE
160    let mut last_subframe_excl = backtrace
161        .frames()
162        .last()
163        .map(|frame| frame.symbols().len())
164        .unwrap_or(0);
165
166    // This code tries to be really paranoid about boundary conditions although in practice
167    // most of them are impossible because there's always going to be gunk on either side
168    // of the short backtrace to smooth out the boundaries, and panic_fmt is basically
169    // impossible to optimize out. Still, don't trust backtracers!!!
170    //
171    // This library has a fuckton of tests to try to catch all the little corner cases here.
172
173    // If we found the start bound...
174    if let Some((idx, sub_idx)) = short_start {
175        if frames[idx].symbols().len() == sub_idx + 1 {
176            // If it was the last subframe of this frame, we want to just
177            // use the whole next frame! It's ok if this takes us to `first_frame = len`,
178            // that will be properly handled as an empty output
179            first_frame = idx + 1;
180            first_subframe = 0;
181        } else {
182            // Otherwise use this frame, and all the subframes after it
183            first_frame = idx;
184            first_subframe = sub_idx + 1;
185        }
186    }
187
188    // If we found the end bound...
189    if let Some((idx, sub_idx)) = short_end {
190        if sub_idx == 0 {
191            // If it was the first subframe of this frame, we want to just
192            // use the whole previous frame!
193            if idx == 0 {
194                // If we were *also* on the first frame, set subframe_excl
195                // to 0, indicating an empty output
196                last_frame = 0;
197                last_subframe_excl = 0;
198            } else {
199                last_frame = idx - 1;
200                last_subframe_excl = frames[last_frame].symbols().len();
201            }
202        } else {
203            // Otherwise use this frame (no need subframe math, exclusive bound!)
204            last_frame = idx;
205            last_subframe_excl = sub_idx;
206        }
207    }
208
209    // If the two subframes managed to perfectly line up with eachother, just
210    // throw everything out and yield an empty range. We don't need to fix any
211    // other values at this point as they won't be used for anything with an
212    // empty iterator
213    let final_frames = {
214        let start = (first_frame, first_subframe);
215        let end = (last_frame, last_subframe_excl);
216        if start == end {
217            &frames[0..0]
218        } else {
219            &frames[first_frame..=last_frame]
220        }
221    };
222
223    // Get the index of the last frame when starting from the first frame
224    let adjusted_last_frame = last_frame.saturating_sub(first_frame);
225
226    // finally do the iteration
227    final_frames.iter().enumerate().map(move |(idx, frame)| {
228        // Default to all subframes being yielded
229        let mut sub_start = 0;
230        let mut sub_end_excl = frame.symbols().len();
231        // If we're on first frame, apply its subframe clamp
232        if idx == 0 {
233            sub_start = first_subframe;
234        }
235        // If we're on the last frame, apply its subframe clamp
236        if idx == adjusted_last_frame {
237            sub_end_excl = last_subframe_excl;
238        }
239        (frame, sub_start..sub_end_excl)
240    })
241}
242
243pub(crate) trait Backtraceish {
244    type Frame: Frameish;
245    fn frames(&self) -> &[Self::Frame];
246}
247
248pub(crate) trait Frameish {
249    type Symbol: Symbolish;
250    fn symbols(&self) -> &[Self::Symbol];
251}
252
253pub(crate) trait Symbolish {
254    fn name_str(&self) -> Option<&str>;
255}
256
257impl Backtraceish for Backtrace {
258    type Frame = BacktraceFrame;
259    fn frames(&self) -> &[Self::Frame] {
260        self.frames()
261    }
262}
263
264impl Frameish for BacktraceFrame {
265    type Symbol = BacktraceSymbol;
266    fn symbols(&self) -> &[Self::Symbol] {
267        self.symbols()
268    }
269}
270
271impl Symbolish for BacktraceSymbol {
272    // We need to shortcut SymbolName here because
273    // HRTB isn't in our msrv
274    fn name_str(&self) -> Option<&str> {
275        self.name().and_then(|n| n.as_str())
276    }
277}