Skip to content

ClusterFuzzLite batch fuzzing #265

ClusterFuzzLite batch fuzzing

ClusterFuzzLite batch fuzzing #265

Workflow file for this run

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