Skip to main content

crashrustler/
exploitability.rs

1use crate::types::{AccessType, CpuType, ExploitabilityRating};
2
3/// Result of exploitability classification.
4#[derive(Debug, Clone)]
5pub struct ExploitabilityResult {
6    /// The exploitability rating.
7    pub rating: ExploitabilityRating,
8    /// POSIX signal number derived from the exception.
9    pub signal: u32,
10    /// The real exception type (after EXC_CRASH demux).
11    pub real_exception: i32,
12    /// Memory access type of the crashing instruction.
13    pub access_type: AccessType,
14    /// Address being accessed at crash time.
15    pub access_address: u64,
16    /// Program counter at crash time.
17    pub pc: u64,
18    /// Disassembly of the crashing instruction.
19    pub disassembly: String,
20    /// Human-readable analysis messages.
21    pub messages: Vec<String>,
22}
23
24/// Configuration for exploitability classification.
25#[derive(Debug, Clone, Default)]
26pub struct ClassifyConfig {
27    /// If true, read-access crashes are considered exploitable (CR_EXPLOITABLE_READS).
28    pub exploitable_reads: bool,
29    /// If true, crashes in JIT code (frame 0 in ???) are exploitable (CR_EXPLOITABLE_JIT).
30    pub exploitable_jit: bool,
31    /// If true, frame pointer inconsistency is ignored (CR_IGNORE_FRAME_POINTER).
32    pub ignore_frame_pointer: bool,
33}
34
35/// Verdict from stack/backtrace analysis.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum StackVerdict {
38    /// No change to the exploitability rating.
39    NoChange,
40    /// Override to exploitable based on suspicious backtrace.
41    ChangeToExploitable,
42    /// Override to not exploitable based on known-safe pattern.
43    ChangeToNotExploitable,
44}
45
46/// Functions whose presence at frame 0 (or nearby) suggests exploitability
47/// (heap corruption, stack smashing, use-after-free, etc.).
48const SUSPICIOUS_FUNCTIONS: &[&str] = &[
49    "__stack_chk_fail",
50    "szone_error",
51    "CFRelease",
52    "CFRetain",
53    "malloc",
54    "calloc",
55    "realloc",
56    "objc_msgSend",
57    "objc_msgSend_stret",
58    "objc_msgSendSuper",
59    "objc_msgSendSuper_stret",
60    "objc_msgSend_fpret",
61    "szone_free",
62    "free_small",
63    "tiny_free_list_add_ptr",
64    "small_free_list_add_ptr",
65    "large_entries_free_no_lock",
66    "free",
67    "CSMemDisposeHandle",
68    "CSMemDisposePtr",
69    "WTF::fastFree",
70    "WTF::fastMalloc",
71    "WTFCrashWithSecurityImplication",
72    "__chk_fail_overflow",
73];
74
75/// Functions whose presence means the crash is NOT exploitable.
76const NON_EXPLOITABLE_FUNCTIONS: &[&str] = &["ABORTING_DUE_TO_OUT_OF_MEMORY"];
77
78/// WTFCrash marker address (WebKit intentional crash).
79const WTFCRASH_ADDR: u64 = 0xbbad_beef;
80
81/// Returns the page size for the given CPU type.
82/// ARM64 uses 16 KB pages (0x4000), x86/x86_64 uses 4 KB pages (0x1000).
83fn page_size_for_cpu(cpu_type: CpuType) -> u64 {
84    if cpu_type == CpuType::ARM64 || cpu_type == CpuType::ARM {
85        0x4000
86    } else {
87        0x1000
88    }
89}
90
91/// Returns true if the CPU type is an x86 variant (32-bit or 64-bit).
92fn is_x86(cpu_type: CpuType) -> bool {
93    cpu_type == CpuType::X86 || cpu_type == CpuType::X86_64
94}
95
96/// Returns true if the CPU type is an ARM variant (32-bit or 64-bit).
97fn is_arm(cpu_type: CpuType) -> bool {
98    cpu_type == CpuType::ARM || cpu_type == CpuType::ARM64
99}
100
101/// Core exploitability analysis from exception + disassembly.
102///
103/// Ported from CrashWrangler's `catch_mach_exception_raise_state_identity`
104/// (exc_handler.m lines 461-782).
105pub fn classify_exception(
106    exception_type: i32,
107    exception_codes: &[i64],
108    disassembly: &str,
109    pc: u64,
110    cpu_type: CpuType,
111    config: &ClassifyConfig,
112) -> ExploitabilityResult {
113    let mut result = ExploitabilityResult {
114        rating: ExploitabilityRating::Unknown,
115        signal: 0,
116        real_exception: exception_type,
117        access_type: AccessType::Unknown,
118        access_address: 0,
119        pc,
120        disassembly: disassembly.to_string(),
121        messages: Vec::new(),
122    };
123
124    // Extract access address from exception codes
125    if exception_codes.len() > 1 {
126        result.access_address = exception_codes[1] as u64;
127    }
128
129    // Determine signal and access type based on exception type
130    match exception_type {
131        1 => {
132            // EXC_BAD_ACCESS
133            result.signal = 11; // SIGSEGV (or SIGBUS)
134            if !exception_codes.is_empty() {
135                match exception_codes[0] {
136                    1 => {
137                        // KERN_INVALID_ADDRESS
138                        result.signal = 11; // SIGSEGV
139                    }
140                    2 => {
141                        // KERN_PROTECTION_FAILURE
142                        result.signal = 10; // SIGBUS
143                    }
144                    _ => {}
145                }
146            }
147
148            // Determine access type from disassembly using arch-specific logic
149            result.access_type = get_access_type(disassembly, cpu_type);
150
151            // Check for null deref (address < page_size * 8)
152            let page_size = page_size_for_cpu(cpu_type);
153            if result.access_address < page_size * 8 {
154                result.rating = ExploitabilityRating::NotExploitable;
155                result.messages.push(format!(
156                    "Near-null dereference at 0x{:x} — not exploitable",
157                    result.access_address
158                ));
159                return result;
160            }
161
162            // WTFCrash marker
163            if result.access_address == WTFCRASH_ADDR {
164                result.rating = ExploitabilityRating::NotExploitable;
165                result
166                    .messages
167                    .push("WTFCrash (0xbbadbeef) — not exploitable".into());
168                return result;
169            }
170
171            // Write access → exploitable
172            if result.access_type == AccessType::Write {
173                result.rating = ExploitabilityRating::Exploitable;
174                result.messages.push(format!(
175                    "Write to 0x{:x} — exploitable",
176                    result.access_address
177                ));
178                return result;
179            }
180
181            // Exec access → exploitable
182            if result.access_type == AccessType::Exec {
183                result.rating = ExploitabilityRating::Exploitable;
184                result.messages.push(format!(
185                    "Execution at 0x{:x} — exploitable",
186                    result.access_address
187                ));
188                return result;
189            }
190
191            // Read access — depends on config
192            if result.access_type == AccessType::Read {
193                if config.exploitable_reads {
194                    result.rating = ExploitabilityRating::Exploitable;
195                    result.messages.push(format!(
196                        "Read from 0x{:x} — exploitable (CR_EXPLOITABLE_READS)",
197                        result.access_address
198                    ));
199                } else {
200                    result.rating = ExploitabilityRating::NotExploitable;
201                    result.messages.push(format!(
202                        "Read from 0x{:x} — not exploitable",
203                        result.access_address
204                    ));
205                }
206                return result;
207            }
208
209            // Unknown access type — conservatively mark as unknown
210            result.rating = ExploitabilityRating::Unknown;
211            result
212                .messages
213                .push("Could not determine access type".into());
214        }
215        2 => {
216            // EXC_BAD_INSTRUCTION
217            result.signal = 4; // SIGILL
218            result.rating = ExploitabilityRating::Exploitable;
219            result.messages.push("Bad instruction — exploitable".into());
220        }
221        3 => {
222            // EXC_ARITHMETIC
223            result.signal = 8; // SIGFPE
224            result.rating = ExploitabilityRating::NotExploitable;
225            result
226                .messages
227                .push("Arithmetic exception — not exploitable".into());
228        }
229        5 => {
230            // EXC_SOFTWARE
231            result.signal = 6; // SIGABRT
232            result.rating = ExploitabilityRating::NotExploitable;
233            result
234                .messages
235                .push("Software exception (abort) — not exploitable".into());
236        }
237        6 => {
238            // EXC_BREAKPOINT
239            result.signal = 5; // SIGTRAP
240
241            // Check for explicit trap instruction matching the target architecture:
242            // ARM64 uses `brk`, x86 uses `int3`.
243            let is_trap = if !disassembly.is_empty() {
244                if is_arm(cpu_type) {
245                    disassembly.contains("brk")
246                } else if is_x86(cpu_type) {
247                    disassembly.contains("int3")
248                } else {
249                    disassembly.contains("brk") || disassembly.contains("int3")
250                }
251            } else {
252                false
253            };
254
255            if is_trap {
256                result.rating = ExploitabilityRating::NotExploitable;
257                result
258                    .messages
259                    .push("Breakpoint trap — not exploitable".into());
260            } else {
261                result.rating = ExploitabilityRating::Exploitable;
262                result
263                    .messages
264                    .push("EXC_BREAKPOINT (non-trap) — exploitable".into());
265            }
266        }
267        10 => {
268            // EXC_CRASH — should have been demuxed by init, but handle gracefully
269            result.signal = 6; // SIGABRT
270            result.rating = ExploitabilityRating::NotExploitable;
271            result
272                .messages
273                .push("EXC_CRASH (undemuxed) — not exploitable".into());
274        }
275        _ => {
276            // Unknown exception type
277            result.signal = 6; // default to SIGABRT
278            result.rating = ExploitabilityRating::Unknown;
279            result.messages.push(format!(
280                "Unknown exception type {} — unknown exploitability",
281                exception_type
282            ));
283        }
284    }
285
286    result
287}
288
289/// Backtrace-based exploitability override.
290///
291/// Examines the crash log backtrace for patterns that indicate exploitability
292/// (or lack thereof). This can override the initial classification.
293///
294/// Ported from CrashWrangler's `is_stack_suspicious` (exc_handler.m lines 791-1006).
295pub fn is_stack_suspicious(
296    crash_log: &str,
297    access_address: u64,
298    exception_type: i32,
299    cpu_type: CpuType,
300    config: &ClassifyConfig,
301) -> StackVerdict {
302    // Extract only backtrace frame lines: lines whose trimmed form starts with
303    // digits followed by a space (e.g. "0  libFoo ...", "10  libBar ...").
304    // This excludes binary image lines ("0x1234..."), register lines ("x0: ..."), etc.
305    let frame_lines: Vec<&str> = crash_log
306        .lines()
307        .filter(|l| {
308            let trimmed = l.trim();
309            let mut chars = trimmed.chars();
310            // Must start with a digit
311            if !matches!(chars.next(), Some(c) if c.is_ascii_digit()) {
312                return false;
313            }
314            // Skip remaining digits, then require a space/tab
315            for c in chars {
316                if c == ' ' || c == '\t' {
317                    return true;
318                }
319                if !c.is_ascii_digit() {
320                    return false;
321                }
322            }
323            false
324        })
325        .collect();
326
327    // Count frames to detect recursion
328    if frame_lines.len() > 300 {
329        return StackVerdict::ChangeToNotExploitable;
330    }
331
332    // Concatenate frame lines for substring searches (avoids matching binary
333    // image paths like "libsystem_malloc.dylib" against "malloc")
334    let frame_text: String = frame_lines.join("\n");
335
336    // Check for non-exploitable functions in backtrace
337    for func in NON_EXPLOITABLE_FUNCTIONS {
338        if frame_text.contains(func) {
339            return StackVerdict::ChangeToNotExploitable;
340        }
341    }
342
343    // Check for libdispatch/libxpc crash at frame 0
344    for line in &frame_lines {
345        let trimmed = line.trim();
346        if trimmed.starts_with("0 ") || trimmed.starts_with("0\t") {
347            if trimmed.contains("libdispatch") || trimmed.contains("libxpc") {
348                return StackVerdict::ChangeToNotExploitable;
349            }
350            break;
351        }
352    }
353
354    // Check for suspicious functions in the backtrace
355    for func in SUSPICIOUS_FUNCTIONS {
356        if frame_text.contains(func) {
357            // __stack_chk_fail → always exploitable
358            if *func == "__stack_chk_fail" || *func == "WTFCrashWithSecurityImplication" {
359                return StackVerdict::ChangeToExploitable;
360            }
361            // szone_error, heap functions → exploitable for write access
362            if exception_type == 1 {
363                return StackVerdict::ChangeToExploitable;
364            }
365        }
366    }
367
368    // Check for JIT code (frame 0 in ???)
369    if config.exploitable_jit {
370        for line in &frame_lines {
371            let trimmed = line.trim();
372            if (trimmed.starts_with("0 ") || trimmed.starts_with("0\t")) && trimmed.contains("???")
373            {
374                return StackVerdict::ChangeToExploitable;
375            }
376        }
377    }
378
379    // Check for corrupted return addresses / control flow hijack indicators.
380    // Different heuristics apply per architecture.
381    if access_address != 0 {
382        let addr_bytes = access_address.to_be_bytes();
383
384        // Repeating single-byte pattern (e.g., 0x4141414141414141).
385        // Architecture-independent — indicates attacker-controlled data.
386        if addr_bytes[0] != 0 && addr_bytes.iter().all(|&b| b == addr_bytes[0]) {
387            return StackVerdict::ChangeToExploitable;
388        }
389
390        // Repeating two-byte pattern (e.g., 0x4142414241424142).
391        // Architecture-independent — another common controlled-data pattern.
392        if access_address > 0xFFFF {
393            let lo = (access_address & 0xFFFF) as u16;
394            let hi = ((access_address >> 16) & 0xFFFF) as u16;
395            let hi2 = ((access_address >> 32) & 0xFFFF) as u16;
396            let hi3 = ((access_address >> 48) & 0xFFFF) as u16;
397            if lo != 0 && lo == hi && lo == hi2 && lo == hi3 {
398                return StackVerdict::ChangeToExploitable;
399            }
400        }
401
402        // x86_64: non-canonical address check.
403        // x86_64 requires bits 48-63 to match bit 47. An address that violates
404        // this cannot be a valid pointer and suggests corruption.
405        if cpu_type == CpuType::X86_64 {
406            let bit47 = (access_address >> 47) & 1;
407            let high_bits = access_address >> 48;
408            if high_bits != 0 && high_bits != 0xFFFF && bit47 == 0 {
409                return StackVerdict::ChangeToExploitable;
410            }
411        }
412
413        // ARM64: suspicious address range checks.
414        // macOS ARM64 uses a 47-bit user VA space (0x0 to 0x0000_7FFF_FFFF_FFFF)
415        // and kernel addresses start at 0xFFFF_FE00_0000_0000. Addresses that
416        // fall in the unmapped gap between user and kernel space, or that have
417        // non-zero bits in the PAC/TBI region (bits 48-55) with an otherwise
418        // user-space address, suggest pointer corruption.
419        if cpu_type == CpuType::ARM64 {
420            let top_byte = (access_address >> 56) as u8;
421            let pac_bits = (access_address >> 48) & 0xFF;
422            let user_bits = access_address & 0x0000_FFFF_FFFF_FFFF;
423
424            // Address in the unmapped gap: above user space max but below kernel base.
425            // User max: 0x0000_7FFF_FFFF_FFFF, kernel base: ~0xFFFF_FE00_0000_0000.
426            // Anything with bits 47+ set that isn't all-ones in bits 48-63 is in the gap.
427            if access_address > 0x0000_7FFF_FFFF_FFFF && access_address < 0xFFFF_FE00_0000_0000 {
428                return StackVerdict::ChangeToExploitable;
429            }
430
431            // Non-zero top byte with low user-space address: likely corrupted pointer.
432            // TBI (Top Byte Ignore) allows bits 56-63 for tagging, but bits 48-55
433            // should be zero for valid user pointers. Non-zero PAC region bits on
434            // a user-space address suggest a corrupted or forged pointer.
435            if pac_bits != 0 && user_bits < 0x0000_8000_0000_0000 && top_byte == 0 {
436                return StackVerdict::ChangeToExploitable;
437            }
438        }
439    }
440
441    StackVerdict::NoChange
442}
443
444/// Determines memory access type from disassembly text, dispatching to the
445/// appropriate architecture-specific classifier based on the crashing process's CPU type.
446fn get_access_type(disassembly: &str, cpu_type: CpuType) -> AccessType {
447    if disassembly.is_empty() {
448        return AccessType::Unknown;
449    }
450
451    if is_arm(cpu_type) {
452        get_access_type_arm64(disassembly)
453    } else if is_x86(cpu_type) {
454        get_access_type_x86(disassembly)
455    } else {
456        AccessType::Unknown
457    }
458}
459
460/// ARM64-specific access type from disassembly.
461pub fn get_access_type_arm64(disassembly: &str) -> AccessType {
462    if disassembly.is_empty() {
463        return AccessType::Unknown;
464    }
465
466    // Extract the mnemonic (first whitespace-delimited token)
467    let mnemonic = disassembly
468        .split_whitespace()
469        .next()
470        .unwrap_or("")
471        .to_lowercase();
472
473    // Store instructions
474    if mnemonic.starts_with("str")
475        || mnemonic.starts_with("stp")
476        || mnemonic.starts_with("stur")
477        || mnemonic.starts_with("stlr")
478        || mnemonic.starts_with("stxr")
479        || mnemonic.starts_with("stlxr")
480        || mnemonic.starts_with("st1")
481        || mnemonic.starts_with("st2")
482        || mnemonic.starts_with("st3")
483        || mnemonic.starts_with("st4")
484    {
485        return AccessType::Write;
486    }
487
488    // Load instructions
489    if mnemonic.starts_with("ldr")
490        || mnemonic.starts_with("ldp")
491        || mnemonic.starts_with("ldur")
492        || mnemonic.starts_with("ldar")
493        || mnemonic.starts_with("ldxr")
494        || mnemonic.starts_with("ldaxr")
495        || mnemonic.starts_with("ld1")
496        || mnemonic.starts_with("ld2")
497        || mnemonic.starts_with("ld3")
498        || mnemonic.starts_with("ld4")
499    {
500        return AccessType::Read;
501    }
502
503    // Branch instructions → exec
504    if mnemonic == "bl"
505        || mnemonic == "blr"
506        || mnemonic == "br"
507        || mnemonic == "ret"
508        || mnemonic == "b"
509    {
510        return AccessType::Exec;
511    }
512
513    AccessType::Unknown
514}
515
516/// x86/x86_64-specific access type from disassembly.
517pub fn get_access_type_x86(disassembly: &str) -> AccessType {
518    if disassembly.is_empty() {
519        return AccessType::Unknown;
520    }
521
522    let lower = disassembly.to_lowercase();
523
524    // x86 store: mov to memory operand (contains "],")
525    if lower.starts_with("mov") && lower.contains("],") {
526        return AccessType::Write;
527    }
528    // x86 load: mov from memory operand (contains "[")
529    if lower.starts_with("mov") && lower.contains("[") {
530        return AccessType::Read;
531    }
532
533    // push/pop
534    if lower.starts_with("push") {
535        return AccessType::Write;
536    }
537    if lower.starts_with("pop") {
538        return AccessType::Read;
539    }
540
541    // call/jmp to bad address → exec
542    if lower.starts_with("call") || lower.starts_with("jmp") || lower.starts_with("ret") {
543        return AccessType::Exec;
544    }
545
546    AccessType::Unknown
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use crate::types::{AccessType, ExploitabilityRating};
553
554    mod classify {
555        use super::*;
556
557        #[test]
558        fn null_deref_not_exploitable_arm64() {
559            let r = classify_exception(
560                1,
561                &[1, 0x10],
562                "",
563                0x1000,
564                CpuType::ARM64,
565                &ClassifyConfig::default(),
566            );
567            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
568            assert_eq!(r.signal, 11);
569        }
570
571        #[test]
572        fn near_null_deref_arm64_page_boundary() {
573            // ARM64 page size = 0x4000, threshold = 0x4000 * 8 = 0x20000
574            let r = classify_exception(
575                1,
576                &[1, 0x1_FFFF],
577                "",
578                0x1000,
579                CpuType::ARM64,
580                &ClassifyConfig::default(),
581            );
582            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
583        }
584
585        #[test]
586        fn near_null_deref_x86_page_boundary() {
587            // x86 page size = 0x1000, threshold = 0x1000 * 8 = 0x8000
588            // Address 0x7FFF is within threshold → not exploitable
589            let r = classify_exception(
590                1,
591                &[1, 0x7FFF],
592                "",
593                0x1000,
594                CpuType::X86_64,
595                &ClassifyConfig::default(),
596            );
597            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
598
599            // Address 0x8000 is at threshold boundary → not exploitable (< 0x8000 check)
600            // Actually 0x8000 < 0x8000 is false, so this should NOT be near-null
601            let r2 = classify_exception(
602                1,
603                &[1, 0x8000],
604                "mov eax, [ecx]",
605                0x1000,
606                CpuType::X86_64,
607                &ClassifyConfig::default(),
608            );
609            // 0x8000 is not < 0x8000, so it's not near-null — should classify by access type
610            assert_ne!(r2.rating, ExploitabilityRating::Unknown);
611        }
612
613        #[test]
614        fn x86_higher_null_deref_not_caught_as_arm64() {
615            // Address 0x9000 is within ARM64 near-null range (< 0x20000)
616            // but outside x86 near-null range (< 0x8000).
617            // With x86_64 cpu_type, this should NOT be classified as near-null.
618            let r = classify_exception(
619                1,
620                &[1, 0x9000],
621                "mov eax, [ecx]",
622                0x1000,
623                CpuType::X86_64,
624                &ClassifyConfig::default(),
625            );
626            // Should be classified by access type (Read), not as near-null
627            assert_eq!(r.access_type, AccessType::Read);
628        }
629
630        #[test]
631        fn wtfcrash_not_exploitable() {
632            let r = classify_exception(
633                1,
634                &[1, 0xbbad_beef],
635                "",
636                0x1000,
637                CpuType::ARM64,
638                &ClassifyConfig::default(),
639            );
640            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
641        }
642
643        #[test]
644        fn write_access_exploitable_arm64() {
645            let r = classify_exception(
646                1,
647                &[2, 0x4141_4141],
648                "str x0, [x1]",
649                0x1000,
650                CpuType::ARM64,
651                &ClassifyConfig::default(),
652            );
653            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
654            assert_eq!(r.access_type, AccessType::Write);
655        }
656
657        #[test]
658        fn write_access_exploitable_x86() {
659            let r = classify_exception(
660                1,
661                &[2, 0x4141_4141],
662                "mov dword ptr [eax], ecx",
663                0x1000,
664                CpuType::X86_64,
665                &ClassifyConfig::default(),
666            );
667            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
668            assert_eq!(r.access_type, AccessType::Write);
669        }
670
671        #[test]
672        fn exec_access_exploitable_arm64() {
673            let r = classify_exception(
674                1,
675                &[2, 0x4141_4141],
676                "blr x8",
677                0x1000,
678                CpuType::ARM64,
679                &ClassifyConfig::default(),
680            );
681            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
682            assert_eq!(r.access_type, AccessType::Exec);
683        }
684
685        #[test]
686        fn exec_access_exploitable_x86() {
687            let r = classify_exception(
688                1,
689                &[2, 0x4141_4141],
690                "call rax",
691                0x1000,
692                CpuType::X86_64,
693                &ClassifyConfig::default(),
694            );
695            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
696            assert_eq!(r.access_type, AccessType::Exec);
697        }
698
699        #[test]
700        fn read_access_not_exploitable_by_default() {
701            let r = classify_exception(
702                1,
703                &[1, 0x4141_4141],
704                "ldr x0, [x1]",
705                0x1000,
706                CpuType::ARM64,
707                &ClassifyConfig::default(),
708            );
709            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
710            assert_eq!(r.access_type, AccessType::Read);
711        }
712
713        #[test]
714        fn read_access_exploitable_with_config() {
715            let config = ClassifyConfig {
716                exploitable_reads: true,
717                ..ClassifyConfig::default()
718            };
719            let r = classify_exception(
720                1,
721                &[1, 0x4141_4141],
722                "ldr x0, [x1]",
723                0x1000,
724                CpuType::ARM64,
725                &config,
726            );
727            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
728        }
729
730        #[test]
731        fn bad_instruction_exploitable() {
732            let r = classify_exception(
733                2,
734                &[1],
735                "",
736                0x1000,
737                CpuType::ARM64,
738                &ClassifyConfig::default(),
739            );
740            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
741            assert_eq!(r.signal, 4);
742        }
743
744        #[test]
745        fn arithmetic_not_exploitable() {
746            let r = classify_exception(
747                3,
748                &[1],
749                "",
750                0x1000,
751                CpuType::ARM64,
752                &ClassifyConfig::default(),
753            );
754            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
755            assert_eq!(r.signal, 8);
756        }
757
758        #[test]
759        fn software_exception_not_exploitable() {
760            let r = classify_exception(
761                5,
762                &[1],
763                "",
764                0x1000,
765                CpuType::ARM64,
766                &ClassifyConfig::default(),
767            );
768            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
769            assert_eq!(r.signal, 6);
770        }
771
772        #[test]
773        fn breakpoint_brk_not_exploitable_on_arm64() {
774            let r = classify_exception(
775                6,
776                &[1],
777                "brk #0x1",
778                0x1000,
779                CpuType::ARM64,
780                &ClassifyConfig::default(),
781            );
782            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
783            assert_eq!(r.signal, 5);
784        }
785
786        #[test]
787        fn breakpoint_int3_not_exploitable_on_x86() {
788            let r = classify_exception(
789                6,
790                &[1],
791                "int3",
792                0x1000,
793                CpuType::X86_64,
794                &ClassifyConfig::default(),
795            );
796            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
797        }
798
799        #[test]
800        fn breakpoint_brk_exploitable_on_x86() {
801            // "brk" is not an x86 trap instruction — should be treated as non-trap
802            let r = classify_exception(
803                6,
804                &[1],
805                "brk #0x1",
806                0x1000,
807                CpuType::X86_64,
808                &ClassifyConfig::default(),
809            );
810            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
811        }
812
813        #[test]
814        fn breakpoint_int3_exploitable_on_arm64() {
815            // "int3" is not an ARM64 trap instruction — should be treated as non-trap
816            let r = classify_exception(
817                6,
818                &[1],
819                "int3",
820                0x1000,
821                CpuType::ARM64,
822                &ClassifyConfig::default(),
823            );
824            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
825        }
826
827        #[test]
828        fn breakpoint_non_trap_exploitable() {
829            let r = classify_exception(
830                6,
831                &[1],
832                "",
833                0x1000,
834                CpuType::ARM64,
835                &ClassifyConfig::default(),
836            );
837            assert_eq!(r.rating, ExploitabilityRating::Exploitable);
838        }
839
840        #[test]
841        fn exc_crash_not_exploitable() {
842            let r = classify_exception(
843                10,
844                &[0],
845                "",
846                0x1000,
847                CpuType::ARM64,
848                &ClassifyConfig::default(),
849            );
850            assert_eq!(r.rating, ExploitabilityRating::NotExploitable);
851        }
852
853        #[test]
854        fn unknown_exception_unknown_rating() {
855            let r = classify_exception(
856                99,
857                &[0],
858                "",
859                0x1000,
860                CpuType::ARM64,
861                &ClassifyConfig::default(),
862            );
863            assert_eq!(r.rating, ExploitabilityRating::Unknown);
864        }
865
866        #[test]
867        fn protection_failure_signal_is_sigbus() {
868            let r = classify_exception(
869                1,
870                &[2, 0x4141_4141],
871                "str x0, [x1]",
872                0x1000,
873                CpuType::ARM64,
874                &ClassifyConfig::default(),
875            );
876            assert_eq!(r.signal, 10); // SIGBUS
877        }
878
879        #[test]
880        fn arm64_disasm_not_recognized_on_x86() {
881            // ARM64 instruction "str x0, [x1]" should not be recognized on x86
882            let r = classify_exception(
883                1,
884                &[2, 0x4141_4141],
885                "str x0, [x1]",
886                0x1000,
887                CpuType::X86_64,
888                &ClassifyConfig::default(),
889            );
890            assert_eq!(r.access_type, AccessType::Unknown);
891        }
892
893        #[test]
894        fn x86_disasm_not_recognized_on_arm64() {
895            // x86 instruction "push rbp" should not be recognized on ARM64
896            let r = classify_exception(
897                1,
898                &[2, 0x4141_4141],
899                "push rbp",
900                0x1000,
901                CpuType::ARM64,
902                &ClassifyConfig::default(),
903            );
904            assert_eq!(r.access_type, AccessType::Unknown);
905        }
906    }
907
908    mod stack_suspicious {
909        use super::*;
910
911        // ================================================================
912        // ARM64 architectural difference: integer division by zero
913        // ================================================================
914        // ARM64 hardware silently returns zero for integer division by zero
915        // rather than raising EXC_ARITHMETIC. This means EXC_ARITHMETIC (type 3)
916        // is effectively x86-only for integer operations — on ARM64, only
917        // floating-point exceptions can trigger it. The classify_exception
918        // handler for type 3 returns NotExploitable, which is correct for both
919        // architectures: x86 integer div-by-zero is not exploitable, and ARM64
920        // FP exceptions are also not exploitable.
921
922        // ================================================================
923        // ARM64 architectural difference: __builtin_trap and _FORTIFY_SOURCE
924        // ================================================================
925        // Security mechanisms like __builtin_trap and _FORTIFY_SOURCE generate
926        // EXC_BREAKPOINT (via `brk` instruction) on ARM64 instead of EXC_CRASH
927        // (via abort()) on x86. The classify_exception handler for type 6
928        // (EXC_BREAKPOINT) dispatches on architecture: ARM64 checks for `brk`,
929        // x86 checks for `int3`. Both are classified as NotExploitable when the
930        // corresponding trap instruction is detected. See tests in mod classify:
931        // breakpoint_brk_not_exploitable_on_arm64, breakpoint_int3_not_exploitable_on_x86.
932
933        // ================================================================
934        // ARM64 architectural difference: symbol resolution
935        // ================================================================
936        // The CrashWrangler ARM64 fork notes that CoreSymbolication may
937        // mis-symbolicate __stack_chk_fail on ARM64. CrashRustler avoids this
938        // class of issues entirely by resolving symbols directly via Mach-O
939        // nlist parsing (image_enum.rs) rather than using CoreSymbolication.
940        // The __stack_chk_fail backtrace check below works correctly regardless
941        // of architecture.
942
943        #[test]
944        fn recursion_not_exploitable() {
945            let mut log = String::new();
946            for i in 0..301 {
947                log.push_str(&format!("{i}  libfoo.dylib  0x1000  foo + 0\n"));
948            }
949            let v =
950                is_stack_suspicious(&log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
951            assert_eq!(v, StackVerdict::ChangeToNotExploitable);
952        }
953
954        #[test]
955        fn out_of_memory_not_exploitable() {
956            let log = "0  libfoo.dylib  0x1000  ABORTING_DUE_TO_OUT_OF_MEMORY + 0\n";
957            let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
958            assert_eq!(v, StackVerdict::ChangeToNotExploitable);
959        }
960
961        #[test]
962        fn libdispatch_frame0_not_exploitable() {
963            let log = "0  libdispatch.dylib  0x1000  _dispatch_main + 0\n1  libfoo.dylib  0x2000  main + 0\n";
964            let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
965            assert_eq!(v, StackVerdict::ChangeToNotExploitable);
966        }
967
968        #[test]
969        fn stack_chk_fail_exploitable() {
970            let log = "0  libSystem.B.dylib  0x1000  __stack_chk_fail + 0\n1  libfoo.dylib  0x2000  bar + 0\n";
971            let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
972            assert_eq!(v, StackVerdict::ChangeToExploitable);
973        }
974
975        #[test]
976        fn wtf_security_exploitable() {
977            let log = "0  WebCore  0x1000  WTFCrashWithSecurityImplication + 0\n";
978            let v = is_stack_suspicious(log, 0x4141, 1, CpuType::ARM64, &ClassifyConfig::default());
979            assert_eq!(v, StackVerdict::ChangeToExploitable);
980        }
981
982        #[test]
983        fn repeating_byte_address_exploitable() {
984            let v = is_stack_suspicious(
985                "0  libfoo.dylib  0x1000  main + 0\n",
986                0x4141_4141_4141_4141,
987                1,
988                CpuType::ARM64,
989                &ClassifyConfig::default(),
990            );
991            assert_eq!(v, StackVerdict::ChangeToExploitable);
992        }
993
994        #[test]
995        fn normal_stack_no_change() {
996            let log = "0  libfoo.dylib  0x1000  main + 0\n1  libbar.dylib  0x2000  start + 0\n";
997            let v = is_stack_suspicious(
998                log,
999                0x7fff_1234,
1000                1,
1001                CpuType::ARM64,
1002                &ClassifyConfig::default(),
1003            );
1004            assert_eq!(v, StackVerdict::NoChange);
1005        }
1006
1007        #[test]
1008        fn jit_frame0_exploitable_with_config() {
1009            let config = ClassifyConfig {
1010                exploitable_jit: true,
1011                ..ClassifyConfig::default()
1012            };
1013            let log = "0  ???  0x1000  0x1000 + 0\n";
1014            let v = is_stack_suspicious(log, 0x7fff_1234, 1, CpuType::ARM64, &config);
1015            assert_eq!(v, StackVerdict::ChangeToExploitable);
1016        }
1017
1018        #[test]
1019        fn jit_frame0_not_flagged_without_config() {
1020            let log = "0  ???  0x1000  0x1000 + 0\n";
1021            let v = is_stack_suspicious(
1022                log,
1023                0x7fff_1234,
1024                1,
1025                CpuType::ARM64,
1026                &ClassifyConfig::default(),
1027            );
1028            assert_eq!(v, StackVerdict::NoChange);
1029        }
1030
1031        #[test]
1032        fn non_canonical_x86_64_exploitable() {
1033            // Non-canonical address on x86_64: bits 48-63 don't match bit 47
1034            let v = is_stack_suspicious(
1035                "0  libfoo.dylib  0x1000  main + 0\n",
1036                0x0001_0000_0000_0000, // bit 47 = 0, high bits = 0x0001 (not 0 or 0xFFFF)
1037                1,
1038                CpuType::X86_64,
1039                &ClassifyConfig::default(),
1040            );
1041            assert_eq!(v, StackVerdict::ChangeToExploitable);
1042        }
1043
1044        #[test]
1045        fn non_canonical_not_checked_on_arm64() {
1046            // Same non-canonical x86_64 address on ARM64 falls into the ARM64
1047            // unmapped gap check instead (above user max, below kernel base).
1048            let v = is_stack_suspicious(
1049                "0  libfoo.dylib  0x1000  main + 0\n",
1050                0x0001_0000_0000_0000,
1051                1,
1052                CpuType::ARM64,
1053                &ClassifyConfig::default(),
1054            );
1055            // This address is in the ARM64 unmapped gap → exploitable
1056            assert_eq!(v, StackVerdict::ChangeToExploitable);
1057        }
1058
1059        // ================================================================
1060        // Two-byte repeating pattern detection
1061        // ================================================================
1062
1063        #[test]
1064        fn two_byte_repeating_pattern_exploitable() {
1065            // 0x4142414241424142 — repeating two-byte pattern
1066            let v = is_stack_suspicious(
1067                "0  libfoo.dylib  0x1000  main + 0\n",
1068                0x4142_4142_4142_4142,
1069                1,
1070                CpuType::ARM64,
1071                &ClassifyConfig::default(),
1072            );
1073            assert_eq!(v, StackVerdict::ChangeToExploitable);
1074        }
1075
1076        #[test]
1077        fn two_byte_non_repeating_no_match() {
1078            // 0x4142_4143_4142_4142 — not a perfect two-byte repeat
1079            let v = is_stack_suspicious(
1080                "0  libfoo.dylib  0x1000  main + 0\n",
1081                0x4142_4143_4142_4142,
1082                1,
1083                CpuType::X86_64,
1084                &ClassifyConfig::default(),
1085            );
1086            // Doesn't match repeating patterns, but is non-canonical on x86_64
1087            // (bit47=0, high_bits=0x4142, not 0 or 0xFFFF)
1088            assert_eq!(v, StackVerdict::ChangeToExploitable);
1089        }
1090
1091        // ================================================================
1092        // ARM64 unmapped gap detection
1093        // ================================================================
1094
1095        #[test]
1096        fn arm64_unmapped_gap_exploitable() {
1097            // Address above user space max (0x0000_7FFF_FFFF_FFFF) but below
1098            // kernel base (~0xFFFF_FE00_0000_0000) — falls in unmapped gap.
1099            let v = is_stack_suspicious(
1100                "0  libfoo.dylib  0x1000  main + 0\n",
1101                0x0000_8000_0000_0000, // just above user max
1102                1,
1103                CpuType::ARM64,
1104                &ClassifyConfig::default(),
1105            );
1106            assert_eq!(v, StackVerdict::ChangeToExploitable);
1107        }
1108
1109        #[test]
1110        fn arm64_deep_gap_exploitable() {
1111            // Well into the unmapped gap
1112            let v = is_stack_suspicious(
1113                "0  libfoo.dylib  0x1000  main + 0\n",
1114                0x4141_0000_0000_0000,
1115                1,
1116                CpuType::ARM64,
1117                &ClassifyConfig::default(),
1118            );
1119            assert_eq!(v, StackVerdict::ChangeToExploitable);
1120        }
1121
1122        #[test]
1123        fn arm64_valid_user_address_no_change() {
1124            // Valid user-space address — should not trigger
1125            let v = is_stack_suspicious(
1126                "0  libfoo.dylib  0x1000  main + 0\n",
1127                0x0000_0001_0000_0000,
1128                1,
1129                CpuType::ARM64,
1130                &ClassifyConfig::default(),
1131            );
1132            assert_eq!(v, StackVerdict::NoChange);
1133        }
1134
1135        #[test]
1136        fn arm64_kernel_address_no_false_positive() {
1137            // Valid kernel address — should not trigger gap check
1138            let v = is_stack_suspicious(
1139                "0  libfoo.dylib  0x1000  main + 0\n",
1140                0xFFFF_FE00_0000_0000,
1141                1,
1142                CpuType::ARM64,
1143                &ClassifyConfig::default(),
1144            );
1145            // Not in gap, but the repeating byte check doesn't apply either.
1146            // This is a valid kernel address — no change.
1147            assert_eq!(v, StackVerdict::NoChange);
1148        }
1149
1150        // ================================================================
1151        // ARM64 PAC region corruption detection
1152        // ================================================================
1153
1154        #[test]
1155        fn arm64_pac_bits_on_user_address_exploitable() {
1156            // Non-zero PAC bits (48-55) on a user-space address with zero top byte
1157            // suggests a corrupted or forged pointer.
1158            let v = is_stack_suspicious(
1159                "0  libfoo.dylib  0x1000  main + 0\n",
1160                0x0012_0000_1234_5678, // PAC bits 0x12, user addr, top byte 0
1161                1,
1162                CpuType::ARM64,
1163                &ClassifyConfig::default(),
1164            );
1165            assert_eq!(v, StackVerdict::ChangeToExploitable);
1166        }
1167
1168        #[test]
1169        fn arm64_pac_bits_not_checked_on_x86() {
1170            // Same address on x86_64 should use non-canonical check, not PAC
1171            let v = is_stack_suspicious(
1172                "0  libfoo.dylib  0x1000  main + 0\n",
1173                0x0012_0000_1234_5678,
1174                1,
1175                CpuType::X86_64,
1176                &ClassifyConfig::default(),
1177            );
1178            // Non-canonical on x86_64 (bit47=0, high_bits=0x0012)
1179            assert_eq!(v, StackVerdict::ChangeToExploitable);
1180        }
1181    }
1182
1183    mod access_type_detection {
1184        use super::*;
1185
1186        #[test]
1187        fn arm64_store_is_write() {
1188            assert_eq!(get_access_type_arm64("str x0, [x1]"), AccessType::Write);
1189            assert_eq!(get_access_type_arm64("stp x0, x1, [sp]"), AccessType::Write);
1190            assert_eq!(
1191                get_access_type_arm64("stur x0, [x1, #-8]"),
1192                AccessType::Write
1193            );
1194        }
1195
1196        #[test]
1197        fn arm64_load_is_read() {
1198            assert_eq!(get_access_type_arm64("ldr x0, [x1]"), AccessType::Read);
1199            assert_eq!(get_access_type_arm64("ldp x0, x1, [sp]"), AccessType::Read);
1200            assert_eq!(
1201                get_access_type_arm64("ldur x0, [x1, #-8]"),
1202                AccessType::Read
1203            );
1204        }
1205
1206        #[test]
1207        fn arm64_branch_is_exec() {
1208            assert_eq!(get_access_type_arm64("blr x8"), AccessType::Exec);
1209            assert_eq!(get_access_type_arm64("br x16"), AccessType::Exec);
1210            assert_eq!(get_access_type_arm64("ret"), AccessType::Exec);
1211        }
1212
1213        #[test]
1214        fn empty_is_unknown() {
1215            assert_eq!(get_access_type_arm64(""), AccessType::Unknown);
1216            assert_eq!(get_access_type_x86(""), AccessType::Unknown);
1217        }
1218
1219        #[test]
1220        fn x86_store_is_write() {
1221            assert_eq!(
1222                get_access_type_x86("mov dword ptr [eax], ecx"),
1223                AccessType::Write
1224            );
1225            assert_eq!(get_access_type_x86("push rbp"), AccessType::Write);
1226        }
1227
1228        #[test]
1229        fn x86_load_is_read() {
1230            assert_eq!(get_access_type_x86("mov eax, [ecx]"), AccessType::Read);
1231            assert_eq!(get_access_type_x86("pop rbp"), AccessType::Read);
1232        }
1233
1234        #[test]
1235        fn x86_branch_is_exec() {
1236            assert_eq!(get_access_type_x86("call 0x1234"), AccessType::Exec);
1237            assert_eq!(get_access_type_x86("jmp rax"), AccessType::Exec);
1238            assert_eq!(get_access_type_x86("ret"), AccessType::Exec);
1239        }
1240
1241        #[test]
1242        fn dispatch_uses_correct_arch() {
1243            // ARM64 instruction via dispatch
1244            assert_eq!(
1245                get_access_type("str x0, [x1]", CpuType::ARM64),
1246                AccessType::Write
1247            );
1248            // x86 instruction via dispatch
1249            assert_eq!(
1250                get_access_type("push rbp", CpuType::X86_64),
1251                AccessType::Write
1252            );
1253            // ARM64 instruction should not be recognized on x86
1254            assert_eq!(
1255                get_access_type("str x0, [x1]", CpuType::X86_64),
1256                AccessType::Unknown
1257            );
1258            // x86 instruction should not be recognized on ARM64
1259            assert_eq!(
1260                get_access_type("push rbp", CpuType::ARM64),
1261                AccessType::Unknown
1262            );
1263        }
1264    }
1265}