name: Fuzzing
on:
workflow_dispatch:
inputs:
target:
description: 'Fuzz target to run'
required: true
default: 'fuzz_tree_ops'
type: choice
options:
- fuzz_tree_ops
- fuzz_proof_decode
- fuzz_code_chunk
- fuzz_basic_data
- fuzz_streaming
- fuzz_embedding
- all
duration:
description: 'Duration in seconds (18000 = 5 hours). If target=all, must be >= number of targets.'
required: true
default: '18000'
type: string
jobs:
fuzz:
runs-on: ubuntu-latest
env:
FUZZ_TARGETS: fuzz_tree_ops fuzz_proof_decode fuzz_code_chunk fuzz_basic_data fuzz_streaming fuzz_embedding
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with:
submodules: recursive
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@881ba7bf39a41cda34ac9e123fb41b44ed08232f with:
toolchain: nightly-2026-02-01
- name: Install cargo-fuzz
run: cargo +nightly-2026-02-01 install cargo-fuzz --version 0.13.1 --locked --force
- name: Validate fuzz inputs (single target)
if: inputs.target != 'all'
env:
TARGET: ${{ inputs.target }}
DURATION: ${{ inputs.duration }}
run: |
set -euo pipefail
set -f
if ! [[ "$DURATION" =~ ^[0-9]+$ ]]; then
echo "::error::duration must be an integer number of seconds (got: $DURATION)"
exit 1
fi
DURATION_INT=$((10#$DURATION))
if (( DURATION_INT < 1 )); then
echo "::error::duration must be >= 1 second (got: $DURATION_INT)"
exit 1
fi
read -r -a targets <<<"$FUZZ_TARGETS"
num_targets=${#targets[@]}
if (( num_targets == 0 )); then
echo "::error::FUZZ_TARGETS is empty"
exit 1
fi
valid_target=false
for t in "${targets[@]}"; do
if [[ "$t" == "$TARGET" ]]; then
valid_target=true
break
fi
done
if ! $valid_target; then
echo "::error::invalid fuzz target: $TARGET"
exit 1
fi
printf '%s=%s\n' "FUZZ_TARGET" "$TARGET" >>"$GITHUB_ENV"
printf '%s=%s\n' "FUZZ_DURATION_INT" "$DURATION_INT" >>"$GITHUB_ENV"
- name: Run fuzz (single target)
if: inputs.target != 'all'
run: |
set -euo pipefail
echo "Fuzzing $FUZZ_TARGET for ${FUZZ_DURATION_INT} seconds"
cargo +nightly-2026-02-01 fuzz run "$FUZZ_TARGET" -- \
-max_total_time="$FUZZ_DURATION_INT" \
-print_final_stats=1
- name: Validate fuzz inputs (all targets)
if: inputs.target == 'all'
env:
DURATION: ${{ inputs.duration }}
run: |
set -euo pipefail
set -f
if ! [[ "$DURATION" =~ ^[0-9]+$ ]]; then
echo "::error::duration must be an integer number of seconds (got: $DURATION)"
exit 1
fi
DURATION_INT=$((10#$DURATION))
if (( DURATION_INT < 1 )); then
echo "::error::duration must be >= 1 second (got: $DURATION_INT)"
exit 1
fi
read -r -a targets <<<"$FUZZ_TARGETS"
num_targets=${#targets[@]}
if (( num_targets == 0 )); then
echo "::error::FUZZ_TARGETS is empty"
exit 1
fi
if (( DURATION_INT < num_targets )); then
echo "::error::duration must be >= ${num_targets} seconds when target=all (got: $DURATION_INT)"
exit 1
fi
DURATION_PER_TARGET=$(( DURATION_INT / num_targets ))
if (( DURATION_PER_TARGET < 1 )); then
DURATION_PER_TARGET=1
fi
printf '%s=%s\n' "FUZZ_DURATION_PER_TARGET" "$DURATION_PER_TARGET" >>"$GITHUB_ENV"
- name: Run fuzz (all targets)
if: inputs.target == 'all'
run: |
set -euo pipefail
set -f
read -r -a targets <<<"$FUZZ_TARGETS"
failed=0
echo "Running all targets, ${FUZZ_DURATION_PER_TARGET}s each"
for target in "${targets[@]}"; do
echo "=== Fuzzing $target for ${FUZZ_DURATION_PER_TARGET}s ==="
if ! cargo +nightly-2026-02-01 fuzz run "$target" -- \
-max_total_time="$FUZZ_DURATION_PER_TARGET" \
-print_final_stats=1; then
echo "::warning::fuzz run failed for target: $target"
failed=1
fi
done
exit "$failed"
- name: Upload crash artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: always()
with:
name: fuzz-crashes-${{ inputs.target }}
path: |
fuzz/artifacts/
retention-days: 30
if-no-files-found: ignore
- name: Upload corpus
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: always()
with:
name: fuzz-corpus-${{ inputs.target }}
path: |
fuzz/corpus/
retention-days: 7
if-no-files-found: ignore
- name: Summary
if: always()
run: |
echo "## Fuzzing Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY
echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Target | ${{ inputs.target }} |" >> $GITHUB_STEP_SUMMARY
echo "| Duration | ${{ inputs.duration }}s |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -d "fuzz/artifacts" ] && [ "$(ls -A fuzz/artifacts 2>/dev/null)" ]; then
echo "### [!] Crashes Found" >> $GITHUB_STEP_SUMMARY
echo "Check the uploaded artifacts for crash inputs." >> $GITHUB_STEP_SUMMARY
find fuzz/artifacts -type f | head -20 >> $GITHUB_STEP_SUMMARY
else
echo "### [OK] No crashes found" >> $GITHUB_STEP_SUMMARY
fi