CrashWrangler
August 10, 2026 ยท View on GitHub
CrashWrangler is a set of tools used to determine if a crash is an exploitable security issue, and if a crash is a duplicate of another known crash.
The exploitability diagnosis is intended to be used when you have a reproducible test case, but the duplicate detection can be run on any crash log. CrashWrangler uses heuristics, and false positives and false negatives are possible. It's intended for quick assessment; a detailed manual inspection is the only way to be sure something is or isn't exploitable.
NOTE: A crash can only be a security issue if it is triggered by untrusted input. CrashWrangler does not try to determine whether or not the crash was triggered by untrusted input; this is up to the user.
If a crash is determined to be non-exploitable, it's recommended to run the test case again with libgmalloc(3) enabled, and see if the crash changes to one that is considered exploitable.
CrashWrangler does not send crash data to Apple or anyone else. Forwarding to the system CrashReporter is disabled by default; set CW_FORWARD_CRASH_REPORTER=1 to opt in.
Bugs and issues can be reported on this project's GitHub repository.
Components
CrashWrangler is one Rust executable with three modes:
exc_handler <program> [arguments...](orexc_handler run ...) captures and classifies a live crash.exc_handler analyze [--json] <report>analyzes a saved CrashWrangler log, legacy.crashreport, or modern.ipsreport.exc_handler bucket <directory>groups saved reports by normalized crash signature.
The sample scripts demonstrate running currently shipped macOS applications through the compatible exc_handler interface. Application locations can change between macOS releases.
Don't run these programs in a world-writable directory if you don't trust other users. They assume the current working directory is safe.
Platform Support
- macOS on Apple Silicon (arm64) is the supported target for CrashWrangler v3.
- Runtime and offline analysis are implemented in Rust's standard library. There are no crates or third-party native libraries.
- Live capture calls Mach, libSystem, and Apple's private CoreSymbolication framework directly. The build uses Apple's MIG tool to generate a small exception-message server from the
mach_exc.defssupplied by the active macOS SDK.
arm64 Behavioral Differences
- On arm64, integer division by zero does not raise
EXC_ARITHMETIC(the hardware silently returns zero). Only floating-point exceptions may trigger it. - Some runtime checks (e.g.
__builtin_trap,_FORTIFY_SOURCE) usebrkinstructions instead ofabort(), resulting inEXC_BREAKPOINTrather thanEXC_CRASH. __stack_chk_failmay be mis-symbolicated by CoreSymbolication on arm64; CrashWrangler includes heuristic detection of corrupted return addresses in backtraces to compensate.
Build & Install
Prerequisites
- Rust 1.85 or newer
- Xcode Command Line Tools (
xcode-select --install), includingmigand the macOS SDK
The executable has no package dependencies. Its JSON parser, Base64 decoder, crash normalizer, and bounded arm64 instruction classifier are built in.
Building
make
Running the test suite
make check
This builds and runs unit tests plus a corpus of intentionally crashing helper programs.
The macOS service-integration checks exercise launchd registration, exception forwarding, and no-kill behavior. They may create Apple diagnostic reports because two checks deliberately hand a crash to the next system exception handler:
make check-system-integration
The same command also exercises PID attachment when passwordless sudo is available; otherwise it reports that check as skipped.
Installing
cp exc_handler [the directory where you are running test cases]
Homebrew
This repository can also be used directly as a Homebrew tap:
brew tap ant4g0nist/crashwrangler https://github.com/ant4g0nist/crashwrangler
brew install crashwrangler
The formula installs crashwrangler and a compatible exc_handler symlink. It builds from source and therefore needs the Xcode Command Line Tools. To build the newest development revision, use brew install --HEAD crashwrangler.
Quick Start
Testing one crash
Here's an example of using exc_handler to reproduce a crash and see if it's exploitable:
env CW_CURRENT_CASE=foo ./exc_handler "/System/Applications/QuickTime Player.app/Contents/MacOS/QuickTime Player" file_that_crashes.mov
It creates a log in crashlogs/foo.crashlog.txt.
When you look at the log, there's a header that looks like:
exception=EXC_BAD_ACCESS:signal=11:is_exploitable=yes:instruction_disassembly=.long 0x3900011f:instruction_address=0x0000000100003f40:access_type=write:access_address=0x0000000041414141:
The most interesting field is is_exploitable.
Checking for duplicates
Put the crash logs in a directory named crashlogs:
mkdir crashlogs
cp ~/Library/Logs/CrashReporter/* crashlogs
Run the built-in bucketer (CrashWrangler logs also retain their live exploitability result):
./exc_handler bucket crashlogs
Crash at 0 + 16384 / start + 54
exploitable=unknown: ./crashlogs/a.out_2009-04-10-173926_hostname.crash
exploitable=unknown: ./crashlogs/a.out_2009-04-10-173919_hostname.crash
Crash at 0 + 23047272
exploitable=unknown: ./crashlogs/Meeting Maker Calendar_2008-10-17-143822_hostname.crash
Analyzing a log
If you have a reproducible crash, live capture is preferable. For a saved report, use the built-in analyzer:
./exc_handler analyze crashlogs/crashread.crashlog.txt
exception_type=EXC_BAD_ACCESS:signal=SIGSEGV:is_exploitable=no:instruction_disassembly=.long 0x39400020:instruction_address=0x0000000100000f38:access_type=read:access_address=0x0000000041414141:
Use --json for a structured result suitable for automation. Both legacy text reports and modern .ips reports are accepted.
Fuzzing
Here's an example of running file fuzzing on QuickTime movies:
- Generate a bunch of fuzzed movie files in a directory.
- For each file, run
sample_scripts/run_qt.sh [path to the file]. - This automates running the movie files in QuickTime. For each crash, a log file named something like
crashlogs/00000000.mov.crashlog.txtwill be created. - When done, run
./exc_handler bucket crashlogsto print the crashes grouped by signature.
Writing Automation Scripts
If you're using your own automation, the only thing you need to do is set the environment variable CW_CURRENT_CASE to the name of the test case being run:
export CW_CURRENT_CASE=\$1
If external automation is going to kill the program, make sure the file named cw.lck (or the file specified by CW_LOCK_FILE) does not exist first. The lock file exists only while a crash is being processed. Prefer setting CW_TIMEOUT, which performs this check inside CrashWrangler and avoids orphaning the target.
For smaller command-line programs where slow performance is not an issue, you can enable libgmalloc(3):
env CW_USE_GMAL=1 ./exc_handler myprogram \$1
exc_handler Live-Capture Return Values
| Return value | Meaning |
|---|---|
0 | No crash |
-1 (255) | Error |
-2 | Externally generated signals (SIGINT, SIGHUP, SIGKILL) |
| Signal number | Crash, not exploitable (e.g. SIGSEGV = 11) |
| Signal number + 100 | Crash, exploitable (e.g. exploitable SIGSEGV = 111) |
Crash Log Header Reference
This is the format for the header of logs generated by exc_handler.
Example:
exception=EXC_BAD_ACCESS:signal=11:is_exploitable=no:instruction_disassembly=.long 0x3900011f:instruction_address=0x0000000100003f40:access_type=write:access_address=0x0000000000000000:
| Field | Values |
|---|---|
exception | EXC_BAD_ACCESS, EXC_BAD_INSTRUCTION, EXC_ARITHMETIC, EXC_CRASH, EXC_BREAKPOINT |
signal | Signal number (see man signal) |
is_exploitable | yes or no |
instruction_disassembly | The raw crashing arm64 instruction encoding (for example, .long 0x3900011f). Access classification is performed by the built-in bounded decoder; CrashWrangler does not include a general-purpose disassembler. |
instruction_address | Address of the crashing instruction |
access_type | read, write, exec, recursion, or unknown (only for EXC_BAD_ACCESS) |
access_address | Bad address that caused the crash (0x0 if not EXC_BAD_ACCESS) |
Exploitability Algorithm
The algorithm for determining exploitability:
Exploitable if:
- Crash on write instruction
- Crash executing an invalid address
- Crash calling an invalid address
- Illegal instruction exception, except recognized deliberate CoreFoundation retain/release traps
- Abort due to
-fstack-protector,_FORTIFY_SOURCE, heap corruption detected - Stack trace of crashing thread contains suspicious functions (malloc, free, szone_error, objc_msgSend, etc.)
- Corrupted return address detected in backtrace (arm64 heuristic for mis-symbolicated
__stack_chk_fail)
Not exploitable if:
- Divide by zero exception (note: on arm64, integer div-by-zero does not raise an exception at all)
- Stack grows too large due to recursion
- Null dereference (read or write)
- Other abort
- Crash on read instruction (unless
CW_EXPLOITABLE_READSis set)
Environment Variables
| Variable | Description |
|---|---|
CW_CURRENT_CASE | Arbitrary string identifier for the case currently being run. Used for generating the log name. Suitable for use when the program is killed and relaunched for each case. |
CW_CASE_FILE | File holding the identifier for the current case. Takes precedence over CW_CURRENT_CASE. Suitable for long-running programs. |
CW_LOG_PATH | Path for the log file. Takes precedence over both CW_CURRENT_CASE and CW_CASE_FILE. Useful for sending all logs to e.g. /dev/null. |
CW_LOG_DIR | Directory to output crash logs to. Default: ./crashlogs |
CW_LOCK_FILE | Lock file path. Default: cw.lck. LaunchAgents without a writable WorkingDirectory should set this to an absolute writable path. |
CW_PID_FILE | If set, exc_handler writes the child PID to this file. Useful for targeting kill at a specific process instance. |
CW_ATTACH_PID | PID of a process to monitor for one crash. Requires root or an appropriate task-inspection entitlement; hardened targets may still refuse attachment. |
CW_USE_GMAL | If set, load libgmalloc(3) and enable MALLOC_FILL_SPACE in the child process. |
CW_NO_KILL_CHILD | If set, don't terminate the child with SIGKILL after capture; hand the exception to the next system handler instead. |
CW_QUIET | If set, suppress stdout output unless there was an internal error. |
CW_EXPLOITABLE_READS | If set, non-NULL read access violations are considered exploitable. Useful for C++ where reading an invalid object could indicate reading a vtable pointer from an invalid address. |
CW_REGISTER_LAUNCHD_NAME | Name of a Mach service preconfigured by launchd. CrashWrangler checks in to it and waits for one exception; use this with a matching LaunchAgent/LaunchDaemon configuration. |
CW_FORWARD_CRASH_REPORTER | If set, hand the exception to the next system handler after capture instead of consuming it. This may create a system diagnostic report. |
CW_MACHINE_READABLE | If set, omit the human-readable classification summary and write only the machine-readable first-line header before the crash report. |
CW_TIMEOUT | Positive number of seconds to wait before terminating a non-crashing child. A timeout returns -2 (254 in a shell). |
CW_TEST_CASE_PATH | Path to the test case being run. Only needed when the path differs from CW_CURRENT_CASE. |
CWE_* | Any env var prefixed with CWE_ has the prefix stripped and is set in the child process only. E.g. CWE_DYLD_INSERT_LIBRARIES=foo.dylib becomes DYLD_INSERT_LIBRARIES=foo.dylib in the child. |
CW_NO_LOG | If set, don't write a crash log. Useful for testing. |
CW_IGNORE_FRAME_POINTER | If set, don't flag exploitable based on frame/base pointer discrepancy. Set this if the target was built with -fomit-frame-pointer. |
CW_EXPLOITABLE_JIT | If set, any crash outside of a known library or the main executable is considered exploitable. For programs with JIT-compiled code. |
CW_LOG_INFO | If set, the value is included in the crash log. General-purpose mechanism for logging extra info. |
CW_DEBUG | If set, emit Mach port, spawn, wait, and exception-server diagnostics to stderr. |
The attach, launchd, and forwarding modes depend on macOS privileges and external service configuration.
Notes
CrashWrangler may malfunction if you have anything set in ~/Library/Preferences/com.apple.DebugSymbols.plist. This preference would not normally exist; if you don't know what it is, don't worry about it.
Licensing
- The Rust implementation and other code authored for this repository are licensed under Apache-2.0.
- The crash fixtures in
tests_src/and automation scripts insample_scripts/remain under Apple's Sample Code License.
See LICENSE for the licensing map, LICENSE-APACHE for Apache-2.0, and LICENSE-APPLE-SAMPLE-CODE for Apple's terms.