FuzzTest Documentation

repository·main·Indexed 21 days ago

https://github.com/google/fuzztest

A 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.

Tokens
19.2K
Snippets
59
Records
73
Agent score
76%

What's inside FuzzTest

  1. What is FuzzTest?

    main
    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.
  2. Avoid state mutation in mutable test fixtures

    main

    When 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:

    1. Break the fuzzer's assumptions about how inputs affect coverage.
    2. 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);
  3. How to write a property-based fuzz test

    main

    FuzzTest 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:

    1. Define a function (the property function) that accepts the parameters you want to fuzz.
    2. Use the FUZZ_TEST macro to instantiate the test.
    3. Use .WithDomains(...) to specify the input domains for each parameter using domain specifiers like Arbitrary<T>() or InRegexp(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.*"));
  4. How Centipede manages persistent state and avoids redundant executions

    main

    To support large, slow targets and minimize startup costs on preemptible cloud VMs, Centipede avoids redundant executions by maintaining two types of persistent state:

    1. Corpus: A set of inputs. The corpus is shared by a group of fuzz targets that use the same input data format.
    2. 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.

  5. Avoid static initialization issues with Seed Providers

    main

    Because FUZZ_TEST expands 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...>>, where Args... 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);
  6. How Centipede reasons about execution feedback via features

    main

    Centipede 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.

  7. Understand FuzzTest operating modes

    main

    FuzzTest 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 GoogleTest TESTs. 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.

  8. Use a custom corpus_type for complex Domains

    main

    In 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 resulting std::string (value_type).

    To implement a custom corpus_type, follow these steps:

    1. Define using value_type = ....
    2. Set static constexpr bool has_custom_corpus_type = true;.
    3. Define using corpus_type = ....
    4. Implement corpus_type Init(absl::BitGenRef prng).
    5. Implement void Mutate(corpus_type& val, absl::BitGenRef prng, bool only_shrink).
    6. Implement value_type GetValue(const corpus_type& value) const to 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_;
    };
  9. Adapt GoogleTest fixtures for fuzz tests

    main

    You 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);
  10. Centipede Terminology

    main

    Understanding 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 sancov coverage.
    • 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.
  11. How Centipede handles concurrent execution

    main

    Centipede 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.
  12. Perform differential fuzzing with an oracle

    main

    Differential 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);