OSS-Fuzz Integration Guide: Continuous Fuzzing for Open Source Projects

OSS-Fuzz Integration Guide: Continuous Fuzzing for Open Source Projects

OSS-Fuzz is Google's free continuous fuzzing service for open source software. Once integrated, your project gets fuzzed 24/7 on Google's infrastructure using libFuzzer, AFL++, and Honggfuzz. Crashes are automatically deduplicated, tracked, and reported. As of 2024, OSS-Fuzz has found over 10,000 vulnerabilities in 1,000+ projects.

What OSS-Fuzz Provides

  • Continuous fuzzing on thousands of cores, running indefinitely
  • Multiple fuzzing engines: libFuzzer, AFL++, Honggfuzz — all in parallel
  • Sanitizer coverage: ASan, UBSan, MSan, ThreadSanitizer
  • Automatic deduplication: Similar crashes are grouped
  • Bug tracking: Automated filing of issues when crashes are found
  • Disclosure policy: Security bugs are embargoed for 90 days; non-security bugs for 30 days
  • Integration dashboard: View coverage, crash counts, and bug status

Requirements

  • Open source project (publicly accessible repository)
  • Build system that works in Docker
  • At least one libFuzzer or AFL harness function

Project Structure

OSS-Fuzz stores project configurations in the google/oss-fuzz repository:

oss-fuzz/projects/
  myproject/
    Dockerfile
    build.sh
    project.yaml

Your fuzzing harnesses live in your project's own repository.

Writing Fuzzing Harnesses

Before submitting to OSS-Fuzz, write harnesses locally.

C/C++ harness:

// fuzz/parse_input_fuzzer.cc
#include <stdint.h>
#include <stdlib.h>
#include "mylib/parser.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Exercise the parsing code
    ParserContext ctx;
    ctx.Parse(data, size);
    return 0;
}

Go harness:

// fuzz/fuzz_parse.go
package fuzz

import "github.com/org/mylib/parser"

func FuzzParse(data []byte) int {
    p := parser.New()
    _, err := p.Parse(data)
    if err != nil {
        return 0
    }
    return 1
}

Python harness (via atheris):

# fuzz/fuzz_parse.py
import sys
import atheris
from mylib import parse

def TestOneInput(data):
    fdp = atheris.FuzzedDataProvider(data)
    input_str = fdp.ConsumeUnicodeNoSurrogates(256)
    try:
        parse(input_str)
    except ValueError:
        pass

if __name__ == "__main__":
    atheris.Setup(sys.argv, TestOneInput)
    atheris.Fuzz()

Creating the OSS-Fuzz Configuration

1. Dockerfile

# projects/myproject/Dockerfile
FROM gcr.io/oss-fuzz-base/base-builder

# Install build dependencies
RUN apt-get update && apt-get install -y cmake libssl-dev

# Clone your project
RUN git clone --depth 1 https://github.com/org/myproject.git $SRC/myproject

# Copy the build script
COPY build.sh $SRC/

WORKDIR $SRC/myproject

2. build.sh

#!/bin/bash -eu
# projects/myproject/build.sh

cd $SRC/myproject

# Build with OSS-Fuzz's provided flags
mkdir build && cd build
cmake .. \
  -DCMAKE_CXX_COMPILER=$CXX \
  -DCMAKE_C_COMPILER=$CC \
  -DCMAKE_CXX_FLAGS="$CXXFLAGS" \
  -DCMAKE_C_FLAGS="$CFLAGS" \
  -DBUILD_FUZZING=ON

make -j$(nproc)

# Copy fuzzer binaries to $OUT
for fuzzer in fuzz_parser fuzz_decoder fuzz_encoder; do
  cp build/fuzzers/$fuzzer $OUT/
done

# Copy seed corpus (as a zip named {fuzzer}_seed_corpus.zip)
zip -r $OUT/fuzz_parser_seed_corpus.zip fuzz/corpus/parser/
zip -r $OUT/fuzz_decoder_seed_corpus.zip fuzz/corpus/decoder/

3. project.yaml

# projects/myproject/project.yaml
homepage: "https://github.com/org/myproject"
language: c++

# Security contact for embargoed bug reports
primary_contact: "security@example.com"

# Additional CCs for non-security findings
auto_ccs:
  - "maintainer@example.com"

# Optional: coverage reporting config
coverage_extra_args: "-ignore_filename_regex=.*/fuzz/.*"

