Fuzz Testing #370
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Fuzz Testing | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| schedule: | |
| # Run daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| duration: | |
| description: 'Fuzzing duration in seconds' | |
| required: false | |
| default: '600' | |
| target: | |
| description: 'Fuzz target to run (all_protocols or validate_with_python)' | |
| required: false | |
| default: 'all_protocols' | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| fuzz-fast: | |
| name: Fast Fuzzing (all_protocols) | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' || github.event_name == 'push' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f | |
| with: | |
| toolchain: nightly-2026-04-16 | |
| - name: Install cargo-fuzz | |
| run: cargo install cargo-fuzz --locked --version 0.13.1 | |
| - name: Cache fuzz corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: fuzz/corpus/all_protocols | |
| key: fuzz-corpus-all-protocols-${{ github.sha }} | |
| restore-keys: | | |
| fuzz-corpus-all-protocols- | |
| - name: Run fast fuzzing (5 minutes) | |
| run: | | |
| cargo fuzz run all_protocols -- \ | |
| -max_total_time=300 \ | |
| -print_final_stats=1 \ | |
| -verbosity=1 | |
| continue-on-error: true | |
| - name: Check for crashes | |
| if: always() | |
| run: | | |
| if [ -d "fuzz/artifacts/all_protocols" ]; then | |
| # Only fail on actual crashes, not memory leaks (often false positives) | |
| if ls fuzz/artifacts/all_protocols/crash-* 2>/dev/null || \ | |
| ls fuzz/artifacts/all_protocols/timeout-* 2>/dev/null || \ | |
| ls fuzz/artifacts/all_protocols/oom-* 2>/dev/null; then | |
| echo "::error::Fuzzing found crashes!" | |
| ls -la fuzz/artifacts/all_protocols/ | |
| exit 1 | |
| fi | |
| # Log leaks but don't fail | |
| if ls fuzz/artifacts/all_protocols/leak-* 2>/dev/null; then | |
| echo "::warning::Memory leaks detected (not failing build)" | |
| ls -la fuzz/artifacts/all_protocols/leak-* | |
| fi | |
| else | |
| echo "::error::Fuzzing did not create fuzz/artifacts/all_protocols. cargo fuzz likely failed before execution." | |
| exit 1 | |
| fi | |
| - name: Upload crash artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fuzz-crashes-all-protocols | |
| path: fuzz/artifacts/all_protocols/ | |
| if-no-files-found: ignore | |
| fuzz-thorough: | |
| name: Thorough Fuzzing (validate_with_python) | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f | |
| with: | |
| toolchain: nightly-2026-04-16 | |
| - name: Install Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install cargo-fuzz | |
| run: cargo install cargo-fuzz --locked --version 0.13.1 | |
| - name: Cache fuzz corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: fuzz/corpus/validate_with_python | |
| key: fuzz-corpus-validate-python-${{ github.sha }} | |
| restore-keys: | | |
| fuzz-corpus-validate-python- | |
| - name: Determine fuzzing duration | |
| id: duration | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| INPUT_DURATION: ${{ github.event.inputs.duration }} | |
| run: | | |
| duration=1800 | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| case "$INPUT_DURATION" in | |
| ''|*[!0-9]*) | |
| echo "::error::workflow_dispatch input 'duration' must be a whole number of seconds" | |
| exit 1 | |
| ;; | |
| esac | |
| duration="$INPUT_DURATION" | |
| fi | |
| printf 'duration=%s\n' "$duration" >> "$GITHUB_OUTPUT" | |
| - name: Run thorough fuzzing with Python validation | |
| env: | |
| FUZZ_DURATION: ${{ steps.duration.outputs.duration }} | |
| PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python_and_ld_library_path | |
| run: | | |
| cargo fuzz run validate_with_python -- \ | |
| -max_total_time="$FUZZ_DURATION" \ | |
| -print_final_stats=1 \ | |
| -verbosity=1 | |
| continue-on-error: true | |
| - name: Check for crashes | |
| if: always() | |
| run: | | |
| if [ -d "fuzz/artifacts/validate_with_python" ]; then | |
| # Only fail on actual crashes, not memory leaks (often false positives) | |
| if ls fuzz/artifacts/validate_with_python/crash-* 2>/dev/null || \ | |
| ls fuzz/artifacts/validate_with_python/timeout-* 2>/dev/null || \ | |
| ls fuzz/artifacts/validate_with_python/oom-* 2>/dev/null; then | |
| echo "::error::Fuzzing found crashes!" | |
| ls -la fuzz/artifacts/validate_with_python/ | |
| exit 1 | |
| fi | |
| # Log leaks but don't fail | |
| if ls fuzz/artifacts/validate_with_python/leak-* 2>/dev/null; then | |
| echo "::warning::Memory leaks detected (not failing build)" | |
| ls -la fuzz/artifacts/validate_with_python/leak-* | |
| fi | |
| else | |
| echo "::error::Fuzzing did not create fuzz/artifacts/validate_with_python. cargo fuzz likely failed before execution." | |
| exit 1 | |
| fi | |
| - name: Upload crash artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fuzz-crashes-validate-python | |
| path: fuzz/artifacts/validate_with_python/ | |
| if-no-files-found: ignore | |
| fuzz-custom: | |
| name: Custom Fuzzing Run | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f | |
| with: | |
| toolchain: nightly-2026-04-16 | |
| - name: Install Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install cargo-fuzz | |
| run: cargo install cargo-fuzz --locked --version 0.13.1 | |
| - name: Validate custom fuzz target | |
| id: target | |
| env: | |
| INPUT_TARGET: ${{ github.event.inputs.target }} | |
| run: | | |
| case "$INPUT_TARGET" in | |
| all_protocols|validate_with_python) | |
| printf 'target=%s\n' "$INPUT_TARGET" >> "$GITHUB_OUTPUT" | |
| ;; | |
| *) | |
| echo "::error::workflow_dispatch input 'target' must be one of: all_protocols, validate_with_python" | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Cache fuzz corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: fuzz/corpus/${{ steps.target.outputs.target }} | |
| key: fuzz-corpus-${{ steps.target.outputs.target }}-${{ github.sha }} | |
| restore-keys: | | |
| fuzz-corpus-${{ steps.target.outputs.target }}- | |
| - name: Validate custom fuzz duration | |
| id: duration | |
| env: | |
| INPUT_DURATION: ${{ github.event.inputs.duration }} | |
| run: | | |
| case "$INPUT_DURATION" in | |
| ''|*[!0-9]*) | |
| echo "::error::workflow_dispatch input 'duration' must be a whole number of seconds" | |
| exit 1 | |
| ;; | |
| esac | |
| printf 'duration=%s\n' "$INPUT_DURATION" >> "$GITHUB_OUTPUT" | |
| - name: Run custom fuzzing | |
| env: | |
| FUZZ_TARGET: ${{ steps.target.outputs.target }} | |
| FUZZ_DURATION: ${{ steps.duration.outputs.duration }} | |
| PICKLE_FUZZ_PYTHON_ENV_POLICY: strip_setup_python_and_ld_library_path | |
| run: | | |
| cargo fuzz run "$FUZZ_TARGET" -- \ | |
| -max_total_time="$FUZZ_DURATION" \ | |
| -print_final_stats=1 \ | |
| -verbosity=1 | |
| continue-on-error: true | |
| - name: Check for crashes | |
| if: always() && steps.target.outputs.target != '' | |
| env: | |
| FUZZ_TARGET: ${{ steps.target.outputs.target }} | |
| run: | | |
| if [ -d "fuzz/artifacts/$FUZZ_TARGET" ]; then | |
| # Only fail on actual crashes, not memory leaks (often false positives) | |
| if ls "fuzz/artifacts/$FUZZ_TARGET"/crash-* 2>/dev/null || \ | |
| ls "fuzz/artifacts/$FUZZ_TARGET"/timeout-* 2>/dev/null || \ | |
| ls "fuzz/artifacts/$FUZZ_TARGET"/oom-* 2>/dev/null; then | |
| echo "::error::Fuzzing found crashes!" | |
| ls -la "fuzz/artifacts/$FUZZ_TARGET"/ | |
| exit 1 | |
| fi | |
| # Log leaks but don't fail | |
| if ls "fuzz/artifacts/$FUZZ_TARGET"/leak-* 2>/dev/null; then | |
| echo "::warning::Memory leaks detected (not failing build)" | |
| ls -la "fuzz/artifacts/$FUZZ_TARGET"/leak-* | |
| fi | |
| else | |
| echo "::error::Fuzzing did not create fuzz/artifacts/$FUZZ_TARGET. cargo fuzz likely failed before execution." | |
| exit 1 | |
| fi | |
| - name: Upload crash artifacts | |
| if: failure() && steps.target.outputs.target != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fuzz-crashes-${{ steps.target.outputs.target }} | |
| path: fuzz/artifacts/${{ steps.target.outputs.target }}/ | |
| if-no-files-found: ignore | |
| minimize-corpus: | |
| name: Minimize Corpus | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| needs: [fuzz-thorough] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f | |
| with: | |
| toolchain: nightly-2026-04-16 | |
| - name: Install cargo-fuzz | |
| run: cargo install cargo-fuzz --locked --version 0.13.1 | |
| - name: Restore corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| fuzz/corpus/all_protocols | |
| fuzz/corpus/validate_with_python | |
| key: fuzz-corpus-all-${{ github.sha }} | |
| restore-keys: | | |
| fuzz-corpus-all- | |
| - name: Minimize all_protocols corpus | |
| run: cargo fuzz cmin all_protocols | |
| continue-on-error: true | |
| - name: Minimize validate_with_python corpus | |
| run: cargo fuzz cmin validate_with_python | |
| continue-on-error: true | |
| - name: Report corpus stats | |
| run: | | |
| echo "## Corpus Statistics" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### all_protocols" >> $GITHUB_STEP_SUMMARY | |
| echo "Files: $(find fuzz/corpus/all_protocols -type f | wc -l)" >> $GITHUB_STEP_SUMMARY | |
| echo "Size: $(du -sh fuzz/corpus/all_protocols | cut -f1)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### validate_with_python" >> $GITHUB_STEP_SUMMARY | |
| echo "Files: $(find fuzz/corpus/validate_with_python -type f | wc -l)" >> $GITHUB_STEP_SUMMARY | |
| echo "Size: $(du -sh fuzz/corpus/validate_with_python | cut -f1)" >> $GITHUB_STEP_SUMMARY |