Skip to main content

crashrustler/
types.rs

1use std::collections::{BTreeMap, HashMap};
2
3/// Exploitability classification of a crash.
4/// Matches CrashWrangler's exit code semantics:
5/// signal = not exploitable, signal+100 = exploitable.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ExploitabilityRating {
8    /// Crash is not exploitable (exit code = signal number).
9    NotExploitable,
10    /// Crash is exploitable (exit code = signal + 100).
11    Exploitable,
12    /// Exploitability could not be determined.
13    Unknown,
14}
15
16/// Memory access type for the crashing instruction.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AccessType {
19    /// Reading from memory.
20    Read,
21    /// Writing to memory.
22    Write,
23    /// Executing code.
24    Exec,
25    /// Stack recursion (>300 frames).
26    Recursion,
27    /// Access type could not be determined.
28    Unknown,
29}
30
31/// Pre-gathered crash data passed to `CrashRustler::new()`.
32/// The binary (exc_handler) is responsible for all Mach/system calls;
33/// this struct carries the results to the library's pure-data constructor.
34#[derive(Debug, Clone)]
35pub struct CrashParams {
36    /// Mach task port for the crashed process.
37    pub task: u32,
38    /// Process ID.
39    pub pid: i32,
40    /// Parent process ID.
41    pub ppid: i32,
42    /// User ID.
43    pub uid: u32,
44    /// Whether the process is 64-bit.
45    pub is_64_bit: bool,
46    /// Mach thread port of the crashing thread.
47    pub thread: u32,
48    /// Mach exception type (raw i32).
49    pub exception_type: i32,
50    /// Mach exception codes.
51    pub exception_codes: Vec<i64>,
52    /// Thread state (register values + flavor).
53    pub thread_state: ThreadState,
54    /// Exception state from the faulting thread.
55    pub exception_state: ExceptionState,
56    /// Process name (from proc_pidpath last component).
57    pub process_name: Option<String>,
58    /// Full executable path.
59    pub executable_path: Option<String>,
60    /// Responsible process PID.
61    pub r_process_pid: i32,
62    /// Crash date as a formatted string.
63    pub date: Option<String>,
64    /// System uptime (awake time) at crash, in seconds.
65    pub awake_system_uptime: u64,
66    /// CPU type of the crashed process.
67    pub cpu_type: CpuType,
68}
69
70/// Mach exception types mirroring mach/exception_types.h
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[repr(i32)]
73pub enum ExceptionType {
74    BadAccess = 1,
75    BadInstruction = 2,
76    Arithmetic = 3,
77    Emulation = 4,
78    Software = 5,
79    Breakpoint = 6,
80    Syscall = 7,
81    MachSyscall = 8,
82    RpcAlert = 9,
83    Crash = 10,
84    Resource = 11,
85    Guard = 12,
86    CorpseNotify = 13,
87}
88
89impl ExceptionType {
90    /// Converts a raw `i32` value to an `ExceptionType`, if valid.
91    ///
92    /// Returns `None` for values outside the range 1..=13.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use crashrustler::ExceptionType;
98    ///
99    /// let exc = ExceptionType::from_raw(1).unwrap();
100    /// assert_eq!(exc, ExceptionType::BadAccess);
101    /// assert_eq!(exc.raw(), 1);
102    ///
103    /// assert!(ExceptionType::from_raw(0).is_none());
104    /// assert!(ExceptionType::from_raw(99).is_none());
105    /// ```
106    pub fn from_raw(val: i32) -> Option<Self> {
107        match val {
108            1 => Some(Self::BadAccess),
109            2 => Some(Self::BadInstruction),
110            3 => Some(Self::Arithmetic),
111            4 => Some(Self::Emulation),
112            5 => Some(Self::Software),
113            6 => Some(Self::Breakpoint),
114            7 => Some(Self::Syscall),
115            8 => Some(Self::MachSyscall),
116            9 => Some(Self::RpcAlert),
117            10 => Some(Self::Crash),
118            11 => Some(Self::Resource),
119            12 => Some(Self::Guard),
120            13 => Some(Self::CorpseNotify),
121            _ => None,
122        }
123    }
124
125    /// Returns the raw `i32` value of this exception type.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use crashrustler::ExceptionType;
131    ///
132    /// assert_eq!(ExceptionType::BadAccess.raw(), 1);
133    /// assert_eq!(ExceptionType::Breakpoint.raw(), 6);
134    /// ```
135    pub fn raw(&self) -> i32 {
136        *self as i32
137    }
138}
139
140/// CPU type constants mirroring mach/machine.h
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct CpuType(pub i32);
143
144impl CpuType {
145    /// 32-bit Intel x86.
146    pub const X86: Self = Self(7);
147    /// 64-bit Intel x86_64.
148    pub const X86_64: Self = Self(0x0100_0007);
149    /// 32-bit ARM.
150    pub const ARM: Self = Self(12);
151    /// 64-bit ARM (Apple Silicon).
152    pub const ARM64: Self = Self(0x0100_000c);
153    /// 32-bit PowerPC.
154    pub const POWERPC: Self = Self(18);
155    /// 64-bit PowerPC.
156    pub const POWERPC64: Self = Self(0x0100_0012);
157
158    /// Returns `true` if this CPU type has the 64-bit flag set (bit 24).
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use crashrustler::CpuType;
164    ///
165    /// assert!(CpuType::X86_64.is_64_bit());
166    /// assert!(CpuType::ARM64.is_64_bit());
167    /// assert!(!CpuType::X86.is_64_bit());
168    /// assert!(!CpuType::ARM.is_64_bit());
169    /// ```
170    pub fn is_64_bit(&self) -> bool {
171        self.0 & 0x0100_0000 != 0
172    }
173
174    /// Returns a new `CpuType` with the 64-bit flag (bit 24) set.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use crashrustler::CpuType;
180    ///
181    /// let cpu64 = CpuType::X86.with_64_bit();
182    /// assert_eq!(cpu64, CpuType::X86_64);
183    /// assert!(cpu64.is_64_bit());
184    /// ```
185    pub fn with_64_bit(self) -> Self {
186        Self(self.0 | 0x0100_0000)
187    }
188}
189
190/// Represents a binary image loaded in the crashed process.
191#[derive(Debug, Clone)]
192pub struct BinaryImage {
193    /// Short name of the binary (e.g., `"libsystem_kernel.dylib"`).
194    pub name: String,
195    /// Full filesystem path to the binary image.
196    pub path: String,
197    /// UUID of the binary image for symbolication.
198    pub uuid: Option<String>,
199    /// Load address (start) of the binary image in virtual memory.
200    pub base_address: u64,
201    /// End address (exclusive) of the binary image in virtual memory.
202    pub end_address: u64,
203    /// Architecture string (e.g., `"x86_64"`, `"arm64"`).
204    pub arch: Option<String>,
205    /// Bundle identifier or derived name used for crash report formatting.
206    pub identifier: Option<String>,
207    /// Bundle version string (CFBundleVersion).
208    pub version: Option<String>,
209}
210
211/// Represents a single frame in a backtrace.
212#[derive(Debug, Clone)]
213pub struct BacktraceFrame {
214    /// Zero-based index of this frame in the backtrace.
215    pub frame_number: u32,
216    /// Name of the binary image containing this frame's address.
217    pub image_name: String,
218    /// Virtual memory address of this frame.
219    pub address: u64,
220    /// Symbolicated function name, if available.
221    pub symbol_name: Option<String>,
222    /// Byte offset from the start of the symbol.
223    pub symbol_offset: u64,
224    /// Source file path, if debug info is available.
225    pub source_file: Option<String>,
226    /// Source line number, if debug info is available.
227    pub source_line: Option<u32>,
228}
229
230/// Represents the backtrace of a single thread.
231#[derive(Debug, Clone)]
232pub struct ThreadBacktrace {
233    /// Index of this thread in the process.
234    pub thread_number: u32,
235    /// Dispatch queue name or pthread name, if set.
236    pub thread_name: Option<String>,
237    /// Mach thread ID (`thread_identifier_info`).
238    pub thread_id: Option<u64>,
239    /// Whether this thread triggered the crash.
240    pub is_crashed: bool,
241    /// Stack frames for this thread, ordered from top (most recent) to bottom.
242    pub frames: Vec<BacktraceFrame>,
243}
244
245/// Thread state register values.
246#[derive(Debug, Clone)]
247pub struct ThreadState {
248    /// Mach thread state flavor (e.g., `x86_THREAD_STATE = 7`, `ARM_THREAD_STATE64 = 6`).
249    pub flavor: u32,
250    /// Raw register values as 32-bit words. 64-bit registers span two consecutive words.
251    pub registers: Vec<u32>,
252}
253
254/// Exception state from the faulting thread.
255#[derive(Debug, Clone)]
256pub struct ExceptionState {
257    /// Raw exception state register values (e.g., `trapno`, `cpu`, `err`, `faultvaddr`).
258    pub state: Vec<u32>,
259    /// Number of valid words in `state`.
260    pub count: u32,
261}
262
263/// VM region information for the crash report.
264#[derive(Debug, Clone)]
265pub struct VmRegion {
266    /// Start address of the VM region.
267    pub address: u64,
268    /// Size of the VM region in bytes.
269    pub size: u64,
270    /// User tag name for the region (e.g., `"MALLOC_TINY"`, `"__TEXT"`).
271    pub name: Option<String>,
272    /// Protection flags as a human-readable string (e.g., `"r-x/rwx"`).
273    pub protection: String,
274}
275
276/// External modification information.
277#[derive(Debug, Clone)]
278pub struct ExternalModInfo {
279    /// Human-readable description of external modifications.
280    pub description: Option<String>,
281    /// Warning messages about external modifications.
282    pub warnings: Option<String>,
283    /// Key-value pairs of external modification metadata.
284    pub dictionary: HashMap<String, String>,
285}
286
287/// Work queue limits extracted from the process.
288#[derive(Debug, Clone)]
289pub struct WorkQueueLimits {
290    /// Constrained thread limit (`kern.wq_max_constrained_threads`).
291    pub min_threads: Option<u32>,
292    /// Total thread limit (`kern.wq_max_threads`).
293    pub max_threads: Option<u32>,
294}
295
296/// Heterogeneous plist value type for dictionary/plist output methods.
297/// Mirrors the NSObject types used in CrashReport's NSDictionary outputs.
298#[derive(Debug, Clone)]
299pub enum PlistValue {
300    /// A string value.
301    String(String),
302    /// A signed 64-bit integer value.
303    Int(i64),
304    /// A boolean value.
305    Bool(bool),
306    /// An array of dictionaries (maps to NSArray of NSDictionary).
307    Array(Vec<BTreeMap<String, PlistValue>>),
308    /// A nested dictionary (maps to NSDictionary).
309    Dict(BTreeMap<String, PlistValue>),
310}
311
312/// Represents a mapped region of process memory for pointer reads.
313/// Used by `_readAddressFromMemory:atAddress:` to read pointer-sized values
314/// from a pre-mapped buffer without additional Mach VM calls.
315#[derive(Debug, Clone)]
316pub struct MappedMemory {
317    /// Raw bytes of the mapped memory region.
318    pub data: Vec<u8>,
319    /// Virtual address corresponding to `data[0]`.
320    pub base_address: u64,
321}
322
323impl MappedMemory {
324    /// Reads a pointer-sized value from the mapped memory at the given virtual address.
325    /// Reads 8 bytes for 64-bit processes, 4 bytes for 32-bit.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use crashrustler::MappedMemory;
331    ///
332    /// let mem = MappedMemory {
333    ///     data: vec![0x78, 0x56, 0x34, 0x12, 0xEF, 0xBE, 0xAD, 0xDE],
334    ///     base_address: 0x1000,
335    /// };
336    ///
337    /// // 64-bit read: all 8 bytes as little-endian u64
338    /// assert_eq!(mem.read_pointer(0x1000, true), Some(0xDEADBEEF_12345678));
339    ///
340    /// // 32-bit read: first 4 bytes as little-endian u32
341    /// assert_eq!(mem.read_pointer(0x1000, false), Some(0x12345678));
342    ///
343    /// // Out of range
344    /// assert_eq!(mem.read_pointer(0x2000, true), None);
345    /// ```
346    pub fn read_pointer(&self, address: u64, is_64_bit: bool) -> Option<u64> {
347        let offset = address.checked_sub(self.base_address)? as usize;
348        if is_64_bit {
349            if offset + 8 > self.data.len() {
350                return None;
351            }
352            let bytes: [u8; 8] = self.data[offset..offset + 8].try_into().ok()?;
353            Some(u64::from_le_bytes(bytes))
354        } else {
355            if offset + 4 > self.data.len() {
356                return None;
357            }
358            let bytes: [u8; 4] = self.data[offset..offset + 4].try_into().ok()?;
359            Some(u32::from_le_bytes(bytes) as u64)
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use crate::*;
367
368    mod types {
369        use super::*;
370
371        #[test]
372        fn exception_type_from_raw_all_valid() {
373            let cases = [
374                (1, ExceptionType::BadAccess),
375                (2, ExceptionType::BadInstruction),
376                (3, ExceptionType::Arithmetic),
377                (4, ExceptionType::Emulation),
378                (5, ExceptionType::Software),
379                (6, ExceptionType::Breakpoint),
380                (7, ExceptionType::Syscall),
381                (8, ExceptionType::MachSyscall),
382                (9, ExceptionType::RpcAlert),
383                (10, ExceptionType::Crash),
384                (11, ExceptionType::Resource),
385                (12, ExceptionType::Guard),
386                (13, ExceptionType::CorpseNotify),
387            ];
388            for (raw, expected) in cases {
389                assert_eq!(ExceptionType::from_raw(raw), Some(expected), "raw={raw}");
390            }
391        }
392
393        #[test]
394        fn exception_type_from_raw_invalid() {
395            assert_eq!(ExceptionType::from_raw(0), None);
396            assert_eq!(ExceptionType::from_raw(14), None);
397            assert_eq!(ExceptionType::from_raw(-1), None);
398            assert_eq!(ExceptionType::from_raw(i32::MAX), None);
399        }
400
401        #[test]
402        fn exception_type_raw_round_trip() {
403            for val in 1..=13 {
404                let et = ExceptionType::from_raw(val).unwrap();
405                assert_eq!(et.raw(), val);
406            }
407        }
408
409        #[test]
410        fn cpu_type_is_64_bit() {
411            assert!(!CpuType::X86.is_64_bit());
412            assert!(CpuType::X86_64.is_64_bit());
413            assert!(!CpuType::ARM.is_64_bit());
414            assert!(CpuType::ARM64.is_64_bit());
415            assert!(!CpuType::POWERPC.is_64_bit());
416            assert!(CpuType::POWERPC64.is_64_bit());
417        }
418
419        #[test]
420        fn cpu_type_with_64_bit() {
421            assert_eq!(CpuType::X86.with_64_bit(), CpuType::X86_64);
422            assert_eq!(CpuType::ARM.with_64_bit(), CpuType::ARM64);
423            assert_eq!(CpuType::POWERPC.with_64_bit(), CpuType::POWERPC64);
424            // Already 64-bit stays the same
425            assert_eq!(CpuType::X86_64.with_64_bit(), CpuType::X86_64);
426        }
427
428        #[test]
429        fn mapped_memory_read_pointer_64bit() {
430            let data = 0xDEAD_BEEF_CAFE_BABEu64.to_le_bytes().to_vec();
431            let mem = MappedMemory {
432                data,
433                base_address: 0x1000,
434            };
435            assert_eq!(mem.read_pointer(0x1000, true), Some(0xDEAD_BEEF_CAFE_BABE));
436        }
437
438        #[test]
439        fn mapped_memory_read_pointer_32bit() {
440            let data = 0xCAFE_BABEu32.to_le_bytes().to_vec();
441            let mem = MappedMemory {
442                data,
443                base_address: 0x1000,
444            };
445            assert_eq!(mem.read_pointer(0x1000, false), Some(0xCAFE_BABE));
446        }
447
448        #[test]
449        fn mapped_memory_read_pointer_out_of_bounds() {
450            let mem = MappedMemory {
451                data: vec![0u8; 4],
452                base_address: 0x1000,
453            };
454            // 64-bit read needs 8 bytes but only 4 available
455            assert_eq!(mem.read_pointer(0x1000, true), None);
456            // Address past end
457            assert_eq!(mem.read_pointer(0x1008, false), None);
458        }
459
460        #[test]
461        fn mapped_memory_read_pointer_address_below_base() {
462            let mem = MappedMemory {
463                data: vec![0u8; 8],
464                base_address: 0x2000,
465            };
466            assert_eq!(mem.read_pointer(0x1000, true), None);
467        }
468
469        #[test]
470        fn mapped_memory_read_pointer_exact_boundary() {
471            // Exactly 8 bytes from offset 0 — should succeed for 64-bit
472            let mem = MappedMemory {
473                data: vec![0xFF; 8],
474                base_address: 0x1000,
475            };
476            assert!(mem.read_pointer(0x1000, true).is_some());
477            // One byte past — should fail
478            assert_eq!(mem.read_pointer(0x1001, true), None);
479        }
480    }
481}