pmcp 2.22.5

High-quality Rust SDK for Model Context Protocol (MCP) with full TypeScript SDK compatibility
Documentation
name: Fuzzing

on:
  schedule:
    # Run daily at 2 AM UTC
    - cron: '0 2 * * *'
  workflow_dispatch:
  pull_request:
    paths:
      - 'src/**'
      - 'fuzz/**'
      - '.github/workflows/fuzz.yml'

env:
  CARGO_TERM_COLOR: always

jobs:
  fuzz:
    name: Fuzz Testing
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - protocol_parsing
          - jsonrpc_handling
          - transport_layer
          - auth_flows
          # Phase 125-05: SEP-2640 skill entry synthesis. Registering a target
          # in fuzz/Cargo.toml does NOT enrol it here — this matrix is an
          # explicit list, and several registered targets are deliberately
          # absent from it. The CLAUDE.md ALWAYS/FUZZ requirement's substance is
          # RECURRING execution against hostile input; a registered-but-unrun
          # target satisfies only its letter. Arbitrary author-supplied bytes
          # reaching serde_yaml is exactly what a scheduled campaign is for.
          - fuzz_skill_entry
          # Phase 126-03: the workflow-to-skill projection. Same reasoning as
          # the row above, one boundary further in — `as_skill()` is public and
          # INFALLIBLE, so author prose reaching its slug truncation and its
          # YAML frontmatter encoder has a panic as its only failure mode, and
          # those bytes are published as a sha256 pin (D-14).
          - fuzz_workflow_projection
          # Phase 128-10 (SC-7 / T-128-47): the `tools/call` INPUT-schema
          # enforcement path and its value-free refusal renderer. Same reasoning
          # as the two rows above — a registered-but-unrun target satisfies only
          # the letter of the CLAUDE.md ALWAYS/FUZZ requirement — with one
          # addition specific to this pair. These two are ALSO run by
          # `make test-fuzz-strict`, which `make quality-gate` chains and which
          # the `quality-gate` CI job provisions nightly + cargo-fuzz for. That
          # is the PR gate; this matrix is the recurring DEEP campaign
          # (`-max_total_time=300` versus the gate's short bound), and the two
          # are complementary rather than redundant.
          #
          # MEASURED 2026-09-27: the org ruleset "Green Main — unified gate
          # enforcement" requires exactly ONE status context, `gate`. This
          # workflow is NOT a required check, so enrolling here alone would give
          # recurring execution and no merge gate — which is why the leg is
          # provisioned in `ci.yml`'s quality-gate job as well. Do not read this
          # matrix row as the gate.
          - fuzz_input_schema_enforcement
          # Phase 128-10 (T-128-48): the LOOK at RESEARCH assumption A2 — a
          # config-supplied `pattern` as a ReDoS vector. The daily schedule is
          # most of this target's value: A2 was not falsified at n <= 28, and a
          # recurring campaign is how a residual that needs more budget than a
          # PR gate can spend would surface. A timeout here is a finding to FILE,
          # not a licence to hand-roll a regex guard — see the target's header.
          - fuzz_placeholder_pattern_redos

    steps:
      - uses: actions/checkout@v7

      - name: Remove rust-toolchain.toml to allow nightly
        run: rm -f rust-toolchain.toml

      - name: Install Rust nightly
        uses: dtolnay/rust-toolchain@nightly
        with:
          components: llvm-tools-preview

      - name: Install cargo-fuzz
        run: cargo install cargo-fuzz
      
      - name: Cache fuzz corpus
        uses: actions/cache@v6
        with:
          path: fuzz/corpus
          key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }}
          restore-keys: |
            fuzz-corpus-${{ matrix.target }}-
      
      - name: Run fuzzing (${{ matrix.target }})
        run: |
          cargo fuzz run ${{ matrix.target }} -- \
            -max_total_time=300 \
            -print_final_stats=1 \
            -detect_leaks=0
        # libFuzzer caps active fuzzing at 300s. The step budget covers
        # corpus flush + actions/cache save + sanitizer shutdown tail —
        # which can spike when a target's input space expands (e.g.
        # adding a `HashMap<String, Value>` field) and a single run adds
        # thousands of new corpus entries. 15 min gives that tail room
        # to land without losing CI signal.
        timeout-minutes: 15
      
      - name: Minimize corpus
        if: github.event_name == 'schedule'
        run: cargo fuzz cmin ${{ matrix.target }}
      
      - name: Upload crash artifacts
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-crashes-${{ matrix.target }}
          path: fuzz/artifacts/${{ matrix.target }}/
      
      - name: Upload corpus
        if: github.event_name == 'schedule'
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-corpus-${{ matrix.target }}
          path: fuzz/corpus/${{ matrix.target }}/

  fuzz-coverage:
    name: Fuzzing Coverage
    runs-on: ubuntu-latest
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

    steps:
      - uses: actions/checkout@v7

      - name: Remove rust-toolchain.toml to allow nightly
        run: rm -f rust-toolchain.toml

      - name: Install Rust nightly
        uses: dtolnay/rust-toolchain@nightly
        with:
          components: llvm-tools-preview
      
      - name: Install tools
        run: |
          cargo install cargo-fuzz
          cargo install rustfilt
      
      - name: Cache fuzz corpus
        uses: actions/cache@v6
        with:
          path: fuzz/corpus
          key: fuzz-corpus-all-${{ github.sha }}
          restore-keys: |
            fuzz-corpus-all-
            fuzz-corpus-
      
      - name: Generate minimal corpus if needed
        run: |
          for target in protocol_parsing jsonrpc_handling transport_layer auth_flows; do
            # Create corpus directory if it doesn't exist
            mkdir -p fuzz/corpus/$target
            
            # If corpus is empty, run fuzzer briefly to generate some inputs
            if [ -z "$(ls -A fuzz/corpus/$target 2>/dev/null)" ]; then
              echo "No corpus found for $target, generating minimal corpus..."
              cargo fuzz run $target -- \
                -max_total_time=10 \
                -print_final_stats=1 \
                -detect_leaks=0 || true
            fi
          done
      
      - name: Generate coverage
        run: |
          for target in protocol_parsing jsonrpc_handling transport_layer auth_flows; do
            cargo fuzz coverage $target
          done
      
      - name: Upload coverage reports
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-coverage
          path: fuzz/coverage/

  fuzz-24h:
    name: 24-Hour Fuzzing
    runs-on: ubuntu-latest
    if: github.event_name == 'workflow_dispatch'

    steps:
      - uses: actions/checkout@v7

      - name: Remove rust-toolchain.toml to allow nightly
        run: rm -f rust-toolchain.toml

      - name: Install Rust nightly
        uses: dtolnay/rust-toolchain@nightly
      
      - name: Install cargo-fuzz
        run: cargo install cargo-fuzz
      
      - name: Run 24-hour fuzzing
        run: |
          # Run each target for 6 hours (total 24 hours)
          for target in protocol_parsing jsonrpc_handling transport_layer auth_flows; do
            echo "Starting 6-hour fuzz for $target"
            cargo fuzz run $target -- \
              -max_total_time=21600 \
              -print_final_stats=1 \
              -detect_leaks=0 || true
          done
        timeout-minutes: 1440  # 24 hours
      
      - name: Upload results
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-24h-results
          path: |
            fuzz/corpus/
            fuzz/artifacts/
            fuzz/*.log