ClusterFuzzLite batch fuzzing #270
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: ClusterFuzzLite batch fuzzing | |
| on: | |
| push: | |
| branches: | |
| - feat/clusterfuzzlite | |
| schedule: | |
| # Run every 6 hours: 00:00, 06:00, 12:00, 18:00 | |
| - cron: '0 */6 * * *' | |
| # Allow manual trigger | |
| workflow_dispatch: | |
| inputs: | |
| fuzz-seconds: | |
| description: 'Fuzzing duration in 60 seconds' | |
| required: false | |
| default: '60' | |
| type: string | |
| permissions: | |
| contents: read | |
| security-events: write | |
| # ============================================================================== | |
| # Storage Repository Configuration for Organization | |
| # ============================================================================== | |
| # Required secrets/variables: | |
| # - Secret: CLUSTERFUZZLITE_STORAGE_TOKEN (PAT with repo write access) | |
| # - Variable: CLUSTERFUZZLITE_STORAGE_REPO (e.g., morph-l2/fuzz_corpora) | |
| # | |
| # Note: GitHub Actions has a 6-hour job time limit. | |
| # ClusterFuzzLite will automatically run all compiled fuzzers in round-robin. | |
| # ============================================================================== | |
| env: | |
| STORAGE_REPO_URL: ${{ secrets.CLUSTERFUZZLITE_STORAGE_TOKEN && vars.CLUSTERFUZZLITE_STORAGE_REPO && format('https://{0}@github.com/{1}.git', secrets.CLUSTERFUZZLITE_STORAGE_TOKEN, vars.CLUSTERFUZZLITE_STORAGE_REPO) || '' }} | |
| STORAGE_REPO_BRANCH: main | |
| # push: 120s (2min), schedule: 120s (2min), manual: use input | |
| FUZZ_SECONDS: ${{ github.event_name == 'push' && '300' || github.event_name == 'schedule' && '14400' || inputs.fuzz-seconds || '19800' }} | |
| jobs: | |
| BatchFuzzing: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Build Fuzzers | |
| id: build | |
| uses: google/clusterfuzzlite/actions/build_fuzzers@v1 | |
| with: | |
| language: go | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| sanitizer: address | |
| keep-unaffected-fuzz-targets: ${{ github.event_name == 'push' }} | |
| storage-repo: ${{ env.STORAGE_REPO_URL }} | |
| storage-repo-branch: ${{ env.STORAGE_REPO_BRANCH }} | |
| - name: Run Fuzzers | |
| id: run | |
| uses: google/clusterfuzzlite/actions/run_fuzzers@v1 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| fuzz-seconds: ${{ env.FUZZ_SECONDS }} | |
| mode: 'batch' | |
| sanitizer: address | |
| output-sarif: true | |
| parallel-fuzzing: false | |
| storage-repo: ${{ env.STORAGE_REPO_URL }} | |
| storage-repo-branch: ${{ env.STORAGE_REPO_BRANCH }} | |
| - name: Upload Crash Artifacts | |
| uses: actions/upload-artifact@v4 | |
| if: failure() && steps.run.outcome == 'failure' | |
| with: | |
| name: crash-artifacts | |
| path: ./out/artifacts | |
| retention-days: 90 | |
| # ======================================================================== | |
| # Upload crashes to fuzz_corpora repository | |
| # ======================================================================== | |
| - name: Upload crashes to storage repository | |
| if: always() && env.STORAGE_REPO_URL != '' | |
| run: | | |
| set -e | |
| ARTIFACTS_PATH="./out/artifacts" | |
| # Check if there are any crash files | |
| if [ ! -d "$ARTIFACTS_PATH" ]; then | |
| echo "No artifacts directory found, skipping" | |
| exit 0 | |
| fi | |
| CRASH_COUNT=$(find "$ARTIFACTS_PATH" -name "crash-*" -o -name "leak-*" -o -name "timeout-*" 2>/dev/null | grep -v "\.summary$" | wc -l) | |
| if [ "$CRASH_COUNT" -eq 0 ]; then | |
| echo "No crashes found, skipping" | |
| exit 0 | |
| fi | |
| echo "Found $CRASH_COUNT crash files, uploading to storage repo..." | |
| # Clone storage repo | |
| git clone --depth 1 --branch "${{ env.STORAGE_REPO_BRANCH }}" "${{ env.STORAGE_REPO_URL }}" /tmp/fuzz_corpora | |
| # Create crashes directory with date | |
| DATE=$(date +%Y-%m-%d) | |
| RUN_ID="${{ github.run_id }}" | |
| CRASH_DIR="/tmp/fuzz_corpora/crashes/${DATE}_run${RUN_ID}" | |
| mkdir -p "$CRASH_DIR" | |
| # Create a summary report | |
| REPORT_FILE="$CRASH_DIR/REPORT.md" | |
| cat > "$REPORT_FILE" << EOF | |
| # Fuzzing Crash Report | |
| - **Date**: $(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| - **Repository**: ${{ github.repository }} | |
| - **Branch**: ${{ github.ref_name }} | |
| - **Commit**: ${{ github.sha }} | |
| - **Run ID**: [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| - **Sanitizer**: address | |
| - **Fuzz Duration**: ${{ env.FUZZ_SECONDS }}s | |
| ## Crashes Found | |
| EOF | |
| # Copy crash files and add to report | |
| for crash_file in $(find "$ARTIFACTS_PATH" -name "crash-*" -o -name "leak-*" -o -name "timeout-*" 2>/dev/null | grep -v "\.summary$"); do | |
| filename=$(basename "$crash_file") | |
| fuzzer_dir=$(basename $(dirname "$crash_file")) | |
| parent_dir=$(basename $(dirname $(dirname "$crash_file"))) | |
| # Determine fuzzer name | |
| if [[ "$parent_dir" == fuzz_* ]]; then | |
| fuzzer_name="${parent_dir#fuzz_}" | |
| else | |
| fuzzer_name="$fuzzer_dir" | |
| fi | |
| # Create fuzzer subdirectory | |
| mkdir -p "$CRASH_DIR/$fuzzer_name" | |
| # Copy crash file | |
| cp "$crash_file" "$CRASH_DIR/$fuzzer_name/" | |
| # Copy summary if exists | |
| if [ -f "${crash_file}.summary" ]; then | |
| cp "${crash_file}.summary" "$CRASH_DIR/$fuzzer_name/" | |
| fi | |
| # Add to report | |
| echo "### $fuzzer_name / $filename" >> "$REPORT_FILE" | |
| echo "" >> "$REPORT_FILE" | |
| if [ -f "${crash_file}.summary" ]; then | |
| echo '```' >> "$REPORT_FILE" | |
| head -50 "${crash_file}.summary" >> "$REPORT_FILE" | |
| echo '```' >> "$REPORT_FILE" | |
| else | |
| echo "No summary available" >> "$REPORT_FILE" | |
| fi | |
| echo "" >> "$REPORT_FILE" | |
| done | |
| # Commit and push | |
| cd /tmp/fuzz_corpora | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add . | |
| git commit -m "Add crashes from ${{ github.repository }} run ${{ github.run_id }}" || echo "No changes to commit" | |
| git pull --rebase origin ${{ env.STORAGE_REPO_BRANCH }} || true | |
| git push || echo "Failed to push, maybe no changes" | |
| echo "✅ Crashes uploaded to storage repository" | |
| echo "📁 Location: crashes/${DATE}_run${RUN_ID}/" | |
| # ======================================================================== | |
| # SARIF upload (keep trying, might work someday) | |
| # ======================================================================== | |
| - name: Create enhanced SARIF | |
| if: always() && steps.run.outcome != 'skipped' | |
| run: | | |
| python3 << 'EOF' | |
| import json | |
| import os | |
| import glob | |
| original_sarif_path = "./cifuzz-sarif/results.sarif" | |
| enhanced_sarif_path = "./enhanced-results.sarif" | |
| artifacts_path = "./out/artifacts" | |
| if not os.path.exists(original_sarif_path): | |
| print("No SARIF file found, skipping") | |
| exit(0) | |
| with open(original_sarif_path, 'r') as f: | |
| sarif = json.load(f) | |
| crash_files = [] | |
| if os.path.exists(artifacts_path): | |
| crash_files = glob.glob(f"{artifacts_path}/**/crash-*", recursive=True) | |
| crash_files.extend(glob.glob(f"{artifacts_path}/**/leak-*", recursive=True)) | |
| crash_files.extend(glob.glob(f"{artifacts_path}/**/timeout-*", recursive=True)) | |
| # Filter out .summary files | |
| crash_files = [f for f in crash_files if not f.endswith('.summary')] | |
| print(f"Found {len(crash_files)} crash artifacts") | |
| if not crash_files: | |
| with open(enhanced_sarif_path, 'w') as f: | |
| json.dump(sarif, f, indent=2) | |
| exit(0) | |
| fuzzer_to_source = { | |
| "bn256": "tests/fuzzers/bn256/bn256_fuzz.go", | |
| "bls12381": "tests/fuzzers/bls12381/precompile_fuzzer.go", | |
| "difficulty": "tests/fuzzers/difficulty/difficulty-fuzz.go", | |
| "bitutil": "tests/fuzzers/bitutil/compress_fuzz.go", | |
| "keystore": "tests/fuzzers/keystore/keystore-fuzzer.go", | |
| "runtime": "tests/fuzzers/runtime/runtime_fuzz.go", | |
| "stacktrie": "tests/fuzzers/stacktrie/trie_fuzzer.go", | |
| "rlp": "tests/fuzzers/rlp/rlp_fuzzer.go", | |
| "abi": "tests/fuzzers/abi/abifuzzer.go", | |
| "secp256k1": "tests/fuzzers/secp256k1/secp_fuzzer.go", | |
| "trie": "tests/fuzzers/trie/trie-fuzzer.go", | |
| "txfetcher": "tests/fuzzers/txfetcher/txfetcher_fuzzer.go", | |
| "vflux": "tests/fuzzers/vflux/client-fuzzer.go", | |
| } | |
| results = [] | |
| for crash_file in crash_files: | |
| crash_name = os.path.basename(crash_file) | |
| path_parts = crash_file.split(os.sep) | |
| fuzzer_dir = None | |
| for part in path_parts: | |
| if part.startswith("fuzz_"): | |
| fuzzer_dir = part.replace("fuzz_", "") | |
| break | |
| if not fuzzer_dir: | |
| fuzzer_dir = os.path.basename(os.path.dirname(crash_file)) | |
| source_file = None | |
| for fuzzer_key, source_path in fuzzer_to_source.items(): | |
| if fuzzer_key in fuzzer_dir.lower(): | |
| source_file = source_path | |
| break | |
| if not source_file: | |
| source_file = f"tests/fuzzers/{fuzzer_dir}/fuzz.go" | |
| if crash_name.startswith("crash-"): | |
| rule_id, message = "no-crashes", f"Fuzzer '{fuzzer_dir}' found a crash" | |
| elif crash_name.startswith("leak-"): | |
| rule_id, message = "direct-leak", f"Fuzzer '{fuzzer_dir}' found a memory leak" | |
| elif crash_name.startswith("timeout-"): | |
| rule_id, message = "no-crashes", f"Fuzzer '{fuzzer_dir}' caused a timeout" | |
| else: | |
| rule_id, message = "no-crashes", f"Fuzzer '{fuzzer_dir}' found an issue" | |
| summary_file = crash_file + ".summary" | |
| if os.path.exists(summary_file): | |
| try: | |
| with open(summary_file, 'r', errors='ignore') as f: | |
| message += f"\n\nCrash summary:\n{f.read(1000)}" | |
| except: | |
| pass | |
| results.append({ | |
| "ruleId": rule_id, | |
| "level": "error", | |
| "message": {"text": message}, | |
| "locations": [{ | |
| "physicalLocation": { | |
| "artifactLocation": {"uri": source_file}, | |
| "region": {"startLine": 1} | |
| } | |
| }] | |
| }) | |
| sarif["runs"][0]["results"] = results | |
| with open(enhanced_sarif_path, 'w') as f: | |
| json.dump(sarif, f, indent=2) | |
| print(f"Created enhanced SARIF with {len(results)} results") | |
| EOF | |
| - name: Upload SARIF | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() && hashFiles('./enhanced-results.sarif') != '' | |
| with: | |
| sarif_file: ./enhanced-results.sarif | |
| continue-on-error: true | |
| # ======================================================================== | |
| # Feishu Notification - Crash Alert (only when crashes found) | |
| # ======================================================================== | |
| - name: Send crash alert to Feishu | |
| if: failure() && steps.run.outcome == 'failure' && env.STORAGE_REPO_URL != '' | |
| run: | | |
| # Count crash files | |
| CRASH_COUNT=0 | |
| CRASH_DETAILS="" | |
| if [ -d "./out/artifacts" ]; then | |
| CRASH_COUNT=$(find "./out/artifacts" -name "crash-*" -o -name "leak-*" -o -name "timeout-*" 2>/dev/null | grep -v "\.summary$" | wc -l) | |
| # Get first few crash details | |
| for crash_file in $(find "./out/artifacts" -name "crash-*" -o -name "leak-*" -o -name "timeout-*" 2>/dev/null | grep -v "\.summary$" | head -5); do | |
| filename=$(basename "$crash_file") | |
| fuzzer_dir=$(basename $(dirname "$crash_file")) | |
| CRASH_DETAILS="${CRASH_DETAILS}\n- ${fuzzer_dir}/${filename}" | |
| done | |
| fi | |
| # Generate corpus repository URL for the crash report | |
| DATE=$(date +%Y-%m-%d) | |
| RUN_ID="${{ github.run_id }}" | |
| STORAGE_REPO="${{ vars.CLUSTERFUZZLITE_STORAGE_REPO }}" | |
| CRASH_REPORT_URL="https://github.com/${STORAGE_REPO}/blob/main/crashes/${DATE}_run${RUN_ID}/REPORT.md" | |
| # Send to Feishu | |
| curl -X POST "${{ secrets.FEISHU_WEBHOOK_URL }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d @- << EOF | |
| { | |
| "msg_type": "interactive", | |
| "card": { | |
| "config": { | |
| "wide_screen_mode": true | |
| }, | |
| "header": { | |
| "title": { | |
| "tag": "plain_text", | |
| "content": "🚨 Fuzzing Crash Alert" | |
| }, | |
| "template": "red" | |
| }, | |
| "elements": [ | |
| { | |
| "tag": "div", | |
| "text": { | |
| "tag": "lark_md", | |
| "content": "**⚠️ Fuzzing discovered ${CRASH_COUNT} potential vulnerabilities!**\n\n**Repository:** ${{ github.repository }}\n**Branch:** ${{ github.ref_name }}\n**Commit:** \`${{ github.sha }}\`\n**Fuzz Duration:** ${{ env.FUZZ_SECONDS }}s" | |
| } | |
| }, | |
| { | |
| "tag": "hr" | |
| }, | |
| { | |
| "tag": "div", | |
| "text": { | |
| "tag": "lark_md", | |
| "content": "**Crash Files:**${CRASH_DETAILS}" | |
| } | |
| }, | |
| { | |
| "tag": "action", | |
| "actions": [ | |
| { | |
| "tag": "button", | |
| "text": { | |
| "tag": "plain_text", | |
| "content": "View Crash Report" | |
| }, | |
| "type": "danger", | |
| "url": "${CRASH_REPORT_URL}" | |
| } | |
| ] | |
| }, | |
| { | |
| "tag": "note", | |
| "elements": [ | |
| { | |
| "tag": "plain_text", | |
| "content": "Please investigate and fix the crashes as soon as possible. Detailed crash report is available in the corpus repository." | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| EOF | |
| continue-on-error: true |