Merge pull request 'Fuzz replay' (#19) from replay into main

Reviewed-on: https://git.ts.mattnite.net/mattnite/cairn/pulls/19
This commit is contained in:
Matthew Knight 2026-03-06 19:54:03 +00:00
commit 965f48a3e7
1 changed files with 51 additions and 24 deletions

View File

@ -1,5 +1,5 @@
name: 'Cairn Zig Fuzz (AFL++)' name: 'Cairn Zig Fuzz (AFL++)'
description: 'Build and run Zig AFL++ fuzz targets, reporting crashes and corpus to Cairn. Each target is built via `zig build fuzz -Dfuzz-target=<name>`.' description: 'Build and run Zig AFL++ fuzz targets, reporting crashes and corpus to Cairn. Each target is built twice: `zig build fuzz -Dfuzz-target=<name>` (AFL-instrumented) and `zig build fuzz-replay -Dfuzz-target=<name>` (plain, for crash replay).'
inputs: inputs:
cairn_server: cairn_server:
@ -115,7 +115,30 @@ runs:
echo "Building ${TARGET_COUNT} target(s)" echo "Building ${TARGET_COUNT} target(s)"
echo "==========================================" echo "=========================================="
# Helper to find the single executable in a bin/ directory.
find_bin() {
local BIN_DIR="$1"
local LABEL="$2"
if [ -n "${FUZZ_BINARY}" ]; then
echo "${BIN_DIR}/${FUZZ_BINARY}"
return
fi
local BIN
BIN=$(find "${BIN_DIR}" -maxdepth 1 -type f -executable)
local COUNT
COUNT=$(echo "${BIN}" | wc -l)
if [ "${COUNT}" -eq 0 ] || [ -z "${BIN}" ]; then
echo "ERROR: No executable found in ${BIN_DIR} (${LABEL})" >&2
return 1
elif [ "${COUNT}" -gt 1 ]; then
echo "ERROR: Multiple executables in ${BIN_DIR} (${LABEL}), specify fuzz_binary input" >&2
return 1
fi
echo "${BIN}"
}
declare -A TARGET_BINS declare -A TARGET_BINS
declare -A REPLAY_BINS
for i in "${!TARGET_NAMES[@]}"; do for i in "${!TARGET_NAMES[@]}"; do
FUZZ_TARGET="${TARGET_NAMES[$i]}" FUZZ_TARGET="${TARGET_NAMES[$i]}"
NUM=$((i + 1)) NUM=$((i + 1))
@ -124,33 +147,32 @@ runs:
echo "[${NUM}/${TARGET_COUNT}] Building ${FUZZ_TARGET}..." echo "[${NUM}/${TARGET_COUNT}] Building ${FUZZ_TARGET}..."
# Each target gets its own zig-out to avoid binary name collisions. # Each target gets its own output dir to avoid binary name collisions.
ZIG_OUT="${WORK}/zig-out" # 1) AFL-instrumented binary for fuzzing.
rm -rf "${ZIG_OUT}" AFL_OUT="${WORK}/afl-out"
zig build fuzz -Dfuzz-target="${FUZZ_TARGET}" --prefix "${ZIG_OUT}" rm -rf "${AFL_OUT}"
zig build fuzz -Dfuzz-target="${FUZZ_TARGET}" --prefix "${AFL_OUT}"
# Locate fuzz binary.
if [ -n "${FUZZ_BINARY}" ]; then
FUZZ_BIN="${ZIG_OUT}/bin/${FUZZ_BINARY}"
else
FUZZ_BIN=$(find "${ZIG_OUT}/bin" -maxdepth 1 -type f -executable)
BIN_COUNT=$(echo "${FUZZ_BIN}" | wc -l)
if [ "${BIN_COUNT}" -eq 0 ] || [ -z "${FUZZ_BIN}" ]; then
echo "ERROR: No executable found in ${ZIG_OUT}/bin/"
exit 1
elif [ "${BIN_COUNT}" -gt 1 ]; then
echo "ERROR: Multiple executables in ${ZIG_OUT}/bin/, specify fuzz_binary input"
exit 1
fi
fi
FUZZ_BIN=$(find_bin "${AFL_OUT}/bin" "afl")
if [ ! -x "${FUZZ_BIN}" ]; then if [ ! -x "${FUZZ_BIN}" ]; then
echo "ERROR: Fuzz binary not found or not executable: ${FUZZ_BIN}" echo "ERROR: Fuzz binary not found or not executable: ${FUZZ_BIN}"
exit 1 exit 1
fi fi
TARGET_BINS["${FUZZ_TARGET}"]="${FUZZ_BIN}" TARGET_BINS["${FUZZ_TARGET}"]="${FUZZ_BIN}"
echo " Built: ${FUZZ_BIN}" echo " AFL binary: ${FUZZ_BIN}"
# 2) Plain binary for crash replay (no AFL instrumentation).
REPLAY_OUT="${WORK}/replay-out"
rm -rf "${REPLAY_OUT}"
zig build fuzz-replay -Dfuzz-target="${FUZZ_TARGET}" --prefix "${REPLAY_OUT}"
REPLAY_BIN=$(find_bin "${REPLAY_OUT}/bin" "replay")
if [ ! -x "${REPLAY_BIN}" ]; then
echo "ERROR: Replay binary not found or not executable: ${REPLAY_BIN}"
exit 1
fi
REPLAY_BINS["${FUZZ_TARGET}"]="${REPLAY_BIN}"
echo " Replay binary: ${REPLAY_BIN}"
done done
# ── Phase 2: Prepare seeds and Cairn targets (sequential, network I/O) ── # ── Phase 2: Prepare seeds and Cairn targets (sequential, network I/O) ──
@ -326,6 +348,7 @@ runs:
FUZZ_TARGET="${TARGET_NAMES[$i]}" FUZZ_TARGET="${TARGET_NAMES[$i]}"
NUM=$((i + 1)) NUM=$((i + 1))
FUZZ_BIN="${TARGET_BINS[${FUZZ_TARGET}]}" FUZZ_BIN="${TARGET_BINS[${FUZZ_TARGET}]}"
REPLAY_BIN="${REPLAY_BINS[${FUZZ_TARGET}]}"
RUN_ID="${RUN_IDS[${FUZZ_TARGET}]}" RUN_ID="${RUN_IDS[${FUZZ_TARGET}]}"
CAIRN_TARGET_ID="${CAIRN_TARGET_IDS[${FUZZ_TARGET}]}" CAIRN_TARGET_ID="${CAIRN_TARGET_IDS[${FUZZ_TARGET}]}"
FINDINGS="work/${FUZZ_TARGET}/findings" FINDINGS="work/${FUZZ_TARGET}/findings"
@ -346,16 +369,20 @@ runs:
;; ;;
esac esac
# Replay the crash input to capture the stack trace. # Replay using the non-AFL binary to get a proper stack trace.
STACK_TRACE="" STACK_TRACE=""
CRASH_MSG="AFL++ crash (${FUZZ_TARGET}): ${CRASH_NAME}" CRASH_MSG="AFL++ crash (${FUZZ_TARGET}): ${CRASH_NAME}"
REPLAY_OUTPUT=$(timeout 10 "${FUZZ_BIN}" < "${crash_file}" 2>&1 || true) REPLAY_OUTPUT=$(timeout 10 "${REPLAY_BIN}" < "${crash_file}" 2>&1 || true)
if [ -n "${REPLAY_OUTPUT}" ]; then if [ -n "${REPLAY_OUTPUT}" ]; then
STACK_TRACE="${REPLAY_OUTPUT}" STACK_TRACE="${REPLAY_OUTPUT}"
FIRST_LINE=$(echo "${REPLAY_OUTPUT}" | grep -m1 -iE 'panic|error|fault|abort|overflow|undefined|sanitizer|SUMMARY' || true) FIRST_LINE=$(echo "${REPLAY_OUTPUT}" | grep -m1 -iE 'panic|error|fault|abort|overflow|undefined|sanitizer|SUMMARY' || true)
if [ -n "${FIRST_LINE}" ]; then if [ -n "${FIRST_LINE}" ]; then
CRASH_MSG="${FIRST_LINE}" CRASH_MSG="${FIRST_LINE}"
fi fi
echo " Replay (${CRASH_NAME}):"
echo "${REPLAY_OUTPUT}" | head -10 | sed 's/^/ /'
else
echo " Replay produced no output for ${CRASH_NAME}"
fi fi
echo " Uploading crash: ${CRASH_NAME}" echo " Uploading crash: ${CRASH_NAME}"