#!/usr/bin/env bash # Overnight fuzz run: all soliton targets in parallel. # Covers both core (soliton) and CAPI (soliton_capi) fuzz targets. # # Usage: ./fuzz_overnight.sh [hours] [workers] # hours — how long to fuzz (default: 8) # workers — libFuzzer workers per target (default: 1) # 1 worker → 32 total threads (one per target) # 2 workers → 64 total threads (two per target) set -euo pipefail if ! command -v parallel &>/dev/null; then echo "ERROR: GNU parallel is required." >&2 exit 1 fi HOURS="${1:-8}" WORKERS="${2:-1}" SECONDS_TOTAL=$((HOURS * 1)) CORE_DIR="soliton" CAPI_DIR="soliton_capi" LOG_BASE="fuzz_logs" 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 ) ALL_TARGETS=("${CORE_TARGETS[@]}" "${CAPI_TARGETS[@]}") # Create timestamped log directory + "latest" symlink for fuzz_stats.sh. # Must be absolute — parallel commands cd into different dirs before tee. LOG_DIR="$(pwd)/${LOG_BASE}/$(date +%Y-%m-%d_%H%M%S)" mkdir -p "${LOG_DIR}" ln -sfn "$(basename "${LOG_DIR}")" "${LOG_BASE}/latest" THREADS=$(( ${#ALL_TARGETS[@]} * WORKERS )) echo "Fuzzing for ${HOURS}h (${SECONDS_TOTAL}s), ${WORKERS} worker(s) per target" echo " Core targets (${#CORE_TARGETS[@]}): ${CORE_TARGETS[*]}" echo " CAPI targets (${#CAPI_TARGETS[@]}): ${CAPI_TARGETS[*]}" echo " Total CPU threads: ${THREADS}" echo " Logs: ${LOG_DIR}/" echo "---" # Ensure seed corpus exists for both core and CAPI targets. # gen_corpus writes to both soliton/fuzz/corpus/ and soliton_capi/fuzz/corpus/. # Cheap and idempotent — safe to re-run. echo "Generating seed corpus (core + CAPI)..." (cd "${CORE_DIR}/fuzz" && cargo +nightly run --bin gen_corpus --quiet) echo "---" # Build a list of "fuzz_dir target" pairs for GNU parallel. # -max_len=65536: required for targets with large minimum input (hybrid_verify needs 6541 bytes; # libFuzzer's default max_len is 4096, which would make those targets produce zero progress). # -print_final_stats=1: emit machine-parseable stats at end (for fuzz_stats.sh). # Output is tee'd to per-target log files and also shown live via parallel --lb. PAIRS=() for t in "${CORE_TARGETS[@]}"; do PAIRS+=("${CORE_DIR} ${t}") done for t in "${CAPI_TARGETS[@]}"; do PAIRS+=("${CAPI_DIR} ${t}") done printf '%s\n' "${PAIRS[@]}" | parallel --lb --colsep ' ' --tagstring '{= s/.*fuzz_// =}' \ "cd {1} && cargo +nightly fuzz run {2} -- -max_total_time=${SECONDS_TOTAL} -jobs=${WORKERS} -workers=${WORKERS} -max_len=65536 -print_final_stats=1 2>&1 | tee ${LOG_DIR}/{2}.log" echo "---" echo "Done. Logs saved to: ${LOG_DIR}/" echo "Run ./fuzz_stats.sh for summary."