Skip to content

test(fuzz): add a coverage-guided fuzzing harness for the pool surface#115

Merged
danielPoloWork merged 1 commit into
masterfrom
test/coverage-guided-fuzzing
Jul 9, 2026
Merged

test(fuzz): add a coverage-guided fuzzing harness for the pool surface#115
danielPoloWork merged 1 commit into
masterfrom
test/coverage-guided-fuzzing

Conversation

@danielPoloWork

Copy link
Copy Markdown
Owner

Summary

Implements ROADMAP item 9.3 — a coverage-guided fuzzing harness (issue #108), decided in ADR-0044. The test tier was example-based (doctest) and the benchmark a fixed loop; there was no fuzz target anywhere in the repo. An allocator's interesting defects live in sequences — allocate-to-exhaustion, LIFO vs. FIFO frees, free-then-reallocate across a growth event, foreign-pointer rejections interleaved with valid traffic — which a coverage-guided fuzzer explores automatically.

The harness — stateful interpreter + shadow oracle

pool_fuzz.cpp reads the fuzzer's bytes as a small program: the leading bytes configure a pool (block size / count / fixed-vs-dynamic + growth factor, valid by construction under ADR-0009 §2) and each remaining byte is an opcode — alloc, free(valid) (an index byte picks which live block), free(NULL), free(foreign). A shadow model of the live blocks asserts, after every step, the invariants the pool promises — turning a silent corruption into a saved reproducer:

  • no aliasing — a freshly vended block is never already live;
  • canary integrity — each block is stamped on allocation and verified intact at free time (catches overlap even without a sanitizer);
  • defined foreign/NULL frees — no-ops that never corrupt the free list (ADR-0012);
  • accounting — an InstrumentedPool (ADR-0025) is the oracle: live_ tracks the shadow set exactly, and after draining the pool reports zero live, balanced counts, and a matching peak-live.

Both fixed and dynamic pools are exercised. A double-free of an in-range block is deliberately not fuzzed — the default build does not detect it by design (ADR-0012); hardening (ADR-0043) owns that.

Two build roles from one source

  • pool_fuzz — the libFuzzer target, opt-in behind PBR_MEMORY_POOL_BUILD_FUZZERS (OFF by default) and Clang-only (a FATAL_ERROR guards other compilers). A fuzz preset compiles every TU with -fsanitize=fuzzer-no-link,address,undefined (coverage spanning the library) and links the target with -fsanitize=fuzzer.
  • pool_fuzz_replay — built with the normal test suite (no engine); its standalone main replays the seed corpus (CTest pool_fuzz_replay), making the corpus a portable regression gate everywhere — including MSVC, where libFuzzer is unavailable — and keeping the harness inside the clang-tidy gate.

CI

A dedicated fuzz job (Clang/POSIX) builds the target, replays the seed corpus as a gate, then fuzzes for a bounded 60 s on every PR; a crash fails the job and uploads the input as an artifact, feeding the bug ledger (ADR-0039).

Validation

  • Clean debug build + full ctest green locally (MSVC 19.51), including the new pool_fuzz_replay corpus gate; the harness was additionally stress-run over 1,500 randomized inputs via the standalone replay with no false positives.
  • clang-format clean; clang-tidy (-p build/debug) clean on pool_fuzz.cpp; the non-Clang FATAL_ERROR guard verified.
  • consistency_lint.py OK; all doc links resolve; the spec's translation rows are already stale, so this PR adds no i18n debt.
  • The coverage-guided libFuzzer build itself runs on the new Clang CI fuzz job (libFuzzer is unavailable on the maintainer's MSVC box by design).

Docs

ADR-0044 (+ index row), spec §6.4 / §7, ROADMAP.md (9.3 → done), CHANGELOG.md [Unreleased], and a corpus README. Per ADR-0044, the README.md test-strategy refresh is deferred to the v1.2.0 release PR to keep this PR off the translated docs surface (same sequencing as ADR-0043).

Closes #108.

🤖 Generated with Claude Code

Add pool_fuzz.cpp, a stateful opcode interpreter that drives the pool's
public surface — create -> alloc/free -> destroy, plus dynamic growth —
through randomized operation sequences and asserts, against a shadow
model, the invariants the pool promises:

- no two live blocks alias (a free list vending a block twice is caught);
- a per-block canary written on allocation is intact at free time;
- free(NULL) and free(foreign) are defined no-ops that never corrupt the
  free list (ADR-0012);
- the InstrumentedPool live count tracks the shadow set exactly, and after
  draining the pool reports zero live and balanced counters (ADR-0025).

Both fixed and dynamic pools are exercised. A double-free of an in-range
block is deliberately not exercised — the default build does not detect it
by design (ADR-0012); hardening (ADR-0043) owns that.

One engine-agnostic source yields two build roles:

- pool_fuzz — the libFuzzer target, opt-in behind PBR_MEMORY_POOL_BUILD_FUZZERS
  (OFF by default) and Clang-only. A `fuzz` preset compiles every TU with
  -fsanitize=fuzzer-no-link,address,undefined and links the target with
  -fsanitize=fuzzer, so coverage feedback spans the library. A dedicated CI
  job replays the seed corpus and fuzzes for a bounded 60s on every PR,
  uploading any crash as a reproducer.
- pool_fuzz_replay — built with the normal test suite (no engine): its
  standalone main replays the seed corpus (CTest pool_fuzz_replay), making
  the corpus a portable regression gate everywhere — including MSVC, where
  libFuzzer is unavailable — and keeping the harness inside the clang-tidy
  gate.

Test-only and additive; the release build and the ADR-0014 benchmark
numbers are untouched. Decided in ADR-0044; ROADMAP item 9.3.

Closes #108.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielPoloWork danielPoloWork added this to the v1.2.0 milestone Jul 9, 2026
@danielPoloWork danielPoloWork added the test Adding or correcting tests (Conventional Commit: test) label Jul 9, 2026
@danielPoloWork danielPoloWork self-assigned this Jul 9, 2026
@danielPoloWork danielPoloWork marked this pull request as ready for review July 9, 2026 19:28
@danielPoloWork danielPoloWork merged commit 91883cb into master Jul 9, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Adding or correcting tests (Conventional Commit: test)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(ci): add a coverage-guided fuzzing harness for the pool surface

1 participant