# Secure Fuzz Testing

> Coverage-guided fuzzing with Atheris, cargo-fuzz, and native Go fuzzing, combined with compilers/sanitizers (ASan, MSan, UBSan) for security validation. Use whenever the user wants to fuzz test code, find memory safety bugs, validate parsers/deserializers against malformed input, or harden security-critical code paths. Trigger on mentions of fuzzing, fuzz testing, AddressSanitizer, libFuzzer, cargo-fuzz, Atheris, go test -fuzz, or security validation of parsing/deserialization logic.

- Skill: `roedyrustam/secure-fuzz-testing-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add roedyrustam/secure-fuzz-testing-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/roedyrustam/secure-fuzz-testing-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: roedyrustam (https://skillmd.com/u/roedyrustam)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/roedyrustam/secure-fuzz-testing-2

---


# Secure Fuzz Testing

Coverage-guided fuzzing across Python, Rust, and Go with sanitizer integration.

---

## Why Fuzz

Fuzzing finds bugs unit tests miss: crashes, panics, memory corruption, infinite loops, and
logic errors triggered by malformed or unexpected input. Critical for any code that parses
untrusted input — file formats, network protocols, deserializers, APIs.

---

## Python Fuzzing with Atheris

### Setup
```bash
uv add --dev atheris
```

### Basic Fuzz Harness
```python
# fuzz/fuzz_parser.py
import atheris
import sys

with atheris.instrument_imports():
    from myapp.parser import parse_config

def TestOneInput(data: bytes) -> None:
    fdp = atheris.FuzzedDataProvider(data)
    text = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())

    try:
        parse_config(text)
    except (ValueError, KeyError):
        pass  # Expected exceptions — not bugs
    # Anything else (TypeError, IndexError, unhandled) = potential bug

atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
```

```bash
# Run fuzzing
python fuzz/fuzz_parser.py

# With a corpus directory (saves interesting inputs)
mkdir corpus
python fuzz/fuzz_parser.py corpus/

# Limit runtime
python fuzz/fuzz_parser.py -max_total_time=300  # 5 minutes

# Reproduce a crash
python fuzz/fuzz_parser.py crash-<hash>
```

### Structured Fuzzing (JSON-like inputs)
```python
import atheris
import json

def TestOneInput(data: bytes) -> None:
    fdp = atheris.FuzzedDataProvider(data)

    # Build structured input from fuzz bytes
    payload = {
        "user_id": fdp.ConsumeIntInRange(-1000, 1000000),
        "name": fdp.ConsumeUnicodeNoSurrogates(50),
        "active": fdp.ConsumeBool(),
        "tags": [fdp.ConsumeUnicodeNoSurrogates(20) for _ in range(fdp.ConsumeIntInRange(0, 5))],
    }

    try:
        validate_user_payload(payload)
    except ValidationError:
        pass

atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
```

---

## Rust Fuzzing with cargo-fuzz

### Setup
```bash
cargo install cargo-fuzz
cargo fuzz init
```

### Fuzz Target
```rust
// fuzz/fuzz_targets/parse_input.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use myapp::parser::parse_config;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = parse_config(s); // panics are caught and reported as crashes
    }
});
```

### Structured Fuzzing with `arbitrary`
```rust
// fuzz/fuzz_targets/structured.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use arbitrary::Arbitrary;

#[derive(Arbitrary, Debug)]
struct UserPayload {
    id: u32,
    name: String,
    age: u8,
    tags: Vec<String>,
}

fuzz_target!(|payload: UserPayload| {
    let _ = validate_user(&payload);
});
```

```toml
# fuzz/Cargo.toml
[dependencies]
libfuzzer-sys = "0.4"
arbitrary = { version = "1", features = ["derive"] }
myapp = { path = ".." }
```

### Running with Sanitizers
```bash
# AddressSanitizer (default) — detects memory corruption
cargo fuzz run parse_input

# Run for a fixed duration
cargo fuzz run parse_input -- -max_total_time=300

# Run with a larger corpus
cargo fuzz run parse_input fuzz/corpus/parse_input/

# MemorySanitizer — detects uninitialized memory reads
RUSTFLAGS="-Z sanitizer=memory" cargo fuzz run parse_input

# Minimize a crashing input
cargo fuzz tmin parse_input fuzz/artifacts/parse_input/crash-<hash>

# Reproduce a specific crash
cargo fuzz run parse_input fuzz/artifacts/parse_input/crash-<hash>
```

---

## Go Native Fuzzing

### Fuzz Test (Go 1.18+, built-in)
```go
// parser_fuzz_test.go
package parser

import "testing"

