devsecops

Implementing Fuzz Testing in CI/CD with AFL++

Integrate AFL++ coverage-guided fuzz testing into CI/CD pipelines to discover memory corruption, input handling, and logic vulnerabilities in C/C++ and compiled applications.

aflaflpluspluscicdcoverage-guided-fuzzingfuzz-testingsecurity-testingvulnerability-discovery
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

AFL++ (American Fuzzy Lop Plus Plus) is a community-maintained fork of AFL that provides state-of-the-art coverage-guided fuzz testing for discovering vulnerabilities in compiled applications. AFL++ uses genetic algorithms to mutate inputs, tracking code coverage to find new execution paths that trigger crashes, hangs, and undefined behavior. In CI/CD environments, AFL++ can be integrated to continuously test parsers, protocol handlers, file format processors, and any code that handles untrusted input. AFL++ supports persistent mode for high-speed fuzzing (up to 100,000+ executions per second), custom mutators, QEMU mode for binary-only fuzzing, and CmpLog/RedQueen for automatic dictionary extraction.

When to Use

  • When deploying or configuring implementing fuzz testing in cicd with aflplusplus capabilities in your environment
  • When establishing security controls aligned to compliance requirements
  • When building or improving security architecture for this domain
  • When conducting security assessments that require this implementation

Prerequisites

  • Linux-based CI runners (AFL++ does not support Windows natively)
  • GCC or Clang compiler toolchain
  • AFL++ installed (apt install aflplusplus or built from source)
  • Target application with harness functions isolating input processing
  • Seed corpus of valid input samples

Core Concepts

Coverage-Guided Fuzzing

AFL++ instruments the target binary at compile time (or via QEMU/Frida for binary-only targets) to track which code paths each input exercises. When a mutated input triggers a new code path, it is saved to the corpus for further mutation. This feedback loop enables AFL++ to systematically explore program state space.

Instrumentation Modes

Mode Use Case Performance
afl-clang-fast (LTO) Source available, best performance Highest
afl-clang-fast Source available, standard High
afl-gcc-fast GCC-based projects High
QEMU mode Binary-only, no source Medium
Frida mode Binary-only, cross-platform Medium
Unicorn mode Firmware, embedded Low

Persistent Mode

Persistent mode avoids fork overhead by fuzzing within a loop:

#include <unistd.h>
 
__AFL_FUZZ_INIT();
 
int main() {
    __AFL_INIT();
    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
 
    while (__AFL_LOOP(10000)) {
        int len = __AFL_FUZZ_TESTCASE_LEN;
        // Process buf[0..len-1]
        parse_input(buf, len);
    }
    return 0;
}

Workflow

Step 1 --- Build the Fuzzing Harness

Create a harness that feeds AFL++ input to the target function:

// fuzz_harness.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "target_parser.h"
 
__AFL_FUZZ_INIT();
 
int main() {
    __AFL_INIT();
    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
 
    while (__AFL_LOOP(10000)) {
        int len = __AFL_FUZZ_TESTCASE_LEN;
        if (len < 4) continue;
 
        // Reset state between iterations
        parser_context_t ctx;
        parser_init(&ctx);
        parser_process(&ctx, buf, len);
        parser_cleanup(&ctx);
    }
    return 0;
}

Step 2 --- Compile with AFL++ Instrumentation

# Standard instrumentation
export CC=afl-clang-fast
export CXX=afl-clang-fast++
 
# Enable AddressSanitizer for better crash detection
export AFL_USE_ASAN=1
 
# Build the target with instrumentation
$CC -o fuzz_harness fuzz_harness.c -ltarget_parser -fsanitize=address
 
# Build a CmpLog binary for better coverage
$CC -o fuzz_harness_cmplog fuzz_harness.c -ltarget_parser \
  -fsanitize=address -DCMPLOG

Step 3 --- Prepare Seed Corpus

