FuzzTest Documentation
repository·main·Indexed 21 days ago
https://github.com/google/fuzztestA C++ testing framework that combines property-based testing with coverage-guided fuzzing to automatically discover edge cases. It includes the Centipede fuzzing engine, which uses features to reason about execution feedback and supports corpus distillation, text-based and HTML coverage reports, and persistent state management to minimize redundant executions.
What's inside FuzzTest
- FuzzTest is a C++ testing framework designed for writing and executing fuzz tests. It bridges the gap between property-based testing and coverage-guided fuzzing. Unlike traditional unit tests that check specific inputs against specific outputs, FuzzTest allows you to define generic properties that should hold true for a range of inputs. Under the hood, it uses a coverage-guided fuzzing engine (similar to libFuzzer or AFL) to automatically discover edge cases that violate these properties.
Avoid state mutation in mutable test fixtures
mainWhen using a fixture that persists across iterations, be careful with mutating the fixture's state. If a property function modifies a member variable (e.g., a flag or a counter), the test no longer depends solely on the fuzzer's input. This can:
- Break the fuzzer's assumptions about how inputs affect coverage.
- Make crashes non-reproducible because the crash depends on the specific state of the mutated variable.
BEST PRACTICE: If your property function must mutate the fixture, ensure it resets the mutated state so that each fuzz test iteration starts in the same state.
class MyFuzzTest { public: void MyProperty(int n) { MyApi(n, flag_); flag_ = !flag_; // DANGER: Mutating state across iterations } private: bool flag_ = false; }; FUZZ_TEST_F(MyFuzzTest, MyProperty);How to write a property-based fuzz test
mainFuzzTest allows you to move from specific unit tests to property-based testing. Instead of providing a single input/output pair, you define a property function that takes parameters and asserts that certain properties hold true for any valid input within specified domains.
To implement this:
- Define a function (the property function) that accepts the parameters you want to fuzz.
- Use the
FUZZ_TESTmacro to instantiate the test. - Use
.WithDomains(...)to specify the input domains for each parameter using domain specifiers likeArbitrary<T>()orInRegexp(string).
void ParsesIntCorrectly(int number, const std::string& suffix) { const std::string input = absl::StrCat(number, suffix); const std::optional<int> output = ParseLeadingDigits(input); EXPECT_THAT(output, Optional(Eq(number))); } FUZZ_TEST(ParseLeadingDigitsTest, ParsesIntCorrectly) .WithDomains(/*number=*/Arbitrary<int>(), /*suffix=*/InRegexp("^\D.*"));How Centipede manages persistent state and avoids redundant executions
mainTo support large, slow targets and minimize startup costs on preemptible cloud VMs, Centipede avoids redundant executions by maintaining two types of persistent state:
- Corpus: A set of inputs. The corpus is shared by a group of fuzz targets that use the same input data format.
- Feature sets: A mapping of features to specific corpus elements. Features are unique to a specific target binary (e.g., different builds or revisions will have different feature sets). A feature set is associated with an input via the input's hash.
Startup Workflow: On startup, Centipede loads the corpus and checks for existing feature sets. It only recomputes features for corpus elements where the corresponding feature set is missing.
Avoid static initialization issues with Seed Providers
mainBecause
FUZZ_TESTexpands into a global variable, initializing seeds with other global objects can lead to the static initialization order fiasco. To prevent this, use a seed provider instead of a raw list of values.A seed provider is an invocable (lambda, function pointer, or member function) that returns a
std::vector<std::tuple<Args...>>, whereArgs...matches the property function's parameters. FuzzTest calls this provider at runtime, ensuring safe initialization.Using a Lambda as a Seed Provider
FUZZ_TEST(MyApiTestSuite, CallingMyApiNeverCrashes) .WithSeeds([]() -> std::vector<std::tuple<int, std::string>> { return {{5, "Foo"}, {10, "Bar"}}; });Using a Test Fixture Member Function
If using
FUZZ_TEST_F, the seed provider can be a pointer to a non-const member function of your fixture class.class MyFuzzTest { public: void CallingMyApiNeverCrashes(int x, const std::string& s); std::vector<std::tuple<int, std::string>> seeds() { return seeds_; } private: std::vector<std::tuple<int, std::string>> seeds_ = {{5, "Foo"}, {10, "Bar"}}; }; FUZZ_TEST_F(MyFuzzTest, CallingMyApiNeverCrashes) .WithSeeds(&MyFuzzTest::seeds);How Centipede reasons about execution feedback via features
mainCentipede uses the concept of features to reason about execution feedback. A feature represents a unique behavior of the target exercised by a specific input. By executing an input, Centipede computes its associated features to determine its coverage and uniqueness.
Currently supported features include:
- Control flow edges with 8-bit counters.
- Simplified data flow edges: either
{store-PC, load-PC}or{global-address, load-PC}. - Bounded control flow paths.
- Instrumented CMP instructions.
Note that the target can generate its own custom feature types without requiring explicit support from the Centipede engine itself.
Understand FuzzTest operating modes
mainFuzzTest can be run in three distinct modes depending on your testing requirements:
- Unit test mode (default):
FUZZ_TESTs are built without coverage instrumentation. They run with random inputs for a short period alongside regular GoogleTestTESTs. This is ideal for quick local verification. - Fuzzing mode: Controlled by setting
-DFUZZTEST_FUZZING_MODE=on. In this mode,FUZZ_TESTs are built with coverage instrumentation and run individually and indefinitely (or for a specified duration). This is the primary mode for finding deep bugs. - Compatibility mode: Controlled by setting
-DFUZZTEST_COMPATIBILITY_MODE=libfuzzer. This uses an external engine (currently libFuzzer) instead of the built-in FuzzTest engine.
Warning: Compatibility mode is experimental and does not guarantee full FuzzTest features.
- Unit test mode (default):
Use a custom corpus_type for complex Domains
mainIn some cases, the
value_type(the output) is not the most efficient way to represent or mutate the internal state. For example, when fuzzing strings generated by a regular expression, it is more efficient to mutate a DFA path (corpus_type) than to mutate the resultingstd::string(value_type).To implement a custom
corpus_type, follow these steps:- Define
using value_type = .... - Set
static constexpr bool has_custom_corpus_type = true;. - Define
using corpus_type = .... - Implement
corpus_type Init(absl::BitGenRef prng). - Implement
void Mutate(corpus_type& val, absl::BitGenRef prng, bool only_shrink). - Implement
value_type GetValue(const corpus_type& value) constto convert the internal state back to the output type.
class RegexDomain { public: using value_type = std::string; static constexpr bool has_custom_corpus_type = true; using corpus_type = DFAPath; MyVectorDomain(std::string regex) : dfa_(DFA::Parse(regex)) {} corpus_type Init(absl::BitGenRef prng) { return dfa_.Generate(prng); } void Mutate(corpus_type& val, absl::BitGenRef prng, bool only_shrink) { dfa_.Mutate(prng, val); } value_type GetValue(const corpus_type& value) const { static_assert(has_custom_corpus_type); return dfa_.ToString(value); } private: DFA dfa_; };- Define
Adapt GoogleTest fixtures for fuzz tests
mainYou can reuse existing GoogleTest fixtures (classes derived from
::testing::Test) in FuzzTest by using adapters. You must choose between two different instantiation semantics depending on whether your fixture is mutable or expensive to set up.Semantics 1: Per-iteration instantiation
Use
fuzztest::PerIterationFixtureAdapter<Fixture>when the fixture is mutable. This follows the standard GoogleTest invariant where a fresh fixture object is used for every test run (in this case, every fuzz iteration). This prevents state leakage between iterations.Semantics 2: Per-fuzz-test instantiation
Use
fuzztest::PerFuzzTestFixtureAdapter<Fixture>when the fixture is expensive to set up but immutable (or easily resettable). This instantiates the fixture once per fuzz test, reusing the same object across all iterations to maximize performance.// Example: Per-iteration semantics for a mutable fixture class SumVecFuzzTest : public fuzztest::PerIterationFixtureAdapter<SumVecTest> { public: void SumsLastEntry(int last_entry) { int previous_sum = SumVec(vec_); vec_.push_back(last_entry); EXPECT_EQ(SumVec(vec_), previous_sum + last_entry); } }; FUZZ_TEST_F(SumVecFuzzTest, SumsLastEntry); // Example: Per-fuzz-test semantics for an expensive, immutable fixture class EchoServerFuzzTest : public fuzztest::PerFuzzTestFixtureAdapter<EchoServerTest> { public: void ReturnsTheSameString(const std::string& request) { std::string response; SendRequest("localhost:9999", request, &response); EXPECT_EQ(response, request); } }; FUZZ_TEST_F(EchoServerFuzzTest, ReturnsTheSameString);Centipede Terminology
mainUnderstanding the core concepts of Centipede:
- Fuzzing engine (fuzzer): Orchestrates execution and produces an infinite stream of inputs.
- Fuzz target: A binary or library that consumes bytes and produces coverage data.
- Input: A sequence of bytes (arbitrary or structured).
- Feature: A number representing unique target behavior (e.g., a specific basic block execution).
- Feature set: The set of features observed during a specific input's execution.
- Coverage: Information about target behavior, usually represented as a feature set.
- Mutator: A function that produces small random mutations of an input.
- Executor: A function that feeds input to a target and retrieves coverage.
- Centipede runner: A library that implements the executor interface, allowing targets to be run by Centipede and collect
sancovcoverage. - Corpus: A set of inputs.
- Distillation: Selecting a subset of a corpus that maintains the same coverage features.
- Shard: A file representing a subset of the corpus and its associated feature sets.
- Job: A single fuzzer process that writes to one shard and may read multiple shards.
- Workdir (WD): The directory containing fuzzer data.
How Centipede handles concurrent execution
mainCentipede jobs run concurrently in separate processes and can run on different machines.
- State Sharing: Jobs periodically peek at each other's corpus and feature sets.
- Concurrency Model: Every job writes only to its own persistent state. However, a job can read any other job's state concurrently while that job is writing to it.
- Implementation: This is achieved using an appendable storage format.
Perform differential fuzzing with an oracle
mainDifferential fuzzing involves comparing the output of your implementation against an 'oracle'—a second implementation that is assumed to be correct. This is often a simpler or more established version of the same logic. If the two implementations return different values for the same input, a bug is detected.
void EqualsConsistentWithMessageDifferencerProto3( const testdata::TestProto3Type& m1, const testdata::TestProto3Type& m2) { EXPECT_EQ(testdata::Equals(m1, m2), util::MessageDifferencer::Equals(m1, m2)); } FUZZ_TEST(CppEqualsGeneratorTest, EqualsConsistentWithMessageDifferencerProto3);