#!/usr/bin/env bash # Parallel fuzz runner + stats for source2rosetta — unifies the run + report steps. # # Fans all offline-derivation fuzz targets out across the box with GNU parallel (each target getting # N libFuzzer workers), tee's per-target logs to a timestamped dir, then prints a coverage/execs/crash # table. 8 targets x 3 workers = 24 threads by default — above an 8C/16T box's thread count, so pass a # smaller `workers` there rather than trusting the default (the thread count is computed from TARGETS, # so it moves when a target is added; this comment is the part that does not). # # Usage: # ./fuzz.sh [seconds] [workers] run for `seconds` (default 60) with `workers`/target (default 3), # then print stats. # ./fuzz.sh --stats skip fuzzing, just re-print stats from the latest run + corpus. set -euo pipefail CRATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # source2rosetta/ — cargo fuzz runs from here cd "$CRATE_DIR" TARGETS=(fuzz_elf fuzz_schema fuzz_rtti fuzz_sig_abi fuzz_xref fuzz_valvetab fuzz_pulse fuzz_concmd) FUZZ_ROOT="$CRATE_DIR/fuzz" LOG_BASE="$FUZZ_ROOT/logs" # Ignore iced_x86's intentional one-time 'static decoder-table allocation (see lsan_suppressions.txt); # a real leak in our own code still fails the run. # # CAVEAT: an LSan suppression matches on SYMBOLIZED frames, so if symbolization stalls — which it can # under this script's own load, every target symbolizing at once — the frame list comes back bare and the # suppression misses. That surfaces as a spurious iced_x86 leak artifact under a decoder-heavy target # (fuzz_xref, fuzz_sig_abi). Before triaging one, replay it single-target: a false positive reports # "Suppressions used: iced_x86" and exits 0. export LSAN_OPTIONS="suppressions=$FUZZ_ROOT/lsan_suppressions.txt" STATS_ONLY=false if [[ "${1:-}" == "--stats" ]]; then STATS_ONLY=true; fi SECS="${1:-60}" WORKERS="${2:-3}" human() { # human-readable count awk -v n="$1" 'BEGIN{if(n>=1e9)printf"%.1fB",n/1e9;else if(n>=1e6)printf"%.1fM",n/1e6;else if(n>=1e3)printf"%.1fK",n/1e3;else printf"%d",n}' } parse_sum() { grep -h "stat::$2:" "$1"/*.log 2>/dev/null | awk -F: '{s+=$NF}END{print s+0}'; } parse_max() { grep -h "stat::$2:" "$1"/*.log 2>/dev/null | awk -F: '{v=$NF+0;if(v>m)m=v}END{print m+0}'; } run_fuzz() { command -v parallel >/dev/null || { echo "ERROR: GNU parallel required." >&2; exit 1; } echo "== seeding corpus ==" (cd fuzz && cargo +nightly run --bin gen_corpus --quiet) echo "== building targets ==" cargo +nightly fuzz build >/dev/null 2>&1 LOG_DIR="$LOG_BASE/$(date +%Y-%m-%d_%H%M%S)" mkdir -p "$LOG_DIR" ln -sfn "$(basename "$LOG_DIR")" "$LOG_BASE/latest" echo "== fuzzing ${#TARGETS[@]} targets x ${WORKERS} workers = $(( ${#TARGETS[@]} * WORKERS )) threads, ${SECS}s each ==" echo " logs: $LOG_DIR" # -jobs/-workers=N → N concurrent libFuzzer workers per target; -max_len=65536 so real-ELF seeds # aren't truncated to the 4 KiB default. `parallel -j` launches all targets at once. printf '%s\n' "${TARGETS[@]}" | parallel -j "${#TARGETS[@]}" --lb --tagstring '{}' \ "cargo +nightly fuzz run {} -- -max_total_time=${SECS} -jobs=${WORKERS} -workers=${WORKERS} -max_len=65536 -print_final_stats=1 >${LOG_DIR}/{}.log 2>&1; echo {} done" # libFuzzer with -jobs writes fuzz-*.log turds into the crate dir; sweep them into the log dir. mv fuzz-*.log "$LOG_DIR/" 2>/dev/null || true echo "LATEST_LOG=$LOG_DIR" } print_stats() { local log_dir="" [[ -L "$LOG_BASE/latest" ]] && log_dir="$(cd "$LOG_BASE/latest" && pwd)" echo echo "source2rosetta fuzzing stats ${log_dir:+(logs: $log_dir)}" printf '%-16s %7s %8s %7s %9s %8s %8s %5s %4s\n' Target Corpus Size Cov Execs Exec/s Crashes T/O OOM printf '%-16s %7s %8s %7s %9s %8s %8s %5s %4s\n' --- --- --- --- --- --- --- --- --- local tc=0 tx=0 tcr=0 tto=0 too=0 for t in "${TARGETS[@]}"; do local cdir="$FUZZ_ROOT/corpus/$t" adir="$FUZZ_ROOT/artifacts/$t" local n=0 bytes=0 [[ -d "$cdir" ]] && { n=$(find "$cdir" -maxdepth 1 -type f | wc -l); bytes=$(find "$cdir" -maxdepth 1 -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1}END{print s+0}'); } local cr=0 to=0 oo=0 [[ -d "$adir" ]] && { cr=$(find "$adir" -maxdepth 1 -name 'crash-*' | wc -l); to=$(find "$adir" -maxdepth 1 -name 'timeout-*' | wc -l); oo=$(find "$adir" -maxdepth 1 -name 'oom-*' | wc -l); } local execs="-" execps="-" cov="-" if [[ -n "$log_dir" && -f "$log_dir/$t.log" ]]; then execs=$(human "$(grep -h 'stat::number_of_executed_units:' "$log_dir/$t.log" 2>/dev/null | awk -F: '{s+=$NF}END{print s+0}')") execps=$(human "$(grep -h 'stat::average_exec_per_sec:' "$log_dir/$t.log" 2>/dev/null | awk -F: '{v=$NF+0;if(v>m)m=v}END{print m+0}')") fi # coverage: replay the corpus once (-runs=0). Skipped when empty. if (( n > 0 )); then cov=$(cargo +nightly fuzz run "$t" "fuzz/corpus/$t" -- -runs=0 -max_len=65536 2>&1 | grep -oP 'cov: \K[0-9]+' | tail -1 || true) cov="${cov:--}" fi printf '%-16s %7d %8s %7s %9s %8s %8d %5d %4d\n' "$t" "$n" "$(human "$bytes")B" "$cov" "$execs" "$execps" "$cr" "$to" "$oo" tc=$((tc+n)); tcr=$((tcr+cr)); tto=$((tto+to)); too=$((too+oo)) done printf '%-16s %7s %8s %7s %9s %8s %8s %5s %4s\n' --- --- --- --- --- --- --- --- --- printf '%-16s %7d %8s %7s %9s %8s %8d %5d %4d\n' "TOTAL" "$tc" "" "" "" "" "$tcr" "$tto" "$too" if (( tcr + tto + too > 0 )); then echo; echo "!! artifacts found — triage: cargo +nightly fuzz tmin /crash-...>" find "$FUZZ_ROOT/artifacts" -type f 2>/dev/null | sed 's/^/ /' else echo; echo "no crashes / timeouts / OOMs — the offline derivation held on every explored input." fi } $STATS_ONLY || run_fuzz print_stats