fuzz/package_crash.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -e
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
print_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
print_error() { echo -e "${RED}[ERROR]${NC} $*"; }
print_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
usage() {
echo "Usage: $0 <crash_id> [crashes_dir]"
echo ""
echo "Packages a crash and its RECORD files into a self-contained archive"
echo "that can be sent to another developer for reproduction."
echo ""
echo "Arguments:"
echo " crash_id Crash ID (e.g. 000000)"
echo " crashes_dir Path to crashes directory (default: fuzz/artifacts/resp/default/crashes)"
echo ""
echo "Example:"
echo " $0 000000"
echo " $0 000001 /path/to/crashes"
exit 1
}
if [[ $# -lt 1 ]]; then
usage
fi
CRASH_ID="$1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CRASHES_DIR="${2:-$SCRIPT_DIR/artifacts/resp/default/crashes}"
if [[ ! -d "$CRASHES_DIR" ]]; then
print_error "Crashes directory not found: $CRASHES_DIR"
exit 1
fi
# Find the crash input file
CRASH_FILE=$(find "$CRASHES_DIR" -maxdepth 1 -name "id:${CRASH_ID},*" ! -name "RECORD:*" | head -1)
if [[ -z "$CRASH_FILE" ]]; then
print_error "Crash input not found for id:${CRASH_ID} in $CRASHES_DIR"
exit 1
fi
# AFL numbers RECORD sets by crash event, counting unsaved calibration/trim aborts too, while the
# crash file's id: counts only saved crashes — so RECORD:${CRASH_ID} is usually another crash's
# history. The ring saves every testcase before running it, so the crash's own set is the one whose
# newest (highest-cnt) record is byte-identical to the crash input; pair by that.
declare -A NEWEST_REC
while IFS= read -r f; do
base=${f##*/}
ev=${base#RECORD:}; ev=${ev%%,*}
cur=${NEWEST_REC[$ev]:-}
if [[ -z "$cur" ]] || (( 10#${base##*,cnt:} > 10#${cur##*,cnt:} )); then
NEWEST_REC[$ev]=$base
fi
done < <(find "$CRASHES_DIR" -maxdepth 1 -name 'RECORD:*,cnt:*')
RECORD_EVENT=""
MATCHES=0
for ev in $(printf '%s\n' "${!NEWEST_REC[@]}" | sort -n); do
if cmp -s "$CRASHES_DIR/${NEWEST_REC[$ev]}" "$CRASH_FILE"; then
MATCHES=$((MATCHES + 1))
if [[ -z "$RECORD_EVENT" ]]; then
RECORD_EVENT="$ev"
fi
fi
done
ARCHIVE_NAME="crash-${CRASH_ID}"
TMPDIR=$(mktemp -d)
DEST="$TMPDIR/$ARCHIVE_NAME"
mkdir -p "$DEST/crashes"
print_info "Packaging crash ${CRASH_ID}..."
print_info "Crash input: $(basename "$CRASH_FILE")"
# Copy the crash input, then this crash's RECORD history renamed to RECORD:${CRASH_ID} so the
# bundled replay (which keys off the crash id) finds it; the cnt suffix is kept so replay order is
# unchanged. The set's newest record is byte-identical to the crash input, and replay sends the
# crash input on its own after the records, so drop that newest record to avoid replaying it twice.
cp "$CRASH_FILE" "$DEST/crashes/"
RECORD_COUNT=0
if [[ -n "$RECORD_EVENT" ]]; then
if [[ $MATCHES -gt 1 ]]; then
print_warn "The crash input is the newest record of ${MATCHES} sets whose earlier inputs"
print_warn "differ; their histories cannot be told apart. Using set ${RECORD_EVENT} — replay"
print_warn "may rebuild a different state than the fuzz run."
fi
NEWEST_BASE="${NEWEST_REC[$RECORD_EVENT]}"
while IFS= read -r rec; do
base=${rec##*/}
if [[ "$base" == "$NEWEST_BASE" ]]; then
continue
fi
cp "$rec" "$DEST/crashes/RECORD:${CRASH_ID},${base#*,}"
RECORD_COUNT=$((RECORD_COUNT + 1))
done < <(find "$CRASHES_DIR" -maxdepth 1 -name "RECORD:${RECORD_EVENT},cnt:*")
print_info "RECORD files: ${RECORD_COUNT} (from set ${RECORD_EVENT}; crash input sent separately)"
else
print_warn "No RECORD set matches the crash input by content; its true history was not saved"
print_warn "(an unsaved abort reused its number). Packaging the crash input alone."
fi
# Copy replay_crash.py
cp "$SCRIPT_DIR/replay_crash.py" "$DEST/"
# Copy repro.env — contains exact Dragonfly flags + memory limit used during fuzzing.
# triage_crashes.sh reads this to start Dragonfly identically to the fuzz run.
# repro.env lives one level above the fuzzer instance dir (i.e. OUTPUT_DIR):
# crashes_dir = .../artifacts/<target>/default/crashes
# repro.env = .../artifacts/<target>/repro.env
REPRO_ENV="$(dirname "$(dirname "$CRASHES_DIR")")/repro.env"
GUESSED_ARCHIVE=0
if [[ -f "$REPRO_ENV" ]]; then
cp "$REPRO_ENV" "$DEST/"
grep -q '^GUESSED=1' "$REPRO_ENV" && GUESSED_ARCHIVE=1
print_info "Reproduction environment: repro.env included"
else
# Without the run's repro.env the archive would replay under guessed flags and the wrong
# protocol. Write one from the run_fuzzer.sh defaults; the protocol comes from the
# artifacts path (fuzz/artifacts/<protocol>/...) or from PROTOCOL in the environment.
# The artifacts path is authoritative; PROTOCOL only settles an ambiguous path.
PATH_PROTOCOL=""
[[ "$CRASHES_DIR" == */artifacts/memcache/* ]] && PATH_PROTOCOL=memcache
[[ "$CRASHES_DIR" == */artifacts/resp/* ]] && PATH_PROTOCOL=resp
if [[ -n "${PROTOCOL:-}" && "$PROTOCOL" != "resp" && "$PROTOCOL" != "memcache" ]]; then
print_error "PROTOCOL must be 'resp' or 'memcache', got: $PROTOCOL"
rm -rf "$TMPDIR"
exit 1
fi
if [[ -n "$PATH_PROTOCOL" && -n "${PROTOCOL:-}" && "$PROTOCOL" != "$PATH_PROTOCOL" ]]; then
print_error "PROTOCOL=$PROTOCOL contradicts the artifacts path ($PATH_PROTOCOL): $CRASHES_DIR"
rm -rf "$TMPDIR"
exit 1
fi
GUESSED_PROTOCOL="${PATH_PROTOCOL:-${PROTOCOL:-}}"
if [[ -z "$GUESSED_PROTOCOL" ]]; then
print_error "repro.env not found at $REPRO_ENV and the protocol cannot be inferred from" \
"$CRASHES_DIR; set PROTOCOL=resp|memcache"
rm -rf "$TMPDIR"
exit 1
fi
GUESSED_ARCHIVE=1
print_warn "repro.env not found at $REPRO_ENV — writing one from the run_fuzzer.sh defaults (protocol: $GUESSED_PROTOCOL)"
{
echo "# Dragonfly reproduction environment — generated by package_crash.sh from the"
echo "# run_fuzzer.sh defaults because the fuzz run's repro.env was missing."
echo "# Start the server on an EMPTY --dir: the fuzzer clears it on every restart."
echo "GUESSED=1"
echo "PROTOCOL=${GUESSED_PROTOCOL}"
echo "MEM_LIMIT_KB=$((4096 * 1024))"
echo "--port=6379"
echo "--logtostderr"
echo "--minloglevel=2"
echo "--proactor_threads=1"
echo "--dbfilename="
echo "--omit_basic_usage"
echo "--restricted_commands=SHUTDOWN,DEBUG,FLUSHALL,FLUSHDB"
echo "--max_bulk_len=1048576"
[[ "$GUESSED_PROTOCOL" == "memcache" ]] && echo "--memcached_port=11211"
} > "$DEST/repro.env"
fi
# A surviving replay proves a false positive only if it rebuilt the fuzz-run state. Record in the
# archive when that cannot be guaranteed — the history is missing (no set matched) or ambiguous
# (several sets share this crash's final input but differ earlier) — so the recipient's triage
# reports INCONCLUSIVE instead of a confident false positive (the stdout warnings above are not shipped).
if [[ -z "$RECORD_EVENT" ]]; then
echo "HISTORY_MISSING=1" >> "$DEST/repro.env"
elif [[ $MATCHES -gt 1 ]]; then
echo "HISTORY_AMBIGUOUS=${MATCHES}" >> "$DEST/repro.env"
fi
REPLAY_PORT=6379
MODE_HINT="resp"
if [[ -f "$DEST/repro.env" ]]; then
_mc_port=$(grep '^--memcached_port=' "$DEST/repro.env" | cut -d= -f2 || true)
if [[ -n "$_mc_port" ]]; then
REPLAY_PORT="$_mc_port"
MODE_HINT="memcache"
else
_resp_port=$(grep '^--port=' "$DEST/repro.env" | cut -d= -f2 || true)
REPLAY_PORT="${_resp_port:-6379}"
fi
fi
# Create archive
OUTPUT="$(pwd)/${ARCHIVE_NAME}.tar.gz"
tar -czf "$OUTPUT" -C "$TMPDIR" "$ARCHIVE_NAME"
rm -rf "$TMPDIR"
SIZE=$(du -h "$OUTPUT" | cut -f1)
print_info "Archive created: ${OUTPUT} (${SIZE})"
echo ""
echo "To reproduce:"
echo " 1. Extract the archive:"
echo " tar xzf ${ARCHIVE_NAME}.tar.gz && cd ${ARCHIVE_NAME}"
if [[ $GUESSED_ARCHIVE -eq 1 ]]; then
echo " 2. Start a plain (non-AFL) Dragonfly with the DEFAULT flags in repro.env (the fuzz run's"
echo " own flags were not available; a surviving server is inconclusive)"
else
echo " 2. Start a plain (non-AFL) Dragonfly with the exact flags from the fuzz run (repro.env)"
fi
echo " on an empty --dir:"
echo " MEM_KB=\$(grep '^MEM_LIMIT_KB=' repro.env | cut -d= -f2)"
echo " readarray -t DF_FLAGS < <(grep '^--' repro.env)"
echo " WORK=\$(mktemp -d) # empty --dir; also the cwd, where a relative --tiered_prefix lands"
echo " (cd \"\$WORK\" && ulimit -v \"\$MEM_KB\" && exec <absolute-path-to-dragonfly> \"\${DF_FLAGS[@]}\" --dir=\"\$WORK\" --bind=127.0.0.1) &"
echo " DF_PID=\$! # loopback only: the replay server has no authentication"
echo " 3. Replay:"
if [[ "$MODE_HINT" == "memcache" ]]; then
echo " python3 replay_crash.py crashes ${CRASH_ID} 127.0.0.1 ${REPLAY_PORT} --protocol memcache --pid \"\$DF_PID\""
else
echo " python3 replay_crash.py crashes ${CRASH_ID} 127.0.0.1 ${REPLAY_PORT} --pid \"\$DF_PID\""
fi
echo " 4. Stop the server and remove its directory:"
echo " kill \"\$DF_PID\"; wait \"\$DF_PID\" 2>/dev/null; rm -rf \"\$WORK\""
echo ""
echo "Or use the triage script to reproduce all crashes from a zip:"
echo " ./fuzz/triage_crashes.sh <path-to-dragonfly> ${MODE_HINT:-resp} <crashes.zip>"