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}