crash-triage-reference
Pure-reference catalog for manually triaging individual fuzzer crash artifacts - reading ASan, UBSan, and MSan output; classifying findings as LIKELY-EXPLOITABLE, MEDIUM, or BENIGN; deduplicating by stack-hash; and minimizing reproducers with -minimize_crash. Use when you need to understand what a specific crash means, build exploitability intuition, or manually work a small set of findings. For automated bulk triage across a full artifact directory, run automated findings triage instead.
Install with skills.sh (any agent)
npx skills add testland/qa --skill crash-triage-referencecrash-triage-reference
Overview
Pure-reference catalog for working with fuzzer crash artifacts produced by libFuzzer, AFL++, or cargo-fuzz campaigns using clang sanitisers. Covers reading crash output from ASan, UBSan, and MSan; distinguishing LIKELY-EXPLOITABLE from BENIGN findings; collapsing duplicates by stack-hash; and minimizing reproducers. These steps can be automated across a full artifact directory for bulk triage. For sanitiser build flags and compatibility, see sanitiser-integration-reference.
When to use
Reading ASan crash output
AddressSanitizer (per clang.llvm.org/docs/AddressSanitizer.html (opens in new window)) reports all memory errors through a structured output block. The ==ERROR: line always carries the bug class, and the access line carries the direction (READ or WRITE) and size.
Annotated example:
==1234==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f...
WRITE of size 4 at 0x7f... thread T0 <-- direction + size
#0 0x4015a3 in process_input src/parser.c:42:5
#1 0x4012f0 in LLVMFuzzerTestOneInput fuzz_target.cc:10:3
#2 ...
0x7f... is located 0 bytes to the right of 16-byte region [0x7f..., 0x7f...)
allocated by thread T0 here: <-- allocation site
#0 0x40e7c0 in __interceptor_malloc
#1 0x4015a3 in process_input src/parser.c:39:9
freed by thread T0 here: <-- deallocation site (UAF only)
#0 0x40e8b0 in __interceptor_free
...Key fields (per clang.llvm.org/docs/AddressSanitizer.html (opens in new window)):
| Field | Location in output | What to extract |
|---|---|---|
| Bug class | ==ERROR: AddressSanitizer: <class> line | e.g. heap-buffer-overflow, use-after-free |
| Access direction | READ / WRITE of size N | determines exploitability tier |
| Crash site | #0 frame after ==ERROR | source file + line of the fault |
| Allocation site | after allocated by thread T0 here: | where the corrupted memory came from |
| Deallocation site | after freed by thread T0 here: | present only for use-after-free |
ASan detects: out-of-bounds accesses to heap, stack, and globals; use-after-free; double-free; invalid free; memory leaks (per clang.llvm.org/docs/AddressSanitizer.html (opens in new window)). It does not produce false positives.
Reading UBSan crash output
UndefinedBehaviorSanitizer (per clang.llvm.org/docs/UndefinedBehaviorSanitizer.html (opens in new window)) uses a runtime error: prefix rather than ==ERROR::
src/math.c:17:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'The general pattern is: <file>:<line>: runtime error: <check>: <detail>.
Common UBSan check identifiers to recognize:
| Identifier in output | Check |
|---|---|
signed integer overflow: | Signed overflow; per UBSan docs |
division by zero | Integer divide-by-zero |
null pointer dereference | Null pointer use |
misaligned address | Alignment violation |
index N out of bounds | Array subscript OOB (static bounds) |
load of value N is not valid for type | Invalid enum / bool load |
call to function through pointer to incorrect function type | Function-pointer type mismatch |
UBSan's runtime is "not expected to produce false positives" (per clang.llvm.org/docs/UndefinedBehaviorSanitizer.html (opens in new window)), but its production use needs care: recovery modes that continue execution instead of aborting can mask bugs from the fuzzer. Always build with -fno-sanitize-recover=all for fuzzing (see sanitiser-integration-reference).
Reading MSan crash output
MemorySanitizer (per clang.llvm.org/docs/MemorySanitizer.html (opens in new window)) reports with a WARNING: prefix rather than ==ERROR::
WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x... in check_header src/decode.c:55
...
Uninitialized value was created by a heap allocation
#0 0x... in parse_frame src/decode.c:22When built with -fsanitize-memory-track-origins the report also shows where the uninitialised value was created and the intermediate stores it passed through. Without origins, the report names only the use site, making root cause harder to locate (per clang.llvm.org/docs/MemorySanitizer.html (opens in new window)).
MSan findings classify as MEDIUM by default (uninitialized reads rarely give an attacker write primitives), but escalate if the value flows into a branch that controls a WRITE operation.
Exploitability classification
Classify each deduplicated finding by the bug class and access direction. The access direction (READ vs WRITE) is in the ASan line immediately after ==ERROR:.
| Bug class | Direction | Exploitability | Rationale |
|---|---|---|---|
heap-buffer-overflow | WRITE | LIKELY-EXPLOITABLE | Attacker-controlled write to adjacent heap; classic exploitation primitive |
use-after-free | WRITE | LIKELY-EXPLOITABLE | Write to freed memory; allocator-reuse exploitation |
double-free / invalid-free | any | LIKELY-EXPLOITABLE | Corrupts allocator metadata; exploitation is well-documented |
heap-buffer-overflow | READ | MEDIUM | Leaks heap contents; information disclosure |
use-after-free | READ | MEDIUM | Information disclosure; no write primitive directly |
stack-buffer-overflow | WRITE | MEDIUM | Stack corruption; exploitability depends on stack layout |
stack-buffer-overflow | READ | MEDIUM | Stack disclosure |
signed integer overflow | any | MEDIUM | Context-dependent; may widen to a write if used as an array index |
null pointer dereference | any | BENIGN | Crash-only in user-space protected-zero-page model |
memory-leak | any | BENIGN | DoS risk only; no memory corruption |
use-of-uninitialized-value (MSan) | - | MEDIUM | Information disclosure or branch confusion; escalate if controls a WRITE |
division by zero | any | BENIGN | Process termination; no memory corruption |
timeout / OOM artifact | any | BENIGN | Denial-of-service risk only |
Note: LIKELY-EXPLOITABLE is a triage signal, not a CVE severity. A security engineer must confirm before disclosure.
Deduplication by stack-hash
libFuzzer saves one artifact per unique crash input. A single bug can produce dozens of artifacts with slightly different inputs. Deduplicate before counting bugs.
Stack-hash key: take the top 3 non-sanitiser frames from the symbolised #N lines of the crash report. Exclude frames whose function names contain sanitizer, interceptor, or LLVMFuzzerTestOneInput - they are harness frames, not the fault site.
# Extract top 3 meaningful frames and hash them
grep -E '^\s+#[0-9]+ 0x' /tmp/report.txt \
| grep -v 'sanitizer\|interceptor\|LLVMFuzzerTestOneInput' \
| head -3 \
| sha1sum | cut -c1-8Artifacts sharing the same 8-character stack-hash represent the same bug. Keep the smallest artifact per hash - it is the easiest reproducer to attach to a bug report.
If the binary was built without -g (no debug info), the #N lines carry only hex addresses. The hash still works for dedup within a campaign but loses file/line attribution needed for bug tickets. Always build fuzz targets with -g -O1 (per clang.llvm.org/docs/AddressSanitizer.html (opens in new window) and the libFuzzer build examples at llvm.org/docs/LibFuzzer.html (opens in new window)).
Reproducer minimization with -minimize_crash
A crash artifact produced during fuzzing is often much larger than necessary. Minimizing it reduces review time, makes root-cause analysis easier, and produces a cleaner bug-report attachment.
libFuzzer's -minimize_crash=1 flag reduces the crash input to its smallest form that still reproduces the same crash (per llvm.org/docs/LibFuzzer.html (opens in new window)):
# Minimize a single crash artifact
# -minimize_crash=1 requires a time or run budget
./fuzz_target -minimize_crash=1 \
-max_total_time=60 \
-exact_artifact_path=./minimized_crash \
./crash-a3f2c1b0
# Or bound by iteration count instead of time
./fuzz_target -minimize_crash=1 \
-runs=10000 \
-exact_artifact_path=./minimized_crash \
./crash-a3f2c1b0The -exact_artifact_path flag writes the minimized result to a single named file instead of using the default checksum-prefixed naming; -artifact_prefix can be used instead to write to a directory (per llvm.org/docs/LibFuzzer.html (opens in new window)).
For AFL++ crashes, use afl-tmin (from the AFL++ toolchain) rather than libFuzzer minimization - they use different transport formats.
After minimization, re-run the minimized artifact to confirm it still triggers the same crash class and the same stack-hash before attaching it to the bug report:
ASAN_OPTIONS=abort_on_error=1:symbolize=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
./fuzz_target -runs=1 ./minimized_crash 2>&1The -runs=1 flag re-runs the file as a single test input without fuzzing, as described in the libFuzzer options at llvm.org/docs/LibFuzzer.html (opens in new window).
Artifact naming quick-reference
libFuzzer saves artifacts with a class prefix followed by a content checksum (per llvm.org/docs/LibFuzzer.html (opens in new window)):
| Artifact prefix | Meaning |
|---|---|
crash-<sha1> | Input triggered a crash or sanitiser abort |
leak-<sha1> | Input triggered LSan memory-leak detection |
timeout-<sha1> | Input exceeded -timeout wall-clock limit |
oom-<sha1> | Input exceeded -rss_limit_mb in fork mode |
AFL++ crash artifacts land under output/default/crashes/ with filenames of the form id:N,sig:N,src:N,.... They carry the same information but require symbolization separately via the AFL++ target binary - the class is not encoded in the filename.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Counting raw artifact files as bug count | One bug produces many artifacts with varied inputs | Deduplicate by stack-hash first |
| Classifying BENIGN without checking direction | A heap-buffer-overflow READ is MEDIUM, not BENIGN | Always read the READ/WRITE line before classifying |
| Minimizing before verifying the stack-hash match | Minimized input may trigger a different code path | Confirm stack-hash matches after minimization |
| Treating integer-overflow as always BENIGN | May feed into an index that drives a WRITE | Trace the value's use before downgrading to BENIGN |
Skipping -g in fuzz target builds | Stack traces become raw hex; dedup still works but root cause is unattributable | Always build with -g -O1 |
| Filing bugs on un-minimized artifacts | Large inputs slow review and bisection | Run -minimize_crash=1 before filing |
Limitations
References
Related skills
afl-plus-plus
Author and run AFL++ - out-of-process coverage-guided fuzzer (a community fork of Google's original AFL with improved mutations and instrumentation). Covers afl-cc / afl-clang-fast instrumented build, afl-fuzz invocation, parallel master/slave (-M / -S), dictionary support (-x), QEMU mode (-Q) for binaries without source, output structure (queue / crashes / hangs), crash minimisation (afl-tmin), corpus minimisation (afl-cmin), crash filename triage, and CI integration. Use for fuzzing standalone binaries (file processors, command-line tools) where libFuzzer's in-process model doesn't fit; for cross-fuzzer corpus strategy see corpus-management-reference.
atheris-python-fuzzing
Author and run Atheris - Google's Python coverage-guided fuzzer built on libFuzzer. Covers pip installation, atheris.Setup + atheris.Fuzz invocation, TestOneInput(data: bytes) target signature, FuzzedDataProvider for structured input, instrument_imports() / instrument_func decorators for coverage instrumentation, and libFuzzer-passthrough flags (-atheris_runs, -max_total_time, -dict). Use for fuzzing Python libraries - also supports CPython native-extension fuzzing.
cargo-fuzz-rust
Author and run cargo-fuzz - Rust fuzzing via libFuzzer with cargo integration. Covers `cargo install cargo-fuzz`, `cargo fuzz init` + `cargo fuzz add {target}` for harness scaffolding, the `fuzz_target!` macro for entry-point declaration, the `Arbitrary` trait for structured input mutation, and `cargo fuzz run` invocation. Requires Rust nightly. Use for fuzz testing Rust libraries - cargo-fuzz wraps libFuzzer with native Rust ergonomics.
corpus-management-reference
Pure-reference catalog of fuzz-corpus management practices. Defines what a corpus is (seed corpus + evolved corpus saved by the fuzzer), corpus directory layout per libFuzzer / AFL++ / Go native / cargo-fuzz / OSS-Fuzz, the canonical crash-artefact naming (crash-{sha1} / leak-{sha1} / timeout-{sha1}), seed corpus construction strategies (sample-from-prod, sample-from-test-fixtures, from-spec-keywords), corpus minimisation, dictionary files, and the OSS-Fuzz integration corpus sync. Use as the corpus-discipline reference when building a fuzz target or maintaining a long-running fuzz campaign.
fuzz-tool-selector
Routes a fuzz-target authoring task to the right fuzzer for the detected language and build type. Decision tree: C/C++ → libfuzzer-cpp + afl-plus-plus; Rust → cargo-fuzz-rust (or libfuzzer-cpp via FFI); Go → go-native-fuzzing; Python → atheris-python-fuzzing; JVM → jazzer-jvm-fuzzing; closed-source binary → afl-plus-plus in QEMU mode; mature open-source project → ossfuzz-integration. Use when a project needs coverage-guided fuzzing and no fuzzer has been chosen for its language or toolchain yet.
go-native-fuzzing
Author and run Go's native fuzzing (Go 1.18+) - coverage-guided fuzzing built into the standard testing package via FuzzXxx functions. Covers f.Add seed-corpus declaration, f.Fuzz callback signature with typed parameters, testdata/fuzz/{FuzzXxx}/ directory layout for seeds + regression cases, the -fuzz flag for `go test`, and CI integration via short smoke runs. Use for fuzz testing Go libraries - Go's native approach integrates seamlessly with standard `go test` rather than requiring a separate toolchain like AFL++.
jazzer-jvm-fuzzing
Author and run Jazzer - Code Intelligence's JVM coverage-guided fuzzer built on libFuzzer. Covers Maven / Gradle / standalone JAR installation, the @FuzzTest annotation (JUnit 5 integration), typed parameter mutation (String, primitives, byte[]), built-in JVM sanitisers (SSRF / path traversal / OS command injection / deserialization gadget / ReDoS), and the JAZZER_FUZZ=1 env var to switch between regression and fuzzing modes. Use for fuzz testing Java / Kotlin libraries - particularly effective against parsing, deserialization, and HTTP-handling code.
libfuzzer-cpp
Author and run LLVM libFuzzer for C/C++ - in-process coverage-guided fuzzing. Covers harness authoring (LLVMFuzzerTestOneInput entry point), build with -fsanitize=fuzzer,address,undefined, runtime flags (-max_total_time, -runs, -dict, -fork, -workers), corpus + crash-artefact handling, and CI integration. Use for libraries / parsers / decoders in C/C++ where in-process fuzzing of a function is the right scope. Compose with ASan + UBSan from sanitiser-integration-reference and corpus discipline from corpus-management-reference.
ossfuzz-integration
Author and submit a project to Google OSS-Fuzz - the open-source continuous fuzzing service that runs libFuzzer / AFL++ / Honggfuzz campaigns on Google infrastructure 24x7. Covers the project.yaml + Dockerfile + build.sh contract, the $OUT/$WORK conventions, supported languages + sanitisers, seed-corpus + dictionary submission, the OSS-Fuzz Build Status dashboard, and the disclosure SLA (issues filed in Monorail with 90-day deadline). Use to offload long-running fuzz campaigns to dedicated infrastructure rather than self-hosting.
sanitiser-integration-reference
Pure-reference catalog of compiler sanitisers used with fuzz testing - AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan), MemorySanitizer (MSan), ThreadSanitizer (TSan), and LeakSanitizer (LSan). Explains what each detects, compatibility (can ASan + UBSan combine? - yes; ASan + MSan? - no), build flags, runtime options (ASAN_OPTIONS / UBSAN_OPTIONS env vars), and the typical ~2x slowdown per ASan. Use to pick the right sanitiser per fuzz target, configure the build, and interpret crash reports.