| #!/bin/bash | |
| # Run libFuzzer targets with persistent corpus and dictionary management. | |
| # | |
| # Usage: iree-bazel-fuzz [options] <target> [-- fuzzer-args...] | |
| # | |
| # Examples: | |
| # iree-bazel-fuzz //runtime/src/iree/base/internal:unicode_fuzz | |
| # iree-bazel-fuzz //path/to:target -- -max_total_time=60 -jobs=8 | |
| # iree-bazel-fuzz -m //path/to:target # minimize corpus first | |
| set -e | |
| # Source shared library. | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| source "${SCRIPT_DIR}/iree-bazel-lib" | |
| iree_bazel_init "iree-bazel-fuzz" | |
| # Expand combined short flags (e.g., -nv -> -n -v). | |
| eval "set -- $(iree_expand_combined_flags "$@")" | |
| # Fuzz-specific globals. | |
| FUZZ_CACHE_DIR="${IREE_FUZZ_CACHE:-${HOME}/.cache/iree-fuzz-cache}" | |
| FUZZ_CORPUS_DIR="${IREE_FUZZ_CORPUS:-${HOME}/.cache/iree-fuzz-corpus}" | |
| FUZZ_MINIMIZE=0 | |
| FUZZ_DICT_ONLY=0 | |
| FUZZ_MIN_USES=10 # Minimum usage count for dict entries | |
| FUZZ_AUTO_MINIMIZE=1000 # Auto-minimize when corpus exceeds this count | |
| show_help() { | |
| cat << 'EOF' | |
| iree-bazel-fuzz - Run libFuzzer targets with persistent corpus/dict | |
| USAGE | |
| iree-bazel-fuzz [options] <target> [-- fuzzer-args...] | |
| OPTIONS | |
| -n, --dry_run Show command without executing | |
| -v, --verbose Show command before executing | |
| -m, --minimize Minimize corpus with set_cover_merge before fuzzing | |
| -d, --dict_only Update dict from corpus without new fuzzing | |
| -h, --help Show this help | |
| NOTE: Short flags can be combined: -nv is equivalent to -n -v | |
| ARGUMENTS | |
| target Bazel fuzz target (required) | |
| fuzzer-args Arguments passed to libFuzzer (after --) | |
| EXAMPLES | |
| # Basic fuzzing (Ctrl+C to stop) | |
| iree-bazel-fuzz //runtime/src/iree/base/internal:unicode_fuzz | |
| # Time-limited with parallel workers | |
| iree-bazel-fuzz //path/to:target -- -max_total_time=60 -jobs=8 | |
| # Minimize corpus before fuzzing | |
| iree-bazel-fuzz -m //path/to:target | |
| # Just update dictionary from existing corpus | |
| iree-bazel-fuzz -d //path/to:target | |
| # Overnight fuzzing: all fuzz targets under a path (Ctrl+C to stop) | |
| iree-bazel-fuzz //runtime/src/iree/tokenizer/... -- -jobs=128 | |
| # Time-limited multi-target fuzzing | |
| iree-bazel-fuzz //runtime/... -- -jobs=64 -max_total_time=3600 | |
| CORPUS & DICTIONARY | |
| Corpus: ~/.cache/iree-fuzz-cache/<relative-path>/<target>/corpus/ | |
| Artifacts: ~/.cache/iree-fuzz-cache/<relative-path>/<target>/artifacts/ | |
| Dictionary: ~/.cache/iree-fuzz-corpus/<relative-path>/<target>.dict | |
| Paths mirror the source tree structure. Artifacts (crash-*, slow-unit-*) | |
| persist across runs. Dictionary entries (Uses > 10) are auto-appended. | |
| Corpus is auto-minimized when it exceeds 1000 files. | |
| COMMON FUZZER FLAGS | |
| -max_total_time=N Stop after N seconds | |
| -jobs=N Run N parallel fuzzing jobs | |
| -workers=M Use M worker processes (default: min(jobs, cores/2)) | |
| -dict=file Load additional dictionary | |
| SEE ALSO | |
| iree-bazel-build, iree-bazel-run, iree-bazel-test | |
| EOF | |
| } | |
| # Extract target name from bazel label (e.g., "//foo/bar:baz" -> "baz") | |
| get_target_name() { | |
| local target="${1}" | |
| echo "${target##*:}" | |
| } | |
| # Get the repo-relative directory for a target (e.g., "runtime/src/iree/base"). | |
| # Uses bazel query to find source file location. | |
| get_target_relative_dir() { | |
| local target="${1}" | |
| local BAZEL_BIN | |
| BAZEL_BIN=$(iree_get_bazel_command) | |
| # Query for source files of this target. | |
| local src_location | |
| src_location=$("${BAZEL_BIN}" query "labels(srcs, ${target})" --output=location 2>/dev/null | head -1) | |
| if [[ -n "${src_location}" ]]; then | |
| # Extract directory from "path/to/file.cc:1:1" and make it relative. | |
| local src_file="${src_location%%:*}" | |
| local src_dir | |
| src_dir=$(dirname "${src_file}") | |
| # Make path relative to worktree root. | |
| echo "${src_dir#${IREE_BAZEL_WORKTREE_DIR}/}" | |
| fi | |
| } | |
| # Get the dict file path for a target in the fuzz-corpus directory. | |
| # Returns path even if file doesn't exist yet (for creation). | |
| find_dict_file() { | |
| local target="${1}" | |
| local target_name | |
| target_name=$(get_target_name "${target}") | |
| local relative_dir | |
| relative_dir=$(get_target_relative_dir "${target}") | |
| if [[ -n "${relative_dir}" ]]; then | |
| local dict_dir="${FUZZ_CORPUS_DIR}/${relative_dir}" | |
| mkdir -p "${dict_dir}" | |
| echo "${dict_dir}/${target_name}.dict" | |
| fi | |
| } | |
| # Parse recommended dictionary from fuzzer output. | |
| # Filters to entries with usage count >= threshold. | |
| # Converts octal escapes (\NNN) to hex escapes (\xNN) for libFuzzer compatibility. | |
| parse_dict_entries() { | |
| local output_file="${1}" | |
| local min_uses="${2}" | |
| # Extract lines between "Recommended dictionary" and "End of recommended" | |
| # Format: "\NNN\NNN" # Uses: NNN (libFuzzer outputs octal, but only parses hex) | |
| sed -n '/^###### Recommended dictionary/,/^###### End of recommended/p' "${output_file}" \ | |
| | grep -E '^"' \ | |
| | while IFS= read -r line; do | |
| # Extract usage count | |
| local uses | |
| uses=$(echo "${line}" | grep -oE 'Uses: [0-9]+' | grep -oE '[0-9]+') | |
| if [[ -n "${uses}" ]] && [[ "${uses}" -ge "${min_uses}" ]]; then | |
| # Output just the string part (before #), converting octal to hex. | |
| # \NNN (octal) -> \xNN (hex) | |
| echo "${line}" | sed 's/ *#.*//' | perl -pe 's/\\([0-7]{3})/sprintf("\\x%02x", oct($1))/ge' | |
| fi | |
| done | |
| } | |
| # Merge new dict entries into dict file (creates file if needed). | |
| merge_dict_entries() { | |
| local dict_file="${1}" | |
| local new_entries="${2}" | |
| local temp_file | |
| local valid_entries | |
| if [[ -z "${new_entries}" ]]; then | |
| iree_debug "No new dictionary entries to add" | |
| return 0 | |
| fi | |
| temp_file=$(mktemp) | |
| valid_entries=$(mktemp) | |
| trap "rm -f '${temp_file}' '${valid_entries}'" RETURN | |
| # Filter new entries to only valid ones (start with " and end with "). | |
| # Use printf to avoid bash escape interpretation. | |
| printf '%s\n' "${new_entries}" | while IFS= read -r entry; do | |
| # Validate: must start with " and end with " | |
| if [[ "${entry}" =~ ^\".*\"$ ]]; then | |
| printf '%s\n' "${entry}" | |
| else | |
| iree_debug "Skipping malformed dict entry: ${entry}" | |
| fi | |
| done > "${valid_entries}" | |
| if [[ ! -s "${valid_entries}" ]]; then | |
| iree_debug "No valid dictionary entries to add" | |
| return 0 | |
| fi | |
| # Combine existing entries (if any) with new entries, sort, dedupe. | |
| { | |
| grep -E '^"' "${dict_file}" 2>/dev/null || true | |
| cat "${valid_entries}" | |
| } | sort -u > "${temp_file}" | |
| local old_count new_count added_count | |
| old_count=$(grep -cE '^"' "${dict_file}" 2>/dev/null || true) | |
| old_count=${old_count:-0} | |
| new_count=$(wc -l < "${temp_file}") | |
| added_count=$((new_count - old_count)) | |
| if [[ "${added_count}" -gt 0 ]]; then | |
| # Preserve comments from original file (if exists), then add sorted entries. | |
| { | |
| grep -E '^#|^$' "${dict_file}" 2>/dev/null || true | |
| cat "${temp_file}" | |
| } > "${dict_file}.new" | |
| mv "${dict_file}.new" "${dict_file}" | |
| iree_info "Added ${added_count} new dictionary entries to ${dict_file}" | |
| else | |
| iree_debug "No new unique dictionary entries" | |
| fi | |
| } | |
| # Run corpus minimization using set_cover_merge. | |
| minimize_corpus() { | |
| local binary="${1}" | |
| local corpus_dir="${2}" | |
| local corpus_count | |
| corpus_count=$(ls -1 "${corpus_dir}" 2>/dev/null | wc -l) | |
| if [[ "${corpus_count}" -eq 0 ]]; then | |
| iree_debug "Corpus is empty, nothing to minimize" | |
| return 0 | |
| fi | |
| iree_info "Minimizing corpus (${corpus_count} files)..." | |
| local temp_corpus | |
| temp_corpus=$(mktemp -d) | |
| trap "rm -rf '${temp_corpus}'" RETURN | |
| if "${binary}" -set_cover_merge=1 "${temp_corpus}" "${corpus_dir}" 2>/dev/null; then | |
| local new_count | |
| new_count=$(ls -1 "${temp_corpus}" | wc -l) | |
| # Use find -delete to avoid "argument list too long" with large corpora. | |
| find "${corpus_dir}" -mindepth 1 -delete | |
| mv "${temp_corpus}"/* "${corpus_dir}"/ 2>/dev/null || true | |
| iree_info "Corpus minimized: ${corpus_count} -> ${new_count} files" | |
| else | |
| iree_warn "Corpus minimization failed" | |
| fi | |
| } | |
| # Parse arguments. | |
| TARGET="" | |
| BAZEL_ARGS=() | |
| FUZZER_ARGS=() | |
| PARSING_FUZZER_ARGS=0 | |
| while [[ $# -gt 0 ]]; do | |
| if [[ "${PARSING_FUZZER_ARGS}" == "1" ]]; then | |
| FUZZER_ARGS+=("${1}") | |
| shift | |
| continue | |
| fi | |
| case "${1}" in | |
| -h|--help) | |
| show_help | |
| exit 0 | |
| ;; | |
| --agent-md|--agent_md) | |
| iree_show_agent_md | |
| exit 0 | |
| ;; | |
| -n|--dry_run|--dry-run) | |
| IREE_BAZEL_DRY_RUN=1 | |
| shift | |
| ;; | |
| -v|--verbose) | |
| IREE_BAZEL_VERBOSE=1 | |
| shift | |
| ;; | |
| -m|--minimize) | |
| FUZZ_MINIMIZE=1 | |
| shift | |
| ;; | |
| -d|--dict_only|--dict-only) | |
| FUZZ_DICT_ONLY=1 | |
| shift | |
| ;; | |
| --) | |
| PARSING_FUZZER_ARGS=1 | |
| shift | |
| ;; | |
| -*) | |
| BAZEL_ARGS+=("${1}") | |
| shift | |
| ;; | |
| *) | |
| if [[ -z "${TARGET}" ]]; then | |
| TARGET="${1}" | |
| else | |
| BAZEL_ARGS+=("${1}") | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Target is required. | |
| if [[ -z "${TARGET}" ]]; then | |
| iree_error "Target is required" | |
| echo "" | |
| show_help | |
| exit 1 | |
| fi | |
| # Set up worktree. | |
| iree_setup_worktree | |
| #===----------------------------------------------------------------------===# | |
| # Multi-target pattern support (e.g., //path/to/...) | |
| #===----------------------------------------------------------------------===# | |
| if [[ "${TARGET}" == *"..."* ]]; then | |
| # Expand pattern to all fuzz targets. | |
| BAZEL_BIN=$(iree_get_bazel_command) | |
| iree_info "Discovering fuzz targets matching: ${TARGET}" | |
| # Query for all targets ending in _fuzz under the pattern. | |
| FUZZ_TARGETS=() | |
| while IFS= read -r target; do | |
| if [[ "${target}" == *"_fuzz" ]]; then | |
| FUZZ_TARGETS+=("${target}") | |
| fi | |
| done < <("${BAZEL_BIN}" query "${TARGET}" 2>/dev/null | sort) | |
| if [[ ${#FUZZ_TARGETS[@]} -eq 0 ]]; then | |
| iree_error "No fuzz targets found matching: ${TARGET}" | |
| exit 1 | |
| fi | |
| iree_info "Found ${#FUZZ_TARGETS[@]} fuzz targets" | |
| # Dry-run: just list targets. | |
| if iree_is_dry_run; then | |
| iree_info "Would run these fuzz targets:" | |
| for target in "${FUZZ_TARGETS[@]}"; do | |
| echo " ${target}" | |
| done | |
| iree_info "Fuzzer args: ${FUZZER_ARGS[*]:-<none>}" | |
| exit 0 | |
| fi | |
| # Build all targets first. | |
| iree_info "Building all fuzz targets..." | |
| iree_bazel_build_default_configs | |
| BUILD_ARGS=("${IREE_BAZEL_DEFAULT_CONFIGS[@]}" "--config=fuzzer" "${BAZEL_ARGS[@]}") | |
| if ! iree_bazel_build_quiet "${TARGET}" "${BUILD_ARGS[@]}"; then | |
| iree_error "Build failed" | |
| exit 1 | |
| fi | |
| # Track results using indexed arrays (bash 3 compatible). | |
| declare -a FUZZ_PIDS=() | |
| declare -a FUZZ_TARGETS_RUNNING=() | |
| CRASHED_TARGETS=() | |
| CLEAN_TARGETS=() | |
| # Trap Ctrl+C to stop all fuzzers gracefully. | |
| cleanup_multi() { | |
| echo "" | |
| iree_info "Stopping all fuzzers..." | |
| for pid in "${FUZZ_PIDS[@]}"; do | |
| kill -INT "${pid}" 2>/dev/null || true | |
| done | |
| wait 2>/dev/null || true | |
| echo "" | |
| iree_info "=== Fuzzing Summary ===" | |
| iree_info "Total targets: ${#FUZZ_TARGETS[@]}" | |
| if [[ ${#CRASHED_TARGETS[@]} -gt 0 ]]; then | |
| iree_warn "Crashed (${#CRASHED_TARGETS[@]}):" | |
| for t in "${CRASHED_TARGETS[@]}"; do | |
| echo " - ${t}" | |
| done | |
| fi | |
| if [[ ${#CLEAN_TARGETS[@]} -gt 0 ]]; then | |
| iree_info "Clean (${#CLEAN_TARGETS[@]}): ${#CLEAN_TARGETS[@]} targets" | |
| fi | |
| exit 0 | |
| } | |
| trap cleanup_multi INT TERM | |
| # Build forwarded options for child processes. | |
| declare -a FORWARD_ARGS=() | |
| [[ "${FUZZ_MINIMIZE}" == "1" ]] && FORWARD_ARGS+=("-m") | |
| [[ "${FUZZ_DICT_ONLY}" == "1" ]] && FORWARD_ARGS+=("-d") | |
| [[ "${IREE_BAZEL_VERBOSE}" == "1" ]] && FORWARD_ARGS+=("-v") | |
| FORWARD_ARGS+=("${BAZEL_ARGS[@]}") | |
| # Run all fuzzers. They each manage their own corpus/artifacts. | |
| iree_info "Starting ${#FUZZ_TARGETS[@]} fuzzers..." | |
| echo "" | |
| for target in "${FUZZ_TARGETS[@]}"; do | |
| # Spawn fuzzer in subshell that ignores SIGINT (parent handles cleanup). | |
| ( | |
| trap '' INT | |
| exec "${SCRIPT_DIR}/iree-bazel-fuzz" "${FORWARD_ARGS[@]}" "${target}" -- "${FUZZER_ARGS[@]}" | |
| ) & | |
| FUZZ_PIDS+=($!) | |
| FUZZ_TARGETS_RUNNING+=("${target}") | |
| done | |
| # Wait for all fuzzers and collect results. | |
| for i in "${!FUZZ_PIDS[@]}"; do | |
| local pid="${FUZZ_PIDS[$i]}" | |
| local target="${FUZZ_TARGETS_RUNNING[$i]}" | |
| if wait "${pid}"; then | |
| CLEAN_TARGETS+=("${target}") | |
| else | |
| CRASHED_TARGETS+=("${target}") | |
| fi | |
| done | |
| # Final summary. | |
| echo "" | |
| iree_info "=== Fuzzing Complete ===" | |
| iree_info "Total targets: ${#FUZZ_TARGETS[@]}" | |
| if [[ ${#CRASHED_TARGETS[@]} -gt 0 ]]; then | |
| iree_warn "Crashed (${#CRASHED_TARGETS[@]}):" | |
| for t in "${CRASHED_TARGETS[@]}"; do | |
| echo " - ${t}" | |
| done | |
| fi | |
| iree_info "Clean: ${#CLEAN_TARGETS[@]} targets" | |
| exit 0 | |
| fi | |
| # Extract target info for directory structure. | |
| TARGET_NAME=$(get_target_name "${TARGET}") | |
| TARGET_RELATIVE_DIR=$(get_target_relative_dir "${TARGET}") | |
| # Set up corpus and artifact directories with repo-relative paths. | |
| # Structure: ~/.cache/iree-fuzz-cache/<relative-path>/<target>/corpus/ | |
| # ~/.cache/iree-fuzz-cache/<relative-path>/<target>/artifacts/ | |
| if [[ -n "${TARGET_RELATIVE_DIR}" ]]; then | |
| FUZZ_TARGET_DIR="${FUZZ_CACHE_DIR}/${TARGET_RELATIVE_DIR}/${TARGET_NAME}" | |
| else | |
| # Fallback if we can't determine relative path. | |
| FUZZ_TARGET_DIR="${FUZZ_CACHE_DIR}/${TARGET_NAME}" | |
| fi | |
| CORPUS_DIR="${FUZZ_TARGET_DIR}/corpus" | |
| ARTIFACT_DIR="${FUZZ_TARGET_DIR}/artifacts" | |
| # Find dictionary file. | |
| DICT_FILE=$(find_dict_file "${TARGET}") | |
| # Set up bazel args with fuzzer config. | |
| iree_bazel_build_default_configs | |
| BAZEL_ARGS=("${IREE_BAZEL_DEFAULT_CONFIGS[@]}" "--config=fuzzer" "${BAZEL_ARGS[@]}") | |
| # Create corpus and artifact directories. | |
| mkdir -p "${CORPUS_DIR}" "${ARTIFACT_DIR}" | |
| # Verbose/dry-run output. | |
| if iree_is_verbose || iree_is_dry_run; then | |
| iree_info "Target: ${TARGET}" | |
| iree_info "Corpus: ${CORPUS_DIR}" | |
| iree_info "Artifacts: ${ARTIFACT_DIR}" | |
| if [[ -n "${DICT_FILE}" ]]; then | |
| iree_info "Dict: ${DICT_FILE}" | |
| fi | |
| iree_info "Bazel args: ${BAZEL_ARGS[*]}" | |
| iree_info "Fuzzer args: ${FUZZER_ARGS[*]:-<none>}" | |
| fi | |
| if iree_is_dry_run; then | |
| exit 0 | |
| fi | |
| # Build the target. | |
| iree_debug "Building ${TARGET}..." | |
| if ! iree_bazel_build_quiet "${TARGET}" "${BAZEL_ARGS[@]}"; then | |
| exit $? | |
| fi | |
| # Get the binary path. | |
| BINARY_PATH=$(iree_bazel_get_binary_path "${TARGET}" "${BAZEL_ARGS[@]}") | |
| if [[ -z "${BINARY_PATH}" ]] || [[ ! -x "${BINARY_PATH}" ]]; then | |
| iree_error "Could not find built binary for ${TARGET}" | |
| exit 1 | |
| fi | |
| iree_debug "Binary: ${BINARY_PATH}" | |
| # Minimize corpus if requested or if corpus is too large. | |
| CORPUS_COUNT=$(ls -1 "${CORPUS_DIR}" 2>/dev/null | wc -l) | |
| if [[ "${FUZZ_MINIMIZE}" == "1" ]]; then | |
| minimize_corpus "${BINARY_PATH}" "${CORPUS_DIR}" | |
| elif [[ "${CORPUS_COUNT}" -gt "${FUZZ_AUTO_MINIMIZE}" ]]; then | |
| iree_info "Corpus has ${CORPUS_COUNT} files (threshold: ${FUZZ_AUTO_MINIMIZE}), auto-minimizing..." | |
| minimize_corpus "${BINARY_PATH}" "${CORPUS_DIR}" | |
| fi | |
| # Dict-only mode: just run briefly to generate dictionary recommendations. | |
| if [[ "${FUZZ_DICT_ONLY}" == "1" ]]; then | |
| if [[ -z "${DICT_FILE}" ]]; then | |
| iree_error "No .dict file found for ${TARGET}" | |
| exit 1 | |
| fi | |
| iree_info "Running corpus to extract dictionary entries..." | |
| OUTPUT_FILE=$(mktemp) | |
| trap "rm -f '${OUTPUT_FILE}'" EXIT | |
| # Run with existing corpus, short time limit. | |
| "${BINARY_PATH}" "${CORPUS_DIR}" -max_total_time=5 2>&1 | tee "${OUTPUT_FILE}" | |
| # Extract and merge dictionary entries. | |
| NEW_ENTRIES=$(parse_dict_entries "${OUTPUT_FILE}" "${FUZZ_MIN_USES}") | |
| merge_dict_entries "${DICT_FILE}" "${NEW_ENTRIES}" | |
| exit 0 | |
| fi | |
| # Set up log directory for this instance (temp, cleaned on success). | |
| FUZZ_LOG_DIR="/tmp/iree-bazel-fuzz/${TARGET_NAME}/$$" | |
| mkdir -p "${FUZZ_LOG_DIR}" | |
| # Build fuzzer command. | |
| FUZZ_CMD=("${BINARY_PATH}" "${CORPUS_DIR}") | |
| # Redirect artifacts to persistent location, logs to temp. | |
| FUZZ_CMD+=("-artifact_prefix=${ARTIFACT_DIR}/") | |
| # Add dictionary if it exists. | |
| if [[ -n "${DICT_FILE}" ]] && [[ -f "${DICT_FILE}" ]]; then | |
| FUZZ_CMD+=("-dict=${DICT_FILE}") | |
| iree_info "Using dictionary: ${DICT_FILE}" | |
| elif [[ -n "${DICT_FILE}" ]]; then | |
| iree_debug "Dictionary will be created at: ${DICT_FILE}" | |
| fi | |
| # Add per-input timeout if not specified but max_total_time is. | |
| # This prevents pathological inputs from hanging the fuzzer. | |
| HAS_TIMEOUT=0 | |
| MAX_TOTAL_TIME=0 | |
| for arg in "${FUZZER_ARGS[@]}"; do | |
| if [[ "${arg}" =~ ^-timeout= ]]; then | |
| HAS_TIMEOUT=1 | |
| elif [[ "${arg}" =~ ^-max_total_time=([0-9]+) ]]; then | |
| MAX_TOTAL_TIME="${BASH_REMATCH[1]}" | |
| fi | |
| done | |
| if [[ "${HAS_TIMEOUT}" -eq 0 ]] && [[ "${MAX_TOTAL_TIME}" -gt 0 ]]; then | |
| # Default timeout to 2x max_total_time (minimum 5 seconds). | |
| DEFAULT_TIMEOUT=$((MAX_TOTAL_TIME * 2)) | |
| if [[ "${DEFAULT_TIMEOUT}" -lt 5 ]]; then | |
| DEFAULT_TIMEOUT=5 | |
| fi | |
| FUZZ_CMD+=("-timeout=${DEFAULT_TIMEOUT}") | |
| iree_debug "Auto-setting timeout=${DEFAULT_TIMEOUT}s (2x max_total_time)" | |
| fi | |
| # Add user fuzzer args. | |
| FUZZ_CMD+=("${FUZZER_ARGS[@]}") | |
| # Cleanup and dict update on exit. | |
| CLEANUP_DONE=0 | |
| cleanup() { | |
| local exit_code=$? | |
| # Prevent running twice (INT then EXIT). | |
| if [[ "${CLEANUP_DONE}" -eq 1 ]]; then | |
| exit ${exit_code} | |
| fi | |
| CLEANUP_DONE=1 | |
| trap - EXIT INT TERM | |
| # Extract dictionary from log files using grep to find exact section. | |
| if [[ -n "${DICT_FILE}" ]] && [[ -d "${FUZZ_LOG_DIR}" ]]; then | |
| # Use grep -A to get lines after the dictionary marker. | |
| # The dictionary section ends with "End of recommended", so we stop there. | |
| local dict_output | |
| dict_output=$(grep -h -A 1000 '^###### Recommended dictionary' "${FUZZ_LOG_DIR}"/*.log 2>/dev/null \ | |
| | sed '/^###### End of recommended/q' \ | |
| | grep -E '^"' || true) | |
| if [[ -n "${dict_output}" ]]; then | |
| NEW_ENTRIES=$(echo "${dict_output}" \ | |
| | while IFS= read -r line; do | |
| local uses | |
| uses=$(echo "${line}" | grep -oE 'Uses: [0-9]+' | grep -oE '[0-9]+') | |
| if [[ -n "${uses}" ]] && [[ "${uses}" -ge "${FUZZ_MIN_USES}" ]]; then | |
| echo "${line}" | sed 's/ *#.*//' | perl -pe 's/\\([0-7]{3})/sprintf("\\x%02x", oct($1))/ge' | |
| fi | |
| done) | |
| merge_dict_entries "${DICT_FILE}" "${NEW_ENTRIES}" | |
| fi | |
| fi | |
| # Report corpus size. | |
| local corpus_count | |
| corpus_count=$(ls -1 "${CORPUS_DIR}" 2>/dev/null | wc -l) | |
| iree_info "Corpus: ${corpus_count} files in ${CORPUS_DIR}" | |
| # Check for crash/leak/timeout artifacts in persistent location. | |
| local artifact_count | |
| artifact_count=$(ls -1 "${ARTIFACT_DIR}" 2>/dev/null | wc -l) | |
| if [[ "${artifact_count}" -gt 0 ]]; then | |
| iree_warn "Artifacts (${artifact_count} files): ${ARTIFACT_DIR}" | |
| ls -1 "${ARTIFACT_DIR}" | head -5 | while read -r artifact; do | |
| echo " ${artifact}" | |
| done | |
| if [[ "${artifact_count}" -gt 5 ]]; then | |
| echo " ... and $((artifact_count - 5)) more" | |
| fi | |
| fi | |
| # Clean up log directory on success, keep on failure for debugging. | |
| if [[ "${exit_code}" -eq 0 ]]; then | |
| rm -rf "${FUZZ_LOG_DIR}" | |
| else | |
| # Show error messages from logs on failure. | |
| if [[ "${exit_code}" -ne 0 ]]; then | |
| local errors | |
| errors=$(grep -h -E '(ERROR|FATAL|ASAN|SUMMARY:|ParseDictionaryFile:)' "${FUZZ_LOG_DIR}"/*.log 2>/dev/null | head -20 || true) | |
| if [[ -n "${errors}" ]]; then | |
| iree_error "Fuzzer failed:" | |
| echo "${errors}" >&2 | |
| fi | |
| fi | |
| iree_info "Logs and artifacts: ${FUZZ_LOG_DIR}" | |
| fi | |
| exit ${exit_code} | |
| } | |
| trap cleanup EXIT INT TERM | |
| # Run the fuzzer from log directory so fuzz-N.log files go there. | |
| # Output goes to log files only - we just show status. | |
| iree_info "Starting fuzzer (Ctrl+C to stop)..." | |
| iree_info "Logs: ${FUZZ_LOG_DIR}" | |
| cd "${FUZZ_LOG_DIR}" | |
| "${FUZZ_CMD[@]}" > main.log 2>&1 |