Integrating Fuzzing with Property-Based Generators

Combining property-based testing (PBT) generators with fuzzers gives the best of both worlds: generators ensure structurally valid, semantically relevant inputs; coverage-guided fuzzers steer mutations toward unexplored program behavior. Use this hybrid when testing parsers, serializers, deserializers, and input validators that reject most random byte blobs before reaching interesting logic.

1) Choose the integration mode

– Parametric-generator fuzzing: back a PBT generator with a deterministic byte stream (a “seed blob”) so the fuzzer mutates the byte stream while the generator decodes it into valid structured inputs. This preserves structural constraints while allowing byte-level mutation and coverage feedback.

– Seed + mutation hybrid: produce a corpus of generator-produced valid inputs, then feed them as seeds to a coverage-guided mutational fuzzer (AFL/AFL++/libFuzzer). Useful when a direct parametric wrapper is hard to implement.

2) Seed corpus and corpus management

– Start with diverse, small, high-quality seeds from the generator: include corner cases, known-regression examples, and both minimal and maximal valid documents.

– Maintain a curated seed set: keep seeds that reach new coverage or exercise different semantic variants. Prune near-duplicates (structural equivalence) to focus compute budget.

3) Mutation strategies that respect structure

– Parametric approach: mutate the underlying byte stream that drives generation; this produces structure-preserving changes (lengths, token choices) rather than arbitrary byte flips.

– Grammar- or AST-aware mutations: if you can parse generated inputs into ASTs, mutate subtrees (replace, splice, delete) or swap tokens from a dictionary derived from real inputs.

– Hybrid atomic mutators: combine token-level edits (insert/remove field), semantic mutations (change numeric ranges, switch encodings), and lightweight byte mutations near non-critical bytes to discover parser edge cases.

4) Coverage and feedback

– Use coverage feedback from the target executable (edge/branch coverage) to decide which mutated byte streams or generated inputs to keep and further mutate.

– Instrument both the generator-decoder layer and the target when possible to detect generator-level predicates that block deep exploration; this helps focus mutations on bytes that influence those predicates.

5) Reduce the “havoc effect”

– The havoc effect occurs when mutations change early generator decisions and produce huge structural jumps. Mitigate it by:

– assigning mutation budgets with locality (prefer mutating bytes that map to later generator choices),

– biasing mutations toward structure-preserving operators first, and

– using incremental mutations (small deltas) before larger ones.

6) Minimization and de-duplication

– On crash, minimize the mutated byte stream using the generator as an oracle: find the shortest byte stream that reproduces the crash when decoded.

– Deduplicate crashes using stack traces and execution hashes (e.g., coverage or sanitized fault signatures). For parser logic, also record the generated structured input fingerprint (AST hash) to group semantically identical failures.

7) Triage and prioritization workflow

– Automated triage steps:

– classify by crash type (OOB, UAF, assertion, exception),

– rank by coverage novelty (how many unique branches or functions the input exercised), and

– prioritize crashes that are reproducible after minimization and that affect high-risk code paths (parsing/canonicalization, memory management).

8) Observability and safety

– Run fuzzing in sandboxed/instrumented environments (ASAN/UBSAN, seccomp, VMs) to capture memory safety issues safely and obtain useful diagnostics.

– Log both the raw mutated byte stream and the decoded structured input plus generator choices to aid debugging.

9) Practical tool choices

– Parametric generator frameworks: JQF/Zest (Java), libFuzzer with deterministic generator wrapper, or custom generator backed by a byte stream.

– Coverage-guided engines: AFL++, libFuzzer, honggfuzz. Use sanitizers (ASAN/UBSAN/MSAN) and a crash dedup tool (e.g., cmin, afl-cmin) for minimization.

10) Continuous workflow

– Integrate hybrid fuzzing into CI with long-running background jobs for coverage growth and shorter CI runs that exercise generator-based unit properties; periodically harvest new seeds discovered by the fuzzer back into the generator test harness to close the loop.

Combining PBT generators and coverage-guided fuzzers amplifies each approach: generators move tests past shallow validation into meaningful program states, and fuzzers guide exploration toward rare behaviors and crashes. Start with parametric generators when feasible; otherwise, bootstrap with a rich generator-produced corpus and iterate on mutation strategies and triage automation.

Sources

日本語