diff --git a/actions/cairn-zig-fuzz-afl/action.yml b/actions/cairn-zig-fuzz-afl/action.yml index ae3ca1d..ac7886d 100644 --- a/actions/cairn-zig-fuzz-afl/action.yml +++ b/actions/cairn-zig-fuzz-afl/action.yml @@ -1,5 +1,5 @@ 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=`.' +description: 'Build and run Zig AFL++ fuzz targets, reporting crashes and corpus to Cairn. Each target is built twice: `zig build fuzz -Dfuzz-target=` (AFL-instrumented) and `zig build fuzz-replay -Dfuzz-target=` (plain, for crash replay).' inputs: cairn_server: @@ -115,7 +115,30 @@ runs: echo "Building ${TARGET_COUNT} target(s)" 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 REPLAY_BINS for i in "${!TARGET_NAMES[@]}"; do FUZZ_TARGET="${TARGET_NAMES[$i]}" NUM=$((i + 1)) @@ -124,33 +147,32 @@ runs: echo "[${NUM}/${TARGET_COUNT}] Building ${FUZZ_TARGET}..." - # Each target gets its own zig-out to avoid binary name collisions. - ZIG_OUT="${WORK}/zig-out" - rm -rf "${ZIG_OUT}" - zig build fuzz -Dfuzz-target="${FUZZ_TARGET}" --prefix "${ZIG_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 + # Each target gets its own output dir to avoid binary name collisions. + # 1) AFL-instrumented binary for fuzzing. + AFL_OUT="${WORK}/afl-out" + rm -rf "${AFL_OUT}" + zig build fuzz -Dfuzz-target="${FUZZ_TARGET}" --prefix "${AFL_OUT}" + FUZZ_BIN=$(find_bin "${AFL_OUT}/bin" "afl") if [ ! -x "${FUZZ_BIN}" ]; then echo "ERROR: Fuzz binary not found or not executable: ${FUZZ_BIN}" exit 1 fi - 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 # ── Phase 2: Prepare seeds and Cairn targets (sequential, network I/O) ── @@ -326,6 +348,7 @@ runs: FUZZ_TARGET="${TARGET_NAMES[$i]}" NUM=$((i + 1)) FUZZ_BIN="${TARGET_BINS[${FUZZ_TARGET}]}" + REPLAY_BIN="${REPLAY_BINS[${FUZZ_TARGET}]}" RUN_ID="${RUN_IDS[${FUZZ_TARGET}]}" CAIRN_TARGET_ID="${CAIRN_TARGET_IDS[${FUZZ_TARGET}]}" FINDINGS="work/${FUZZ_TARGET}/findings" @@ -346,16 +369,20 @@ runs: ;; esac - # Replay the crash input to capture the stack trace. + # Replay using the non-AFL binary to get a proper stack trace. STACK_TRACE="" 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 STACK_TRACE="${REPLAY_OUTPUT}" FIRST_LINE=$(echo "${REPLAY_OUTPUT}" | grep -m1 -iE 'panic|error|fault|abort|overflow|undefined|sanitizer|SUMMARY' || true) if [ -n "${FIRST_LINE}" ]; then CRASH_MSG="${FIRST_LINE}" fi + echo " Replay (${CRASH_NAME}):" + echo "${REPLAY_OUTPUT}" | head -10 | sed 's/^/ /' + else + echo " Replay produced no output for ${CRASH_NAME}" fi echo " Uploading crash: ${CRASH_NAME}"