mkdir -p corpus/
# Add valid input samples
cp test_inputs/* corpus/
# Minimize the corpus
afl-cmin -i corpus/ -o corpus_min/ -- ./fuzz_harness @@
# Further minimize individual inputs
mkdir -p corpus_tmin/
for f in corpus_min/*; do
    afl-tmin -i "$f" -o "corpus_tmin/$(basename $f)" -- ./fuzz_harness @@
done

Step 4 --- Configure CI/CD Integration

GitHub Actions:

name: Fuzz Testing
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Nightly fuzzing
 
jobs:
  fuzz:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
 
      - name: Install AFL++
        run: |
          sudo apt-get update
          sudo apt-get install -y aflplusplus
 
      - name: Restore corpus cache
        uses: actions/cache@v4
        with:
          path: corpus/
          key: fuzz-corpus-${{ github.sha }}
          restore-keys: fuzz-corpus-
 
      - name: Build fuzzing harness
        run: |
          export CC=afl-clang-fast
          export AFL_USE_ASAN=1
          make fuzz_harness
 
      - name: Run AFL++ fuzzing (CI mode)
        env:
          AFL_CMPLOG_ONLY_NEW: 1
          AFL_FAST_CAL: 1
          AFL_NO_STARTUP_CALIBRATION: 1
        run: |
          mkdir -p findings/
          timeout 7200 afl-fuzz \
            -S ci_fuzzer \
            -i corpus/ \
            -o findings/ \
            -t 5000 \
            -- ./fuzz_harness @@ || true
 
      - name: Check for crashes
        run: |
          CRASHES=$(find findings/ -path "*/crashes/*" -not -name "README.txt" | wc -l)
          echo "Found $CRASHES unique crashes"
          if [ "$CRASHES" -gt 0 ]; then
            echo "::error::AFL++ found $CRASHES crashes"
            for crash in findings/*/crashes/*; do
              [ -f "$crash" ] && echo "Crash: $crash ($(wc -c < $crash) bytes)"
            done
            exit 1
          fi
 
      - name: Update corpus cache
        if: always()
        run: |
          afl-cmin -i findings/ci_fuzzer/queue/ -o corpus/ -- ./fuzz_harness @@

Step 5 --- Parallel Fuzzing for Nightly Runs

# Launch multiple secondary instances for better coverage
for i in $(seq 1 $(nproc)); do
    afl-fuzz -S fuzzer_$i \
      -i corpus/ \
      -o findings/ \
      -- ./fuzz_harness @@ &
done
 
# Wait for all fuzzers
wait
 
# Merge and minimize corpus
afl-cmin -i findings/*/queue/ -o corpus_merged/ -- ./fuzz_harness @@

Step 6 --- Crash Triage

