Crash Triage
Turn a crash into a decision. Given a crashing input plus a target binary — or a
trace you already have — classify the crash by bug class, judge its severity and
exploitability, point at the responsible source frame, and recommend one action.
This is the post-crash counterpart to static (angr) and dynamic (Frida) analysis:
it answers "is this crash a real, reportable bug, and how bad is it?"
When to Use
- A fuzzer (AFL++, libFuzzer, honggfuzz) produced crashing inputs to review
- You have an ASAN/UBSan report, a GDB/LLDB backtrace, or a core dump to interpret
- Deciding whether a crash is a genuine memory-safety bug or working-as-intended
input rejection (an assert/abort on malformed data)
- Prioritizing a pile of crashes — which to file upstream first, which to ignore
- Distinguishing a write primitive from a read, a null deref from controlled corruption
When NOT to Use
- You need to find bugs in a binary without source → use angr-analyzer
- You need live runtime hooking/tracing of a process → use frida-instrument
- The crash is in managed code (Python/JS/Java) with a native stack trace already
pointing at the line — read it directly
- You want to set up the fuzzer itself — that's harness/rig work, out of scope here
Quick Reference
| Task |
Command |
| Reproduce + classify (file arg) |
{baseDir}/scripts/crash-triage.sh -b ./bin-asan -i poc -x "@@" |
| Reproduce + classify (stdin) |
{baseDir}/scripts/crash-triage.sh -b ./bin-asan -i poc -m stdin |
| Classify an existing trace |
{baseDir}/scripts/crash-triage.sh -t asan.log --label <id> |
| Classify from stdin |
cat trace.txt | {baseDir}/scripts/crash-triage.sh -t - |
| JSON verdict only |
{baseDir}/scripts/crash-triage.sh -t asan.log --json |
| Triage a directory |
for f in crashes/*; do {baseDir}/scripts/crash-triage.sh -b ./bin -i "$f" --label "$(basename "$f")"; done |
Prerequisites
- python3 (stdlib only — no pip install needed for classification)
- For reproduction: the target binary, ideally an ASAN+UBSan build
(
-fsanitize=address,undefined -g), which gives the most precise verdict
- Optional: gdb or lldb for a backtrace fallback when there's no ASAN report
Classification Taxonomy
The classifier maps a trace to one class. Severity and action follow from it:
| Class |
Signature |
Severity |
Default action |
| memory-bug |
ASAN: heap/stack/global-buffer-overflow, heap-use-after-free, double-free, etc. |
HIGH |
file-upstream |
| segv |
SIGSEGV / EXC_BAD_ACCESS / ASAN SEGV; non-null addr |
HIGH |
file-upstream |
| segv (null) |
Faulting address < 0x1000 (null/small-offset deref) |
MED |
investigate |
| ubsan |
UndefinedBehaviorSanitizer / runtime error: |
MED–HIGH |
investigate |
| assertion |
SIGABRT, __assert_fail, Assertion … failed, abort |
LOW |
ignore-intended |
| trap |
SIGTRAP / EXC_BREAKPOINT / __builtin_trap / SIGILL |
MED |
investigate |
| arith |
SIGFPE / division by zero |
MED |
investigate |
| leak |
LeakSanitizer / detected memory leaks |
LOW |
investigate |
| hang |
timeout / no crash signal |
LOW |
investigate |
| no-frames |
no usable frames or signal |
UNKNOWN |
investigate (re-capture) |
Severity nuance the verdict applies for you:
- A write overflow outranks a read (write primitives are more exploitable).
- A null deref (addr near 0) is real but usually lower impact than corruption at
a controlled address — confirm whether the pointer/offset is attacker-influenced.
- An assertion/abort is usually the target correctly rejecting bad input. Escalate
only when the assert guards an invariant the input shouldn't be able to violate.
- A trap is often a compiler-inserted check (checked arithmetic / bounds guard),
not raw corruption — read the top-frame source to tell which.
Workflow
Scope. One crash or many? For a batch, loop the directory (see Quick
Reference) and collect verdicts before reasoning about priority.
Get a trace. Either point the tool at the binary + PoC to reproduce it, or
feed a trace you already have with -t. Prefer an ASAN build — it converts
a vague SIGSEGV into a precise "heap-buffer-overflow WRITE of size 4".
Read the verdict. The tool prints class / severity / top frame / action /
reasoning. The top frame is the first application frame (sanitizer and libc
interceptor frames are skipped).
Confirm at the source. Open the top-frame file:line and read the few lines
around it. This is what separates "file-upstream" from "ignore-intended":
- Is the faulting access bounded by something derived from input?
- Is this the target's own validation rejecting malformed input (→ intended)?
- For a trap: is it a
__builtin_trap from a safe-math/bounds check (→ likely WAI)?
De-duplicate. Two crashes with the same top frame are probably one bug.
Group them and report the cluster once.
Output. For a batch, produce one markdown table, severity-descending:
id | class | severity | top_frame | action | rationale. Follow with a short
paragraph: counts by class and what you'd do next.
Reproducing with the right build
The single biggest lever on verdict quality is building the target with sanitizers:
# C/C++: ASAN + UBSan with frame pointers and line info
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
CXXFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
<your normal configure/make>
Then triage against that binary. Without it, you fall back to a debugger backtrace,
which still classifies the signal but can't tell a heap overflow read from a write.
Tone
Be decisive. "This is a heap-buffer-overflow WRITE in Parser::readToken at
parser.c:996 — file upstream" beats "this could potentially be a memory issue that
may warrant further review." When something is genuinely ambiguous, say so and pick
the next step rather than hedging forever.
Common Mistakes
- Trusting a bare SIGSEGV. A debugger-only SEGV can't distinguish null deref from
controlled corruption. Rebuild with ASAN before declaring severity.
- Filing every assert as a bug. Most asserts on fuzzed input are the parser doing
its job. Check whether the input was supposed to be rejectable.
- Reporting duplicates. Cluster by top frame first; one root cause can spray
dozens of distinct inputs.
- Triaging non-reproducing crashes silently. If the PoC doesn't reproduce on a
fresh build, say so — don't classify a stale
trace.txt as if it were live.
Don't
- Don't try to fix the bug from this skill — classifying and (optionally) drafting
an upstream report is the end state.
- Don't modify the crash inputs or overwrite a captured trace without saying so.
1---2name: crash-triage3description: Triages and classifies program crashes by bug class and exploitability. Use when reviewing a crashing input, an AddressSanitizer report, a GDB/LLDB backtrace, or a core dump; when deciding whether a crash is a real memory-safety bug worth reporting or a benign input rejection; or when prioritizing a batch of fuzzer-found crashes. Reproduces a PoC under sanitizers/debugger and emits a severity-ranked verdict.4---56# Crash Triage78Turn a crash into a decision. Given a crashing input plus a target binary — or a9trace you already have — classify the crash by bug class, judge its severity and10exploitability, point at the responsible source frame, and recommend one action.1112This is the post-crash counterpart to static (angr) and dynamic (Frida) analysis:13it answers "is this crash a real, reportable bug, and how bad is it?"1415## When to Use1617- A fuzzer (AFL++, libFuzzer, honggfuzz) produced crashing inputs to review18- You have an ASAN/UBSan report, a GDB/LLDB backtrace, or a core dump to interpret19- Deciding whether a crash is a genuine memory-safety bug or working-as-intended20 input rejection (an assert/abort on malformed data)21- Prioritizing a pile of crashes — which to file upstream first, which to ignore22- Distinguishing a write primitive from a read, a null deref from controlled corruption2324## When NOT to Use2526- You need to *find* bugs in a binary without source → use angr-analyzer27- You need live runtime hooking/tracing of a process → use frida-instrument28- The crash is in managed code (Python/JS/Java) with a native stack trace already29 pointing at the line — read it directly30- You want to set up the fuzzer itself — that's harness/rig work, out of scope here3132## Quick Reference3334| Task | Command |35|------|---------|36| Reproduce + classify (file arg) | `{baseDir}/scripts/crash-triage.sh -b ./bin-asan -i poc -x "@@"` |37| Reproduce + classify (stdin) | `{baseDir}/scripts/crash-triage.sh -b ./bin-asan -i poc -m stdin` |38| Classify an existing trace | `{baseDir}/scripts/crash-triage.sh -t asan.log --label <id>` |39| Classify from stdin | `cat trace.txt \| {baseDir}/scripts/crash-triage.sh -t -` |40| JSON verdict only | `{baseDir}/scripts/crash-triage.sh -t asan.log --json` |41| Triage a directory | `for f in crashes/*; do {baseDir}/scripts/crash-triage.sh -b ./bin -i "$f" --label "$(basename "$f")"; done` |4243## Prerequisites4445- **python3** (stdlib only — no pip install needed for classification)46- For reproduction: the **target binary**, ideally an **ASAN+UBSan build**47 (`-fsanitize=address,undefined -g`), which gives the most precise verdict48- Optional: **gdb** or **lldb** for a backtrace fallback when there's no ASAN report4950## Classification Taxonomy5152The classifier maps a trace to one class. Severity and action follow from it:5354| Class | Signature | Severity | Default action |55|-------|-----------|----------|----------------|56| **memory-bug** | ASAN: `heap/stack/global-buffer-overflow`, `heap-use-after-free`, `double-free`, etc. | HIGH | file-upstream |57| **segv** | `SIGSEGV` / `EXC_BAD_ACCESS` / ASAN `SEGV`; non-null addr | HIGH | file-upstream |58| **segv** (null) | Faulting address < 0x1000 (null/small-offset deref) | MED | investigate |59| **ubsan** | `UndefinedBehaviorSanitizer` / `runtime error:` | MED–HIGH | investigate |60| **assertion** | `SIGABRT`, `__assert_fail`, `Assertion … failed`, `abort` | LOW | ignore-intended |61| **trap** | `SIGTRAP` / `EXC_BREAKPOINT` / `__builtin_trap` / `SIGILL` | MED | investigate |62| **arith** | `SIGFPE` / division by zero | MED | investigate |63| **leak** | `LeakSanitizer` / detected memory leaks | LOW | investigate |64| **hang** | timeout / no crash signal | LOW | investigate |65| **no-frames** | no usable frames or signal | UNKNOWN | investigate (re-capture) |6667**Severity nuance the verdict applies for you:**68- A **write** overflow outranks a **read** (write primitives are more exploitable).69- A **null deref** (addr near 0) is real but usually lower impact than corruption at70 a controlled address — confirm whether the pointer/offset is attacker-influenced.71- An **assertion/abort** is usually the target correctly rejecting bad input. Escalate72 only when the assert guards an invariant the input shouldn't be able to violate.73- A **trap** is often a compiler-inserted check (checked arithmetic / bounds guard),74 not raw corruption — read the top-frame source to tell which.7576## Workflow77781. **Scope.** One crash or many? For a batch, loop the directory (see Quick79 Reference) and collect verdicts before reasoning about priority.80812. **Get a trace.** Either point the tool at the binary + PoC to reproduce it, or82 feed a trace you already have with `-t`. Prefer an **ASAN build** — it converts83 a vague SIGSEGV into a precise "heap-buffer-overflow WRITE of size 4".84853. **Read the verdict.** The tool prints class / severity / top frame / action /86 reasoning. The top frame is the first *application* frame (sanitizer and libc87 interceptor frames are skipped).88894. **Confirm at the source.** Open the top-frame `file:line` and read the few lines90 around it. This is what separates "file-upstream" from "ignore-intended":91 - Is the faulting access bounded by something derived from input?92 - Is this the target's own validation rejecting malformed input (→ intended)?93 - For a trap: is it a `__builtin_trap` from a safe-math/bounds check (→ likely WAI)?94955. **De-duplicate.** Two crashes with the same top frame are probably one bug.96 Group them and report the cluster once.97986. **Output.** For a batch, produce one markdown table, severity-descending:99 `id | class | severity | top_frame | action | rationale`. Follow with a short100 paragraph: counts by class and what you'd do next.101102## Reproducing with the right build103104The single biggest lever on verdict quality is building the target with sanitizers:105106```bash107# C/C++: ASAN + UBSan with frame pointers and line info108CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \109CXXFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \110 <your normal configure/make>111```112113Then triage against that binary. Without it, you fall back to a debugger backtrace,114which still classifies the signal but can't tell a heap overflow read from a write.115116## Tone117118Be decisive. "This is a heap-buffer-overflow WRITE in `Parser::readToken` at119parser.c:996 — file upstream" beats "this could potentially be a memory issue that120may warrant further review." When something is genuinely ambiguous, say so and pick121the next step rather than hedging forever.122123## Common Mistakes124125- **Trusting a bare SIGSEGV.** A debugger-only SEGV can't distinguish null deref from126 controlled corruption. Rebuild with ASAN before declaring severity.127- **Filing every assert as a bug.** Most asserts on fuzzed input are the parser doing128 its job. Check whether the input was *supposed* to be rejectable.129- **Reporting duplicates.** Cluster by top frame first; one root cause can spray130 dozens of distinct inputs.131- **Triaging non-reproducing crashes silently.** If the PoC doesn't reproduce on a132 fresh build, say so — don't classify a stale `trace.txt` as if it were live.133134## Don't135136- Don't try to *fix* the bug from this skill — classifying and (optionally) drafting137 an upstream report is the end state.138- Don't modify the crash inputs or overwrite a captured trace without saying so.