Skip to main content

crashrustler/
memory.rs

1use std::collections::HashMap;
2
3use crate::crash_rustler::{CrashRustler, mac_roman_to_char};
4use crate::types::*;
5
6impl MappedMemory {
7    /// Reads a slice of bytes from this mapped region at the given virtual address.
8    /// Returns None if the range is out of bounds.
9    pub fn read_bytes(&self, address: u64, len: usize) -> Option<&[u8]> {
10        let offset = address.checked_sub(self.base_address)? as usize;
11        let end = offset.checked_add(len)?;
12        if end > self.data.len() {
13            return None;
14        }
15        Some(&self.data[offset..end])
16    }
17
18    /// Returns true if this region contains the given address.
19    pub fn contains_address(&self, address: u64) -> bool {
20        address >= self.base_address && address < self.base_address + self.data.len() as u64
21    }
22}
23
24impl CrashRustler {
25    /// Reads a pointer-sized value from mapped process memory at the given address.
26    /// Returns 0 if the read fails (address out of range).
27    /// Equivalent to -[CrashReport _readAddressFromMemory:atAddress:]
28    pub fn read_address_from_memory(&self, memory: &MappedMemory, address: u64) -> u64 {
29        memory.read_pointer(address, self.is_64_bit).unwrap_or(0)
30    }
31
32    /// Reads a pointer-sized value from mapped memory at a symbol's address.
33    /// `symbol_address` is the start of the symbol's range (from CSSymbolGetRange).
34    /// Equivalent to -[CrashReport _readAddressFromMemory:atSymbol:]
35    pub fn read_address_from_memory_at_symbol(
36        &self,
37        memory: &MappedMemory,
38        symbol_address: u64,
39    ) -> u64 {
40        self.read_address_from_memory(memory, symbol_address)
41    }
42
43    /// Reads a null-terminated string from pre-mapped memory regions.
44    /// Searches through the provided mapped regions for the requested address.
45    /// Tries UTF-8 decoding first; falls back to MacRoman encoding.
46    /// Returns None for address 0 or if the address is not in any mapped region.
47    /// Equivalent to -[CrashReport _readStringFromMemory:atAddress:]
48    pub fn read_string_from_memory(
49        &self,
50        address: u64,
51        mapped_regions: &[MappedMemory],
52    ) -> Option<String> {
53        if address == 0 {
54            return None;
55        }
56
57        // Find the mapped region containing this address
58        let region = mapped_regions
59            .iter()
60            .find(|r| r.contains_address(address))?;
61
62        let offset = (address - region.base_address) as usize;
63        let available = &region.data[offset..];
64
65        // Find null terminator, cap at 0x4000 bytes
66        let max_len = available.len().min(0x4000);
67        let bytes = &available[..max_len];
68        let len = bytes.iter().position(|&b| b == 0).unwrap_or(max_len);
69        let bytes = &bytes[..len];
70
71        if bytes.is_empty() {
72            return None;
73        }
74
75        // Try UTF-8 first
76        if let Ok(s) = std::str::from_utf8(bytes) {
77            return Some(s.to_string());
78        }
79
80        // Fall back to MacRoman
81        Some(bytes.iter().map(|&b| mac_roman_to_char(b)).collect())
82    }
83
84    // =========================================================================
85    // Crash reporter info methods
86    // =========================================================================
87
88    /// Builds the crash reporter info status string from a dictionary of PID→status entries
89    /// and any accumulated internal errors. In CrashWrangler this updates a global C string
90    /// read by the crash reporter daemon; here it returns the composed string.
91    /// Equivalent to -[CrashReport _updateCrashReporterInfoFromCRIDict]
92    pub fn build_crash_reporter_info(
93        cri_dict: &HashMap<i32, String>,
94        cri_errors: &[String],
95    ) -> String {
96        let mut result = String::new();
97        for value in cri_dict.values() {
98            result.push_str(value);
99        }
100        if !cri_errors.is_empty() {
101            let joined = cri_errors.join("\n");
102            result.push_str(&format!("ReportCrash Internal Errors: {joined}"));
103        }
104        result
105    }
106
107    /// Appends an internal error to the crash reporter error list.
108    /// Formats as "\[executablePath\] error" or "\[notfound\] error".
109    /// Equivalent to -[CrashReport _appendCrashReporterInfoInternalError:]
110    pub fn append_crash_reporter_info_internal_error(&mut self, error: &str) {
111        let path = self.executable_path.as_deref().unwrap_or("notfound");
112        let formatted = format!("[{path}] {error}");
113        self.record_internal_error(&formatted);
114    }
115
116    /// Builds the crash reporter status string for this process.
117    /// If ppid is 0: "Analyzing process {name} ({pid}, path={path}),
118    ///   couldn't determine parent process pid"
119    /// Otherwise: "Analyzing process {name} ({pid}, path={path}),
120    ///   parent process {parentName} ({ppid}, path={parentPath})"
121    /// Equivalent to -[CrashReport _setCrashReporterInfo]
122    pub fn crash_reporter_info_string(&self) -> String {
123        let name = self.process_name.as_deref().unwrap_or("");
124        let path = self.executable_path.as_deref().unwrap_or("notfound");
125        if self.ppid == 0 {
126            format!(
127                "Analyzing process {name} ({}, path={path}), \
128                 couldn't determine parent process pid",
129                self.pid
130            )
131        } else {
132            let parent_name = self.parent_process_name.as_deref().unwrap_or("");
133            let parent_path = self.parent_executable_path.as_deref().unwrap_or("notfound");
134            format!(
135                "Analyzing process {name} ({}, path={path}), \
136                 parent process {parent_name} ({}, path={parent_path})",
137                self.pid, self.ppid
138            )
139        }
140    }
141
142    /// Appends a string to the appropriate application-specific info field.
143    /// If the process is not native (Rosetta-translated) and the source binary
144    /// is little-endian, appends to rosetta_info instead. Otherwise appends to
145    /// application_specific_info.
146    /// Equivalent to -[CrashReport _appendApplicationSpecificInfo:withSymbolOwner:]
147    pub fn append_application_specific_info(&mut self, info: &str, is_source_little_endian: bool) {
148        if info.is_empty() {
149            return;
150        }
151        let formatted = format!("{info}\n");
152        if !self.is_native && is_source_little_endian {
153            match &mut self.rosetta_info {
154                Some(existing) => existing.push_str(&formatted),
155                None => self.rosetta_info = Some(formatted),
156            }
157        } else {
158            match &mut self.application_specific_info {
159                Some(existing) => existing.push_str(&formatted),
160                None => self.application_specific_info = Some(formatted),
161            }
162        }
163    }
164
165    /// Extracts crash reporter info from the ___crashreporter_info__ symbol.
166    /// Reads the pointer at the symbol address, then reads the C string it points to
167    /// from pre-mapped memory regions.
168    /// Equivalent to -[CrashReport _extractCrashReporterInfoFromSymbolOwner:withMemory:]
169    pub fn extract_crash_reporter_info(
170        &mut self,
171        memory: &MappedMemory,
172        symbol_address: u64,
173        is_source_little_endian: bool,
174        mapped_regions: &[MappedMemory],
175    ) {
176        let ptr = self.read_address_from_memory(memory, symbol_address);
177        if ptr != 0
178            && let Some(info_string) = self.read_string_from_memory(ptr, mapped_regions)
179        {
180            self.append_application_specific_info(&info_string, is_source_little_endian);
181        }
182    }
183
184    /// Extracts crash annotations from the __DATA __crash_info section data.
185    /// Parses the crashreporter_annotations_t struct.
186    /// String pointers are resolved from pre-mapped memory regions.
187    /// Equivalent to -[CrashReport _extractCrashReporterAnnotationsFromSymbolOwner:withMemory:]
188    pub fn extract_crash_reporter_annotations(
189        &mut self,
190        crash_info_data: &[u8],
191        is_source_little_endian: bool,
192        mapped_regions: &[MappedMemory],
193    ) {
194        if crash_info_data.len() < 16 {
195            return;
196        }
197
198        let read_u64 = |offset: usize| -> u64 {
199            if offset + 8 > crash_info_data.len() {
200                return 0;
201            }
202            let bytes: [u8; 8] = crash_info_data[offset..offset + 8].try_into().unwrap();
203            u64::from_le_bytes(bytes)
204        };
205
206        let version = read_u64(0);
207        if version == 0 {
208            return;
209        }
210
211        // Field at offset 8: message pointer
212        let message_ptr = read_u64(8);
213        if message_ptr != 0
214            && let Some(msg) = self.read_string_from_memory(message_ptr, mapped_regions)
215        {
216            self.append_application_specific_info(&msg, is_source_little_endian);
217        }
218
219        // Field at offset 0x10: signature string pointer
220        let signature_ptr = read_u64(0x10);
221        if signature_ptr != 0
222            && let Some(sig) = self.read_string_from_memory(signature_ptr, mapped_regions)
223            && !sig.is_empty()
224        {
225            self.application_specific_signature_strings.push(sig);
226        }
227
228        // Field at offset 0x18: backtrace string pointer
229        let backtrace_ptr = read_u64(0x18);
230        if backtrace_ptr != 0
231            && let Some(bt) = self.read_string_from_memory(backtrace_ptr, mapped_regions)
232            && !bt.is_empty()
233        {
234            self.application_specific_backtraces.push(bt);
235        }
236
237        // Version 2+: message2 at offset 0x20
238        if version >= 2 {
239            let message2_ptr = read_u64(0x20);
240            if message2_ptr != 0
241                && let Some(msg2) = self.read_string_from_memory(message2_ptr, mapped_regions)
242            {
243                self.append_application_specific_info(&msg2, is_source_little_endian);
244            }
245        }
246
247        // Version 3+: abort cause thread_id at offset 0x28
248        if version >= 3 {
249            let thread_id = read_u64(0x28);
250            if thread_id != 0 {
251                self.thread_id = Some(thread_id);
252            }
253        }
254
255        // Version 4+: dialog mode at offset 0x30
256        if version >= 4 {
257            let dialog_mode_ptr = read_u64(0x30);
258            if dialog_mode_ptr != 0
259                && let Some(mode) = self.read_string_from_memory(dialog_mode_ptr, mapped_regions)
260            {
261                self.application_specific_dialog_mode = Some(mode);
262            }
263        }
264    }
265
266    /// Extracts binary image hints from the ___crashreporter_binary_image_hints__ symbol.
267    /// Reads the pointer at the symbol, reads the plist string it points to from
268    /// pre-mapped memory regions, and stores it in binary_image_hints.
269    /// Equivalent to -[CrashReport _extractCrashReporterBinaryImageHintsFromSymbolOwner:withMemory:]
270    pub fn extract_crash_reporter_binary_image_hints(
271        &mut self,
272        memory: &MappedMemory,
273        symbol_address: u64,
274        mapped_regions: &[MappedMemory],
275    ) {
276        let ptr = self.read_address_from_memory(memory, symbol_address);
277        if ptr == 0 {
278            return;
279        }
280        if let Some(plist_string) = self.read_string_from_memory(ptr, mapped_regions)
281            && !plist_string.is_empty()
282        {
283            self.binary_image_hints.push(plist_string);
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use crate::crash_rustler::CrashRustler;
291    use crate::test_helpers::*;
292    use std::collections::HashMap;
293
294    // =========================================================================
295    // 7. record_error — Internal error recording
296    // =========================================================================
297    mod record_error {
298        use super::*;
299
300        #[test]
301        fn record_internal_error_first_creates() {
302            let mut cr = make_test_cr();
303            cr.record_internal_error("first error");
304            assert_eq!(cr.internal_error, Some("first error".into()));
305        }
306
307        #[test]
308        fn record_internal_error_appends_with_newline() {
309            let mut cr = make_test_cr();
310            cr.record_internal_error("first");
311            cr.record_internal_error("second");
312            assert_eq!(cr.internal_error, Some("first\nsecond".into()));
313        }
314
315        #[test]
316        fn append_crash_reporter_info_internal_error_with_path() {
317            let mut cr = make_test_cr();
318            cr.append_crash_reporter_info_internal_error("some error");
319            let err = cr.internal_error.as_ref().unwrap();
320            assert!(err.starts_with("[/Applications/TestApp.app/Contents/MacOS/TestApp]"));
321            assert!(err.contains("some error"));
322        }
323
324        #[test]
325        fn append_crash_reporter_info_internal_error_no_path() {
326            let mut cr = make_test_cr();
327            cr.executable_path = None;
328            cr.append_crash_reporter_info_internal_error("some error");
329            let err = cr.internal_error.as_ref().unwrap();
330            assert!(err.starts_with("[notfound]"));
331        }
332
333        #[test]
334        fn crash_reporter_info_string_ppid_zero() {
335            let mut cr = make_test_cr();
336            cr.ppid = 0;
337            let info = cr.crash_reporter_info_string();
338            assert!(info.contains("couldn't determine parent process pid"));
339            assert!(info.contains("TestApp"));
340            assert!(info.contains("1234"));
341        }
342    }
343
344    // =========================================================================
345    // 8. crash_reporter_info — Info building
346    // =========================================================================
347    mod crash_reporter_info {
348        use super::*;
349
350        #[test]
351        fn build_crash_reporter_info_empty() {
352            let dict = HashMap::new();
353            let errors: Vec<String> = vec![];
354            assert_eq!(CrashRustler::build_crash_reporter_info(&dict, &errors), "");
355        }
356
357        #[test]
358        fn build_crash_reporter_info_with_entries() {
359            let mut dict = HashMap::new();
360            dict.insert(42, "status for 42".to_string());
361            let errors: Vec<String> = vec![];
362            let result = CrashRustler::build_crash_reporter_info(&dict, &errors);
363            assert_eq!(result, "status for 42");
364        }
365
366        #[test]
367        fn build_crash_reporter_info_with_errors() {
368            let dict = HashMap::new();
369            let errors = vec!["err1".to_string(), "err2".to_string()];
370            let result = CrashRustler::build_crash_reporter_info(&dict, &errors);
371            assert!(result.contains("ReportCrash Internal Errors:"));
372            assert!(result.contains("err1"));
373            assert!(result.contains("err2"));
374        }
375
376        #[test]
377        fn crash_reporter_info_string_with_parent() {
378            let cr = make_test_cr();
379            let info = cr.crash_reporter_info_string();
380            assert!(info.contains("parent process launchd"));
381            assert!(info.contains("1234"));
382            assert!(info.contains(&cr.ppid.to_string()));
383        }
384
385        #[test]
386        fn append_application_specific_info_empty_noop() {
387            let mut cr = make_test_cr();
388            cr.append_application_specific_info("", true);
389            assert!(cr.application_specific_info.is_none());
390            assert!(cr.rosetta_info.is_none());
391        }
392
393        #[test]
394        fn append_application_specific_info_native_little_endian_to_rosetta() {
395            let mut cr = make_test_cr();
396            cr.is_native = false; // translated
397            cr.append_application_specific_info("rosetta data", true);
398            assert!(cr.rosetta_info.is_some());
399            assert!(cr.rosetta_info.as_ref().unwrap().contains("rosetta data"));
400            assert!(cr.application_specific_info.is_none());
401        }
402
403        #[test]
404        fn append_application_specific_info_native_to_app_info() {
405            let mut cr = make_test_cr();
406            cr.is_native = true;
407            cr.append_application_specific_info("app data", true);
408            assert!(cr.application_specific_info.is_some());
409            assert!(
410                cr.application_specific_info
411                    .as_ref()
412                    .unwrap()
413                    .contains("app data")
414            );
415        }
416
417        #[test]
418        fn append_application_specific_info_appends_to_existing() {
419            let mut cr = make_test_cr();
420            cr.application_specific_info = Some("existing\n".into());
421            cr.append_application_specific_info("more data", true);
422            let info = cr.application_specific_info.as_ref().unwrap();
423            assert!(info.contains("existing"));
424            assert!(info.contains("more data"));
425        }
426    }
427}