How fmtlib Is Fuzz-Tested for Security Using OSS-Fuzz

The fmt library protects against security regressions through automated OSS-Fuzz integration that builds and executes libFuzzer harnesses on every pull request, detecting memory corruption and undefined behavior before code reaches production.

The fmt library is a modern C++ formatting library that provides a fast and safe alternative to printf and iostreams. Given its widespread use in performance-critical and security-sensitive applications, maintaining memory safety is paramount. This article examines how fmtlib is fuzz-tested for security using OSS-Fuzz through continuous integration pipelines and specialized fuzzing harnesses.

Continuous Fuzzing via GitHub Actions

The fmt project integrates with OSS-Fuzz through a dedicated GitHub Actions workflow defined in .github/workflows/fuzz.yml. This workflow triggers automatically on every pull request to ensure that proposed changes do not introduce memory safety vulnerabilities or crash conditions.

The pipeline utilizes Google's cifuzz (Continuous Integration Fuzzing) actions from the google/oss-fuzz repository. The configuration employs two primary actions:

  • build_fuzzers: Compiles the fuzz targets using the OSS-Fuzz infrastructure, specifying the project name fmt and the C++ language.
  • run_fuzzers: Executes the compiled fuzzers for a fixed duration of 300 seconds (5 minutes) by default.

When the build succeeds but the fuzz run detects crashes, the workflow automatically uploads crash artifacts for analysis. This ensures that every commit is validated against real-world edge cases and malformed inputs before merging.

Fuzz Target Implementation

The fuzzing logic resides in the test/fuzzing directory, with shared utilities defined in test/fuzzing/fuzzer-common.h. These components transform raw fuzzer input into valid arguments for fmt's formatting API.

The fuzzer-common.h Utilities

The header provides several key abstractions for constructing test cases:

  • data_to_string: A structure that converts raw bytes into a fmt::string_view, with behavior controlled by the FMT_FUZZ_SEPARATE_ALLOCATION preprocessor flag. When enabled, this allocates a separate std::vector<char> buffer to improve AddressSanitizer detection of out-of-bounds reads.
  • assign_from_buf: A template function that safely copies sizeof(Item) bytes from the fuzzer input into a typed value using std::memcpy.
  • Fixed-size partitioning: The constexpr auto fixed_size = 16 constant ensures the fuzzer efficiently mutates data across different type interpretations.
// From test/fuzzing/fuzzer-common.h
#define FMT_FUZZ_SEPARATE_ALLOCATION 1
constexpr auto fixed_size = 16;

template <class Item>
inline Item assign_from_buf(const std::uint8_t* data) {
  Item item{};
  std::memcpy(&item, data, sizeof(Item));
  return item;
}

struct data_to_string {
#if FMT_FUZZ_SEPARATE_ALLOCATION
  std::vector<char> buffer;
  data_to_string(const uint8_t* data, size_t size, bool add_terminator = false)
      : buffer(size + (add_terminator ? 1 : 0)) {
    if (size) std::memcpy(buffer.data(), data, size);
  }
  fmt::string_view get() const { return {buffer.data(), buffer.size()}; }
#else
  fmt::string_view sv;
  data_to_string(const uint8_t* data, size_t size, bool = false)
      : sv(reinterpret_cast<const char*>(data), size) {}
  fmt::string_view get() const { return sv; }
#endif
  const char* data() const { return get().data(); }
};

Harness Execution Flow

Individual fuzz targets (such as fmt_fuzz.cpp) implement the LLVMFuzzerTestOneInput entry point required by libFuzzer. The harness interprets the first 16 bytes as a format string and subsequent bytes as arguments, invoking fmt::format or fmt::vformat within a try-catch block to isolate crashes from expected exceptions:

#include "fuzzer-common.h"
#include <fmt/core.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  // Use the first fixed_size bytes as the format string
  const size_t fmt_len = std::min<size_t>(size, fixed_size);
  data_to_string fmt_str(data, fmt_len);
  
  // Remaining bytes interpreted as arguments
  const uint8_t *arg_data = data + fmt_len;
  int arg = assign_from_buf<int>(arg_data);
  
  // Execute formatting operation
  try {
    fmt::format(fmt_str.get(), arg);
  } catch (...) {
    // Expected exceptions do not indicate security issues
  }
  return 0;
}

Security Detection Capabilities

By running these harnesses continuously on Google's infrastructure, fmt receives comprehensive security validation:

  • Memory-safety verification via AddressSanitizer (detecting buffer overflows and use-after-free) and UndefinedBehaviorSanitizer (detecting integer overflow and invalid shifts).
  • Crash detection for malformed format strings, invalid argument types, and edge-case handling paths that might trigger assertions or segmentation faults.
  • Regression protection through automated OSS-Fuzz issue filing. When the fuzzer discovers a crash, it creates a detailed bug report linked to the specific commit and input data, ensuring developers can reproduce and fix vulnerabilities quickly.

Summary

  • The fmt library uses .github/workflows/fuzz.yml to trigger OSS-Fuzz builds on every pull request via Google's cifuzz actions.
  • Fuzzing harnesses reside in test/fuzzing/, with fuzzer-common.h providing utilities like data_to_string and assign_from_buf to safely convert raw bytes into format arguments.
  • The 300-second fuzzing duration provides rapid feedback while catching memory safety issues through AddressSanitizer and UndefinedBehaviorSanitizer instrumentation.
  • Automated crash reporting ensures that any regressions are immediately flagged and linked to the causing commit, maintaining fmt's security guarantees.

Frequently Asked Questions

How often does fmtlib run fuzz tests?

The fuzzing workflow executes automatically on every pull request through GitHub Actions. Additionally, OSS-Fuzz runs the targets continuously on Google's infrastructure, often for much longer periods than the 300-second CI limit, to discover deep bugs.

What types of vulnerabilities can OSS-Fuzz detect in fmtlib?

The integration primarily detects memory safety violations (buffer overflows, out-of-bounds reads, heap corruption) through AddressSanitizer, and undefined behavior (integer overflow, null pointer dereference, alignment issues) through UndefinedBehaviorSanitizer. It also catches assertion failures and segmentation faults caused by malformed format strings.

Can I run the fmtlib fuzzers locally?

Yes. Developers can build the fuzz targets locally using the same source files in test/fuzzing/ by linking against libFuzzer and compiling with sanitizer flags (-fsanitize=address,undefined). The fuzzer-common.h header is designed to work both in the OSS-Fuzz environment and standalone local builds.

Where are the fuzzing configuration files located?

The CI configuration is in .github/workflows/fuzz.yml, while the fuzz target implementations and shared utilities are in the test/fuzzing/ directory, specifically test/fuzzing/fuzzer-common.h and the various *_fuzz.cpp files.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →