source2rosetta/fuzz.sh
Kamal Tufekcic c458b4cb50
All checks were successful
CI / lint (push) Successful in 17s
CI / fuzz (push) Successful in 1m52s
CI / test (push) Successful in 24s
read what the binary says about itself: names, signatures, prototypes; gen v2
2026-07-29 20:09:21 +03:00

104 lines
5.7 KiB
Bash
Executable file

#!/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 <target> <fuzz/artifacts/<target>/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