Skip to main content

crashrustler/unwind/
mod.rs

1//! Stack unwinding infrastructure for macOS crash reports.
2//!
3//! Provides DWARF CFI, Apple Compact Unwind, and frame pointer walking
4//! to generate accurate thread backtraces. All unwinding algorithms live
5//! in the library crate — no FFI. A [`MemoryReader`] trait abstracts
6//! memory access so the binary crate can provide `mach_vm_read()`.
7
8pub mod arch;
9pub mod compact_unwind;
10pub mod cursor;
11pub mod dwarf_cfi;
12pub mod dwarf_expr;
13pub mod frame_pointer;
14pub mod macho;
15pub mod registers;
16
17use crate::types::{CpuType, ThreadState};
18use registers::RegisterContext;
19
20/// Errors that can occur during stack unwinding.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum UnwindError {
23    /// Memory read failed at the given address.
24    MemoryReadFailed(u64),
25    /// No unwind info found for the given PC.
26    NoUnwindInfo(u64),
27    /// Invalid DWARF CFI data.
28    InvalidDwarf(String),
29    /// Invalid compact unwind encoding.
30    InvalidCompactUnwind(u32),
31    /// Maximum unwind depth exceeded.
32    MaxDepthExceeded(u32),
33    /// PC landed in null page (normal termination).
34    NullPC,
35    /// Frame pointer chain broken.
36    BrokenFrameChain,
37}
38
39/// Trait for reading memory from a target process.
40///
41/// The library crate contains all unwinding algorithms but needs to read
42/// target process memory. The binary crate implements this trait using
43/// `mach_vm_read()`.
44pub trait MemoryReader {
45    /// Reads `size` bytes from the given virtual address.
46    /// Returns `None` if the read fails.
47    fn read_memory(&self, address: u64, size: usize) -> Option<Vec<u8>>;
48
49    /// Reads a single byte.
50    fn read_u8(&self, address: u64) -> Option<u8> {
51        self.read_memory(address, 1).map(|b| b[0])
52    }
53
54    /// Reads a little-endian u16.
55    fn read_u16(&self, address: u64) -> Option<u16> {
56        let b = self.read_memory(address, 2)?;
57        Some(u16::from_le_bytes([b[0], b[1]]))
58    }
59
60    /// Reads a little-endian u32.
61    fn read_u32(&self, address: u64) -> Option<u32> {
62        let b = self.read_memory(address, 4)?;
63        Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
64    }
65
66    /// Reads a little-endian u64.
67    fn read_u64(&self, address: u64) -> Option<u64> {
68        let b = self.read_memory(address, 8)?;
69        Some(u64::from_le_bytes([
70            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
71        ]))
72    }
73
74    /// Reads a little-endian i32.
75    fn read_i32(&self, address: u64) -> Option<i32> {
76        self.read_u32(address).map(|v| v as i32)
77    }
78
79    /// Reads a little-endian i64.
80    fn read_i64(&self, address: u64) -> Option<i64> {
81        self.read_u64(address).map(|v| v as i64)
82    }
83
84    /// Reads a pointer-sized value (4 or 8 bytes depending on `is_64_bit`).
85    fn read_pointer(&self, address: u64, is_64_bit: bool) -> Option<u64> {
86        if is_64_bit {
87            self.read_u64(address)
88        } else {
89            self.read_u32(address).map(|v| v as u64)
90        }
91    }
92}
93
94/// Location of a Mach-O section in virtual memory.
95#[derive(Debug, Clone, Copy)]
96pub struct SectionRef {
97    /// Virtual address of the section.
98    pub vm_addr: u64,
99    /// Size of the section in bytes.
100    pub size: u64,
101}
102
103/// Information about a loaded binary image, with cached section locations.
104#[derive(Debug, Clone)]
105pub struct BinaryImageInfo {
106    /// Short name or path of the binary.
107    pub name: String,
108    /// Load address (start of __TEXT segment) in virtual memory.
109    pub load_address: u64,
110    /// End address (exclusive) in virtual memory.
111    pub end_address: u64,
112    /// Whether this is a 64-bit binary.
113    pub is_64_bit: bool,
114    /// UUID of the binary (for symbolication).
115    pub uuid: Option<[u8; 16]>,
116    /// Cached location of __TEXT,__unwind_info.
117    pub unwind_info: Option<SectionRef>,
118    /// Cached location of __TEXT,__eh_frame.
119    pub eh_frame: Option<SectionRef>,
120    /// Cached location of __TEXT,__text.
121    pub text_section: Option<SectionRef>,
122    /// Whether section locations have been resolved.
123    pub sections_resolved: bool,
124}
125
126impl BinaryImageInfo {
127    /// Returns true if the given address falls within this image.
128    pub fn contains(&self, address: u64) -> bool {
129        address >= self.load_address && address < self.end_address
130    }
131
132    /// Resolves section locations from the Mach-O header using the given reader.
133    pub fn resolve_sections(&mut self, reader: &dyn MemoryReader) {
134        if self.sections_resolved {
135            return;
136        }
137        self.sections_resolved = true;
138
139        let sections = macho::find_sections(reader, self.load_address, self.is_64_bit);
140        self.unwind_info = sections.unwind_info;
141        self.eh_frame = sections.eh_frame;
142        self.text_section = sections.text;
143    }
144}
145
146/// Unwinds a single thread, producing a list of (PC, register context) pairs
147/// from top of stack to bottom.
148///
149/// # Arguments
150/// * `reader` — Memory reader for the target process.
151/// * `initial_state` — Register state of the thread.
152/// * `cpu_type` — CPU type of the process.
153/// * `images` — Binary images loaded in the process (sections resolved lazily).
154pub fn unwind_thread(
155    reader: &dyn MemoryReader,
156    initial_state: &ThreadState,
157    cpu_type: CpuType,
158    images: &mut [BinaryImageInfo],
159) -> Vec<(u64, RegisterContext)> {
160    let is_64_bit = cpu_type.is_64_bit();
161    let Some(regs) = RegisterContext::from_thread_state(initial_state, cpu_type) else {
162        return Vec::new();
163    };
164
165    let mut frame_cursor = cursor::FrameCursor::new(reader, regs, images, is_64_bit);
166    let mut frames = Vec::new();
167
168    // Record initial frame
169    if let Some(pc) = frame_cursor.pc() {
170        frames.push((pc, frame_cursor.registers().clone()));
171    }
172
173    // Step through frames
174    while let Ok(true) = frame_cursor.step() {
175        if let Some(pc) = frame_cursor.pc() {
176            frames.push((pc, frame_cursor.registers().clone()));
177        } else {
178            break;
179        }
180    }
181
182    frames
183}
184
185/// MemoryReader implementation for MappedMemory (for testing).
186impl MemoryReader for crate::MappedMemory {
187    fn read_memory(&self, address: u64, size: usize) -> Option<Vec<u8>> {
188        let offset = address.checked_sub(self.base_address)? as usize;
189        if offset + size > self.data.len() {
190            return None;
191        }
192        Some(self.data[offset..offset + size].to_vec())
193    }
194}
195
196/// A simple in-memory reader backed by a byte buffer at a given base address.
197/// Useful for tests.
198#[derive(Debug, Clone)]
199pub struct SliceMemoryReader {
200    /// Raw bytes.
201    pub data: Vec<u8>,
202    /// Virtual address of `data[0]`.
203    pub base_address: u64,
204}
205
206impl MemoryReader for SliceMemoryReader {
207    fn read_memory(&self, address: u64, size: usize) -> Option<Vec<u8>> {
208        let offset = address.checked_sub(self.base_address)? as usize;
209        if offset + size > self.data.len() {
210            return None;
211        }
212        Some(self.data[offset..offset + size].to_vec())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn slice_memory_reader_basic() {
222        let reader = SliceMemoryReader {
223            data: vec![0x78, 0x56, 0x34, 0x12, 0xEF, 0xBE, 0xAD, 0xDE],
224            base_address: 0x1000,
225        };
226        assert_eq!(reader.read_u32(0x1000), Some(0x12345678));
227        assert_eq!(reader.read_u64(0x1000), Some(0xDEADBEEF_12345678));
228        assert_eq!(reader.read_u8(0x1000), Some(0x78));
229        assert_eq!(reader.read_u16(0x1000), Some(0x5678));
230    }
231
232    #[test]
233    fn slice_memory_reader_out_of_bounds() {
234        let reader = SliceMemoryReader {
235            data: vec![0u8; 4],
236            base_address: 0x1000,
237        };
238        assert!(reader.read_u64(0x1000).is_none());
239        assert!(reader.read_u32(0x1001).is_none());
240        assert!(reader.read_memory(0x0FFF, 1).is_none());
241    }
242
243    #[test]
244    fn slice_memory_reader_pointer() {
245        let reader = SliceMemoryReader {
246            data: 0xDEAD_BEEF_CAFE_BABEu64.to_le_bytes().to_vec(),
247            base_address: 0x2000,
248        };
249        assert_eq!(
250            reader.read_pointer(0x2000, true),
251            Some(0xDEAD_BEEF_CAFE_BABE)
252        );
253        assert_eq!(reader.read_pointer(0x2000, false), Some(0xCAFE_BABE));
254    }
255}