Gunnar

nordisk/heimdall

scripts/fuzz.sh

#!/bin/sh
# SPDX-License-Identifier: AGPL-3.0-only
# Heimdall is a defensive sandbox. This script fuzzes Heimdall's own parsers
# (the cargo-fuzz targets in fuzz/) and nothing else.
#
# Run it from the repo root inside the CI image (ci/ci.Containerfile, which
# pins the nightly and cargo-fuzz):
#   podman run --rm -v "$PWD":/src -w /src \
#     localhost/nordisk-cargo-toolchain:heim-ci scripts/fuzz.sh [target ...]
#
# Every target in fuzz/ (or the ones named) runs for FUZZ_SECONDS each
# (default 1800), at most FUZZ_JOBS targets at a time (default 4).
# Corpus: fuzz/corpus/<target>, git-ignored, seeded from the checked-in
#   fuzz/seeds/<target>; it grows across runs. Only small hand-picked seeds go
#   into git, never the grown corpus.
# Crashes: fuzz/artifacts/<target>/ (git-ignored). Logs: fuzz/logs/<target>.log.
# FUZZ_TARGET_DIR sets cargo's target dir (default fuzz/target).
# Exit status 1 when any target found a crash, timeout or leak.
set -eu
cd "$(dirname "$0")/.."

secs=${FUZZ_SECONDS:-1800}
jobs=${FUZZ_JOBS:-4}
maxlen=${FUZZ_MAX_LEN:-65536}
tdir=${FUZZ_TARGET_DIR:-fuzz/target}

if [ $# -gt 0 ]; then
  targets="$*"
else
  targets=$(cargo +nightly fuzz list --fuzz-dir fuzz)
fi

echo "== build: $targets"
cargo +nightly fuzz build --fuzz-dir fuzz --target-dir "$tdir" -O

mkdir -p fuzz/logs
run_one() {
  t=$1
  mkdir -p "fuzz/corpus/$t" "fuzz/artifacts/$t"
  if [ -d "fuzz/seeds/$t" ]; then
    cp -n "fuzz/seeds/$t"/* "fuzz/corpus/$t"/ 2>/dev/null || true
  fi
  echo "== run $t for ${secs}s"
  cargo +nightly fuzz run --fuzz-dir fuzz --target-dir "$tdir" -O "$t" \
    "fuzz/corpus/$t" -- -max_total_time="$secs" -max_len="$maxlen" \
    -print_final_stats=1 -timeout=10 -rss_limit_mb=2048 \
    >"fuzz/logs/$t.log" 2>&1
}

pids=""
running=0
status=0
for t in $targets; do
  if [ "$running" -ge "$jobs" ]; then
    set -- $pids
    wait "$1" || status=1
    shift
    pids="$*"
    running=$((running - 1))
  fi
  run_one "$t" &
  pids="$pids $!"
  running=$((running + 1))
done
for p in $pids; do wait "$p" || status=1; done

echo
printf '%-22s %12s %8s %8s %8s %8s\n' target execs secs corpus cov crashes
for t in $targets; do
  log="fuzz/logs/$t.log"
  execs=$(sed -n 's/^stat::number_of_executed_units: *//p' "$log" | tail -1)
  took=$(sed -n 's/^stat::.*total_time.*: *//p' "$log" | tail -1)
  cov=$(grep -o 'cov: [0-9]*' "$log" | tail -1 | cut -d' ' -f2)
  corp=$(find "fuzz/corpus/$t" -type f | wc -l)
  crashes=$(find "fuzz/artifacts/$t" -type f | wc -l)
  printf '%-22s %12s %8s %8s %8s %8s\n' "$t" "${execs:-?}" "${took:-$secs}" \
    "$corp" "${cov:-?}" "$crashes"
  [ "$crashes" -eq 0 ] || status=1
done
exit "$status"