Practical, end-to-end rules for building, running, and automating modern fuzz tests with libFuzzer, AFL++, and OSS-Fuzz across C/C++ code bases.
Shipping C/C++ code without fuzz testing is like deploying to production without unit tests. You're essentially hoping attackers won't find the edge cases your manual testing missed. These Cursor Rules transform your development workflow from "test what you remember" to "automatically discover what you don't know."
Your current testing approach leaves massive blind spots:
Meanwhile, attackers are systematically probing every input boundary, memory allocation, and parser edge case. The gap between your test coverage and real-world attack surfaces is where critical vulnerabilities hide.
These Cursor Rules implement a complete fuzz testing ecosystem that automatically discovers security vulnerabilities and crashes before they reach production. You get enterprise-grade security testing with minimal setup overhead.
The rules handle the complex orchestration between libFuzzer, AFL++, and OSS-Fuzz while maintaining the performance and reliability standards required for CI/CD integration.
Before: Manual security testing catches ~20% of input validation bugs, requires security specialist knowledge, and typically happens late in the development cycle.
After: Automated fuzzing discovers 80-95% of parser vulnerabilities, runs continuously in CI, and catches issues within minutes of code changes.
// Traditional approach: hoping manual review catches edge cases
bool ParseHTTPRequest(const std::string& request) {
// Manual testing might miss buffer overflows, integer overflows,
// malformed headers, edge cases in parsing logic
return ProcessRequest(request);
}
// Automated fuzzing discovers edge cases you never considered
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 4 || size > 65536) return 0; // Realistic bounds
std::string request(reinterpret_cast<const char*>(data), size);
// Fuzzer will automatically discover:
// - Buffer overflows in header parsing
// - Integer overflows in content-length handling
// - State machine bugs in HTTP parsing
// - Memory corruption in string operations
ParseHTTPRequest(request);
return 0;
}
Your CI pipeline now automatically:
Add to your project structure:
project/
├── fuzz/
│ ├── http_parser_fuzz.cc
│ ├── json_parser_fuzz.cc
│ ├── http.dict
│ └── corpus/
│ ├── http/
│ └── json/
├── .github/workflows/fuzz.yml
└── README.fuzz.md
# .github/workflows/fuzz.yml
name: Security Fuzzing
on: [push, pull_request]
jobs:
fuzz:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- name: Install LLVM
uses: KyleMayes/install-llvm-action@v1
with: {version: 17}
- run: |
cmake -B build \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_CXX_FLAGS="-O1 -g -fsanitize=fuzzer,address,undefined"
- run: cmake --build build --target all
- run: ./build/http_parser_fuzz -max_total_time=300 -rss_limit_mb=2048
- uses: actions/upload-artifact@v3
if: failure()
with:
name: crash-reports
path: crash-*
// fuzz/http_parser_fuzz.cc - targets specific attack vectors
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 4 || size > 65536) return 0;
HttpParser parser;
parser.Reset(); // Clean state for each iteration
// Let fuzzer discover parsing vulnerabilities
auto result = parser.Parse({data, data + size});
// Validate internal state consistency
if (result.IsValid()) {
assert(result.GetHeaders().size() < 1000); // Catch DoS vectors
assert(result.GetContentLength() >= 0); // Integer overflow protection
}
return 0;
}
Local testing command:
# Build with comprehensive sanitizers
clang++ -O1 -g -fsanitize=fuzzer,address,undefined,integer \
fuzz/http_parser_fuzz.cc src/http_parser.cc -o http_fuzz
# Run with performance optimizations
./http_fuzz -max_total_time=900 -print_final_stats=1 \
-use_value_profile=1 -entropic=1 ./fuzz/corpus/http/
Coverage-guided corpus minimization:
# Reduce corpus bloat while maintaining coverage
afl-cmin -i ./fuzz/corpus/http -o ./fuzz/corpus/http-min -- ./http_fuzz @@
You'll spend less time debugging production security incidents and more time building features, knowing your automated security testing is continuously validating every code path against real-world attack patterns.
The rules handle all the complex configuration details—sanitizer flags, corpus management, CI integration, and performance optimization—so you can focus on writing secure code rather than learning fuzzing toolchains.
You are an expert in C/C++, LLVM/Clang sanitizers, libFuzzer, AFL++, OSS-Fuzz, Docker, and GitHub Actions.
Key Principles
- Shift-left: introduce fuzzing as soon as a compilable unit exists; never postpone until release.
- Automation first: every merge to main must trigger at least one fuzzing job.
- Coverage > Count: measure branch & edge coverage, not number of generated inputs.
- Deterministic harnesses: avoid time, I/O, randomness inside the target; crashes must be reproducible with a single seed.
- One code path per harness: small, modular targets maximize signal-to-noise and minimize corpus bloat.
- Fail fast & minimize: let the fuzzer crash immediately, then run `--minimize_crash=1` to shrink the reproducer.
- Security isolation: run fuzzers inside Docker/Nix shells with capped CPU, memory, and a tmpfs `/tmp`.
C/C++ Rules
- Always compile with: `-O1 -g -fsanitize=fuzzer,address,undefined,integer,null`, then strip extra sanitizers once coverage plateaus.
- Harness signature for libFuzzer:
```cpp
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < kMinSize || size > kMaxSize) return 0; // early reject
Foo f;
if (!f.Parse({data, data+size})) return 0; // validate before use
return 0;
}
```
- Keep global state out of scope; if unavoidable, reset with `__sanitizer_reset_coverage();` between iterations.
- Link statically (`-static`) for AFL++ to simplify crash reproduction on other hosts.
- Prefer `clang++` over `gcc` to unlock in-process coverage instrumentation.
- Split large APIs into independent harnesses in `fuzz/` directory: `<api>_fuzz.cc`, corpus in `fuzz/corpus/<api>/`.
- Place dictionary files next to the harness: `example.dict` with tokens (`"GET ", "HTTP/1.1\r\n"`).
Rust Interop (optional)
- Use `cargo fuzz init`, then add C library under test as `unsafe extern` calls.
- Enable address sanitizer: `RUSTFLAGS="-Zsanitizer=address"`.
Go Interop (optional)
- Replace `main` with a build tag `// +build gofuzz` and export `func Fuzz(data []byte) int`.
Error Handling & Validation
- Catch all exceptions/signals: build with `-fsanitize=fuzzer-no-link` and let libFuzzer install handlers.
- Add explicit assertions (`assert(ptr != nullptr)`); sanitizers convert them into actionable crashes.
- Return early on malformed input; never rely on `try/catch` to guard the happy path.
- Write reproducers to `crash-<hash>`; store as CI artifact for triage.
Framework-Specific Rules
libFuzzer
- Use `-max_total_time=900` for local runs; CI under 15 min.
- Use `-print_final_stats=1 -use_value_profile=1 -entropic=1` for deeper exploration.
- Seed with protobuf-encoded samples via `-dict=proto.dict` when fuzzing serialized formats.
AFL++
- Build with `AFL_HARDEN=1 AFL_USE_ASAN=1 AFL_LLVM_INSTRUMENT=CLASSIC`.
- Run with `afl-fuzz -i seeds -o findings -M main -- ./target @@` and spawn second instance `-S` for parallelism.
- Enable `afl-cmin` post run to shrink corpus; commit minimized corpus to repo.
OSS-Fuzz
- Dockerfile must copy sources into `/src/<project>` and place compiled fuzzers into `/out`.
- Keep build < 10 min; disable tests and examples via `cmake -DBUILD_TESTING=OFF`.
- Add `project.yaml` with `language: c++`, `sanitizers: address,undefined`.
CI/CD Integration
- GitHub Actions sample:
```yaml
jobs:
fuzz:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- name: Install LLVM
uses: KyleMayes/install-llvm-action@v1
with: {version: 17}
- run: cmake -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
- run: cmake --build build --target all
- run: ./build/my_fuzzer -max_total_time=120 -rss_limit_mb=2048
```
- Fail the job on any non-zero exit; upload `crash-*` artifacts.
Testing & Corpus Management
- Check in a minimal seed corpus (one per harness) to keep startup fast (<50 ms).
- Run nightly corpus pruning: `llvm-cov export` → HTML trend chart to detect coverage regressions.
- Deduplicate with `sha1sum **/crash-* | sort | uniq -w 40`.
Performance
- Parallelize: prefer 1 fuzzer core per physical CPU; hyper-threads yield diminishing returns.
- Use `-jobs` (`libFuzzer`) or `-p fast` (`AFL++`) for spawn/collect mode.
- Store corpora on tmpfs for >20% speed boost; rsync back to disk every hour.
Security & Isolation
- Drop network caps: run fuzzers inside `docker run --cap-drop=ALL --security-opt no-new-privileges`.
- Limit memory: `--memory=4g --memory-swap=4g`.
- For privileged targets (parsers for image/video), mount `/dev/shm` with `noexec` to avoid RCE attempts.
Common Pitfalls & How to Avoid Them
- PITFALL: Too many sanitizers slow execution. ACTION: remove MSAN/TSAN once memory safety is stable.
- PITFALL: Shared mutable globals cause flaky crashes. ACTION: zero-state at each iteration or refactor to stateless design.
- PITFALL: Neglecting dictionaries for textual formats. ACTION: Derive tokens from RFC examples; 2-10× coverage boost.
Recommended Documentation Block
Include a `README.fuzz.md` per target explaining: build flags, run examples, corpus location, notable crashes.
Directory Layout Example
```
project/
├─ src/
├─ fuzz/
│ ├─ http_fuzz.cc
│ ├─ http.dict
│ └─ corpus/
│ └─ http/
├─ .github/workflows/fuzz.yml
└─ README.fuzz.md
```
Reference Commands Cheatsheet
- Local smoke test: `clang++ -g -fsanitize=fuzzer,address http_fuzz.cc -o http_fuzz && ./http_fuzz -runs=1000 ./seeds`
- AFL++ triage: `afl-tmin -i crash-123 -o crash-min-123 -- ./target @@`
- Coverage report: `llvm-profdata merge -sparse default.profraw -o all.profdata && llvm-cov show ./target -instr-profile=all.profdata > cov.txt`