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 namefmtand 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 afmt::string_view, with behavior controlled by theFMT_FUZZ_SEPARATE_ALLOCATIONpreprocessor flag. When enabled, this allocates a separatestd::vector<char>buffer to improve AddressSanitizer detection of out-of-bounds reads.assign_from_buf: A template function that safely copiessizeof(Item)bytes from the fuzzer input into a typed value usingstd::memcpy.- Fixed-size partitioning: The
constexpr auto fixed_size = 16constant 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.ymlto trigger OSS-Fuzz builds on every pull request via Google's cifuzz actions. - Fuzzing harnesses reside in
test/fuzzing/, withfuzzer-common.hproviding utilities likedata_to_stringandassign_from_bufto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →