Claude
Skills
Sign in
Back

fuzz

Included with Lifetime
$97 forever

OCaml fuzz testing with Crowbar for protocol implementations. Use when Claude needs to: (1) Write fuzz tests for parsers and encoders, (2) Test roundtrip invariants (parse(encode(x)) = x), (3) Verify boundary conditions and error handling, (4) Test state machines and transitions, (5) Organize fuzz test suites for large codebases, (6) Run long-lived AFL campaigns with Crowbar

Security

What this skill does


# OCaml Fuzz Testing with Crowbar

## Core Philosophy

1. **One fuzz file per module**: `fuzz_foo.ml` tests `lib/foo.ml`. Keeps tests organized and discoverable.
2. **Roundtrip everything**: If you have `encode` and `decode`, test `decode(encode(x)) = x`.
3. **Crash-safety first**: Parsers must never crash on arbitrary input, even malformed data.
4. **Boundary conditions matter**: Test edge cases (0, max values, empty input, overflow).
5. **State machines need transition coverage**: Test all valid and invalid state transitions.

## Build Configuration

### Simple single-file setup (per-package)

For standalone packages, use one fuzz file per package:

```
ocaml-foo/
├── lib/
├── fuzz/
│   ├── dune
│   └── fuzz_foo.ml
└── dune-project
```

**fuzz/dune:**

```lisp
(executable
 (name fuzz_foo)
 (modules fuzz_foo)
 (libraries foo crowbar))

; Quick check with Crowbar (no AFL instrumentation)
(rule
 (alias fuzz)
 (deps fuzz_foo.exe)
 (action
  (run %{exe:fuzz_foo.exe})))

; AFL-instrumented build target (use with --profile=afl)
(rule
 (alias fuzz-afl)
 (deps
  (source_tree input)
  fuzz_foo.exe)
 (action
  (echo "AFL fuzzer built: %{exe:fuzz_foo.exe}\n")))
```

**Seed corpus**: Create `fuzz/input/` with sample inputs:

```bash
mkdir -p fuzz/input
echo -n "" > fuzz/input/empty
# Add representative samples as seed inputs
```

**fuzz/fuzz_foo.ml:**

```ocaml
open Crowbar

let test_parse_crash_safety buf =
  ignore (Foo.parse buf);
  check true

let () =
  add_test ~name:"foo: parse crash safety" [ bytes ] test_parse_crash_safety
```

### Multi-module setup (large codebases)

For larger projects with many modules:

```lisp
(executable
 (name fuzz)
 (libraries crowbar borealis)
 (modules
  fuzz
  fuzz_common
  fuzz_foo
  fuzz_bar))
```

Main entry point (`fuzz/fuzz.ml`):

```ocaml
(* Force linking of modules that register tests via side effects *)
let () =
  Fuzz_common.run ();
  Fuzz_foo.run ();
  Fuzz_bar.run ()
```

Each fuzz module ends with:

```ocaml
let run () = ()
```

This ensures the module is linked and its `add_test` calls execute.

---

## Style Guidelines

When writing fuzz tests, follow these conventions:

1. **Define test functions separately** at the top of the file
2. **Register all tests at the end** with grouped `add_test` calls
3. **Use `bytes` directly** instead of custom generators
4. **Use a `truncate` helper** to limit input size for protocol messages
5. **Return `()` directly** - no need for `check true` in most cases
6. **Add `Crypto_rng_unix.use_default ()`** at the top if crypto is used

### Example structure

```ocaml
(** Fuzz tests for Foo module. *)

open Crowbar
open Fuzz_common

(** Decode - must not crash on arbitrary input. *)
let test_decode buf =
  let buf = truncate buf in
  let _ = Foo.decode (to_bytes buf) in
  ()

(** Roundtrip - valid values must round-trip. *)
let test_roundtrip buf =
  let buf = truncate buf in
  match Foo.decode (to_bytes buf) with
  | Error _ -> ()
  | Ok v ->
      let encoded = Foo.encode v in
      match Foo.decode encoded with
      | Error _ -> fail "re-decode failed"
      | Ok v' -> if v <> v' then fail "roundtrip mismatch"

(** Pretty-print - must not crash. *)
let test_pp n =
  let v = Foo.of_int (n mod 4) in
  let _ = Format.asprintf "%a" Foo.pp v in
  ()

(* All add_test calls in run function - no side effects at module init *)
let run () =
  add_test ~name:"foo: decode crash safety" [ bytes ] test_decode;
  add_test ~name:"foo: roundtrip" [ bytes ] test_roundtrip;
  add_test ~name:"foo: pp" [ uint8 ] test_pp
```

**Main entry point (fuzz/fuzz.ml):**

```ocaml
(* Initialize crypto RNG if needed by any module *)
let () = Crypto_rng_unix.use_default ()

(* Register all fuzz tests *)
let () =
  Fuzz_common.run ();
  Fuzz_foo.run ();
  Fuzz_bar.run ()
```

