| #!/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 || echo 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 |