libsoliton/fuzz_stats.sh
Kamal Tufekcic 1d99048c95
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
initial commit
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-02 23:48:10 +03:00

342 lines
12 KiB
Bash
Executable file

#!/usr/bin/env bash
# Post-run fuzzing statistics for soliton.
#
# Shows per-target and overall stats: corpus size, coverage, crashes, etc.
# If logs from fuzz_overnight.sh are available, also shows execution counts
# and throughput from the run.
# Covers both core (soliton) and CAPI (soliton_capi) fuzz targets.
#
# Usage: ./fuzz_stats.sh [--quick] [--logs <dir>]
# --quick Skip coverage measurement (no build/run, filesystem stats only)
# --logs <dir> Path to log directory (default: latest from fuzz_overnight.sh)
set -euo pipefail
QUICK=false
LOG_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--quick) QUICK=true; shift ;;
--logs) LOG_DIR="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
CORE_DIR="soliton"
CAPI_DIR="soliton_capi"
CORE_FUZZ="${CORE_DIR}/fuzz"
CAPI_FUZZ="${CAPI_DIR}/fuzz"
# Auto-find latest log directory if not specified.
if [ -z "$LOG_DIR" ] && [ -L "fuzz_logs/latest" ]; then
LOG_DIR="$(cd "fuzz_logs/latest" && pwd)"
fi
CORE_TARGETS=(
fuzz_ed25519_verify
fuzz_hybrid_verify
# fuzz_xwing_roundtrip # Excluded: 7 corpus entries after 90.5B execs, fully saturated (keygen→encap→decap, no adversarial input)
fuzz_ratchet_decrypt
fuzz_ratchet_decrypt_stateful
fuzz_ratchet_encrypt
fuzz_kex_receive_session
fuzz_kex_verify_bundle
fuzz_identity_sign_verify
# fuzz_auth_respond # Excluded: 4 corpus entries after 68B execs, fully saturated (single decap→HMAC)
fuzz_storage_decrypt_blob
# fuzz_identity_from_bytes # Excluded: 107 edges, 3 length checks only, saturates instantly
fuzz_decrypt_first_message
fuzz_storage_encrypt_blob
# fuzz_verification_phrase # Excluded: 88 edges, 29 corpus entries, trivial surface (SHA3→wordlist index)
fuzz_ratchet_roundtrip
fuzz_session_init_roundtrip
fuzz_call_derive
# fuzz_auth_verify # Excluded: 88 edges, saturated after ~15 corpus entries (single ct_eq call)
fuzz_ratchet_from_bytes_epoch
fuzz_kex_decode_receive
fuzz_dm_queue_roundtrip
fuzz_dm_queue_decrypt_blob
fuzz_argon2_params
fuzz_stream_decrypt
fuzz_stream_decrypt_at
fuzz_stream_encrypt_decrypt
fuzz_stream_encrypt_at
fuzz_ratchet_state_machine
)
CAPI_TARGETS=(
fuzz_capi_ratchet_from_bytes
fuzz_capi_storage_decrypt
fuzz_capi_decode_session_init
fuzz_capi_dm_queue_decrypt
fuzz_capi_stream_decrypt
fuzz_capi_stream_decrypt_at
fuzz_capi_stream_encrypt_at
)
HAS_LOGS=false
if [ -n "$LOG_DIR" ] && [ -d "$LOG_DIR" ]; then
HAS_LOGS=true
fi
human_size() {
local bytes=$1
if (( bytes >= 1048576 )); then
echo "$((bytes / 1048576))MB"
elif (( bytes >= 1024 )); then
echo "$((bytes / 1024))KB"
else
echo "${bytes}B"
fi
}
human_count() {
local n=$1
if (( n >= 1000000000 )); then
echo "$(awk "BEGIN {printf \"%.1fB\", $n / 1000000000}")"
elif (( n >= 1000000 )); then
echo "$(awk "BEGIN {printf \"%.1fM\", $n / 1000000}")"
elif (( n >= 1000 )); then
echo "$(awk "BEGIN {printf \"%.1fK\", $n / 1000}")"
else
echo "$n"
fi
}
# Parse a log file for libFuzzer stats.
# With -jobs=N there may be multiple stat blocks — sum executions, avg exec/s, max rss.
parse_log() {
local log_file="$1"
local stat="$2"
[ -f "$log_file" ] || { echo "0"; return; }
grep "stat::${stat}:" "$log_file" 2>/dev/null | awk -F: '{s += $NF} END {print s+0}'
}
parse_log_max() {
local log_file="$1"
local stat="$2"
[ -f "$log_file" ] || { echo "0"; return; }
grep "stat::${stat}:" "$log_file" 2>/dev/null | awk -F: '{v=$NF+0; if(v>m)m=v} END {print m+0}'
}
print_header() {
if $QUICK; then
if $HAS_LOGS; then
printf "%-32s %7s %9s %10s %9s %6s %7s %5s %4s\n" \
"Target" "Corpus" "Size" "Execs" "Exec/s" "RSS" "Crashes" "T/O" "OOM"
printf "%-32s %7s %9s %10s %9s %6s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---" "---" "---" "---"
else
printf "%-32s %7s %9s %7s %5s %4s\n" \
"Target" "Corpus" "Size" "Crashes" "T/O" "OOM"
printf "%-32s %7s %9s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---"
fi
else
if $HAS_LOGS; then
printf "%-32s %7s %9s %5s %8s %10s %9s %6s %7s %5s %4s\n" \
"Target" "Corpus" "Size" "Cov" "Features" "Execs" "Exec/s" "RSS" "Crashes" "T/O" "OOM"
printf "%-32s %7s %9s %5s %8s %10s %9s %6s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---" "---" "---" "---" "---" "---"
else
printf "%-32s %7s %9s %5s %8s %7s %5s %4s\n" \
"Target" "Corpus" "Size" "Cov" "Features" "Crashes" "T/O" "OOM"
printf "%-32s %7s %9s %5s %8s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---" "---" "---"
fi
fi
}
total_corpus=0
total_size=0
total_crashes=0
total_timeouts=0
total_ooms=0
total_execs=0
# Process one target group. Arguments: fuzz_dir fuzz_root target...
process_targets() {
local fuzz_dir="$1"
local fuzz_root="$2"
shift 2
local targets=("$@")
for target in "${targets[@]}"; do
corpus_dir="${fuzz_root}/corpus/${target}"
artifact_dir="${fuzz_root}/artifacts/${target}"
# Corpus: file count + total bytes.
if [ -d "$corpus_dir" ]; then
corpus_count=$(find "$corpus_dir" -maxdepth 1 -type f | wc -l)
corpus_bytes=$(find "$corpus_dir" -maxdepth 1 -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1}END{print s+0}')
else
corpus_count=0
corpus_bytes=0
fi
# Artifacts: crashes, timeouts, OOM (libFuzzer names them crash-*, timeout-*, oom-*).
if [ -d "$artifact_dir" ]; then
crashes=$(find "$artifact_dir" -maxdepth 1 -name 'crash-*' -type f | wc -l)
timeouts=$(find "$artifact_dir" -maxdepth 1 -name 'timeout-*' -type f | wc -l)
ooms=$(find "$artifact_dir" -maxdepth 1 -name 'oom-*' -type f | wc -l)
else
crashes=0
timeouts=0
ooms=0
fi
# Log stats (from overnight run).
execs_fmt="-"
execps_fmt="-"
rss_fmt="-"
execs_raw=0
if $HAS_LOGS; then
log_file="${LOG_DIR}/${target}.log"
if [ -f "$log_file" ]; then
execs_raw=$(parse_log "$log_file" "number_of_executed_units")
execps_raw=$(parse_log "$log_file" "average_exec_per_sec")
rss_raw=$(parse_log_max "$log_file" "peak_rss_mb")
execs_fmt=$(human_count "$execs_raw")
if (( execps_raw > 0 )); then
execps_fmt=$(human_count "$execps_raw")
else
execps_fmt="-"
fi
if (( rss_raw > 0 )); then
rss_fmt="${rss_raw}MB"
fi
fi
fi
# Coverage: replay corpus with -runs=0.
cov="-"
ft="-"
if ! $QUICK && [ "$corpus_count" -gt 0 ]; then
output=$(cd "$fuzz_dir" && cargo +nightly fuzz run "$target" \
"fuzz/corpus/${target}" -- -runs=0 -print_final_stats=1 -max_len=65536 2>&1 || true)
cov=$(echo "$output" | grep -oP 'cov: \K[0-9]+' | tail -1) || cov="-"
ft=$(echo "$output" | grep -oP 'ft: \K[0-9]+' | tail -1) || ft="-"
fi
size_fmt=$(human_size "$corpus_bytes")
if $QUICK; then
if $HAS_LOGS; then
printf "%-32s %7d %9s %10s %9s %6s %7d %5d %4d\n" \
"$target" "$corpus_count" "$size_fmt" "$execs_fmt" "$execps_fmt" "$rss_fmt" "$crashes" "$timeouts" "$ooms"
else
printf "%-32s %7d %9s %7d %5d %4d\n" \
"$target" "$corpus_count" "$size_fmt" "$crashes" "$timeouts" "$ooms"
fi
else
if $HAS_LOGS; then
printf "%-32s %7d %9s %5s %8s %10s %9s %6s %7d %5d %4d\n" \
"$target" "$corpus_count" "$size_fmt" "${cov:--}" "${ft:--}" "$execs_fmt" "$execps_fmt" "$rss_fmt" "$crashes" "$timeouts" "$ooms"
else
printf "%-32s %7d %9s %5s %8s %7d %5d %4d\n" \
"$target" "$corpus_count" "$size_fmt" "${cov:--}" "${ft:--}" "$crashes" "$timeouts" "$ooms"
fi
fi
total_corpus=$((total_corpus + corpus_count))
total_size=$((total_size + corpus_bytes))
total_crashes=$((total_crashes + crashes))
total_timeouts=$((total_timeouts + timeouts))
total_ooms=$((total_ooms + ooms))
total_execs=$((total_execs + execs_raw))
done
}
# Header
echo "soliton fuzzing statistics"
echo "============================"
if $HAS_LOGS; then
echo " Logs: ${LOG_DIR}"
fi
echo ""
print_header
echo ""
echo "Core (${#CORE_TARGETS[@]} targets)"
process_targets "$CORE_DIR" "$CORE_FUZZ" "${CORE_TARGETS[@]}"
echo ""
echo "CAPI (${#CAPI_TARGETS[@]} targets)"
process_targets "$CAPI_DIR" "$CAPI_FUZZ" "${CAPI_TARGETS[@]}"
# Totals
total_size_fmt=$(human_size "$total_size")
total_execs_fmt=$(human_count "$total_execs")
TOTAL_TARGETS=$(( ${#CORE_TARGETS[@]} + ${#CAPI_TARGETS[@]} ))
echo ""
if $QUICK; then
if $HAS_LOGS; then
printf "%-32s %7s %9s %10s %9s %6s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---" "---" "---" "---"
printf "%-32s %7d %9s %10s %9s %6s %7d %5d %4d\n" \
"TOTAL (${TOTAL_TARGETS})" "$total_corpus" "$total_size_fmt" "$total_execs_fmt" "" "" "$total_crashes" "$total_timeouts" "$total_ooms"
else
printf "%-32s %7s %9s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---"
printf "%-32s %7d %9s %7d %5d %4d\n" \
"TOTAL (${TOTAL_TARGETS})" "$total_corpus" "$total_size_fmt" "$total_crashes" "$total_timeouts" "$total_ooms"
fi
else
if $HAS_LOGS; then
printf "%-32s %7s %9s %5s %8s %10s %9s %6s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---" "---" "---" "---" "---" "---"
printf "%-32s %7d %9s %5s %8s %10s %9s %6s %7d %5d %4d\n" \
"TOTAL (${TOTAL_TARGETS})" "$total_corpus" "$total_size_fmt" "" "" "$total_execs_fmt" "" "" "$total_crashes" "$total_timeouts" "$total_ooms"
else
printf "%-32s %7s %9s %5s %8s %7s %5s %4s\n" \
"---" "---" "---" "---" "---" "---" "---" "---"
printf "%-32s %7d %9s %5s %8s %7d %5d %4d\n" \
"TOTAL (${TOTAL_TARGETS})" "$total_corpus" "$total_size_fmt" "" "" "$total_crashes" "$total_timeouts" "$total_ooms"
fi
fi
echo ""
# Glossary
echo "Glossary"
echo "--------"
echo " Corpus Accumulated inputs that each trigger unique code paths."
echo " Grows over time as the fuzzer discovers new coverage."
echo " Cov Edge coverage — number of unique code branches reached."
echo " Features Fine-grained coverage (edges + hit counts + value profiles)."
echo " Higher = more behavioral diversity explored."
echo " Execs Total inputs tested during the run (from logs)."
echo " Exec/s Throughput — inputs tested per second."
echo " RSS Peak memory usage of the fuzzer process."
echo " Crashes Inputs that caused a panic, abort, or segfault."
echo " T/O Inputs that exceeded the per-input time limit."
echo " OOM Inputs that exceeded the memory limit."
# Crash details
if (( total_crashes + total_timeouts + total_ooms > 0 )); then
echo ""
echo "Artifact details"
echo "----------------"
for fuzz_root in "$CORE_FUZZ" "$CAPI_FUZZ"; do
if [ "$fuzz_root" = "$CORE_FUZZ" ]; then
targets=("${CORE_TARGETS[@]}")
else
targets=("${CAPI_TARGETS[@]}")
fi
for target in "${targets[@]}"; do
artifact_dir="${fuzz_root}/artifacts/${target}"
[ -d "$artifact_dir" ] || continue
files=$(find "$artifact_dir" -maxdepth 1 -type f 2>/dev/null)
[ -z "$files" ] && continue
echo ""
echo " ${target}:"
echo "$files" | while read -r f; do
echo " $(basename "$f") ($(wc -c < "$f") bytes)"
done
done
done
echo ""
echo "Triage: cargo +nightly fuzz tmin <target> <artifact-file>"
fi