Back to Blog
critical SEVERITY6 min read

How buffer overflow via sprintf happens in C++ fuzzer code and how to fix it

A critical buffer overflow vulnerability was discovered in `prog/fuzzing/recog_basic_fuzzer.cc` where `sprintf` writes to a fixed 256-byte buffer without bounds checking. An attacker providing crafted fuzzer input could exploit this to corrupt memory. The fix replaces `sprintf` with `snprintf`, enforcing the buffer size limit and preventing overflow.

O
By Orbis AppSec
Published June 23, 2026Reviewed June 23, 2026

Answer Summary

This is a buffer overflow vulnerability (CWE-120) in C++ caused by using `sprintf()` with a fixed-size buffer in `prog/fuzzing/recog_basic_fuzzer.cc`. The `sprintf(filename, "/tmp/libfuzzer.%d", getppid())` call writes to a 256-byte `char` array without bounds checking. The fix replaces it with `snprintf(filename, sizeof(filename), "/tmp/libfuzzer.%d", getppid())`, which enforces the buffer size limit and truncates output rather than overflowing.

Vulnerability at a Glance

cweCWE-120
fixReplace sprintf() with snprintf() specifying sizeof(buffer) as the size limit
riskMemory corruption leading to arbitrary code execution or denial of service
languageC++
root causeUsing sprintf() with a fixed-size buffer instead of bounds-checked snprintf()
vulnerabilityBuffer overflow via unbounded sprintf

Introduction

In the Leptonica image processing library, we discovered a critical buffer overflow vulnerability in prog/fuzzing/recog_basic_fuzzer.cc at line 12. The fuzzer entry point function LLVMFuzzerTestOneInput uses sprintf to construct a temporary file path into a fixed 256-byte char filename[256] buffer without any bounds checking. While the current format string /tmp/libfuzzer.%d with a PID value is unlikely to exceed 256 bytes in practice, the use of sprintf violates secure coding principles and establishes a dangerous pattern — one that becomes exploitable if the format string or its arguments ever change.

This matters because fuzzer code runs with untrusted, adversarial input by design. Any weakness in the fuzzer harness itself can be leveraged to corrupt the fuzzing process or the host system.

The Vulnerability Explained

Here's the vulnerable code in prog/fuzzing/recog_basic_fuzzer.cc:

L_RECOG  *recog;
char filename[256];
sprintf(filename, "/tmp/libfuzzer.%d", getppid());

FILE *fp = fopen(filename, "wb");
if (!fp)

The sprintf function writes formatted output to filename without any awareness of the buffer's size. The function signature is:

int sprintf(char *str, const char *format, ...);

Notice there's no size parameter — sprintf will write as many bytes as the formatted string requires, regardless of how much space filename actually has.

Why This Is Dangerous

  1. No bounds enforcement: If getppid() somehow returned a value that, combined with the format string, exceeded 255 characters (plus null terminator), sprintf would happily write past the end of filename.

  2. Stack corruption: Since filename is a stack-allocated buffer, overflow would corrupt adjacent stack variables (like the recog pointer), the saved frame pointer, or the return address — classic stack smashing.

  3. Predictable file path: The temporary file /tmp/libfuzzer.<pid> uses a predictable path based on the parent PID. Combined with the lack of input size validation before writing fuzzer data to this file, an attacker controlling fuzzer input could write arbitrary data to a known location.

  4. Pattern propagation: This code serves as a template for other fuzzers. The unsafe sprintf pattern gets copied into new fuzzer harnesses, multiplying the risk.

Attack Scenario

Consider this exploitation path specific to this code:

  1. An attacker identifies that recog_basic_fuzzer writes untrusted data to /tmp/libfuzzer.<pid> — a predictable path.
  2. The fuzzer harness itself uses sprintf without bounds checking for the filename.
  3. If the code is modified to include additional path components or user-controlled data in the format string (a common evolution in fuzzer harnesses), the buffer overflow becomes directly exploitable.
  4. Stack corruption could redirect execution when LLVMFuzzerTestOneInput returns, giving the attacker code execution in the context of the fuzzing process.

The Fix

The fix is surgical and precise — replacing sprintf with snprintf on line 12:

Before (Vulnerable)

sprintf(filename, "/tmp/libfuzzer.%d", getppid());

After (Fixed)

snprintf(filename, sizeof(filename), "/tmp/libfuzzer.%d", getppid());

How This Solves the Problem

The snprintf function signature includes an explicit size parameter:

int snprintf(char *str, size_t size, const char *format, ...);

By passing sizeof(filename) (which evaluates to 256), snprintf guarantees:

  1. At most 255 characters are written to filename (reserving one byte for the null terminator).
  2. Output is truncated rather than overflowing if the formatted string would exceed the buffer.
  3. The buffer is always null-terminated, preventing string functions from reading past the buffer.

Using sizeof(filename) rather than a hardcoded 256 ensures the size limit automatically stays correct if the buffer declaration ever changes — a defensive programming practice that eliminates an entire class of maintenance bugs.

Prevention & Best Practices

Immediate Actions

  • Ban sprintf in your codebase: Use compiler flags like -Werror=deprecated-declarations or linting rules to flag sprintf usage.
  • Replace all sprintf with snprintf: This is a mechanical transformation that can be automated.
  • Use sizeof() for stack buffers: Always pass sizeof(buffer) rather than magic numbers.

Defensive Patterns for Fuzzer Harnesses

// GOOD: Bounded write with error checking
char filename[256];
int written = snprintf(filename, sizeof(filename), "/tmp/libfuzzer.%d", getppid());
if (written < 0 || (size_t)written >= sizeof(filename)) {
    return 0;  // Truncation occurred — bail safely
}

