OSS-Fuzz: Integrating Continuous Fuzzing into Open Source Projects

OSS-Fuzz: Integrating Continuous Fuzzing into Open Source Projects

Since its launch in 2016, Google's OSS-Fuzz program has found over 10,000 vulnerabilities and bugs in more than 1,000 open source projects. It runs fuzzing continuously, 24 hours a day, across billions of CPU-core-hours of compute. When it finds a crash, it automatically files an issue, minimizes the reproducer, and tracks the fix. For open source projects that handle untrusted input — which is most of them — OSS-Fuzz represents free, continuous security testing at a scale no project could afford independently.

What OSS-Fuzz Is

OSS-Fuzz is a hosted fuzzing service for open source projects. It runs on Google infrastructure and is free for qualifying projects (those that are widely used, have a security surface, and meet basic quality criteria). Google's team handles the infrastructure; you write the fuzz targets.

The fuzzing is coverage-guided, using libFuzzer and AFL++ under the hood. Coverage-guided fuzzing instruments your code to track which branches are executed, then mutates inputs to maximize branch coverage. This approach is dramatically more effective than random fuzzing because it actively seeks unexplored code paths.

What Gets Found

OSS-Fuzz finds:

  • Memory safety bugs: buffer overflows, heap use-after-free, use of uninitialized memory, double free. These are detected by AddressSanitizer (ASan) instrumented builds.
  • Memory leaks: detected by LeakSanitizer (LSan).
  • Undefined behavior: integer overflow, null pointer dereference, out-of-bounds array access. Detected by UBSanitizer (UBSan).
  • Threading bugs: data races. Detected by ThreadSanitizer (TSan).
  • Logic bugs: infinite loops, unexpected crashes, assertion failures in code you write.

Real examples: OSS-Fuzz found a heap buffer overflow in FreeType (used in billions of Android devices), a use-after-free in libssh, dozens of bugs in cURL, and critical vulnerabilities in libpng that had been in the codebase for years.

Writing a Fuzz Target

A fuzz target is a function with the signature LLVMFuzzerTestOneInput. It receives arbitrary bytes and exercises your code with them. The fuzzer controls what bytes are provided.

For a C project:

#include <stdint.h>
#include <stddef.h>
#include "mylib.h"

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Feed arbitrary data to your parsing/processing function
    mylib_parse_input(data, size);
    return 0;  // Non-zero return values are reserved for future use
}

For a C++ project that parses JSON:

#include <stdint.h>
#include <stddef.h>
#include <string>
#include "json_parser.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    std::string input(reinterpret_cast<const char*>(data), size);
    
    try {
        JsonParser parser;
        parser.parse(input);
    } catch (const ParseError&) {
        // Expected: parser properly rejects invalid input
        // Don't crash, don't leak memory
    } catch (...) {
        // Unexpected exception = crash the fuzzer to report it
        __builtin_trap();
    }
    
    return 0;
}

Key principles for fuzz targets:

  1. Never crash on invalid input (unless you are testing crash handling). Parse errors should throw exceptions or return error codes, not crash.
  2. Free all memory. Leaks are bugs that OSS-Fuzz will report.
  3. Be deterministic. The same input should always produce the same behavior. No random number generation, no time-based behavior.
  4. Be fast. Each test case runs in microseconds. Slow fuzz targets reduce coverage.

OSS-Fuzz Project Structure

To onboard to OSS-Fuzz, you submit a PR to github.com/google/oss-fuzz. The structure is:

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

project.yaml:

homepage: "https://myproject.example.com"
language: c++
primary_contact: "security@myproject.example.com"
auto_ccs:
  - "developer@myproject.example.com"
main_repo: "https://github.com/myorg/myproject"

Dockerfile (based on the OSS-Fuzz base image):

FROM gcr.io/oss-fuzz-base/base-builder
RUN apt-get update && apt-get install -y \
    cmake \
    libssl-dev

COPY *.cc $SRC/
COPY build.sh $SRC/