# Reproduce and categorize crashes
for crash in findings/*/crashes/*; do
    echo "=== Testing: $crash ==="
    timeout 5 ./fuzz_harness_asan "$crash" 2>&1 | head -20
    echo "---"
done
 
# Deduplicate crashes by stack trace
afl-collect findings/ crashes_deduped/ -- ./fuzz_harness @@

CI/CD Best Practices for AFL++

Setting CI Short Run Nightly Long Run
Duration 30-60 min 4-24 hours
Mode -S (secondary only) -S (no -M for CI)
AFL_CMPLOG_ONLY_NEW 1 1
AFL_FAST_CAL 1 0
AFL_NO_STARTUP_CALIBRATION 1 0
Corpus caching Required Required
Parallel instances 1-2 nproc

Monitoring Fuzzing Campaigns

# View fuzzing statistics
afl-whatsup findings/
 
# Key metrics to track:
# - Total paths found (code coverage indicator)
# - Unique crashes / unique hangs
# - Stability percentage (should be >90%)
# - Exec speed (execs/sec)
# - Cycles done (full corpus cycles completed)

References

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md2.1 KB

API Reference — Implementing Fuzz Testing in CI/CD with AFL++

Libraries Used

  • subprocess: Execute AFL++ toolchain commands (afl-clang-fast, afl-fuzz, afl-cmin)
  • pathlib: File system operations for corpus and crash management

CLI Interface

python agent.py compile --source target.c --output target_fuzz [--compiler afl-clang-fast]
python agent.py fuzz --binary ./target_fuzz --input seeds/ --output findings/ [--duration 300]
python agent.py triage --binary ./target_fuzz --crashes-dir findings/default/crashes/
python agent.py stats --stats-file findings/default/fuzzer_stats

Core Functions

compile_target(source_file, output_binary, compiler)

Compiles target with AFL++ instrumentation. Sets AFL_HARDEN=1 for memory sanitizers.

run_fuzzer(binary, input_dir, output_dir, duration_seconds, memory_limit)

Runs afl-fuzz with headless mode (AFL_NO_UI=1), time-limited (-V flag).

Environment Variables Set:

Variable Value Purpose
AFL_SKIP_CPUFREQ 1 Skip CPU frequency check (CI/CD)
AFL_NO_UI 1 Headless mode for CI environments
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES 1 Continue on crash dir issues

parse_fuzzer_stats(stats_file)

Parses AFL++ fuzzer_stats file. Key metrics: execs_per_sec, paths_total, saved_crashes, bitmap_cvg.

triage_crashes(binary, crashes_dir)

Re-runs crash inputs through the binary and classifies by signal (SIGSEGV, SIGABRT, etc.).

minimize_corpus(binary, input_dir, output_dir, timeout)

Runs afl-cmin to remove redundant seeds from the corpus.

AFL++ Commands Used

Command Purpose
afl-clang-fast Compile with LLVM-based instrumentation
afl-fuzz -i <in> -o <out> -- <binary> Main fuzzing loop
afl-cmin -i <in> -o <out> -- <binary> Corpus minimization
afl-tmin -i <crash> -o <min> -- <binary> Test case minimization

Dependencies

AFL++ must be installed: apt install aflplusplus or build from source.

pip install  # No Python packages needed beyond stdlib
standards.md1.8 KB

Standards Reference for Fuzz Testing

NIST SP 800-53 Rev 5 Controls

Control Description Fuzzing Alignment
SA-11(5) Penetration Testing Fuzz testing discovers vulnerabilities through automated input mutation
SA-11(8) Dynamic Code Analysis AFL++ provides runtime analysis with instrumented binaries
SI-10 Information Input Validation Fuzzing validates input handling robustness
SI-17 Fail-Safe Procedures Crash detection ensures failures are handled safely

OWASP Testing Guide v4.2

  • WSTG-INPV-07: Testing for Input Validation --- AFL++ systematically tests boundary conditions
  • WSTG-ERRH-01: Error Handling --- Crash analysis reveals improper error handling

CWE Categories Commonly Found by Fuzzing

CWE Name AFL++ Detection Method
CWE-120 Buffer Overflow ASan crash on out-of-bounds write
CWE-125 Out-of-Bounds Read ASan crash on invalid read
CWE-416 Use After Free ASan detects freed memory access
CWE-476 NULL Pointer Dereference SIGSEGV on null deref
CWE-190 Integer Overflow UBSan detects arithmetic overflow
CWE-787 Out-of-Bounds Write ASan detects heap/stack buffer overflow
CWE-400 Uncontrolled Resource Consumption Timeout detection for hangs

Fuzzing Maturity Levels

Level Description CI Integration
1 Basic Manual ad-hoc fuzzing None
2 Structured Harness-based with corpus management PR-triggered short runs
3 Continuous Nightly campaigns with crash tracking Nightly + corpus caching
4 Optimized Multi-tool (AFL++, libFuzzer), crash dedup, coverage tracking Full CI/CD integration with gating
workflows.md1.4 KB

AFL++ Fuzz Testing Workflows

Workflow 1: CI Pipeline Integration

Code pushed to branch
       |
Fuzzing harness compiled with afl-clang-fast + ASan
       |
Corpus restored from CI cache
       |
AFL++ runs in secondary mode for fixed duration
       |
[No crashes] --> Corpus updated in cache, pipeline passes
[Crashes found] --> Pipeline fails, crash artifacts uploaded
       |
Developer triages crashes
       |
Fix applied, re-run confirms no regression

Workflow 2: Nightly Fuzzing Campaign

Scheduled nightly trigger (cron)
       |
Build instrumented binary + CmpLog binary
       |
Restore merged corpus from last run
       |
Launch parallel AFL++ instances (nproc count)
       |
Run for 4-8 hours
       |
Collect results from all instances
       |
afl-cmin merges and minimizes corpus
       |
Deduplicate crashes by stack hash
       |
New crashes create Jira/GitHub issues automatically
       |
Updated corpus cached for next run

Workflow 3: Crash Triage and Fix

Crash file identified in findings/
       |
Reproduce crash with ASan-instrumented binary
       |
Capture ASan stack trace and error type
       |
Minimize crash input with afl-tmin
       |
Identify root cause from stack trace
       |
Develop fix and add crash input as regression test
       |
Verify fix by re-running AFL++ with crash input
       |
Update corpus to include edge case inputs

Scripts 2

agent.py6.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for implementing AFL++ fuzz testing in CI/CD pipelines."""

import json
import argparse
import subprocess
import os
from pathlib import Path


def compile_target(source_file, output_binary, compiler="afl-clang-fast"):
    """Compile target binary with AFL++ instrumentation."""
    cmd = [compiler, "-g", "-O1", "-fno-omit-frame-pointer", "-o", output_binary, source_file]
    env = os.environ.copy()
    env["AFL_HARDEN"] = "1"
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=120)
    return {
        "source": source_file,
        "binary": output_binary,
        "compiler": compiler,
        "returncode": result.returncode,
        "stdout": result.stdout[:500],
        "stderr": result.stderr[:500],
        "instrumented": result.returncode == 0,
    }


def prepare_corpus(seed_dir, corpus_dir):
    """Prepare and minimize seed corpus using afl-cmin."""
    Path(corpus_dir).mkdir(parents=True, exist_ok=True)
    seeds = list(Path(seed_dir).glob("*"))
    if not seeds:
        # Create a minimal seed if none provided
        minimal = Path(seed_dir) / "seed_minimal"
        minimal.write_bytes(b"AAAA")
        seeds = [minimal]
    return {
        "seed_dir": str(seed_dir),
        "corpus_dir": str(corpus_dir),
        "seed_count": len(seeds),
        "seeds": [str(s) for s in seeds[:50]],
    }


def minimize_corpus(binary, input_dir, output_dir, timeout=60):
    """Minimize seed corpus using afl-cmin."""
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    cmd = ["afl-cmin", "-i", input_dir, "-o", output_dir, "-t", str(timeout * 1000), "--", binary]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    minimized = list(Path(output_dir).glob("*"))
    return {
        "input_count": len(list(Path(input_dir).glob("*"))),
        "output_count": len(minimized),
        "returncode": result.returncode,
    }


def run_fuzzer(binary, input_dir, output_dir, duration_seconds=300, memory_limit="512"):
    """Run AFL++ fuzzer for a specified duration."""
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    env = os.environ.copy()
    env["AFL_SKIP_CPUFREQ"] = "1"
    env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1"
    env["AFL_NO_UI"] = "1"
    cmd = [
        "afl-fuzz",
        "-i", input_dir,
        "-o", output_dir,
        "-m", memory_limit,
        "-V", str(duration_seconds),
        "--", binary,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=duration_seconds + 60)
    stats = parse_fuzzer_stats(os.path.join(output_dir, "default", "fuzzer_stats"))
    crashes_dir = os.path.join(output_dir, "default", "crashes")
    crash_files = list(Path(crashes_dir).glob("id:*")) if os.path.isdir(crashes_dir) else []
    return {
        "binary": binary,
        "duration_seconds": duration_seconds,
        "returncode": result.returncode,
        "stats": stats,
        "crashes_found": len(crash_files),
        "crash_files": [str(f) for f in crash_files[:50]],
    }


def parse_fuzzer_stats(stats_file):
    """Parse AFL++ fuzzer_stats file into a dict."""
    stats = {}
    try:
        with open(stats_file, "r") as f:
            for line in f:
                if ":" in line:
                    key, _, value = line.partition(":")
                    stats[key.strip()] = value.strip()
    except FileNotFoundError:
        return {"error": "fuzzer_stats not found"}
    return {
        "execs_done": stats.get("execs_done", "0"),
        "execs_per_sec": stats.get("execs_per_sec", "0"),
        "paths_total": stats.get("paths_total", "0"),
        "paths_found": stats.get("paths_found", "0"),
        "unique_crashes": stats.get("saved_crashes", "0"),
        "unique_hangs": stats.get("saved_hangs", "0"),
        "stability": stats.get("stability", "unknown"),
        "bitmap_cvg": stats.get("bitmap_cvg", "unknown"),
    }


def triage_crashes(binary, crashes_dir):
    """Triage crash inputs to deduplicate and classify."""
    crash_files = sorted(Path(crashes_dir).glob("id:*"))
    results = []
    for crash_file in crash_files[:100]:
        cmd = [binary]
        try:
            proc = subprocess.run(
                cmd, input=crash_file.read_bytes(),
                capture_output=True, timeout=5
            )
            results.append({
                "file": str(crash_file),
                "returncode": proc.returncode,
                "signal": -proc.returncode if proc.returncode < 0 else None,
                "stderr_snippet": proc.stderr[:200].decode("utf-8", errors="replace"),
                "crash_type": _classify_signal(proc.returncode),
            })
        except subprocess.TimeoutExpired:
            results.append({"file": str(crash_file), "crash_type": "hang/timeout"})
    return {
        "total_crashes": len(crash_files),
        "triaged": len(results),
        "by_type": _count_by(results, "crash_type"),
        "results": results,
    }


def _classify_signal(returncode):
    signal_map = {-6: "SIGABRT", -11: "SIGSEGV", -8: "SIGFPE", -4: "SIGILL", -7: "SIGBUS"}
    return signal_map.get(returncode, f"exit({returncode})")


def _count_by(items, key):
    counts = {}
    for item in items:
        val = item.get(key, "unknown")
        counts[val] = counts.get(val, 0) + 1
    return counts


def main():
    parser = argparse.ArgumentParser(description="AFL++ Fuzz Testing CI/CD Agent")
    sub = parser.add_subparsers(dest="command")
    c = sub.add_parser("compile", help="Compile target with AFL++ instrumentation")
    c.add_argument("--source", required=True)
    c.add_argument("--output", required=True)
    c.add_argument("--compiler", default="afl-clang-fast")
    f = sub.add_parser("fuzz", help="Run AFL++ fuzzer")
    f.add_argument("--binary", required=True)
    f.add_argument("--input", required=True)
    f.add_argument("--output", required=True)
    f.add_argument("--duration", type=int, default=300, help="Duration in seconds")
    f.add_argument("--memory", default="512", help="Memory limit in MB")
    t = sub.add_parser("triage", help="Triage crash inputs")
    t.add_argument("--binary", required=True)
    t.add_argument("--crashes-dir", required=True)
    s = sub.add_parser("stats", help="Parse fuzzer stats")
    s.add_argument("--stats-file", required=True)
    args = parser.parse_args()
    if args.command == "compile":
        result = compile_target(args.source, args.output, args.compiler)
    elif args.command == "fuzz":
        result = run_fuzzer(args.binary, args.input, args.output, args.duration, args.memory)
    elif args.command == "triage":
        result = triage_crashes(args.binary, args.crashes_dir)
    elif args.command == "stats":
        result = parse_fuzzer_stats(args.stats_file)
    else:
        parser.print_help()
        return
    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()
process.py5.9 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
AFL++ Fuzzing Results Analyzer

Parses AFL++ output directories and generates reports on
crash findings, corpus growth, and coverage statistics.
"""

import json
import os
import sys
from datetime import datetime
from pathlib import Path
from collections import defaultdict


def parse_fuzzer_stats(stats_file: str) -> dict:
    stats = {}
    if not os.path.exists(stats_file):
        return stats
    with open(stats_file) as f:
        for line in f:
            line = line.strip()
            if ":" in line:
                key, value = line.split(":", 1)
                stats[key.strip()] = value.strip()
    return stats


def count_files_in_dir(directory: str) -> int:
    if not os.path.isdir(directory):
        return 0
    return len([f for f in os.listdir(directory) if f != "README.txt" and os.path.isfile(os.path.join(directory, f))])


def analyze_fuzzer_instance(instance_dir: str) -> dict:
    name = os.path.basename(instance_dir)
    stats = parse_fuzzer_stats(os.path.join(instance_dir, "fuzzer_stats"))

    return {
        "name": name,
        "start_time": stats.get("start_time", ""),
        "last_update": stats.get("last_update", ""),
        "execs_done": int(stats.get("execs_done", 0)),
        "execs_per_sec": float(stats.get("execs_per_sec", 0)),
        "corpus_count": count_files_in_dir(os.path.join(instance_dir, "queue")),
        "crashes_total": count_files_in_dir(os.path.join(instance_dir, "crashes")),
        "hangs_total": count_files_in_dir(os.path.join(instance_dir, "hangs")),
        "paths_total": int(stats.get("paths_total", 0)),
        "paths_found": int(stats.get("paths_found", 0)),
        "stability": stats.get("stability", ""),
        "cycles_done": int(stats.get("cycles_done", 0)),
        "bitmap_cvg": stats.get("bitmap_cvg", ""),
        "command_line": stats.get("command_line", ""),
    }


def collect_crash_info(instance_dir: str) -> list:
    crashes_dir = os.path.join(instance_dir, "crashes")
    crashes = []
    if not os.path.isdir(crashes_dir):
        return crashes
    for fname in sorted(os.listdir(crashes_dir)):
        if fname == "README.txt":
            continue
        fpath = os.path.join(crashes_dir, fname)
        if os.path.isfile(fpath):
            crashes.append({
                "file": fname,
                "path": fpath,
                "size": os.path.getsize(fpath),
                "instance": os.path.basename(instance_dir),
            })
    return crashes


def analyze_campaign(findings_dir: str) -> dict:
    report = {
        "findings_dir": findings_dir,
        "analyzed_at": datetime.utcnow().isoformat() + "Z",
        "instances": [],
        "total_execs": 0,
        "total_crashes": 0,
        "total_hangs": 0,
        "total_corpus": 0,
        "all_crashes": [],
        "avg_execs_per_sec": 0,
    }

    instance_dirs = []
    for entry in sorted(os.listdir(findings_dir)):
        full_path = os.path.join(findings_dir, entry)
        if os.path.isdir(full_path) and os.path.exists(os.path.join(full_path, "fuzzer_stats")):
            instance_dirs.append(full_path)

    if not instance_dirs:
        print(f"No fuzzer instances found in {findings_dir}")
        return report

    exec_speeds = []
    for inst_dir in instance_dirs:
        inst = analyze_fuzzer_instance(inst_dir)
        report["instances"].append(inst)
        report["total_execs"] += inst["execs_done"]
        report["total_crashes"] += inst["crashes_total"]
        report["total_hangs"] += inst["hangs_total"]
        report["total_corpus"] += inst["corpus_count"]
        if inst["execs_per_sec"] > 0:
            exec_speeds.append(inst["execs_per_sec"])

        crashes = collect_crash_info(inst_dir)
        report["all_crashes"].extend(crashes)

    if exec_speeds:
        report["avg_execs_per_sec"] = round(sum(exec_speeds) / len(exec_speeds), 1)

    return report


def print_report(report: dict) -> None:
    print(f"\n{'='*60}")
    print(f"AFL++ Fuzzing Campaign Report")
    print(f"{'='*60}")
    print(f"Findings directory: {report['findings_dir']}")
    print(f"Analyzed at: {report['analyzed_at']}")
    print(f"Fuzzer instances: {len(report['instances'])}")
    print(f"\nAggregate Statistics:")
    print(f"  Total executions: {report['total_execs']:,}")
    print(f"  Avg exec/sec: {report['avg_execs_per_sec']:,.1f}")
    print(f"  Total corpus entries: {report['total_corpus']}")
    print(f"  Total unique crashes: {report['total_crashes']}")
    print(f"  Total hangs: {report['total_hangs']}")

    print(f"\nInstance Details:")
    for inst in report["instances"]:
        print(f"  {inst['name']:20s} | Execs: {inst['execs_done']:>12,} | "
              f"Speed: {inst['execs_per_sec']:>8.1f}/s | "
              f"Crashes: {inst['crashes_total']:3d} | "
              f"Corpus: {inst['corpus_count']:5d} | "
              f"Cycles: {inst['cycles_done']}")

    if report["all_crashes"]:
        print(f"\nCrash Files ({len(report['all_crashes'])} total):")
        for crash in report["all_crashes"][:20]:
            print(f"  [{crash['instance']}] {crash['file']} ({crash['size']} bytes)")
        if len(report["all_crashes"]) > 20:
            print(f"  ... and {len(report['all_crashes']) - 20} more")

    verdict = "PASS" if report["total_crashes"] == 0 else "FAIL"
    print(f"\nCI Verdict: {verdict}")


def main():
    if len(sys.argv) < 2:
        print("Usage: python process.py <findings_directory>")
        sys.exit(1)

    findings_dir = sys.argv[1]
    if not os.path.isdir(findings_dir):
        print(f"Directory not found: {findings_dir}")
        sys.exit(1)

    report = analyze_campaign(findings_dir)
    print_report(report)

    output = os.path.join(findings_dir, "campaign_report.json")
    with open(output, "w") as f:
        json.dump(report, f, indent=2, default=str)
    print(f"\nReport saved to: {output}")

    sys.exit(1 if report["total_crashes"] > 0 else 0)


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 0.9 KB
Keep exploring