Testland
Browse all skills & agents

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-reference
View source

crash-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 a raw ASan / UBSan / MSan crash report and identifying the bug class.
  • Deciding whether a finding blocks a release (LIKELY-EXPLOITABLE) or is a lower-priority issue (MEDIUM or BENIGN).
  • Collapsing 20 crash artifacts into N unique bugs before filing tickets.
  • Reducing a crash input to its minimal reproducer before attaching it to a bug.

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)):

FieldLocation in outputWhat to extract
Bug class==ERROR: AddressSanitizer: <class> linee.g. heap-buffer-overflow, use-after-free
Access directionREAD / WRITE of size Ndetermines exploitability tier
Crash site#0 frame after ==ERRORsource file + line of the fault
Allocation siteafter allocated by thread T0 here:where the corrupted memory came from
Deallocation siteafter 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 outputCheck
signed integer overflow:Signed overflow; per UBSan docs
division by zeroInteger divide-by-zero
null pointer dereferenceNull pointer use
misaligned addressAlignment violation
index N out of boundsArray subscript OOB (static bounds)
load of value N is not valid for typeInvalid enum / bool load
call to function through pointer to incorrect function typeFunction-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:22

When 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 classDirectionExploitabilityRationale
heap-buffer-overflowWRITELIKELY-EXPLOITABLEAttacker-controlled write to adjacent heap; classic exploitation primitive
use-after-freeWRITELIKELY-EXPLOITABLEWrite to freed memory; allocator-reuse exploitation
double-free / invalid-freeanyLIKELY-EXPLOITABLECorrupts allocator metadata; exploitation is well-documented
heap-buffer-overflowREADMEDIUMLeaks heap contents; information disclosure
use-after-freeREADMEDIUMInformation disclosure; no write primitive directly
stack-buffer-overflowWRITEMEDIUMStack corruption; exploitability depends on stack layout
stack-buffer-overflowREADMEDIUMStack disclosure
signed integer overflowanyMEDIUMContext-dependent; may widen to a write if used as an array index
null pointer dereferenceanyBENIGNCrash-only in user-space protected-zero-page model
memory-leakanyBENIGNDoS risk only; no memory corruption
use-of-uninitialized-value (MSan)-MEDIUMInformation disclosure or branch confusion; escalate if controls a WRITE
division by zeroanyBENIGNProcess termination; no memory corruption
timeout / OOM artifactanyBENIGNDenial-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-8

Artifacts 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-a3f2c1b0

The -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>&1

The -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 prefixMeaning
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-patternWhy it failsFix
Counting raw artifact files as bug countOne bug produces many artifacts with varied inputsDeduplicate by stack-hash first
Classifying BENIGN without checking directionA heap-buffer-overflow READ is MEDIUM, not BENIGNAlways read the READ/WRITE line before classifying
Minimizing before verifying the stack-hash matchMinimized input may trigger a different code pathConfirm stack-hash matches after minimization
Treating integer-overflow as always BENIGNMay feed into an index that drives a WRITETrace the value's use before downgrading to BENIGN
Skipping -g in fuzz target buildsStack traces become raw hex; dedup still works but root cause is unattributableAlways build with -g -O1
Filing bugs on un-minimized artifactsLarge inputs slow review and bisectionRun -minimize_crash=1 before filing

Limitations

  • Exploitability is heuristic. The READ/WRITE distinction is a strong signal but not a definitive CVE assessment. A security engineer must review LIKELY-EXPLOITABLE findings before disclosure.
  • JVM and Go formats differ. This reference covers clang sanitiser output only. Jazzer (JVM) and Go native-fuzz panics use different report formats and need manual class mapping.
  • MSan origin tracking requires a full recompile. Without -fsanitize-memory-track-origins, the report names only the use site; tracing the value back to its source requires rebuilding all dependencies with MSan instrumentation (per clang.llvm.org/docs/MemorySanitizer.html (opens in new window)).
  • AFL++ crash class is not in the filename. Unlike libFuzzer, AFL++ does not encode the bug class in the artifact name; symbolization is a separate step.
  • Minimization can obscure secondary bugs. The minimized input exercises a narrower code path. Keep the original artifact alongside the minimized one.

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.