---

## Test Patterns

### 1. Crash-safety test (parsers must not crash)

```ocaml
open Crowbar
open Fuzz_common

(** Decode - must not crash on arbitrary input. *)
let test_decode buf =
  let buf = truncate buf in
  let _ = Foo.decode (to_bytes buf) in
  ()

(** Decode with exceptions - must not crash. *)
let test_decode_exn buf =
  let buf = truncate buf in
  (try ignore (Foo.decode_exn (to_bytes buf)) with _ -> ());
  ()

let run () =
  add_test ~name:"foo: decode crash safety" [ bytes ] test_decode;
  add_test ~name:"foo: decode_exn crash safety" [ bytes ] test_decode_exn
```

**Key points**:
- Use `bytes` generator for arbitrary binary input (produces `string` type)
- Use `ignore` to discard results without warnings
- Use `| exception _ -> ()` to catch any exceptions
- `check true` signals test passed

### 2. Roundtrip test (encode/decode pairs)

```ocaml
(** Roundtrip - valid values must round-trip. *)
let test_roundtrip buf =
  let buf = truncate buf in
  match Foo.decode (to_bytes buf) with
  | Error _ -> ()  (* Invalid input is fine *)
  | Ok original ->
      let encoded = Foo.encode original in
      match Foo.decode encoded with
      | Error _ -> fail "re-decode failed"
      | Ok decoded ->
          if original <> decoded then fail "roundtrip mismatch"

let run () =
  add_test ~name:"foo: roundtrip" [ bytes ] test_roundtrip
```

**Key points**:
- If initial decode fails, that's OK (input was invalid)
- If re-decode fails after encode, that's a bug
- Compare original and decoded values

### 3. Constrained type roundtrip (smart constructors)

```ocaml
(** APID roundtrip - valid values must round-trip. *)
let test_apid_roundtrip n =
  match Apid.of_int n with
  | None -> if n >= 0 && n <= 2047 then fail "should accept valid value"
  | Some apid ->
      let n' = Apid.to_int apid in
      if n <> n' then fail "roundtrip mismatch"

let run () =
  add_test ~name:"apid: roundtrip" [ range 2048 ] test_apid_roundtrip
```

### 4. Boundary tests

```ocaml
(** Max valid value. *)
let test_max_valid () =
  match Apid.of_int 2047 with
  | None -> fail "2047 should be valid"
  | Some apid -> if Apid.to_int apid <> 2047 then fail "value mismatch"

(** Min valid value. *)
let test_min_valid () =
  match Apid.of_int 0 with
  | None -> fail "0 should be valid"
  | Some apid -> if Apid.to_int apid <> 0 then fail "value mismatch"

let run () =
  add_test ~name:"apid: max_valid" [ const () ] test_max_valid;
  add_test ~name:"apid: min_valid" [ const () ] test_min_valid
```

**Key points**:
- Use `[ const () ]` for tests with no random input
- Never use `[]` as generator list (causes type error)

### 5. Invalid input rejection

```ocaml
(** Values above max must be rejected. *)
let test_invalid_above n =
  let invalid = 2048 + n in
  match Apid.of_int invalid with
  | None -> ()
  | Some _ -> fail "should reject values > 2047"

(** Negative values must be rejected. *)
let test_invalid_negative n =
  let invalid = -(n + 1) in
  match Apid.of_int invalid with
  | None -> ()
  | Some _ -> fail "should reject negative values"

let run () =
  add_test ~name:"apid: invalid_above" [ range 1000 ] test_invalid_above;
  add_test ~name:"apid: invalid_negative" [ range 1000 ] test_invalid_negative
```

### 6. Pretty-printer safety

```ocaml
(** Pretty-print - must not crash. *)
let test_pp buf =
  let buf = truncate buf in
  match Foo.decode (to_bytes buf) with
  | Error _ -> ()
  | Ok v -> let _ = Format.asprintf "%a" Foo.pp v in ()

let run () =
  add_test ~name:"foo: pp" [ bytes ] test_pp
```

### 7. State machine transitions

```ocaml
(** Test valid state transitions. *)
let test_activate_pending kid algo material_buf =
  let material = to_bytes material_buf in
  if Bytes.length material = 0 then ()
  else
    let key = Key.v ~kid ~algorithm:algo ~material in
    match Key.activate key with
    | Error _ -> ()  (* May fail if material invalid *)
    | Ok active_key ->
        if Key.state active_key <> Key.Active then fail "wrong state"

(** Test invalid state transitions return errors. *)
let test_activate_empty_fails kid algo =
  let key = Key.empty ~kid ~algorithm:algo in
  match Key.activate key with
  | Ok _ -> fail "should fail on Empty key"
  |

Related in Security