Testing Locally with OSS-Fuzz

Before submitting, test your build locally:

# Clone OSS-Fuzz
git clone https://github.com/google/oss-fuzz
cd oss-fuzz

# Build your project's Docker image
python3 infra/helper.py build_image myproject

# Build the fuzzers
python3 infra/helper.py build_fuzzers myproject

# Run one fuzzer for 60 seconds
python3 infra/helper.py run_fuzzer myproject fuzz_parser -- -max_total_time=60

# Reproduce a specific crash
python3 infra/helper.py reproduce myproject fuzz_parser /path/to/crash

# Check coverage
python3 infra/helper.py coverage myproject --fuzz-target=fuzz_parser

The local build exactly mirrors what OSS-Fuzz will run. If it works locally, it works in production.

Submitting to OSS-Fuzz

  1. Fork google/oss-fuzz on GitHub
  2. Create projects/myproject/ with your three files
  3. Submit a pull request

The OSS-Fuzz team reviews the PR and runs a build test. Once merged, your project is added to the continuous fuzzing queue.

What the OSS-Fuzz team checks:

  • Build succeeds in the Docker environment
  • At least one fuzzer binary is produced
  • No obvious issues with the configuration

Handling OSS-Fuzz Findings

When OSS-Fuzz finds a bug:

  1. You receive an email with a report
  2. Security bugs: embargoed for 90 days, visible only to you and OSS-Fuzz
  3. Non-security bugs: embargoed for 30 days
  4. After embargo: the bug is made public in the OSS-Fuzz issue tracker

Reproducing a bug from the report:

# Download the crashing input from the report
# Reproduce locally
./fuzz_parser path/to/crash_input

# Or with OSS-Fuzz tools
python3 infra/helper.py reproduce myproject fuzz_parser path/to/crash_input

Fixing and closing:

Once you've fixed the bug:

  1. Create a fix in your project
  2. Update your OSS-Fuzz build to use the fixed version (or update your repo)
  3. Comment on the OSS-Fuzz issue with the fix
  4. OSS-Fuzz verifies the fix and closes the issue

Language-Specific Notes

Go Projects

Go fuzzing uses a different harness format:

// fuzz/fuzz_parse_test.go
package fuzz

import "github.com/org/mylib"

func FuzzParse(f *testing.F) {
    f.Add([]byte("example input"))
    f.Fuzz(func(t *testing.T, data []byte) {
        mylib.Parse(data)
    })
}
# build.sh for Go
cd $SRC/myproject
go build ./...

# For Go native fuzzing
compile_go_fuzzer github.com/org/myproject/fuzz FuzzParse fuzz_parse

Python Projects

# build.sh for Python with atheris
pip3 install atheris

compile_python_fuzzer $SRC/myproject/fuzz/fuzz_parse.py fuzz_parse

Rust Projects

// fuzz/fuzz_targets/fuzz_parse.rs
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = mylib::parse(s);
    }
});
# build.sh for Rust
cd $SRC/myproject
cargo fuzz build --release
cp fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_parse $OUT/

Coverage Reporting

OSS-Fuzz generates coverage reports for each project. These show which code paths the fuzzer has reached.

Access at: https://oss-fuzz.com/coverage (requires project access)

Low coverage on a code path means the fuzzer hasn't reached it. Improve coverage by:

  • Adding seeds that exercise that path
  • Writing a more targeted harness
  • Adding a custom mutator

Maintaining Your Integration

After initial integration, maintenance involves:

  1. Keeping the Dockerfile updated when your build dependencies change
  2. Adding new harnesses as your codebase grows
  3. Improving seeds when coverage reports show uncovered areas
  4. Responding to findings within the embargo window

A stale OSS-Fuzz integration that doesn't build is worse than no integration — it consumes project slots without producing results. Check your project's build status at https://oss-fuzz.com/build-status periodically.

For Private Projects

OSS-Fuzz is public-only. For private projects, the equivalent is:

  • ClusterFuzz: Google's open-source fuzzing infrastructure that powers OSS-Fuzz, self-hostable
  • FuzzBench: Google's fuzzer benchmarking service
  • GitHub Actions / CI: Run libFuzzer or AFL directly in CI with time-limited runs
  • Commercial services: Several companies offer hosted fuzzing for private repos

The tooling (libFuzzer, AFL, harness writing) is identical regardless of whether you use OSS-Fuzz or a self-hosted setup.

Read more

Start now free