name: fuzzing
on:
schedule:
- cron: "17 10 * * 3"
workflow_dispatch:
inputs:
minutes:
description: Fuzzing time, in minutes, shared among the targets
type: number
default: 90
workflow_call:
inputs:
minutes:
description: Fuzzing time, in minutes, shared among the targets
type: number
required: true
permissions:
contents: read
actions: read
concurrency:
group: fuzz-${{ github.event_name == 'pull_request' && github.ref || 'corpora' }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
fuzz:
name: fuzz
runs-on: ubuntu-latest
timeout-minutes: 180
env:
FUZZ_TRIPLE: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
- name: Install cargo-fuzz
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- name: Fetch the corpora of the last run
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p fuzz/corpus
# The newest corpora that a run saved, weekly or a release's (those
# are ci.yml's runs); the first run starts from nothing.
run_id=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=fuzz-corpora&per_page=100" \
--jq '[.artifacts[] | select(.expired | not)] | max_by(.created_at) | .workflow_run.id // empty')
if [ -n "$run_id" ]; then
gh run download "$run_id" --repo "$GITHUB_REPOSITORY" \
--name fuzz-corpora --dir fuzz/corpus
echo "Continuing from the corpora of run $run_id"
fi
- name: Build the targets
run: cargo fuzz build --debug-assertions --target "$FUZZ_TRIPLE"
- name: Fuzz
env:
RUN_MINUTES: ${{ inputs.minutes || 90 }}
run: |
# Each target's minutes in the weekly run of 90, by how much code a
# bug there would reach and how much harm it would do
# (doc/08-fuzzing-verification.md); other runs scale them.
budget=(
"map_heap_byte_strings 20"
"user_code_panics 20"
"map_integers 12"
"map_user_inline_keys 12"
"map_plain_keys 10"
"set_algebra 10"
"map_byte_arrays 6"
)
failed=()
echo "| target | seconds | result |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY"
for entry in "${budget[@]}"; do
read -r target weekly_minutes <<< "$entry"
seconds=$(( weekly_minutes * 60 * RUN_MINUTES / 90 ))
echo "::group::$target, $seconds s"
# A crash ends the target's run, not the others'. An input that
# runs over a minute is reported as a hang.
if cargo fuzz run --debug-assertions --target "$FUZZ_TRIPLE" "$target" -- \
-max_total_time="$seconds" -fork="$(nproc)" -timeout=60; then
result=clean
else
result="**crashed**"
failed+=("$target")
fi
echo "::endgroup::"
echo "| $target | $seconds | $result |" >> "$GITHUB_STEP_SUMMARY"
done
if [ ${#failed[@]} -gt 0 ]; then
{
echo
echo "The crashing inputs are in the \`fuzz-crashes\` artifact. To replay"
echo "one, minimize it, and keep it once fixed:"
echo
echo '```'
echo "cargo fuzz run --debug-assertions <target> <input>"
echo "cargo fuzz tmin --debug-assertions <target> <input>"
echo "cp <minimized input> fuzz/regressions/<target>/<what it found>"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::crashed: ${failed[*]}"
exit 1
fi
- name: Save the corpora
if: always()
uses: actions/upload-artifact@v4
with:
name: fuzz-corpora
path: fuzz/corpus
retention-days: 21
- name: Save the crashing inputs
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: fuzz/artifacts
retention-days: 90