Back to Blog
critical SEVERITY7 min read

How buffer overflow in fgets() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in the `readline()` function of `mdbx_load.c`, where an `fgets()` call passed a size parameter exceeding the actual allocated buffer by one byte. This off-by-one error could allow an attacker to trigger heap corruption by supplying oversized input via stdin, potentially leading to arbitrary code execution. The fix corrects the size parameter from `buf->iov_len + 1` to `buf->iov_len`, ensuring reads never exceed buffer boundaries.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published July 6, 2026•Reviewed July 6, 2026

Answer Summary

This is a buffer overflow vulnerability (CWE-120) in C caused by an off-by-one error in an `fgets()` call within `mdbx_load.c:426`. The `readline()` function passed `buf->iov_len + 1` as the size parameter to `fgets()`, allowing one byte more than the allocated buffer to be read. The fix changes the parameter to `buf->iov_len`, ensuring the read operation respects the actual buffer boundary.

Vulnerability at a Glance

cweCWE-120
fixChange `buf->iov_len + 1` to `buf->iov_len` in the fgets() call
riskHeap corruption leading to potential arbitrary code execution
languageC
root causefgets() size parameter exceeds allocated buffer by one byte
vulnerabilityBuffer overflow (off-by-one in fgets)

Introduction

The mdbx_load.c file is a CLI utility for loading data into MDBX databases from stdin or redirected input files. Within its readline() function—a performance-critical hot path marked with __hot—a subtle off-by-one error in an fgets() call at line 426 created a critical buffer overflow vulnerability. By passing buf->iov_len + 1 instead of buf->iov_len as the size parameter, the code allowed fgets() to write one byte beyond the allocated buffer, opening the door to heap corruption and potential arbitrary code execution.

This is the kind of bug that haunts C developers: a single + 1 that transforms a safe read into an exploitable overflow. If you work with buffer management in C, this case study demonstrates exactly why every byte counts.

The Vulnerability Explained

The Vulnerable Code

Here's the problematic code in the readline() function at line 426 of mdbx_load.c:

__hot static int readline(MDBX_val *out, MDBX_val *buf) {
    // ... buffer setup ...
    c1 = buf->iov_base;
    c1 += l2;
    errno = 0;
    if (fgets((char *)c1, (int)buf->iov_len + 1, stdin) == nullptr)
      return errno ? errno : EOF;
    buf->iov_len *= 2;
    len = strlen((char *)c1);
    // ...
}

The critical issue is (int)buf->iov_len + 1. The fgets() function reads at most size - 1 characters (reserving one byte for the null terminator). By passing buf->iov_len + 1, the code tells fgets() it can read up to buf->iov_len characters of actual data—but this exceeds the remaining space in the buffer by one byte.

Why This Is Dangerous

The MDBX_val structure uses iov_len to track the buffer's available length. When fgets() is told the buffer is one byte larger than it actually is, it can write a character (plus null terminator) beyond the allocated region. This is a classic heap buffer overflow because:

  1. The buffer is dynamically allocated — buf->iov_base points to heap memory
  2. The overflow is controllable — an attacker controls the input data via stdin
  3. The function is in a hot loop — marked __hot, it's called repeatedly during data loading

Attack Scenario

An attacker exploiting this vulnerability would:

  1. Craft an input file with lines precisely sized to fill the buffer to its iov_len boundary
  2. Redirect this file as stdin to mdbx_load: ./mdbx_load < malicious_input.dat
  3. The fgets() call writes one byte past the heap allocation
  4. Depending on the heap allocator's metadata layout, this corrupts heap management structures or adjacent data
  5. Through careful heap grooming, the attacker achieves arbitrary write primitives, leading to code execution

While mdbx_load is a local CLI tool (requiring the attacker to control input), it's commonly used in automated pipelines where input may come from untrusted sources—backup restoration, data migration scripts, or CI/CD environments.

The Subtlety of Off-By-One

What makes this bug particularly insidious is that fgets() already subtracts one from its size parameter for the null terminator. Developers sometimes add + 1 thinking they need to account for this, but in reality:

  • fgets(buf, n, stream) reads at most n - 1 chars and appends \0
  • If you have a buffer of size N, you pass N to fgets()—not N + 1

The + 1 here essentially tells fgets() "you have one more byte than you actually do," which is never safe.

The Fix

The fix is elegant in its simplicity—a single character change that eliminates the vulnerability:

Before (Vulnerable)

if (fgets((char *)c1, (int)buf->iov_len + 1, stdin) == nullptr)

After (Fixed)

if (fgets((char *)c1, (int)buf->iov_len, stdin) == nullptr)

By removing the + 1, the code now correctly tells fgets() the actual available buffer size. Since fgets() internally reserves one byte for the null terminator, it will read at most buf->iov_len - 1 characters of data—guaranteeing the write stays within bounds.

Why This Works

The security invariant is now maintained: buffer reads never exceed the declared length. After this fix:

  • fgets() receives buf->iov_len as the size
  • fgets() writes at most buf->iov_len - 1 data bytes + 1 null byte = buf->iov_len total bytes
  • This exactly fills the available space without overflow
  • The subsequent buf->iov_len *= 2 doubling for the next iteration remains correct

Key Takeaways

  • A single + 1 in an fgets() size parameter created a critical heap overflow in mdbx_load.c's hot-path readline() function — off-by-one errors in C are never "just one byte"
  • The fgets() contract already accounts for the null terminator — passing buf->iov_len + 1 double-compensates and overflows; pass the exact buffer size
  • Dynamic buffer tracking with iov_len requires extreme precision — when buf->iov_len *= 2 doubles the size for reallocation, the size passed to fgets() must always reflect current available space, not future space
  • Local CLI tools are not exempt from security review — mdbx_load processes untrusted input in automated pipelines, making this overflow exploitable in real-world deployments
  • AddressSanitizer would have caught this immediately — enabling ASan in CI for C projects with buffer manipulation is a minimal-cost, high-value defense

How Orbis AppSec Detected This

  • Source: User-controlled input from stdin (redirected file or piped data) entering the readline() function in mdbx_load.c
  • Sink: fgets((char *)c1, (int)buf->iov_len + 1, stdin) at mdbx_load.c:426 — the dangerous call site where the oversized length parameter enables heap buffer overflow
  • Missing control: No validation that the size parameter passed to fgets() does not exceed the actual allocated buffer size; the + 1 arithmetic error bypasses the natural safety of fgets()
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Removed the erroneous + 1 from the fgets() size parameter, changing (int)buf->iov_len + 1 to (int)buf->iov_len to ensure reads respect buffer boundaries

Orbis AppSec detects issues like this automatically. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability is a textbook example of how a single arithmetic error in C can create a critical security flaw. The + 1 in the fgets() call within mdbx_load.c's readline() function seemed innocuous—perhaps even intentional by a developer thinking about null terminator accounting—but it violated the fundamental contract of fgets() and created an exploitable heap overflow.

The fix was one character: removing the +. But the lesson is broader. In C, every buffer operation demands precise size tracking. When you use fgets(), pass the exact buffer size—the function handles the null terminator internally. When you track sizes in structures like MDBX_val, document and enforce the invariant that the size reflects available writable space. And always run your input-processing code under AddressSanitizer.

Buffer overflows remain one of the most dangerous vulnerability classes in C code. Stay vigilant with every + 1.

Prevention and further reading

Related Articles

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.