func FuzzParseConfig(f *testing.F) {
    // Seed corpus — known valid/edge-case inputs
    f.Add("key=value")
    f.Add("")
    f.Add("key=")
    f.Add(`key="quoted value"`)

    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParseConfig(input)
        if err != nil {
            return // expected error — not a bug
        }

        // Invariant checks
        if result == nil {
            t.Errorf("ParseConfig returned nil with no error for input: %q", input)
        }

        // Round-trip property: parse(serialize(x)) == x
        serialized := result.Serialize()
        reparsed, err := ParseConfig(serialized)
        if err != nil {
            t.Errorf("Failed to reparse serialized output: %v", err)
        }
        if !reparsed.Equals(result) {
            t.Errorf("Round-trip mismatch for input: %q", input)
        }
    })
}
```

```bash
# Run fuzzing
go test -fuzz=FuzzParseConfig -fuzztime=60s

# Run with sanitizer (requires CGO + clang)
go test -fuzz=FuzzParseConfig -fuzztime=60s -gcflags=all=-d=checkptr

# Crashes saved automatically to testdata/fuzz/FuzzParseConfig/
# Re-run with: go test -run=FuzzParseConfig/<seed>
```

---

## Sanitizers Reference

| Sanitizer | Detects | Use With |
|-----------|---------|----------|
| **ASan** (AddressSanitizer) | Buffer overflows, use-after-free, double-free | C/C++/Rust |
| **MSan** (MemorySanitizer) | Uninitialized memory reads | C/C++/Rust (Clang) |
| **UBSan** (UndefinedBehaviorSanitizer) | Integer overflow, null deref, type confusion | C/C++/Rust |
| **TSan** (ThreadSanitizer) | Data races, deadlocks | Concurrent code |

### C/C++ with libFuzzer + Sanitizers
```bash
# Compile with multiple sanitizers
clang++ -g -O1 -fsanitize=address,fuzzer,undefined \
  -fsanitize-address-use-after-scope \
  fuzz_target.cc -o fuzz_target

./fuzz_target -max_total_time=300 corpus/
```

---

## Building Effective Fuzz Harnesses

### Good Harness Checklist
- [ ] Targets a single, well-defined function or parsing boundary
- [ ] Catches expected errors (don't report them as crashes)
- [ ] Includes a seed corpus of realistic valid + edge-case inputs
- [ ] Checks invariants, not just "doesn't crash" (round-trip, idempotency)
- [ ] Runs fast (microseconds per iteration) — slow targets fuzz poorly
- [ ] Deterministic — same input always produces same behavior

### What to Fuzz First (Priority Order)
1. **Parsers** — JSON, YAML, config files, binary formats
2. **Deserializers** — anything from `bytes` → struct
3. **Network protocol handlers** — anything reading from sockets
4. **Compression/decompression** — zip, gzip bombs, decompression ratio attacks
5. **Auth token / JWT parsing** — malformed tokens shouldn't panic
6. **Regular expressions** — catastrophic backtracking (ReDoS)
7. **Path/URL handling** — path traversal via malformed input

---

## CI Integration

```yaml
# .github/workflows/fuzz.yml
name: Fuzz Testing

on:
  schedule:
    - cron: '0 2 * * *'  # nightly
  pull_request:
    paths:
      - 'src/parser/**'

jobs:
  fuzz-rust:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
      - run: cargo install cargo-fuzz
      - name: Run fuzz targets (5 min each)
        run: |
          for target in $(cargo fuzz list); do
            cargo fuzz run $target -- -max_total_time=300
          done

  fuzz-go:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
      - name: Run Go fuzz tests
        run: go test -fuzz=Fuzz -fuzztime=60s ./...
```

---

## Triaging Fuzz Findings

```bash
# 1. Minimize the crashing input (find smallest reproducer)
cargo fuzz tmin <target> <crash-file>

# 2. Get a backtrace
RUST_BACKTRACE=full cargo fuzz run <target> <crash-file>

# 3. Classify the bug:
#    - Panic/crash → DoS risk, fix immediately
#    - Memory corruption (ASan) → CRITICAL, potential RCE
#    - Logic error (wrong output, no crash) → correctness bug
#    - Infinite loop/hang → DoS risk (add timeout, fix complexity)

# 4. Write a regression test from the minimized crash
#    Add the crashing input to your seed corpus permanently
```

---

## Key Rules

1. **Fuzz parsers and deserializers first** — highest bug density, highest risk
2. **Catch expected errors in the harness** — don't waste cycles on known-invalid input paths
3. **Always minimize crashes** before filing/fixing — smaller reproducers are easier to debug
4. **Run ASan by default** — it's nearly free and catches the most dangerous bugs
5. **Seed corpus matters** — start fuzzing from realistic inputs, not just empty/random
6. **Check invariants, not just crashes** — round-trip properties catch logic bugs
7. **Time-box CI fuzzing** — 1-5 min per target in PR CI, longer runs nightly
8. **Every fixed crash becomes a permanent corpus entry** — prevents regression
9. **Fuzz before every major release** — especially after touching parsing code
10. **Treat OOM and timeouts as bugs too** — not just crashes/panics

