Skip to main content

crashrustler/unwind/
cursor.rs

1//! Frame cursor: drives the compact → DWARF → FP fallback chain.
2//!
3//! Orchestrates the unwinding process for each frame, trying compact unwind
4//! first, then DWARF CFI, then frame pointer walking as a last resort.
5
6use super::compact_unwind::{self, CompactEntry};
7use super::dwarf_cfi;
8use super::frame_pointer;
9use super::registers::RegisterContext;
10use super::{BinaryImageInfo, MemoryReader, UnwindError};
11
12/// Maximum unwind depth to prevent infinite loops.
13const DEFAULT_MAX_DEPTH: u32 = 512;
14
15/// ARM64 PAC mask: strip pointer authentication bits (39-bit VA space).
16const ARM64_PAC_MASK: u64 = 0x0000_007F_FFFF_FFFF;
17
18/// Frame cursor that walks the stack frame by frame.
19pub struct FrameCursor<'a> {
20    reader: &'a dyn MemoryReader,
21    regs: RegisterContext,
22    images: &'a mut [BinaryImageInfo],
23    is_64_bit: bool,
24    depth: u32,
25    max_depth: u32,
26}
27
28impl<'a> FrameCursor<'a> {
29    /// Creates a new frame cursor.
30    pub fn new(
31        reader: &'a dyn MemoryReader,
32        regs: RegisterContext,
33        images: &'a mut [BinaryImageInfo],
34        is_64_bit: bool,
35    ) -> Self {
36        Self {
37            reader,
38            regs,
39            images,
40            is_64_bit,
41            depth: 0,
42            max_depth: DEFAULT_MAX_DEPTH,
43        }
44    }
45
46    /// Returns the current PC.
47    pub fn pc(&self) -> Option<u64> {
48        self.regs.pc()
49    }
50
51    /// Returns the current register context.
52    pub fn registers(&self) -> &RegisterContext {
53        &self.regs
54    }
55
56    /// Steps one frame. Returns `Ok(true)` if a frame was found,
57    /// `Ok(false)` if at the bottom of the stack.
58    pub fn step(&mut self) -> Result<bool, UnwindError> {
59        self.depth += 1;
60        if self.depth >= self.max_depth {
61            return Err(UnwindError::MaxDepthExceeded(self.max_depth));
62        }
63
64        let pc = self.regs.pc().ok_or(UnwindError::NullPC)?;
65
66        // Strip PAC bits on ARM64 targets (regardless of host architecture)
67        let pc = if self.regs.cpu_type == crate::types::CpuType::ARM64 {
68            pc & ARM64_PAC_MASK
69        } else {
70            pc
71        };
72
73        // Null PC = bottom of stack
74        if pc == 0 || (self.is_64_bit && pc < 0x1000) || (!self.is_64_bit && pc < 0x100) {
75            return Ok(false);
76        }
77
78        // Find containing binary image
79        let image_idx = self.find_image(pc);
80
81        if let Some(idx) = image_idx {
82            // Ensure sections are resolved
83            self.images[idx].resolve_sections(self.reader);
84            let image = &self.images[idx];
85
86            // Try compact unwind first
87            if let Some(ref unwind_info) = image.unwind_info
88                && let Some((encoding, func_base)) = compact_unwind::lookup_encoding(
89                    self.reader,
90                    unwind_info,
91                    pc,
92                    image.load_address,
93                )
94            {
95                let entry = compact_unwind::decode_encoding(encoding, self.regs.cpu_type);
96
97                match &entry {
98                    CompactEntry::Dwarf { fde_offset } => {
99                        // Compact says use DWARF
100                        if let Some(ref eh_frame) = self.images[idx].eh_frame {
101                            let target_addr = eh_frame.vm_addr + *fde_offset as u64;
102                            if let Some(fde) = dwarf_cfi::find_fde(
103                                self.reader,
104                                eh_frame,
105                                target_addr,
106                                self.is_64_bit,
107                            ) {
108                                let new_regs = dwarf_cfi::apply_dwarf_unwind(
109                                    &fde,
110                                    pc,
111                                    &self.regs,
112                                    self.reader,
113                                    self.is_64_bit,
114                                )?;
115                                self.regs = new_regs;
116                                return Ok(true);
117                            }
118                        }
119                        // DWARF lookup failed, try FP
120                    }
121                    CompactEntry::None => {
122                        // No unwind info, try DWARF then FP
123                    }
124                    _ => {
125                        // Apply compact unwind with actual function start address
126                        let func_start = self.images[idx].load_address + func_base as u64;
127                        if compact_unwind::apply_entry(
128                            &entry,
129                            &mut self.regs,
130                            self.reader,
131                            func_start,
132                            self.is_64_bit,
133                        )? {
134                            return Ok(true);
135                        }
136                    }
137                }
138            }
139
140            // Try DWARF .eh_frame
141            if let Some(ref eh_frame) = self.images[idx].eh_frame
142                && let Some(fde) = dwarf_cfi::find_fde(self.reader, eh_frame, pc, self.is_64_bit)
143            {
144                let result = dwarf_cfi::apply_dwarf_unwind(
145                    &fde,
146                    pc,
147                    &self.regs,
148                    self.reader,
149                    self.is_64_bit,
150                );
151                if let Ok(new_regs) = result {
152                    self.regs = new_regs;
153                    return Ok(true);
154                }
155                // DWARF failed, fall through to FP
156            }
157        }
158
159        // Fallback: frame pointer walking
160        frame_pointer::step_frame_pointer(self.reader, &mut self.regs, self.is_64_bit)
161    }
162
163    fn find_image(&self, pc: u64) -> Option<usize> {
164        self.images.iter().position(|img| img.contains(pc))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::types::CpuType;
172    use crate::unwind::SliceMemoryReader;
173
174    /// Builds a test setup with a synthetic FP chain (no compact unwind or DWARF).
175    fn build_fp_chain_test() -> (SliceMemoryReader, RegisterContext, Vec<BinaryImageInfo>) {
176        let base = 0x1_0000_0000u64;
177        let mut data = vec![0u8; 0x10000];
178
179        // Frame 0 at FP=base+0x8000
180        let f0 = 0x8000usize;
181        data[f0..f0 + 8].copy_from_slice(&(base + 0x9000u64).to_le_bytes()); // prev FP
182        data[f0 + 8..f0 + 16].copy_from_slice(&(base + 0x2000u64).to_le_bytes()); // return addr
183
184        // Frame 1 at FP=base+0x9000
185        let f1 = 0x9000usize;
186        data[f1..f1 + 8].copy_from_slice(&(base + 0xA000u64).to_le_bytes()); // prev FP
187        data[f1 + 8..f1 + 16].copy_from_slice(&(base + 0x3000u64).to_le_bytes()); // return addr
188
189        // Frame 2 at FP=base+0xA000 (bottom)
190        let f2 = 0xA000usize;
191        data[f2..f2 + 8].copy_from_slice(&0u64.to_le_bytes()); // null FP = bottom
192        data[f2 + 8..f2 + 16].copy_from_slice(&(base + 0x4000u64).to_le_bytes());
193
194        let reader = SliceMemoryReader {
195            data,
196            base_address: base,
197        };
198
199        let mut regs = RegisterContext::new(CpuType::ARM64);
200        regs.set_fp(base + f0 as u64);
201        regs.set_sp(base + f0 as u64 - 16);
202        regs.set_pc(base + 0x1000); // initial PC
203
204        let images = vec![BinaryImageInfo {
205            name: "test".into(),
206            load_address: base,
207            end_address: base + 0x10000,
208            is_64_bit: true,
209            uuid: None,
210            unwind_info: None,
211            eh_frame: None,
212            text_section: None,
213            sections_resolved: true,
214        }];
215
216        (reader, regs, images)
217    }
218
219    #[test]
220    fn fp_fallback_walk() {
221        let (reader, regs, mut images) = build_fp_chain_test();
222        let base = 0x1_0000_0000u64;
223
224        let mut cursor = FrameCursor::new(&reader, regs, &mut images, true);
225
226        // Initial PC
227        assert_eq!(cursor.pc(), Some(base + 0x1000));
228
229        // Step 1
230        assert!(cursor.step().unwrap());
231        assert_eq!(cursor.pc(), Some(base + 0x2000));
232
233        // Step 2
234        assert!(cursor.step().unwrap());
235        assert_eq!(cursor.pc(), Some(base + 0x3000));
236
237        // Step 3 - reads prev_fp=0 and return_addr, sets FP=0
238        assert!(cursor.step().unwrap());
239        assert_eq!(cursor.pc(), Some(base + 0x4000));
240
241        // Step 4 - FP=0, bottom of stack
242        assert!(!cursor.step().unwrap());
243    }
244
245    #[test]
246    fn max_depth_exceeded() {
247        let (reader, regs, mut images) = build_fp_chain_test();
248
249        let mut cursor = FrameCursor::new(&reader, regs, &mut images, true);
250        cursor.max_depth = 2;
251
252        // Step 1 OK
253        assert!(cursor.step().unwrap());
254        // Step 2 hits max depth
255        assert!(matches!(
256            cursor.step(),
257            Err(UnwindError::MaxDepthExceeded(2))
258        ));
259    }
260
261    #[test]
262    fn null_pc_stops() {
263        let reader = SliceMemoryReader {
264            data: vec![0u8; 0x100],
265            base_address: 0x1000,
266        };
267
268        let mut regs = RegisterContext::new(CpuType::ARM64);
269        regs.set_pc(0); // null PC
270        regs.set_fp(0x1080);
271        regs.set_sp(0x1070);
272
273        let mut images: Vec<BinaryImageInfo> = vec![];
274        let mut cursor = FrameCursor::new(&reader, regs, &mut images, true);
275
276        assert!(!cursor.step().unwrap());
277    }
278}