Tools for Detection

  • Compiler warnings: -Wall -Wformat-overflow -Wformat-truncation
  • Static analysis: Coverity, PVS-Studio, and clang-tidy's bugprone-not-null-terminated-result
  • Semgrep rules: Custom rules matching sprintf calls with fixed buffers
  • AddressSanitizer (ASan): Catches overflows at runtime during fuzzing

Security Standards

  • CWE-120: Buffer Copy without Checking Size of Input
  • CERT C Rule STR31-C: Guarantee that storage for strings has sufficient space for character data and the null terminator
  • OWASP: Buffer overflow prevention guidelines

Key Takeaways

  • Never use sprintf() with stack-allocated buffers in fuzzer harness code — fuzzers process adversarial input by design, making any weakness in the harness itself a security risk.
  • The char filename[256] + sprintf pattern in recog_basic_fuzzer.cc is a textbook CWE-120 violation that modern compilers can catch with proper warning flags enabled.
  • Using sizeof(filename) with snprintf creates a self-maintaining size constraint — if the buffer size changes, the bounds check automatically adapts.
  • Fuzzer harness code IS production code — it ships in the repository, runs in CI/CD, and establishes patterns that other developers copy.
  • Predictable temporary file paths (/tmp/libfuzzer.<pid>) combined with unbounded writes compound the risk — fixing the buffer overflow is necessary but the predictable path pattern also deserves review.

How Orbis AppSec Detected This

  • Source: Untrusted data from the LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) fuzzer entry point
  • Sink: sprintf(filename, "/tmp/libfuzzer.%d", getppid()) in prog/fuzzing/recog_basic_fuzzer.cc:12
  • Missing control: No bounds checking on the sprintf output — the 256-byte filename buffer has no size enforcement during the write operation
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Replaced sprintf with snprintf(filename, sizeof(filename), ...) to enforce buffer size limits and prevent overflow

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

Buffer overflows via sprintf remain one of the most common and dangerous vulnerabilities in C/C++ code. This specific instance in recog_basic_fuzzer.cc demonstrates that even "simple" code — constructing a filename from a PID — can harbor critical security flaws when unsafe functions are used. The fix is straightforward: replace sprintf with snprintf and always pass the buffer size. This one-character-difference function (ssn) is the difference between a buffer overflow and safe, bounded string formatting.

For teams maintaining C/C++ codebases, the lesson is clear: audit every sprintf call, enable format-overflow compiler warnings, and treat fuzzer harness code with the same security rigor as any production component.

References

Frequently Asked Questions

What is a buffer overflow via sprintf?

A buffer overflow via sprintf occurs when the formatted output string exceeds the size of the destination buffer, causing data to be written beyond its allocated memory boundary, potentially overwriting adjacent memory.

How do you prevent buffer overflow in C++?

Use bounds-checked functions like snprintf() instead of sprintf(), validate input sizes before processing, use std::string for dynamic allocation, and enable compiler protections like stack canaries and ASLR.

What CWE is buffer overflow via sprintf?

CWE-120 (Buffer Copy without Checking Size of Input) covers this pattern where data is copied to a buffer without verifying that the source data fits within the destination buffer's bounds.

Is using a large buffer enough to prevent sprintf buffer overflow?

No. Even a 256-byte buffer can overflow if the format string produces unexpectedly long output. Always use snprintf() with an explicit size limit regardless of buffer size.

Can static analysis detect sprintf buffer overflow?

Yes. Static analysis tools, compiler warnings (-Wformat-overflow), and security scanners can flag sprintf() usage as potentially unsafe and recommend snprintf() as the bounded alternative.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #798

Related Articles

critical

How unlimited batch API calls happen in React JSX and how to fix it

A missing batch size limit in `BatchModeRunner.jsx` allowed users to trigger unlimited LLM API calls by pasting thousands of items into the batch input field. This could exhaust shared API quotas in organizational settings where a single API key is distributed across multiple users. The fix introduces a hard cap of 25 items (`MAX_BATCH_SIZE = 25`) enforced directly in the `canRun()` validation function.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) was discovered in axios versions prior to 1.15.1, allowing attackers to bypass NO_PROXY configurations using specially crafted URLs. This could expose sensitive internal traffic to proxy servers, potentially leaking credentials or internal data. The fix upgrades axios to version 1.15.1, which properly validates URLs before applying proxy exclusion rules.

high

How buffer overflow via sprintf() happens in C string formatting and how to fix it

A high-severity buffer overflow vulnerability was discovered in `bench/strbuild/strbuild.c` where `sprintf()` wrote formatted output into a 64-byte stack buffer (`line[64]`) without any bounds checking. An attacker who could influence the values in the `NAMES[]`, `c[]`, or `v[]` arrays could overflow this buffer, potentially corrupting the stack and hijacking control flow. The fix replaces `sprintf()` with `snprintf(line, sizeof(line), ...)` to enforce a strict 64-byte write limit.

high

How Middleware/Proxy Bypass Happens in Next.js App Router with Turbopack and Single Locale and How to Fix It

A critical middleware and proxy bypass vulnerability (CVE-2026-64642) was discovered in Next.js 16.0.7 when using the App Router with Turbopack and single locale configurations. This vulnerability could allow attackers to circumvent security middleware and access protected routes. The fix requires upgrading Next.js from version 16.0.7 to 16.2.11, which patches the routing logic that handles locale-based request interception.

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration flaw was discovered in a Dependabot configuration file where no cooldown period was set for package updates. This meant newly published—and potentially malicious or unstable—package versions could be immediately proposed for updates, exposing the project to supply chain attacks. The fix adds a 7-day cooldown period to allow the community to identify compromised packages before they're adopted.

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s