From 3c490bd2dca1c7973a876f2fde6c4ce3b614d2ab Mon Sep 17 00:00:00 2001 From: Dhruv Vaishnav Date: Wed, 28 Oct 2026 02:37:49 +0000 Subject: [PATCH] ci: reuse fuzz corpora across integration runs (#9979) ci: persist fuzz corpora between runs Signed-off-by: dhruvxvaishnav --- .github/actions/fuzz-test/action.yaml | 40 +++++++ .github/scripts/prepare-fuzz-corpus.sh | 38 +++++++ .github/scripts/run-fuzz-targets-test.sh | 128 ++++++++++++++++++++++- .github/scripts/run-fuzz-targets.sh | 10 +- tests-fuzz/README.md | 81 ++++++++++++++ 5 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 .github/scripts/prepare-fuzz-corpus.sh diff --git a/.github/actions/fuzz-test/action.yaml b/.github/actions/fuzz-test/action.yaml index b696d8099dc..b93634b16a2 100644 --- a/.github/actions/fuzz-test/action.yaml +++ b/.github/actions/fuzz-test/action.yaml @@ -28,7 +28,38 @@ inputs: runs: using: composite steps: + - name: Prepare Fuzz Corpus + id: corpus + shell: bash + env: + FUZZ_TARGETS: ${{ inputs.targets }} + run: bash .github/scripts/prepare-fuzz-corpus.sh + + - name: Restore Fuzz Corpus + id: restore-corpus + uses: actions/cache/restore@v4 + continue-on-error: true + with: + path: ${{ steps.corpus.outputs.paths }} + key: ${{ steps.corpus.outputs.key-prefix }}${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ strategy.job-index || 0 }} + restore-keys: ${{ steps.corpus.outputs.key-prefix }} + + - name: Report Fuzz Corpus Cache + shell: bash + env: + CACHE_OUTCOME: ${{ steps.restore-corpus.outcome }} + CACHE_MATCHED_KEY: ${{ steps.restore-corpus.outputs.cache-matched-key }} + run: | + if [[ "${CACHE_OUTCOME}" == failure ]]; then + echo '::warning::Fuzz corpus restore failed; running with available inputs' + elif [[ -n "${CACHE_MATCHED_KEY}" ]]; then + printf 'Restored fuzz corpus: %s\n' "${CACHE_MATCHED_KEY}" + else + echo 'No cached fuzz corpus found; starting with an empty corpus' + fi + - name: Run Fuzz Tests + id: run-fuzz shell: bash env: FUZZ_TARGETS: ${{ inputs.targets }} @@ -39,4 +70,13 @@ runs: FUZZ_COLLECT_CLUSTER_ARTIFACTS: ${{ inputs.collect-cluster-artifacts }} FUZZ_ARTIFACT_ROOT: ${{ inputs.artifact-root }} FUZZ_BIN_DIR: ${{ inputs.prebuilt-binaries-dir }} + FUZZ_CORPUS_ROOT: ${{ github.workspace }}/tests-fuzz/corpus run: .github/scripts/run-fuzz-targets.sh + + - name: Save Fuzz Corpus + if: ${{ !cancelled() && (steps.run-fuzz.outcome == 'success' || steps.run-fuzz.outcome == 'failure') }} + uses: actions/cache/save@v4 + continue-on-error: true + with: + path: ${{ steps.corpus.outputs.paths }} + key: ${{ steps.corpus.outputs.key-prefix }}${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ strategy.job-index || 0 }} diff --git a/.github/scripts/prepare-fuzz-corpus.sh b/.github/scripts/prepare-fuzz-corpus.sh new file mode 100644 index 00000000000..e647cf9ceaf --- /dev/null +++ b/.github/scripts/prepare-fuzz-corpus.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${FUZZ_TARGETS:?FUZZ_TARGETS is required}" +: "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +targets=() +while IFS= read -r target; do + target="${target%$'\r'}" + [[ -z "${target}" ]] && continue + if [[ ! "${target}" =~ ^[a-zA-Z0-9_]+$ ]]; then + printf 'Invalid fuzz target: %s\n' "${target}" >&2 + exit 2 + fi + targets+=("${target}") +done <<<"${FUZZ_TARGETS}" + +if [[ "${#targets[@]}" -eq 0 ]]; then + printf 'FUZZ_TARGETS contains no targets\n' >&2 + exit 2 +fi + +# Corpus identity depends on the targets, not the source revision or their order. +target_set="$(printf '%s\n' "${targets[@]}" | LC_ALL=C sort -u)" +target_hash="$(printf '%s\n' "${target_set}" | sha256sum | cut -d ' ' -f 1)" +corpus_root="${GITHUB_WORKSPACE}/tests-fuzz/corpus" + +{ + printf 'key-prefix=fuzz-corpus-v1-%s-\n' "${target_hash}" + printf 'paths<>"${GITHUB_OUTPUT}" diff --git a/.github/scripts/run-fuzz-targets-test.sh b/.github/scripts/run-fuzz-targets-test.sh index 9705b6bc5af..ddc559c8db2 100755 --- a/.github/scripts/run-fuzz-targets-test.sh +++ b/.github/scripts/run-fuzz-targets-test.sh @@ -5,6 +5,7 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" RUNNER="${SCRIPT_DIR}/run-fuzz-targets.sh" COLLECTOR="${SCRIPT_DIR}/collect-fuzz-target-artifacts.sh" +PREPARE_CORPUS="${SCRIPT_DIR}/prepare-fuzz-corpus.sh" fail() { printf 'FAIL: %s\n' "$*" >&2 @@ -32,6 +33,13 @@ new_fixture() { set -euo pipefail printf '%s\t%s\t%s\n' "${GT_FUZZ_DUMP_DIR}" "$*" "${MOCK_CARGO_MARKER:-}" >>"${MOCK_CARGO_LOG}" target="$4" +corpus="$5" +[[ -d "${corpus}" ]] || exit 18 +printf '%s\t%s\n' "${target}" "${corpus}" >>"${MOCK_CORPUS_LOG}" +if [[ -f "${corpus}/seed" ]]; then + cat "${corpus}/seed" >>"${MOCK_SEED_LOG}" +fi +printf '%s\n' "${target}" >"${corpus}/learned" case " ${MOCK_FAIL_TARGETS:-} " in *" ${target} "*) exit 17 ;; esac @@ -64,12 +72,15 @@ run_fixture() { "MOCK_COLLECTOR_LOG=${fixture}/collector.log" "MOCK_FAIL_TARGETS=${fail_targets}" "MOCK_CARGO_MARKER=fixture" + "MOCK_CORPUS_LOG=${fixture}/corpus.log" + "MOCK_SEED_LOG=${fixture}/seeds.log" "FUZZ_TARGETS=${targets}" "FUZZ_GROUP=test-group" "FUZZ_MAX_TOTAL_TIME=120" "FUZZ_UNSTABLE=${unstable}" "FUZZ_COLLECT_CLUSTER_ARTIFACTS=true" "FUZZ_ARTIFACT_ROOT=${artifact_root}" + "FUZZ_CORPUS_ROOT=${fixture}/corpus with spaces" "FUZZ_ARTIFACT_COLLECTOR=${fixture}/collector" "GT_FUZZ_BINARY_PATH=${GT_FUZZ_BINARY_PATH:-}" "GT_FUZZ_INSTANCE_ROOT_DIR=${GT_FUZZ_INSTANCE_ROOT_DIR:-}" @@ -81,7 +92,7 @@ run_fixture() { fi set +e - env -u FUZZ_FAIL_FAST "${runner_env[@]}" \ + env -u FUZZ_FAIL_FAST -u FUZZ_BIN_DIR "${runner_env[@]}" \ "${RUNNER}" >"${fixture}/stdout.log" 2>&1 fixture_status=$? set -e @@ -89,18 +100,30 @@ run_fixture() { run_prebuilt_fixture() { new_fixture - mkdir -p "${fixture}/prebuilt" + mkdir -p "${fixture}/prebuilt" "${fixture}/corpus with spaces/fuzz_create_table" + printf 'restored prebuilt seed\n' >"${fixture}/corpus with spaces/fuzz_create_table/seed" cat >"${fixture}/prebuilt/fuzz_create_table" <<'EOF' #!/usr/bin/env bash set -euo pipefail printf '%s\t%s\t%s\n' "${GT_FUZZ_DUMP_DIR}" "$*" "${MOCK_BINARY_MARKER:-}" >>"${MOCK_BINARY_LOG}" +[[ -d "$1" ]] || exit 18 +printf '%s\n' "$1" >>"${MOCK_CORPUS_LOG}" +cat "$1/seed" >>"${MOCK_SEED_LOG}" +printf 'new prebuilt input\n' >"$1/learned" +exit "${MOCK_BINARY_EXIT_CODE}" EOF chmod +x "${fixture}/prebuilt/fuzz_create_table" set +e env \ + "PATH=${fixture}/bin:${PATH}" \ + "MOCK_CARGO_LOG=${fixture}/cargo.log" \ "MOCK_BINARY_LOG=${fixture}/binary.log" \ "MOCK_BINARY_MARKER=fixture" \ + "MOCK_BINARY_EXIT_CODE=${1:-0}" \ + "MOCK_CORPUS_LOG=${fixture}/corpus.log" \ + "MOCK_SEED_LOG=${fixture}/seeds.log" \ + "MOCK_COLLECTOR_LOG=${fixture}/collector.log" \ "FUZZ_TARGETS=fuzz_create_table" \ "FUZZ_GROUP=test-group" \ "FUZZ_MAX_TOTAL_TIME=120" \ @@ -108,6 +131,7 @@ EOF "FUZZ_FAIL_FAST=true" \ "FUZZ_COLLECT_CLUSTER_ARTIFACTS=false" \ "FUZZ_ARTIFACT_ROOT=${fixture}/artifacts" \ + "FUZZ_CORPUS_ROOT=${fixture}/corpus with spaces" \ "FUZZ_ARTIFACT_COLLECTOR=${fixture}/collector" \ "FUZZ_BIN_DIR=${fixture}/prebuilt" \ "GITHUB_SHA=deadbeef" \ @@ -143,6 +167,7 @@ test_successful_targets_run_in_order() { "target order" grep -q -- '--features=unstable' "${fixture}/cargo.log" || fail "unstable feature missing" grep -q -- '-max_total_time=120' "${fixture}/cargo.log" || fail "fuzz time missing" + grep -q -- '-print_final_stats=1' "${fixture}/cargo.log" || fail "fuzz statistics missing" grep -q -- '-artifact_prefix=.*/targets/fuzz_create_table/libfuzzer/' "${fixture}/cargo.log" || \ fail "target-scoped libFuzzer prefix missing" assert_eq success "$(jq -r '.[0].status' "${fixture}/artifacts/manifest.json")" "first status" @@ -167,8 +192,49 @@ test_prebuilt_binary_runs_without_cargo() { [[ ! -e "${fixture}/cargo.log" ]] || fail "cargo ran for prebuilt binary" assert_eq 1 "$(wc -l <"${fixture}/binary.log" | tr -d ' ')" "prebuilt invocation count" grep -q -- '-max_total_time=120' "${fixture}/binary.log" || fail "prebuilt fuzz time missing" + grep -q -- '-print_final_stats=1' "${fixture}/binary.log" || fail "prebuilt fuzz statistics missing" grep -q -- '-artifact_prefix=.*/targets/fuzz_create_table/libfuzzer/' "${fixture}/binary.log" || \ fail "prebuilt target-scoped libFuzzer prefix missing" + assert_eq "${fixture}/corpus with spaces/fuzz_create_table" "$(cat "${fixture}/corpus.log")" \ + "prebuilt corpus argument" + assert_eq 'restored prebuilt seed' "$(cat "${fixture}/seeds.log")" "prebuilt restored input" + assert_file "${fixture}/corpus with spaces/fuzz_create_table/learned" + cleanup_fixture +} + +test_cargo_reuses_separate_target_corpora() { + new_fixture + mkdir -p "${fixture}/corpus with spaces/fuzz_create_table" "${fixture}/corpus with spaces/fuzz_insert" + printf 'create seed\n' >"${fixture}/corpus with spaces/fuzz_create_table/seed" + printf 'insert seed\n' >"${fixture}/corpus with spaces/fuzz_insert/seed" + run_fixture $'fuzz_create_table\nfuzz_insert' false false "" + + assert_eq 0 "${fixture_status}" "restored cargo run status" + assert_eq $'create seed\ninsert seed' "$(cat "${fixture}/seeds.log")" "target-scoped restored inputs" + assert_eq \ + "fuzz_create_table"$'\t'"${fixture}/corpus with spaces/fuzz_create_table"$'\n'"fuzz_insert"$'\t'"${fixture}/corpus with spaces/fuzz_insert" \ + "$(cat "${fixture}/corpus.log")" "cargo corpus arguments" + assert_eq fuzz_create_table "$(cat "${fixture}/corpus with spaces/fuzz_create_table/learned")" \ + "first target learned input" + assert_eq fuzz_insert "$(cat "${fixture}/corpus with spaces/fuzz_insert/learned")" \ + "second target learned input" + + run_fixture fuzz_create_table true false "" + assert_eq 0 "${fixture_status}" "repeated cargo run status" + assert_file "${fixture}/corpus with spaces/fuzz_create_table/seed" + assert_file "${fixture}/corpus with spaces/fuzz_insert/learned" + cleanup_fixture +} + +test_prebuilt_failure_preserves_corpus_and_artifacts() { + run_prebuilt_fixture 17 + + assert_eq 1 "${fixture_status}" "prebuilt failure status" + assert_eq 17 "$(jq -r '.[0].exit_code' "${fixture}/artifacts/manifest.json")" \ + "prebuilt failure exit code" + assert_file "${fixture}/corpus with spaces/fuzz_create_table/seed" + assert_file "${fixture}/corpus with spaces/fuzz_create_table/learned" + assert_file "${fixture}/artifacts/targets/fuzz_create_table/mock-artifacts/state.txt" cleanup_fixture } @@ -183,6 +249,8 @@ test_fail_fast_stops_after_first_failure() { assert_eq skipped_after_failure "$(jq -r '.[2].status' "${fixture}/artifacts/manifest.json")" "skipped status" assert_eq true "$(jq -r '.[2].after_prior_failure' "${fixture}/artifacts/manifest.json")" "skipped provenance" assert_file "${fixture}/artifacts/targets/fuzz_insert/mock-artifacts/state.txt" + assert_file "${fixture}/corpus with spaces/fuzz_insert/learned" + [[ ! -e "${fixture}/corpus with spaces/fuzz_alter_table" ]] || fail "skipped target corpus created" grep -q 'title=Fuzz target failed.*target=fuzz_insert' "${fixture}/stdout.log" || \ fail "target failure annotation missing" grep -q 'title=Fuzz target skipped.*target=fuzz_alter_table' "${fixture}/stdout.log" || \ @@ -191,6 +259,8 @@ test_fail_fast_stops_after_first_failure() { fail "reproduction section missing" grep -q 'cargo +nightly fuzz run fuzz_insert' "${fixture}/artifacts/summary.md" || \ fail "reproduction command missing" + grep -q 'fuzz_insert --fuzz-dir tests-fuzz' "${fixture}/artifacts/summary.md" || \ + fail "reproduction command depends on the CI corpus path" cleanup_fixture } @@ -241,6 +311,56 @@ test_invalid_configuration_fails_before_cargo() { cleanup_fixture } +prepare_corpus_fixture() { + local targets="$1" + local output="$2" + set +e + FUZZ_TARGETS="${targets}" \ + GITHUB_WORKSPACE="${fixture}/workspace with spaces" \ + GITHUB_OUTPUT="${output}" \ + bash "${PREPARE_CORPUS}" >"${fixture}/prepare.log" 2>&1 + prepare_status=$? + set -e +} + +test_corpus_cache_identity_and_paths() { + new_fixture + prepare_corpus_fixture $'fuzz_insert\r\n\nfuzz_create_table\r\nfuzz_insert\r' "${fixture}/first-output" + assert_eq 0 "${prepare_status}" "corpus preparation status" + prepare_corpus_fixture $'fuzz_create_table\nfuzz_insert' "${fixture}/second-output" + assert_eq 0 "${prepare_status}" "reordered corpus preparation status" + cmp -s "${fixture}/first-output" "${fixture}/second-output" || \ + fail "cache identity depends on target order, duplicates, or line endings" + assert_eq \ + "${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_create_table"$'\n'"${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_insert" \ + "$(sed '1,2d; $d' "${fixture}/first-output")" "selected corpus cache paths" + [[ -d "${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_insert" ]] || \ + fail "corpus directory not created" + [[ ! -e "${fixture}/workspace with spaces/tests-fuzz/corpus/fuzz_alter_table" ]] || \ + fail "unselected corpus directory created" + + GITHUB_SHA=another-revision prepare_corpus_fixture $'fuzz_create_table\nfuzz_insert' "${fixture}/new-revision-output" + cmp -s "${fixture}/first-output" "${fixture}/new-revision-output" || \ + fail "source revision changed cache identity" + prepare_corpus_fixture fuzz_insert "${fixture}/subset-output" + assert_eq 0 "${prepare_status}" "subset corpus preparation status" + [[ "$(head -n 1 "${fixture}/first-output")" != "$(head -n 1 "${fixture}/subset-output")" ]] || \ + fail "different target sets share cache identity" + cleanup_fixture +} + +test_corpus_preparation_rejects_invalid_targets_before_writing() { + local targets + for targets in $'fuzz_insert\n../escape' 'fuzz target' $'\n\r\n'; do + new_fixture + prepare_corpus_fixture "${targets}" "${fixture}/output" + assert_eq 2 "${prepare_status}" "invalid corpus target status" + [[ ! -e "${fixture}/output" ]] || fail "invalid targets wrote cache outputs" + [[ ! -e "${fixture}/workspace with spaces" ]] || fail "invalid targets created directories" + cleanup_fixture + done +} + test_collector_keeps_target_scopes_separate() { new_fixture service_log="${fixture}/greptime.log" @@ -373,10 +493,14 @@ EOF test_successful_targets_run_in_order test_prebuilt_binary_runs_without_cargo +test_cargo_reuses_separate_target_corpora +test_prebuilt_failure_preserves_corpus_and_artifacts test_fail_fast_stops_after_first_failure test_fail_fast_defaults_to_true test_non_fail_fast_marks_later_targets test_invalid_configuration_fails_before_cargo +test_corpus_cache_identity_and_paths +test_corpus_preparation_rejects_invalid_targets_before_writing test_collector_keeps_target_scopes_separate test_cluster_collector_honors_target_scope_and_namespace test_setup_failure_writes_artifact_contract diff --git a/.github/scripts/run-fuzz-targets.sh b/.github/scripts/run-fuzz-targets.sh index 205ba58c419..1489d20b7f0 100755 --- a/.github/scripts/run-fuzz-targets.sh +++ b/.github/scripts/run-fuzz-targets.sh @@ -5,6 +5,7 @@ set -euo pipefail FUZZ_ARTIFACT_ROOT="${FUZZ_ARTIFACT_ROOT:-/tmp/greptime-fuzz-artifacts}" FUZZ_ARTIFACT_COLLECTOR="${FUZZ_ARTIFACT_COLLECTOR:-.github/scripts/collect-fuzz-target-artifacts.sh}" FUZZ_ARTIFACT_COLLECTION_TIMEOUT_SECS="${FUZZ_ARTIFACT_COLLECTION_TIMEOUT_SECS:-180}" +FUZZ_CORPUS_ROOT="${FUZZ_CORPUS_ROOT:-tests-fuzz/corpus}" log() { printf '[run-fuzz-targets] %s\n' "$*" @@ -187,19 +188,22 @@ for ((index = 0; index < ${#targets[@]}; index++)); do started_epoch="$(date +%s)" echo "::group::Fuzz target: ${target}" log "run target ${target}" + mkdir -p "${FUZZ_CORPUS_ROOT}/${target}" + corpus_dir="$(cd -- "${FUZZ_CORPUS_ROOT}/${target}" && pwd)" + log "corpus directory: ${corpus_dir}" if [[ -n "${FUZZ_BIN_DIR:-}" ]]; then fuzz_binary="${FUZZ_BIN_DIR}/${target}" if [[ ! -x "${fuzz_binary}" ]]; then log "prebuilt fuzz binary does not exist or is not executable: ${fuzz_binary}" exit 2 fi - run_args=("${fuzz_binary}" "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-artifact_prefix=${target_dir}/libfuzzer/") + run_args=("${fuzz_binary}" "${corpus_dir}" "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-print_final_stats=1" "-artifact_prefix=${target_dir}/libfuzzer/") else - run_args=(cargo +"${RUSTUP_TOOLCHAIN:-nightly}" fuzz run "${target}" --fuzz-dir tests-fuzz -D -s none) + run_args=(cargo +"${RUSTUP_TOOLCHAIN:-nightly}" fuzz run "${target}" "${corpus_dir}" --fuzz-dir tests-fuzz -D -s none) if [[ "${fuzz_unstable}" == true ]]; then run_args+=(--features=unstable) fi - run_args+=(-- "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-artifact_prefix=${target_dir}/libfuzzer/") + run_args+=(-- "-max_total_time=${FUZZ_MAX_TOTAL_TIME}" "-print_final_stats=1" "-artifact_prefix=${target_dir}/libfuzzer/") fi if GT_FUZZ_DUMP_DIR="${target_dir}/csv" \ "${run_args[@]}" 2>&1 | tee "${target_dir}/fuzz.log"; then diff --git a/tests-fuzz/README.md b/tests-fuzz/README.md index 7945bae1583..f39a23f2887 100644 --- a/tests-fuzz/README.md +++ b/tests-fuzz/README.md @@ -42,6 +42,87 @@ cargo fuzz list --fuzz-dir tests-fuzz cargo fuzz run fuzz_create_table --fuzz-dir tests-fuzz -D -s none ``` +## CI Corpus Persistence + +The shared `.github/actions/fuzz-test` action restores inputs before running +the selected targets and saves the updated corpus afterward, including when a +target fails. Cache failures are best-effort and do not change the fuzz result. +Crash files, diagnostic artifacts, and fail-fast behavior are unchanged. + +Each target reads and writes its own `tests-fuzz/corpus/` directory. +Both prebuilt executables and the `cargo fuzz` fallback receive that directory +explicitly. For local runs of `.github/scripts/run-fuzz-targets.sh`, set +`FUZZ_CORPUS_ROOT` to use a different corpus root. + +The selected target directories are cached together to preserve the existing +job grouping. The stable key prefix identifies the sorted, deduplicated target +set, not a source hash, so code changes do not invalidate existing inputs. +Each job saves a new immutable entry, distinguished by run ID, attempt, job, +and matrix index. Restore uses the latest accessible entry matching the prefix. +Changing the target set starts a different cache; corpora are not shared between +different target sets. Parallel jobs may start from the same entry, but their +new inputs are not merged automatically. + +The restore step logs the matched key (including prefix matches) or a cache +miss. GitHub's [cache access restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache) +still apply: PR caches are scoped to that PR, while a default-branch cache can +seed other PRs. The existing scheduled workflow does not run fuzz jobs; a +maintainer can populate the default-branch corpus through the existing manual +fuzz workflow. This change does not add scheduling, target selection, or corpus +pruning. Rolling entries remain subject to GitHub's cache limits and eviction. + +### Validation + +The orchestration tests use mock fuzz executables and do not build Rust or +start GreptimeDB. They cover both runner paths, restored inputs, separate +target directories, retained inputs after failure, cache identity, and invalid +target names. Run them on Linux with Bash, jq, and GNU coreutils: + +```bash +bash .github/scripts/run-fuzz-targets-test.sh +``` + +These tests do not exercise the hosted GitHub cache service. To verify that +service, inspect two fuzz workflow runs on the same PR: the first should save +an entry, and the second should report its restored key and load a nonempty +corpus. Also verify that cache save still runs after a target failure, without +turning that failure into a successful fuzz result. + +To compare useful coverage, use the same fuzz executable, GreptimeDB revision, +service configuration, and time budget for empty and restored corpora. Both +runner paths enable `-print_final_stats=1`; the target's `fuzz.log` retains the +libFuzzer coverage and final statistics. Seed the restored corpus with a +separate warm-up run (or a downloaded corpus), then compare it with a fresh, +empty directory. Keep warm-up time separate from the comparison and repeat +trials to account for randomness. Report initial corpus size, final covered +edges (`cov:`), executions, and elapsed time, rather than inferring a coverage +improvement merely from a cache hit. + +For example, with a prebuilt `fuzz_create_table` binary and a running standalone +service, the following creates separate warm-up, empty, and seeded corpora. Set +`FUZZ_BIN_DIR` to the absolute binary directory and `GT_MYSQL_ADDR` to the test +service address first. All runs use the same executable and 120-second budget; +the warm-up is not part of the measured comparison. This creates temporary +inputs, logs, and crash files, and executes DDL against the test service. + +```bash +set -euo pipefail +: "${FUZZ_BIN_DIR:?Set the prebuilt fuzz binary directory}" +: "${GT_MYSQL_ADDR:?Set the test service address}" +comparison_dir="$(mktemp -d -t fuzz-corpus-comparison.XXXXXX)" +for trial in seed cold warm; do + mkdir -p "${comparison_dir}/${trial}" "${comparison_dir}/artifacts/${trial}" + if [[ "${trial}" == warm ]]; then + cp -a "${comparison_dir}/seed/." "${comparison_dir}/warm/" + fi + "${FUZZ_BIN_DIR}/fuzz_create_table" "${comparison_dir}/${trial}" \ + -max_total_time=120 -print_final_stats=1 \ + "-artifact_prefix=${comparison_dir}/artifacts/${trial}/" \ + 2>&1 | tee "${comparison_dir}/${trial}.log" +done +printf 'Comparison logs and corpora: %s\n' "${comparison_dir}" +``` + ## Crash Reproduction If you want to reproduce a crash, you first need to obtain the Base64 encoded code, which usually appears at the end of a crash report, and store it in a file.