name: Fuzz

# Coverage-guided fuzzing via cargo-fuzz / libFuzzer. Requires nightly
# Rust, so this runs out-of-band from the stable PR CI: weekly on
# Monday at 05:47 UTC, plus on manual dispatch. Crashes are uploaded as
# workflow artifacts so they can be added to the per-target seed corpus
# as regression fixtures.
#
# The 29 targets and the `fuzz/` workspace they live in are intentionally
# excluded from the main Cargo workspace, so nothing here affects the
# day-to-day stable build.

on:
  workflow_dispatch:
    inputs:
      duration:
        description: "Seconds to fuzz each target"
        default: "60"
        required: false
  schedule:
    # Weekly. Offset off the hour so we don't pile onto the :00 cron rush.
    - cron: "47 5 * * 1"

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  fuzz:
    name: ${{ matrix.target }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        target:
          - der_reader
          - pem_decode
          - x509_certificate
          - x509_crl
          - x509_csr
          - spki_pubkey
          - pkcs8_rsa
          - pkcs8_ed25519
          - pkcs8_mldsa
          - pkcs8_slhdsa
          - pbes2_decrypt
          - ecdsa_sig_der
          - mlkem_pkcs8
          - dh_share
          - quic_transport_params
          - ocsp_response
          - lms_parse
          - xmss_parse
          - tls_server_feed
          - tls_client_feed
          - dtls_server_feed
          - dtls_client_feed
          - quic_server_feed
          - quic_client_feed
          - tls_legacy_feed
          - ech_config_list
          - ech_retry_configs
          - ech_extension
          - cert_decompress
    steps:
      - uses: actions/checkout@v6

      - name: Install nightly Rust
        uses: dtolnay/rust-toolchain@nightly

      - name: Cache
        uses: Swatinem/rust-cache@v2
        with:
          # Keyed per target so libFuzzer's instrumented build cache
          # survives across matrix entries.
          workspaces: fuzz
          key: fuzz-${{ matrix.target }}

      - name: Install cargo-fuzz
        run: cargo install cargo-fuzz --locked

      - name: Run ${{ matrix.target }}
        run: |
          DURATION="${{ github.event.inputs.duration || '60' }}"
          cd fuzz
          cargo +nightly fuzz run "${{ matrix.target }}" \
            -- -max_total_time="$DURATION" -print_final_stats=1

      - name: Upload crash artifacts
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: fuzz-crashes-${{ matrix.target }}
          path: |
            fuzz/artifacts/${{ matrix.target }}/
          retention-days: 30
          if-no-files-found: ignore