RUN git clone --depth 1 \
    https://github.com/myorg/myproject \
    $SRC/myproject

build.sh:

#!/bin/bash -eu

<span class="hljs-comment"># Build the project with fuzzing instrumentation
<span class="hljs-built_in">cd <span class="hljs-variable">$SRC/myproject
cmake -B build \
    -DCMAKE_C_COMPILER=<span class="hljs-variable">$CC \
    -DCMAKE_CXX_COMPILER=<span class="hljs-variable">$CXX \
    -DCMAKE_C_FLAGS=<span class="hljs-string">"$CFLAGS" \
    -DCMAKE_CXX_FLAGS=<span class="hljs-string">"$CXXFLAGS" \
    -DBUILD_SHARED_LIBS=OFF
make -C build -j$(<span class="hljs-built_in">nproc)

<span class="hljs-comment"># Compile fuzz targets and link against the project
<span class="hljs-keyword">for fuzzer <span class="hljs-keyword">in <span class="hljs-variable">$SRC/*.cc; <span class="hljs-keyword">do
    name=$(<span class="hljs-built_in">basename <span class="hljs-variable">$fuzzer .cc)
    <span class="hljs-variable">$CXX <span class="hljs-variable">$CXXFLAGS <span class="hljs-variable">$LIB_FUZZING_ENGINE \
        <span class="hljs-variable">$fuzzer \
        -I<span class="hljs-variable">$SRC/myproject/include \
        build/libmyproject.a \
        -o <span class="hljs-variable">$OUT/<span class="hljs-variable">$name
<span class="hljs-keyword">done

<span class="hljs-comment"># Copy seed corpus if you have one
<span class="hljs-built_in">cp -r <span class="hljs-variable">$SRC/myproject/tests/corpus <span class="hljs-variable">$OUT/fuzz_target_1_seed_corpus

Building a Seed Corpus

A seed corpus is a collection of valid inputs that serve as starting points for fuzzing. Good seeds dramatically improve fuzzer effectiveness — instead of starting from random bytes, the fuzzer starts from valid inputs and mutates them.

Create a corpus/ directory with example inputs:

mkdir -p corpus/fuzz_json_parser

<span class="hljs-comment"># Add real example inputs
<span class="hljs-built_in">cp tests/fixtures/*.json corpus/fuzz_json_parser/

<span class="hljs-comment"># Add edge cases
<span class="hljs-built_in">echo <span class="hljs-string">'{}' > corpus/fuzz_json_parser/empty_object.json
<span class="hljs-built_in">echo <span class="hljs-string">'[]' > corpus/fuzz_json_parser/empty_array.json
<span class="hljs-built_in">echo <span class="hljs-string">'"hello"' > corpus/fuzz_json_parser/simple_string.json
<span class="hljs-built_in">echo <span class="hljs-string">'{"key": "value", "num": 42, "nested": {"a": true}}' \
    > corpus/fuzz_json_parser/typical.json

Compress the corpus for OSS-Fuzz:

zip -j corpus/fuzz_json_parser_seed_corpus.zip corpus/fuzz_json_parser/*

Testing Your Fuzzer Locally

Before submitting to OSS-Fuzz, test locally using the OSS-Fuzz helper scripts:

# Clone OSS-Fuzz
git <span class="hljs-built_in">clone https://github.com/google/oss-fuzz
<span class="hljs-built_in">cd oss-fuzz

<span class="hljs-comment"># Build your project
python3 infra/helper.py build_image myproject
python3 infra/helper.py build_fuzzers myproject

<span class="hljs-comment"># Run a single fuzzer
python3 infra/helper.py run_fuzzer myproject fuzz_target_1

<span class="hljs-comment"># Check for crashes with a specific input
python3 infra/helper.py reproduce myproject fuzz_target_1 /path/to/crash

<span class="hljs-comment"># Run with AddressSanitizer
python3 infra/helper.py build_fuzzers --sanitizer address myproject
python3 infra/helper.py run_fuzzer myproject fuzz_target_1

<span class="hljs-comment"># Run coverage analysis
python3 infra/helper.py build_fuzzers --sanitizer coverage myproject
python3 infra/helper.py coverage myproject

The coverage report shows which code paths your fuzz target exercises. Low coverage means your seed corpus or target is not reaching important code paths.

Understanding Crash Reports

When OSS-Fuzz finds a crash, it sends an email to your project contacts and files an issue on the OSS-Fuzz tracker. The issue contains:

  • Crash type: e.g., "Heap-buffer-overflow READ 4", "Use-after-free WRITE 8"
  • Stack trace: the call stack at the time of the crash
  • Reproducer: a minimized input that reproduces the crash
  • Regressed revision: the commit that introduced the bug

A typical AddressSanitizer crash report:

==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000001234
READ of size 4 at 0x602000001234 thread T0
    #0 0x7f8a1b2c3d4e in json_parse_number /src/myproject/src/json.c:142:5
    #1 0x7f8a1b2c5678 in json_parse_value /src/myproject/src/json.c:89:12
    #2 0x7f8a1b2c7890 in LLVMFuzzerTestOneInput /src/fuzz_json_parser.cc:12:5

0x602000001234 is located 0 bytes to the right of 4-byte region [0x602000001230,0x602000001234)
allocated by thread T0 here:
    #0 0x7f8a1bc00000 in malloc ...
    #1 0x7f8a1b2c1234 in json_new_number /src/myproject/src/json.c:67:15

The stack trace tells you exactly where the out-of-bounds read occurred: json_parse_number at line 142 of json.c tried to read 4 bytes past the end of a buffer allocated in json_new_number.

To reproduce locally:

# Download the crash input from the OSS-Fuzz issue
wget -O crash_input https://oss-fuzz.com/testcase?key=XXXX

<span class="hljs-comment"># Reproduce
./fuzz_json_parser crash_input

Responsible Disclosure

OSS-Fuzz follows a 90-day disclosure policy. When a vulnerability is found:

  1. The bug is filed as restricted on the OSS-Fuzz tracker, visible only to project maintainers
  2. Maintainers have 90 days to fix and release
  3. After 90 days, the issue becomes public regardless of fix status

For non-security bugs, the timeline is shorter (30 days).

Running Continuous Fuzzing in Your Own CI

If your project is not accepted to OSS-Fuzz, or if you want faster feedback (OSS-Fuzz can take hours to days to file issues), run fuzzing in your own CI:

name: Fuzz Tests
on:
  schedule:
    - cron: '0 2 * * *'  # Nightly at 2 AM
  workflow_dispatch:

jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install clang with fuzzing support
        run: |
          sudo apt-get install -y clang llvm
      - name: Build fuzz targets
        run: |
          cmake -B build \
            -DCMAKE_C_COMPILER=clang \
            -DCMAKE_CXX_COMPILER=clang++ \
            -DCMAKE_C_FLAGS="-fsanitize=fuzzer-no-link,address" \
            -DCMAKE_CXX_FLAGS="-fsanitize=fuzzer-no-link,address"
          make -C build fuzz_json_parser
          clang++ -fsanitize=fuzzer,address fuzz_json_parser.cc \
            build/libmyproject.a -o fuzz_json_parser
      - name: Run fuzzer for 5 minutes
        run: |
          ./fuzz_json_parser corpus/ -max_total_time=300 -jobs=4
      - name: Upload any crashes
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: fuzz-crashes
          path: crash-*

This runs the fuzzer nightly for 5 minutes per target. It is not as thorough as OSS-Fuzz's continuous 24/7 operation, but it catches many bugs quickly and provides immediate feedback on new code.

OSS-Fuzz represents one of the best free security investments available to open source maintainers. The bugs it finds are real, exploitable vulnerabilities that affect real users. Onboarding takes a day or two; the protection it provides is continuous and indefinite.

Read more