Skip to main content

axbacktrace/
lib.rs

1#![no_std]
2
3#[cfg(feature = "alloc")]
4extern crate alloc;
5
6#[cfg(feature = "alloc")]
7use alloc::{boxed::Box, vec::Vec};
8use core::{
9    fmt,
10    ops::Range,
11    sync::atomic::{AtomicUsize, Ordering},
12};
13
14use ax_lazyinit::OnceLock;
15
16#[cfg(feature = "dwarf")]
17mod dwarf;
18
19#[cfg(feature = "dwarf")]
20pub use dwarf::{DwarfReader, FrameIter};
21
22static IP_RANGE: OnceLock<Range<usize>> = OnceLock::new();
23static FP_RANGE: OnceLock<Range<usize>> = OnceLock::new();
24
25#[cfg(target_arch = "x86_64")]
26const TARGET_ARCH: &str = "x86_64";
27#[cfg(target_arch = "aarch64")]
28const TARGET_ARCH: &str = "aarch64";
29#[cfg(target_arch = "riscv64")]
30const TARGET_ARCH: &str = "riscv64";
31#[cfg(target_arch = "riscv32")]
32const TARGET_ARCH: &str = "riscv32";
33#[cfg(target_arch = "loongarch64")]
34const TARGET_ARCH: &str = "loongarch64";
35#[cfg(not(any(
36    target_arch = "x86_64",
37    target_arch = "aarch64",
38    target_arch = "riscv64",
39    target_arch = "riscv32",
40    target_arch = "loongarch64"
41)))]
42const TARGET_ARCH: &str = "unknown";
43
44/// Initializes the backtrace library.
45pub fn init(ip_range: Range<usize>, fp_range: Range<usize>) {
46    IP_RANGE.call_once(|| ip_range);
47    FP_RANGE.call_once(|| fp_range);
48    #[cfg(feature = "dwarf")]
49    dwarf::init();
50}
51
52/// Represents a single stack frame in the unwound stack.
53#[repr(C)]
54#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
55pub struct Frame {
56    /// The frame pointer of the previous stack frame.
57    pub fp: usize,
58    /// The instruction pointer (program counter) after the function call.
59    pub ip: usize,
60}
61
62impl Frame {
63    #[cfg(feature = "alloc")]
64    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
65    const OFFSET: usize = 0;
66    #[cfg(feature = "alloc")]
67    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
68    const OFFSET: usize = 1;
69
70    #[cfg(feature = "alloc")]
71    fn read(fp: usize) -> Option<Self> {
72        if fp == 0 || !fp.is_multiple_of(core::mem::align_of::<Frame>()) {
73            return None;
74        }
75
76        Some(unsafe { (fp as *const Frame).sub(Self::OFFSET).read() })
77    }
78
79    // The stored IP is the return address (instruction after the call).
80    // Subtracting the minimum instruction size gives an address that falls
81    // within the calling function, which is what DWARF/ELF symbolizers expect.
82    #[cfg(target_arch = "x86_64")]
83    pub fn adjust_ip(&self) -> usize {
84        self.ip.wrapping_sub(1) // variable-length, 1 byte minimum
85    }
86    #[cfg(target_arch = "aarch64")]
87    pub fn adjust_ip(&self) -> usize {
88        self.ip.wrapping_sub(4) // fixed 4-byte instructions
89    }
90    #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))]
91    pub fn adjust_ip(&self) -> usize {
92        self.ip.wrapping_sub(2) // C extension: 2-byte minimum
93    }
94    #[cfg(target_arch = "loongarch64")]
95    pub fn adjust_ip(&self) -> usize {
96        self.ip.wrapping_sub(4) // fixed 4-byte instructions
97    }
98}
99
100impl fmt::Display for Frame {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "fp={:#x}, ip={:#x}", self.fp, self.ip)
103    }
104}
105
106/// Capacity of the on-stack capture buffer. Matches the default `max_depth()`.
107#[cfg(feature = "alloc")]
108const CAPTURE_CAPACITY: usize = 32;
109
110/// On-stack scratch buffer used during FP walking to avoid heap allocation
111/// in the hot unwinding loop. Converted to `Box<[Frame]>` after the walk.
112#[cfg(feature = "alloc")]
113#[derive(Clone)]
114struct CaptureBuf {
115    frames: [Frame; CAPTURE_CAPACITY],
116    len: usize,
117}
118
119#[cfg(feature = "alloc")]
120impl CaptureBuf {
121    const EMPTY: Self = Self {
122        frames: [Frame { fp: 0, ip: 0 }; CAPTURE_CAPACITY],
123        len: 0,
124    };
125
126    fn push(&mut self, frame: Frame) -> bool {
127        if self.len < CAPTURE_CAPACITY {
128            self.frames[self.len] = frame;
129            self.len += 1;
130            true
131        } else {
132            false
133        }
134    }
135
136    /// Insert a frame at the front, shifting existing frames right.
137    /// If the buffer is full, the last (deepest) frame is evicted to make room.
138    fn insert_front(&mut self, frame: Frame) {
139        let end = if self.len < CAPTURE_CAPACITY {
140            self.len += 1;
141            self.len
142        } else {
143            CAPTURE_CAPACITY // evict the deepest frame
144        };
145        self.frames.copy_within(0..end - 1, 1);
146        self.frames[0] = frame;
147    }
148
149    fn first_mut(&mut self) -> Option<&mut Frame> {
150        if self.len > 0 {
151            Some(&mut self.frames[0])
152        } else {
153            None
154        }
155    }
156
157    /// Convert to a heap-allocated boxed slice trimmed to the actual length.
158    fn into_boxed_slice(self) -> Box<[Frame]> {
159        self.frames[..self.len].into()
160    }
161}
162
163/// Core frame pointer walking logic. Calls `callback` for each valid frame.
164/// The callback returns `false` to stop unwinding (e.g., buffer full).
165#[cfg(feature = "alloc")]
166fn unwind_core(fp: usize, callback: impl FnMut(Frame) -> bool) {
167    unwind_core_with_max_depth(fp, max_depth(), callback);
168}
169
170#[cfg(feature = "alloc")]
171fn unwind_core_with_max_depth(
172    mut fp: usize,
173    max_depth: usize,
174    mut callback: impl FnMut(Frame) -> bool,
175) {
176    let Some(fp_range) = FP_RANGE.get() else {
177        log::error!("Backtrace not initialized. Call `axbacktrace::init` first.");
178        return;
179    };
180
181    let ip_range = IP_RANGE.get();
182    let mut depth = 0;
183
184    while fp_range.contains(&fp)
185        && depth < max_depth
186        && let Some(frame) = Frame::read(fp)
187    {
188        // Skip frames whose IP is outside the kernel text range.
189        // We continue unwinding rather than stopping, as a corrupted
190        // IP does not necessarily mean the FP chain is broken.
191        // Skipped frames still count against the depth budget to prevent
192        // infinite loops on corrupted FP chains with bad IPs.
193        let next_fp = frame.fp;
194        // Check FP progress before IP filtering: a bad IP can be skipped, but
195        // a non-advancing FP would otherwise keep revisiting the same frame.
196        if next_fp != 0 && next_fp <= fp {
197            break;
198        }
199
200        if let Some(ip_range) = ip_range
201            && !ip_range.contains(&frame.ip)
202        {
203            fp = next_fp;
204            depth += 1;
205            continue;
206        }
207
208        if !callback(frame) {
209            break;
210        }
211
212        if let Some(large_stack_end) = fp.checked_add(8 * 1024 * 1024)
213            && next_fp >= large_stack_end
214        {
215            break;
216        }
217
218        if next_fp == 0 {
219            break;
220        }
221
222        fp = next_fp;
223        depth += 1;
224    }
225}
226
227/// Unwind the stack from the given frame pointer.
228#[cfg(feature = "alloc")]
229pub fn unwind_stack(fp: usize) -> Vec<Frame> {
230    let mut frames = Vec::new();
231    unwind_core(fp, |frame| {
232        frames.push(frame);
233        true
234    });
235    frames
236}
237
238static MAX_DEPTH: AtomicUsize = AtomicUsize::new(32);
239
240/// Sets the maximum depth for stack unwinding.
241pub fn set_max_depth(depth: usize) {
242    if depth > 0 {
243        MAX_DEPTH.store(depth, Ordering::Relaxed);
244    }
245}
246/// Returns the maximum depth for stack unwinding.
247pub fn max_depth() -> usize {
248    MAX_DEPTH.load(Ordering::Relaxed)
249}
250
251/// Returns whether the backtrace feature is enabled.
252pub const fn is_enabled() -> bool {
253    cfg!(feature = "alloc")
254}
255
256#[allow(dead_code)]
257#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
258enum Inner {
259    Unsupported,
260    Disabled,
261    #[cfg(feature = "alloc")]
262    Captured(Box<[Frame]>),
263}
264
265/// A captured OS thread stack backtrace.
266///
267/// Internally stores frames as a `Box<[Frame]>` (trimmed to actual length).
268/// Capture uses a stack-allocated scratch buffer so the FP walking loop
269/// itself is allocation-free; the single `Box` allocation happens only after
270/// the walk completes.
271#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
272pub struct Backtrace {
273    inner: Inner,
274    kind: Option<&'static str>,
275}
276
277impl Backtrace {
278    /// Capture the current thread's stack backtrace.
279    pub fn capture() -> Self {
280        #[cfg(not(feature = "alloc"))]
281        return Self {
282            inner: Inner::Disabled,
283            kind: None,
284        };
285
286        #[cfg(feature = "alloc")]
287        {
288            use core::arch::asm;
289
290            let fp: usize;
291            cfg_if::cfg_if! {
292                if #[cfg(target_arch = "x86_64")] {
293                    unsafe { asm!("mov {ptr}, rbp", ptr = out(reg) fp) };
294                } else if #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] {
295                    unsafe { asm!("addi {ptr}, s0, 0", ptr = out(reg) fp) };
296                } else if #[cfg(target_arch = "aarch64")] {
297                    unsafe { asm!("mov {ptr}, x29", ptr = out(reg) fp) };
298                } else if #[cfg(target_arch = "loongarch64")] {
299                    unsafe { asm!("move {ptr}, $fp", ptr = out(reg) fp) };
300                } else {
301                    return Self {
302                        inner: Inner::Unsupported,
303                        kind: None,
304                    };
305                }
306            }
307
308            let mut buf = CaptureBuf::EMPTY;
309            unwind_core(fp, |frame| buf.push(frame));
310
311            core::hint::black_box(());
312
313            Self {
314                inner: Inner::Captured(buf.into_boxed_slice()),
315                kind: None,
316            }
317        }
318    }
319
320    /// Capture the stack backtrace from a trap.
321    ///
322    /// - `fp`: frame pointer from the trap context
323    /// - `ip`: faulting instruction pointer (the PC from the trap frame)
324    /// - `ra`: return address (link register). On x86_64 this is always 0
325    ///   since the return address is stored on the stack as part of the FP chain.
326    #[allow(unused_variables)]
327    pub fn capture_trap(fp: usize, ip: usize, ra: usize) -> Self {
328        #[cfg(not(feature = "alloc"))]
329        return Self {
330            inner: Inner::Disabled,
331            kind: None,
332        };
333
334        #[cfg(feature = "alloc")]
335        {
336            let mut buf = CaptureBuf::EMPTY;
337            unwind_core(fp, |frame| buf.push(frame));
338
339            // If the first unwound frame's IP is outside the kernel text,
340            // it is likely the saved return address was not yet set (e.g.
341            // leaf function fault). Replace it with the link register (ra)
342            // only when ra is valid and within the kernel text range.
343            // Note: on x86_64, ra=0 is always passed, so this branch
344            // never fires for x86_64.
345            if let Some(first) = buf.first_mut()
346                && let Some(ip_range) = IP_RANGE.get()
347                && !ip_range.contains(&first.ip)
348                && ra != 0
349                && ip_range.contains(&ra)
350            {
351                first.ip = ra;
352            }
353
354            buf.insert_front(Frame {
355                fp,
356                ip: ip.wrapping_add(1),
357            });
358
359            Self {
360                inner: Inner::Captured(buf.into_boxed_slice()),
361                kind: None,
362            }
363        }
364    }
365
366    /// Sets the backtrace kind for machine-parseable raw output via [`Display`].
367    pub fn kind(mut self, kind: &'static str) -> Self {
368        self.kind = Some(kind);
369        self
370    }
371
372    /// Visit each stack frame in the captured backtrace in order.
373    ///
374    /// Returns `None` if the backtrace is not captured.
375    #[cfg(feature = "dwarf")]
376    pub fn frames<'a>(&'a self) -> Option<FrameIter<'a>> {
377        let Inner::Captured(capture) = &self.inner else {
378            return None;
379        };
380
381        Some(FrameIter::new(capture))
382    }
383}
384
385impl Backtrace {
386    fn fmt_raw_block(&self, f: &mut fmt::Formatter<'_>, kind: &'static str) -> fmt::Result {
387        let arch = TARGET_ARCH;
388
389        writeln!(
390            f,
391            "BACKTRACE_BEGIN kind={} arch={} alloc={} dwarf={}",
392            kind,
393            arch,
394            cfg!(feature = "alloc"),
395            cfg!(feature = "dwarf")
396        )?;
397
398        match &self.inner {
399            Inner::Unsupported => {
400                writeln!(f, "BT_ERROR unsupported")?;
401            }
402            Inner::Disabled => {
403                if cfg!(feature = "alloc") {
404                    writeln!(f, "BT_ERROR disabled")?;
405                } else {
406                    writeln!(f, "BT_ERROR requires_alloc")?;
407                }
408            }
409            #[cfg(feature = "alloc")]
410            Inner::Captured(frames) => {
411                for (i, raw) in frames.iter().enumerate() {
412                    writeln!(f, "BT {i} ip={:#x} fp={:#x}", raw.ip, raw.fp)?;
413                }
414            }
415        }
416
417        writeln!(f, "BACKTRACE_END")
418    }
419}
420
421impl fmt::Display for Backtrace {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        if let Some(kind) = self.kind {
424            return self.fmt_raw_block(f, kind);
425        }
426
427        match &self.inner {
428            Inner::Unsupported => {
429                writeln!(f, "<unwinding unsupported>")
430            }
431            Inner::Disabled => {
432                if cfg!(feature = "alloc") {
433                    writeln!(f, "<backtrace disabled>")
434                } else {
435                    writeln!(f, "<backtrace requires alloc>")
436                }
437            }
438            #[cfg(feature = "alloc")]
439            Inner::Captured(frames) => {
440                writeln!(f, "Backtrace:")?;
441                #[cfg(feature = "dwarf")]
442                return dwarf::fmt_frames(f, frames);
443                #[cfg(not(feature = "dwarf"))]
444                {
445                    for (i, raw) in frames.iter().enumerate() {
446                        writeln!(f, "{i:>4}: {raw}")?;
447                    }
448                    Ok(())
449                }
450            }
451        }
452    }
453}
454
455impl fmt::Debug for Backtrace {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        fmt::Display::fmt(self, f)
458    }
459}
460
461#[cfg(all(test, feature = "alloc"))]
462mod tests {
463    use alloc::{boxed::Box, format, vec::Vec};
464
465    use super::*;
466
467    fn init_for_tests() {
468        init(0..usize::MAX, 0..usize::MAX);
469    }
470
471    fn boxed_frame_chain(ips: &[usize]) -> (Box<[Frame]>, usize) {
472        let mut frames = ips
473            .iter()
474            .map(|&ip| Frame { fp: 0, ip })
475            .collect::<Vec<_>>()
476            .into_boxed_slice();
477
478        let ptr = frames.as_mut_ptr();
479        for i in 0..frames.len() {
480            let next_fp = if i + 1 < frames.len() {
481                unsafe { ptr.add(i + 1) as usize }
482            } else {
483                0
484            };
485            frames[i].fp = next_fp;
486        }
487        (frames, ptr as usize)
488    }
489
490    // --- CaptureBuf internal tests ---
491
492    #[test]
493    fn capture_buf_push_and_insert() {
494        let mut buf = CaptureBuf::EMPTY;
495        assert!(buf.push(Frame { fp: 1, ip: 0x10 }));
496        assert!(buf.push(Frame { fp: 2, ip: 0x20 }));
497        assert_eq!(buf.len, 2);
498
499        buf.insert_front(Frame { fp: 0, ip: 0x05 });
500        assert_eq!(buf.len, 3);
501        assert_eq!(
502            &*buf.clone().into_boxed_slice(),
503            &[
504                Frame { fp: 0, ip: 0x05 },
505                Frame { fp: 1, ip: 0x10 },
506                Frame { fp: 2, ip: 0x20 }
507            ]
508        );
509    }
510
511    #[test]
512    fn capture_buf_overflow_evicts_deepest() {
513        let mut buf = CaptureBuf::EMPTY;
514        for i in 0..CAPTURE_CAPACITY {
515            assert!(buf.push(Frame { fp: i, ip: i }));
516        }
517        assert!(!buf.push(Frame { fp: 0, ip: 0 })); // full
518        buf.insert_front(Frame { fp: 99, ip: 0x99 });
519        assert_eq!(buf.len, CAPTURE_CAPACITY);
520        let boxed = buf.into_boxed_slice();
521        assert_eq!(boxed[0], Frame { fp: 99, ip: 0x99 });
522        assert_eq!(boxed.len(), CAPTURE_CAPACITY);
523    }
524
525    #[test]
526    fn into_boxed_slice_trims_to_len() {
527        let mut buf = CaptureBuf::EMPTY;
528        buf.push(Frame { fp: 1, ip: 0x10 });
529        buf.push(Frame { fp: 2, ip: 0x20 });
530        let boxed = buf.into_boxed_slice();
531        assert_eq!(boxed.len(), 2);
532        assert_eq!(boxed[0], Frame { fp: 1, ip: 0x10 });
533    }
534
535    // --- Frame::read / unwind_core internal tests ---
536
537    #[test]
538    fn unwind_stack_collects_fake_frames() {
539        init_for_tests();
540        let (frames, start_fp) = boxed_frame_chain(&[0x1111, 0x2222, 0x3333]);
541        let out = unwind_stack(start_fp);
542        assert_eq!(out, frames.as_ref());
543    }
544
545    #[test]
546    fn unwind_core_callback_stop_early() {
547        init_for_tests();
548        let (_chain, start_fp) = boxed_frame_chain(&[0x1, 0x2, 0x3, 0x4, 0x5]);
549        let mut count = 0;
550        unwind_core(start_fp, |_| {
551            count += 1;
552            count < 3
553        });
554        assert_eq!(count, 3);
555    }
556
557    #[test]
558    fn unwind_stack_stops_on_non_advancing_frame_pointer() {
559        init_for_tests();
560        let mut frames = [Frame { fp: 0, ip: 0x1111 }, Frame { fp: 0, ip: 0x2222 }];
561        let base = frames.as_mut_ptr();
562        frames[0].fp = unsafe { base.add(1) as usize };
563        frames[1].fp = base as usize;
564
565        let out = unwind_stack(base as usize);
566        assert_eq!(out, [frames[0]]);
567    }
568
569    #[test]
570    fn frame_read_rejects_null_and_misaligned() {
571        assert!(Frame::read(0).is_none());
572        assert!(Frame::read(1).is_none());
573        assert!(Frame::read(3).is_none());
574    }
575
576    // --- capture_trap with Inner::Captured verification ---
577
578    #[test]
579    fn capture_trap_ra_not_substituted_with_wide_range() {
580        init_for_tests();
581        let (_chain, start_fp) = boxed_frame_chain(&[0xDEAD]);
582        let bt = Backtrace::capture_trap(start_fp, 0x1000, 0xBEEF);
583        let Inner::Captured(frames) = &bt.inner else {
584            panic!("expected Captured")
585        };
586        assert_eq!(frames[0].ip, 0x1001);
587        assert_eq!(frames[1].ip, 0xDEAD); // not replaced by ra
588    }
589
590    // --- Stress tests ---
591
592    /// Build a chain that fills the buffer to exactly CAPTURE_CAPACITY.
593    /// Then unwind and verify every frame is collected.
594    #[test]
595    fn stress_fill_buffer_exactly() {
596        init_for_tests();
597        let ips: Vec<usize> = (0..CAPTURE_CAPACITY).map(|i| 0xA000 + i).collect();
598        let (chain, start_fp) = boxed_frame_chain(&ips);
599        let out = unwind_stack(start_fp);
600        assert_eq!(out.len(), CAPTURE_CAPACITY);
601        assert_eq!(out.as_slice(), chain.as_ref());
602    }
603
604    /// Build a chain with CAPTURE_CAPACITY - 1 frames, then capture_trap.
605    /// The trap frame is inserted at front, total = CAPTURE_CAPACITY, no eviction.
606    #[test]
607    fn stress_trap_near_capacity() {
608        init_for_tests();
609        let n = CAPTURE_CAPACITY - 1;
610        let ips: Vec<usize> = (0..n).map(|i| 0xB000 + i).collect();
611        let (_chain, start_fp) = boxed_frame_chain(&ips);
612
613        let bt = Backtrace::capture_trap(start_fp, 0xC000, 0);
614        let Inner::Captured(frames) = &bt.inner else {
615            panic!("expected Captured")
616        };
617        assert_eq!(frames.len(), CAPTURE_CAPACITY);
618        // Trap frame is at front with ip = 0xC000 + 1
619        assert_eq!(frames[0].ip, 0xC001);
620        // Remaining frames follow
621        for (i, f) in frames[1..].iter().enumerate() {
622            assert_eq!(f.ip, 0xB000 + i);
623        }
624    }
625
626    /// Build a chain with CAPTURE_CAPACITY frames, then capture_trap.
627    /// The trap insert_front evicts the deepest frame.
628    #[test]
629    fn stress_trap_overflow_evicts_deepest() {
630        init_for_tests();
631        let ips: Vec<usize> = (0..CAPTURE_CAPACITY).map(|i| 0xD000 + i).collect();
632        let (_chain, start_fp) = boxed_frame_chain(&ips);
633
634        let bt = Backtrace::capture_trap(start_fp, 0xE000, 0);
635        let Inner::Captured(frames) = &bt.inner else {
636            panic!("expected Captured")
637        };
638        assert_eq!(frames.len(), CAPTURE_CAPACITY);
639        // Trap frame at front
640        assert_eq!(frames[0].ip, 0xE001);
641        // The first CAPTURE_CAPACITY - 1 unwound frames remain
642        for (i, f) in frames[1..].iter().enumerate() {
643            assert_eq!(f.ip, 0xD000 + i);
644        }
645        // The deepest frame (0xD000 + CAPTURE_CAPACITY - 1) was evicted
646    }
647
648    /// Build a chain deeper than max_depth and verify truncation.
649    #[test]
650    fn stress_deep_chain_truncation() {
651        init_for_tests();
652        let ips: Vec<usize> = (0..64).map(|i| 0xF000 + i).collect();
653        let (chain, start_fp) = boxed_frame_chain(&ips);
654
655        let mut out = Vec::new();
656        unwind_core_with_max_depth(start_fp, 16, |frame| {
657            out.push(frame);
658            true
659        });
660        assert_eq!(out.len(), 16);
661        // Only the first 16 frames should be collected
662        assert_eq!(out.as_slice(), &chain[..16]);
663    }
664
665    /// Repeatedly create and drop Backtrace objects to verify no leaks or corruption.
666    #[test]
667    fn stress_repeated_create_drop() {
668        init_for_tests();
669        let (chain, start_fp) = boxed_frame_chain(&[0x100, 0x200, 0x300]);
670        for _ in 0..500 {
671            let bt = Backtrace::capture_trap(start_fp, 0x400, 0);
672            let Inner::Captured(frames) = &bt.inner else {
673                panic!("expected Captured")
674            };
675            assert!(frames.len() >= 3);
676            drop(bt);
677        }
678        // Ensure the chain memory is still valid after all iterations
679        let _ = &chain;
680    }
681
682    /// Interleave capture, Display formatting, and drop to verify no side effects.
683    #[test]
684    fn stress_interleaved_capture_format() {
685        init_for_tests();
686        let (chain, start_fp) = boxed_frame_chain(&[0x500, 0x600]);
687
688        for i in 0..100 {
689            let bt = Backtrace::capture_trap(start_fp, 0x700, 0);
690            let s = format!("{bt}");
691            // Raw block should contain the trap IP
692            assert!(
693                s.contains("0x701"),
694                "iteration {i}: missing trap IP in output"
695            );
696
697            // Human-readable formatting
698            let bt_human = Backtrace::capture_trap(start_fp, 0x700, 0);
699            let human = format!("{bt_human}");
700            assert!(!human.is_empty(), "iteration {i}: empty human output");
701
702            drop(bt);
703            drop(bt_human);
704        }
705        let _ = &chain;
706    }
707
708    /// Repeatedly clone a Backtrace and verify equality.
709    #[test]
710    fn stress_repeated_clone() {
711        init_for_tests();
712        let (chain, start_fp) = boxed_frame_chain(&[0x800, 0x900, 0xA00]);
713        let original = Backtrace::capture_trap(start_fp, 0xB00, 0);
714
715        for _ in 0..200 {
716            let cloned = original.clone();
717            assert_eq!(cloned, original);
718        }
719        let _ = &chain;
720    }
721
722    /// Verify Frame and Backtrace sizes remain stable (prevent accidental regressions).
723    #[test]
724    fn stress_size_stability() {
725        // Frame is #[repr(C)] with two usize fields
726        assert_eq!(
727            core::mem::size_of::<Frame>(),
728            2 * core::mem::size_of::<usize>()
729        );
730        assert_eq!(
731            core::mem::align_of::<Frame>(),
732            core::mem::align_of::<usize>()
733        );
734
735        // Backtrace contains Inner (discriminant + Box<[Frame]>) + Option<&'static str>
736        // Size should be stable across compilations
737        let bt_size = core::mem::size_of::<Backtrace>();
738        assert!(
739            bt_size > 0 && bt_size <= 48,
740            "Backtrace size unexpected: {bt_size}"
741        );
742
743        // CaptureBuf is stack-allocated; verify it's reasonable
744        let cap_size = core::mem::size_of::<CaptureBuf>();
745        let expected =
746            CAPTURE_CAPACITY * core::mem::size_of::<Frame>() + core::mem::size_of::<usize>();
747        assert_eq!(cap_size, expected, "CaptureBuf size mismatch");
748    }
749
750    /// Verify Frame alignment and that misaligned pointers are rejected.
751    #[test]
752    fn stress_frame_alignment() {
753        let align = core::mem::align_of::<Frame>();
754        assert!(align > 0);
755        assert!(align.is_power_of_two());
756
757        // All valid FP values must be multiples of the alignment
758        for offset in 1..align {
759            assert!(
760                Frame::read(offset).is_none(),
761                "misaligned {offset} should fail"
762            );
763        }
764        // Zero is always rejected
765        assert!(Frame::read(0).is_none());
766    }
767}