Skip to main content
All articles
DevSecOps
February 22, 202611 min read

How to Add Continuous Fuzzing to Your CI/CD Pipeline

How to wire AFL++ and libFuzzer into GitHub Actions, GitLab CI, and other CI/CD pipelines — with real configuration examples you can copy and run.

Continuous fuzzing — running fuzzers automatically on every commit or on a scheduled cadence — is one of the highest-return security investments a development team can make. Google's OSS-Fuzz runs continuous fuzzing on over 1,000 open-source projects and has found more than 10,000 bugs since 2016. Microsoft mandates fuzzing in their Security Development Lifecycle.

Two Approaches to CI Fuzzing

  1. Short runs on every PR — 60–300 seconds to catch obvious regressions and verify the harness still compiles. Not enough to find subtle bugs, but catches crashes introduced by the PR immediately before it merges.
  2. Continuous background fuzzing — long-running campaigns on dedicated infrastructure that run 24/7, accumulate corpora, and report crashes asynchronously. This is where serious bug discovery happens.

GitHub Actions: Short Runs on Every PR

name: Fuzz

on:
  pull_request:
    paths: ['src/**', 'fuzz/**']

jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build fuzz target
        run: |
          clang -g -O1 -fsanitize=address,undefined,fuzzer \
            fuzz/fuzz_parse.c src/parser.c -o fuzz_parse

      - name: Run fuzzer (120 seconds)
        run: |
          mkdir -p corpus artifacts
          ./fuzz_parse corpus/ -max_total_time=120 \
            -artifact_prefix=artifacts/
          ls artifacts/crash-* 2>/dev/null && exit 1 || exit 0

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: fuzz-results
          path: artifacts/

AFL++ in a Nightly Scheduled Job

name: AFL++ Nightly

on:
  schedule:
    - cron: '0 2 * * *'

jobs:
  afl-fuzz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get install -y afl++
      - run: echo core | sudo tee /proc/sys/kernel/core_pattern

      - name: Build with AFL++
        env:
          AFL_USE_ASAN: "1"
        run: afl-cc -o my_parser_afl src/parser.c

      - name: Restore corpus cache
        uses: actions/cache@v4
        with:
          path: corpus/
          key: afl-corpus

      - name: Run AFL++ for 5 minutes
        run: |
          mkdir -p findings corpus
          timeout 300 afl-fuzz -i corpus/ -o findings/ \
            -V 290 -- ./my_parser_afl @@ || true

      - name: Fail on crashes
        run: ls findings/default/crashes/id:* 2>/dev/null && exit 1 || true

      - name: Save corpus
        run: cp findings/default/queue/id:* corpus/ 2>/dev/null || true

Corpus Persistence

Short fuzzing runs in CI are only effective if the fuzzer builds on previous progress. Without corpus persistence, each run starts from scratch. Options:

  • GitHub Actions cache: use actions/cache keyed on a date or hash. Works well for corpora under 500 MB.
  • Git LFS: store the corpus in the repo. Ensures reproducibility but requires discipline about corpus size.
  • External object storage: S3 or GCS. Most flexible for large corpora — pull at job start, push a minimized corpus at the end.
  • Managed fuzzing platform: platforms like Fuzze.rs handle corpus storage automatically, accumulating seeds across all runs.

Integrating via API

- name: Start fuzzing job
  run: |
    JOB_ID=$(curl -sS -X POST https://api.fuzze.rs/v1/jobs \
      -H "Authorization: Bearer $FUZZE_RS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"binary_url":"'"$BINARY_URL"'","fuzzer":"afl++","cores":8}' \
      | jq -r '.job_id')
    echo "JOB_ID=$JOB_ID" >> $GITHUB_ENV

- name: Fail on crashes
  run: |
    CRASHES=$(curl -sS https://api.fuzze.rs/v1/jobs/$JOB_ID/crashes \
      -H "Authorization: Bearer $FUZZE_RS_API_KEY" | jq '.total')
    [ "$CRASHES" -gt 0 ] && exit 1 || exit 0

What to Fuzz First

Prioritize targets that process untrusted external input:

  • File parsers: image decoders, document parsers, archive extractors, config readers.
  • Protocol implementations: anything parsing network packets, HTTP requests, or binary protocols.
  • Deserialization code: JSON parsers, protobuf decoders, XML parsers, binary format readers.
  • API input handling: code that processes user-supplied parameters before reaching business logic.

Making It Sustainable

Continuous fuzzing only works if the team responds to crashes. Build the process around it:

  • Route crash notifications to your existing bug tracker and security channel.
  • Triage crashes weekly and track the fix rate as a team metric.
  • Add crash reproducers to the regression test suite after each fix.
  • The goal isn't zero crashes — it's fast detection and response.

Ready to start fuzzing?

Skip the infrastructure setup. Fuzze.rs runs AFL++, libFuzzer, Centipede, and Honggfuzz at scale — just upload your binary and get crash reports in real time. First month 50% off.