Fuzz #1269
Workflow file for this run
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 | |
| on: | |
| schedule: | |
| - cron: "0 */4 * * *" # every 4 hours | |
| workflow_dispatch: | |
| jobs: | |
| io_fuzz: | |
| name: "IO Fuzz" | |
| timeout-minutes: 230 # almost 4 hours | |
| runs-on: | |
| - runs-on=${{ github.run_id }} | |
| - family=m8g.large | |
| - image=ubuntu24-full-arm64 | |
| - disk=large | |
| - extras=s3-cache | |
| - tag=io-fuzz | |
| outputs: | |
| crashes_found: ${{ steps.check.outputs.crashes_found }} | |
| crash_count: ${{ steps.check.outputs.crash_count }} | |
| first_crash_name: ${{ steps.check.outputs.first_crash_name }} | |
| steps: | |
| - uses: runs-on/action@v2 | |
| with: | |
| sccache: s3 | |
| - uses: actions/checkout@v5 | |
| - uses: ./.github/actions/setup-rust | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| toolchain: nightly | |
| - name: Install llvm | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| compiler: llvm | |
| - name: Install cargo fuzz | |
| run: cargo install --locked cargo-fuzz | |
| - name: Restore corpus | |
| shell: bash | |
| run: | | |
| aws s3api head-object --bucket vortex-fuzz-corpus --key "io_corpus.tar.zst" --query ETag --output text > current_etag | |
| aws s3 cp s3://vortex-fuzz-corpus/io_corpus.tar.zst . | |
| tar -xf io_corpus.tar.zst | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_FUZZ_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_FUZZ_SECRET_ACCESS_KEY }} | |
| AWS_REGION: "us-east-1" | |
| AWS_ENDPOINT_URL: "https://01e9655179bbec953276890b183039bc.r2.cloudflarestorage.com" | |
| - name: Run fuzzing target | |
| id: fuzz | |
| run: | | |
| RUST_BACKTRACE=1 cargo +nightly fuzz run --release --debug-assertions file_io -- -max_total_time=7200 2>&1 | tee fuzz_output.log | |
| continue-on-error: true | |
| - name: Check for crashes | |
| id: check | |
| run: | | |
| if [ -d "fuzz/artifacts" ] && [ "$(ls -A fuzz/artifacts 2>/dev/null)" ]; then | |
| echo "crashes_found=true" >> $GITHUB_OUTPUT | |
| # Get the first crash file only | |
| FIRST_CRASH=$(find fuzz/artifacts -type f \( -name "crash-*" -o -name "leak-*" -o -name "timeout-*" -o -name "oom-*" \) | head -1) | |
| if [ -n "$FIRST_CRASH" ]; then | |
| echo "first_crash=$FIRST_CRASH" >> $GITHUB_OUTPUT | |
| echo "first_crash_name=$(basename $FIRST_CRASH)" >> $GITHUB_OUTPUT | |
| # Count all crashes for reporting | |
| CRASH_COUNT=$(find fuzz/artifacts -type f \( -name "crash-*" -o -name "leak-*" -o -name "timeout-*" -o -name "oom-*" \) | wc -l) | |
| echo "crash_count=$CRASH_COUNT" >> $GITHUB_OUTPUT | |
| echo "Found $CRASH_COUNT crash(es), will process first: $(basename $FIRST_CRASH)" | |
| fi | |
| else | |
| echo "crashes_found=false" >> $GITHUB_OUTPUT | |
| echo "crash_count=0" >> $GITHUB_OUTPUT | |
| echo "No crashes found" | |
| fi | |
| - name: Archive crash artifacts | |
| if: steps.check.outputs.crashes_found == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: io-fuzzing-crash-artifacts | |
| path: fuzz/artifacts | |
| retention-days: 30 | |
| - name: Archive fuzzer output log | |
| if: steps.check.outputs.crashes_found == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: io-fuzzing-logs | |
| path: fuzz_output.log | |
| retention-days: 30 | |
| - name: Persist corpus | |
| shell: bash | |
| run: | | |
| tar -acf io_corpus.tar.zst fuzz/corpus/file_io | |
| aws s3api put-object --bucket vortex-fuzz-corpus --key "io_corpus.tar.zst" --body io_corpus.tar.zst --checksum-algorithm CRC32 --if-match "$(cat current_etag)" | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_FUZZ_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_FUZZ_SECRET_ACCESS_KEY }} | |
| AWS_REGION: "us-east-1" | |
| AWS_ENDPOINT_URL: "https://01e9655179bbec953276890b183039bc.r2.cloudflarestorage.com" | |
| - name: Fail job if fuzz run found a bug | |
| if: steps.check.outputs.crashes_found == 'true' | |
| run: exit 1 | |
| report-io-fuzz-failures: | |
| name: "Report IO Fuzz Failures" | |
| needs: io_fuzz | |
| if: always() && needs.io_fuzz.outputs.crashes_found == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| - name: Download crash artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: io-fuzzing-crash-artifacts | |
| path: ./crash_artifacts | |
| - name: Download fuzzer logs | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: io-fuzzing-logs | |
| path: ./logs | |
| - name: Extract first crash only | |
| run: | | |
| # Only keep the first crash file for analysis | |
| FIRST_CRASH="${{ needs.io_fuzz.outputs.first_crash_name }}" | |
| echo "Processing only first crash: $FIRST_CRASH" | |
| # Create a clean directory with just the first crash | |
| mkdir -p ./first_crash | |
| if [ -f "crash_artifacts/$FIRST_CRASH" ]; then | |
| cp "crash_artifacts/$FIRST_CRASH" ./first_crash/ | |
| echo "Copied first crash to ./first_crash/" | |
| else | |
| echo "Warning: Could not find crash file $FIRST_CRASH" | |
| echo "Available files:" | |
| ls -la crash_artifacts/ || echo "No files in crash_artifacts" | |
| fi | |
| - name: Show crash info | |
| run: | | |
| echo "=== Crash Summary ===" | |
| echo "Total crashes found: ${{ needs.io_fuzz.outputs.crash_count }}" | |
| echo "Processing first crash: ${{ needs.io_fuzz.outputs.first_crash_name }}" | |
| echo "" | |
| echo "=== First crash file size ===" | |
| ls -lh first_crash/ || echo "No crash file" | |
| echo "" | |
| echo "=== Fuzzer log preview (last 100 lines, where crashes are reported) ===" | |
| tail -100 logs/fuzz_output.log || echo "No log file" | |
| # Use Claude Code Action to analyze crash and decide if duplicate | |
| - name: Analyze crash with Claude | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| prompt: | | |
| # Fuzzer Crash Analysis Task | |
| A fuzzing run for the `file_io` target has detected a crash. Your task is to analyze it and determine if it's a duplicate of an existing issue. | |
| ## Step 1: Analyze the Crash | |
| 1. Read the fuzzer log: `logs/fuzz_output.log` | |
| 2. Extract the stack trace (lines starting with `#0`, `#1`, etc.) | |
| 3. Extract the error message (look for "panicked at" or "ERROR:") | |
| 4. Identify the crash location (top frame in the stack trace from user code, not std/core) | |
| 5. Read the source code at the crash location to understand what failed | |
| ## Step 2: Check for Duplicates | |
| 1. List existing fuzzer issues: | |
| ```bash | |
| gh issue list --repo ${{ github.repository }} --label fuzzer --state open --json number,title,body --limit 50 | |
| ``` | |
| 2. For each existing issue: | |
| - Compare stack traces: Do they crash at the same location (same file + function)? | |
| - Compare error messages: Are they the same error pattern? | |
| - IMPORTANT: Normalize values when comparing! | |
| - "index 5 out of bounds" and "index 12 out of bounds" are THE SAME bug | |
| - "len is 100" and "len is 5" are THE SAME bug | |
| - If needed, read source code at the crash location to understand if it's the same assertion/panic | |
| 3. Make a decision: | |
| - **DUPLICATE** if: Same crash location + same error pattern (values can differ) | |
| - **NEW BUG** if: Different location OR different error type | |
| ## Step 3: Output Decision | |
| Create a file `decision.json` with this exact format: | |
| ```json | |
| { | |
| "is_duplicate": true or false, | |
| "duplicate_of": issue_number (if duplicate) or null, | |
| "confidence": "high" | "medium" | "low", | |
| "reasoning": "Explain your decision in 2-3 sentences", | |
| "crash_location": "file.rs:function_name", | |
| "error_message": "the extracted error message", | |
| "stack_trace_preview": "top 5 frames", | |
| "suggested_title": "Brief title for new issue (if not duplicate)" | |
| } | |
| ``` | |
| ## Important Notes | |
| - You have full access to the repository - read any source files you need | |
| - Be conservative: when unsure, mark as NEW BUG (false negatives are better than false positives) | |
| - The goal is to prevent duplicate issues while being careful not to merge distinct bugs | |
| - Focus on the ROOT CAUSE, not the specific values in error messages | |
| ## Context | |
| - Target: file_io | |
| - Crash file: ${{ needs.io_fuzz.outputs.first_crash_name }} | |
| - Total crashes: ${{ needs.io_fuzz.outputs.crash_count }} | |
| - Workflow: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| Start by reading the fuzzer log. | |
| claude_args: | | |
| --model claude-sonnet-4-5-20250929 | |
| --max-turns 20 | |
| - name: Read Claude's decision | |
| id: decision | |
| run: | | |
| if [ ! -f decision.json ]; then | |
| echo "Error: decision.json not found" | |
| exit 1 | |
| fi | |
| cat decision.json | |
| IS_DUP=$(jq -r '.is_duplicate' decision.json) | |
| DUPLICATE_OF=$(jq -r '.duplicate_of // empty' decision.json) | |
| echo "is_duplicate=$IS_DUP" >> $GITHUB_OUTPUT | |
| echo "duplicate_of=$DUPLICATE_OF" >> $GITHUB_OUTPUT | |
| if [ "$IS_DUP" = "true" ]; then | |
| echo "✓ Claude identified this as a duplicate of issue #$DUPLICATE_OF" | |
| else | |
| echo "✓ Claude identified this as a NEW bug" | |
| fi | |
| - name: Create GitHub issue for new bug | |
| if: steps.decision.outputs.is_duplicate == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Extract data from Claude's analysis | |
| TITLE=$(jq -r '.suggested_title' decision.json) | |
| CRASH_LOCATION=$(jq -r '.crash_location' decision.json) | |
| ERROR_MESSAGE=$(jq -r '.error_message' decision.json) | |
| STACK_TRACE=$(jq -r '.stack_trace_preview' decision.json) | |
| REASONING=$(jq -r '.reasoning' decision.json) | |
| # Extract crash type from filename | |
| CRASH_FILE="${{ needs.io_fuzz.outputs.first_crash_name }}" | |
| if [[ "$CRASH_FILE" == crash-* ]]; then | |
| CRASH_TYPE="Crash" | |
| elif [[ "$CRASH_FILE" == leak-* ]]; then | |
| CRASH_TYPE="Memory Leak" | |
| elif [[ "$CRASH_FILE" == timeout-* ]]; then | |
| CRASH_TYPE="Timeout" | |
| elif [[ "$CRASH_FILE" == oom-* ]]; then | |
| CRASH_TYPE="Out of Memory" | |
| else | |
| CRASH_TYPE="Unknown" | |
| fi | |
| # Create issue with gh CLI, using Claude's analysis | |
| gh issue create \ | |
| --repo ${{ github.repository }} \ | |
| --title "Fuzzing $CRASH_TYPE: $TITLE" \ | |
| --label "bug,fuzzer" \ | |
| --body "$(cat <<EOF_BODY | |
| ## Fuzzing Crash Report | |
| The \`file_io\` fuzzing target detected a $CRASH_TYPE during a scheduled fuzzing run. | |
| ### Claude's Analysis | |
| **Crash Location**: \`$CRASH_LOCATION\` | |
| **Error Message**: | |
| \`\`\` | |
| $ERROR_MESSAGE | |
| \`\`\` | |
| **Stack Trace Preview**: | |
| \`\`\` | |
| $STACK_TRACE | |
| \`\`\` | |
| **Analysis**: $REASONING | |
| ### Summary | |
| - **Crash Type**: $CRASH_TYPE | |
| - **Target**: \`file_io\` | |
| - **Crash File**: \`${{ needs.io_fuzz.outputs.first_crash_name }}\` | |
| - **Total Crashes Found**: ${{ needs.io_fuzz.outputs.crash_count }} | |
| - **Workflow Run**: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| - **Timestamp**: $(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| - **Branch**: ${{ github.ref_name }} | |
| - **Commit**: ${{ github.sha }} | |
| ### Crash Artifacts | |
| Download crash artifacts from the workflow run: | |
| **https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}** | |
| Artifacts available: | |
| - \`io-fuzzing-crash-artifacts\` - All crash files found (includes ${{ needs.io_fuzz.outputs.crash_count }} crashes) | |
| - \`io-fuzzing-logs\` - Complete fuzzer output with stack traces | |
| ### Reproduction Steps | |
| 1. Download the \`io-fuzzing-crash-artifacts\` from the workflow run above | |
| 2. Extract the crash file to your local \`fuzz/artifacts/file_io/\` directory | |
| 3. Reproduce the crash locally: | |
| \`\`\`bash | |
| cargo +nightly fuzz run file_io fuzz/artifacts/file_io/${{ needs.io_fuzz.outputs.first_crash_name }} | |
| \`\`\` | |
| 4. Get full backtrace: | |
| \`\`\`bash | |
| RUST_BACKTRACE=full cargo +nightly fuzz run file_io fuzz/artifacts/file_io/${{ needs.io_fuzz.outputs.first_crash_name }} | |
| \`\`\` | |
| 5. Minimize the test case (optional): | |
| \`\`\`bash | |
| cargo +nightly fuzz tmin file_io fuzz/artifacts/file_io/${{ needs.io_fuzz.outputs.first_crash_name }} | |
| \`\`\` | |
| ### Investigation Checklist | |
| - [ ] Download crash artifacts from workflow run | |
| - [ ] Reproduce crash locally with full backtrace | |
| - [ ] Analyze stack trace and identify root cause | |
| - [ ] Determine severity (security vs stability) | |
| - [ ] Minimize test case if needed | |
| - [ ] Create fix PR with reference to this issue | |
| - [ ] Add regression test | |
| - [ ] Verify fix with: `cargo +nightly fuzz run file_io <crash-file>` | |
| ### Environment | |
| - **Runner**: ubuntu24-full-arm64 | |
| - **Rust Toolchain**: nightly | |
| - **Fuzz Duration**: 7200 seconds (2 hours) | |
| - **Fuzzer**: cargo-fuzz (libFuzzer) | |
| ### Additional Context | |
| This issue was automatically created by the fuzzing workflow. If multiple crashes were found, this issue represents the first crash detected. All crash artifacts are available for download. | |
| **Note**: If this issue is a duplicate of an existing bug, please close it and reference the original issue. | |
| --- | |
| *Automatically created by fuzzing workflow with Claude AI analysis* | |
| *Workflow file: [fuzz.yml](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/fuzz.yml)* | |
| *Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}* | |
| EOF_BODY | |
| )" | |
| - name: Comment on duplicate issue | |
| if: steps.decision.outputs.is_duplicate == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ISSUE_NUM="${{ steps.decision.outputs.duplicate_of }}" | |
| REASONING=$(jq -r '.reasoning' decision.json) | |
| CONFIDENCE=$(jq -r '.confidence' decision.json) | |
| gh issue comment "$ISSUE_NUM" \ | |
| --repo ${{ github.repository }} \ | |
| --body "$(cat <<EOF_COMMENT | |
| ## Additional Crash Detected | |
| Another crash was found in the \`file_io\` fuzzing target that appears to be a duplicate of this issue. | |
| ### Claude's Analysis | |
| **Confidence**: $CONFIDENCE | |
| **Reasoning**: $REASONING | |
| ### Details | |
| - **Crash File**: \`${{ needs.io_fuzz.outputs.first_crash_name }}\` | |
| - **Total Crashes**: ${{ needs.io_fuzz.outputs.crash_count }} found in this run | |
| - **Workflow Run**: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| - **Timestamp**: $(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| ### Artifacts | |
| Download crash artifacts from: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| Claude analyzed the crash and determined it matches this issue based on crash location, error pattern, and code analysis. | |
| --- | |
| *Automatically added by fuzzing workflow* | |
| EOF_COMMENT | |
| )" | |
| ops_fuzz: | |
| name: "Array Operations Fuzz" | |
| timeout-minutes: 230 # almost 4 hours | |
| runs-on: | |
| - runs-on=${{ github.run_id }} | |
| - family=m8g.large | |
| - image=ubuntu24-full-arm64 | |
| - disk=large | |
| - extras=s3-cache | |
| - tag=ops-fuzz | |
| steps: | |
| - uses: runs-on/action@v2 | |
| with: | |
| sccache: s3 | |
| - uses: actions/checkout@v5 | |
| - uses: ./.github/actions/setup-rust | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| toolchain: nightly | |
| - name: Install llvm | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| compiler: llvm | |
| - name: Install cargo fuzz | |
| run: cargo install --locked cargo-fuzz | |
| - name: Restore corpus | |
| shell: bash | |
| run: | | |
| aws s3api head-object --bucket vortex-fuzz-corpus --key "array_ops_corpus.tar.zst" --query ETag --output text > current_etag | |
| aws s3 cp s3://vortex-fuzz-corpus/array_ops_corpus.tar.zst . | |
| tar -xf array_ops_corpus.tar.zst | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_FUZZ_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_FUZZ_SECRET_ACCESS_KEY }} | |
| AWS_REGION: "us-east-1" | |
| AWS_ENDPOINT_URL: "https://01e9655179bbec953276890b183039bc.r2.cloudflarestorage.com" | |
| - name: Run fuzzing target | |
| id: fuzz | |
| run: RUST_BACKTRACE=1 cargo +nightly fuzz run --release --debug-assertions array_ops -- -max_total_time=7200 | |
| continue-on-error: true | |
| - name: Archive crash artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: operations-fuzzing-crash-artifacts | |
| path: fuzz/artifacts | |
| - name: Persist corpus | |
| shell: bash | |
| run: | | |
| tar -acf array_ops_corpus.tar.zst fuzz/corpus/array_ops | |
| aws s3api put-object --bucket vortex-fuzz-corpus --key "array_ops_corpus.tar.zst" --body array_ops_corpus.tar.zst --checksum-algorithm CRC32 --if-match "$(cat current_etag)" | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_FUZZ_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_FUZZ_SECRET_ACCESS_KEY }} | |
| AWS_REGION: "us-east-1" | |
| AWS_ENDPOINT_URL: "https://01e9655179bbec953276890b183039bc.r2.cloudflarestorage.com" | |
| - name: Fail job if fuzz run found a bug | |
| if: steps.fuzz.outcome == 'failure' | |
| run: exit 1 |