104 lines
5.7 KiB
Bash
Executable file
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
|