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
Published July 6, 2026Reviewed 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 allocatedbuf->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

Prevention & Best Practices

1. Always Match Buffer Size to fgets() Parameter

// WRONG: Off-by-one overflow
char buf[1024];
fgets(buf, sizeof(buf) + 1, stdin);  // Reads up to 1024 chars into 1024-byte buffer!

// CORRECT: Exact match
char buf[1024];
fgets(buf, sizeof(buf), stdin);  // Reads up to 1023 chars + null

2. Use sizeof() for Stack Buffers

When the buffer is a local array, always use sizeof(buf) rather than a separate size variable that might drift out of sync.

3. Centralize Buffer Size Tracking

For dynamically-sized buffers like MDBX_val, ensure the iov_len field always reflects the remaining writable space, not the total allocation. Document this invariant clearly.

4. Enable AddressSanitizer in CI

Compile with -fsanitize=address during testing. ASan would immediately catch this off-by-one heap overflow:

gcc -fsanitize=address -g mdbx_load.c -o mdbx_load_test
echo "AAAA..." | ./mdbx_load_test

5. Fuzz Input-Parsing Functions

Tools like AFL++ or libFuzzer are exceptionally effective at finding buffer overflows in input-parsing code. The readline() function is an ideal fuzz target.

6. Code Review Checklist for fgets()

Every fgets() call should be reviewed with these questions:
- Does the size parameter match the actual available buffer space?
- Is there any arithmetic (+ 1, - 1) that might be incorrect?
- If the buffer was recently resized, does the size variable reflect the new size?

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 reviewmdbx_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 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

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.

References

Frequently Asked Questions

What is a buffer overflow in fgets()?

A buffer overflow in fgets() occurs when the size parameter passed to fgets() exceeds the actual allocated buffer size, allowing the function to write beyond the buffer boundary and corrupt adjacent memory.

How do you prevent buffer overflow in C?

Always ensure the size parameter passed to buffer-reading functions like fgets() exactly matches or is less than the allocated buffer size. Use sizeof() for stack buffers, and carefully track dynamic allocation sizes for heap buffers.

What CWE is buffer overflow?

Buffer overflow is classified as CWE-120 (Buffer Copy without Checking Size of Input), which is part of the broader CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer) family.

Is using fgets() instead of gets() enough to prevent buffer overflow?

No. While fgets() is safer than gets() because it accepts a size limit, passing an incorrect size parameter—as in this vulnerability—still causes a buffer overflow. The size must accurately reflect the available buffer space.

Can static analysis detect buffer overflow in fgets()?

Yes. Static analysis tools can detect mismatches between allocated buffer sizes and the length parameters passed to fgets(), especially when the relationship between allocation and usage is traceable within the same function.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #364

Related Articles

medium

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

A stack buffer overflow in `PMenu_Do_Update()` within `src/menu/menu.c` allowed repeated unchecked `sprintf()` calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every `sprintf(string + strlen(string), ...)` call with `snprintf(string + len, sizeof(string) - len, ...)`, tracking remaining buffer space and preventing writes beyond the buffer boundary.

critical

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.

critical

How argument injection happens in Node.js Copilot tool bridge and how to fix it

A high-severity argument injection vulnerability was discovered in the Copilot tool bridge (`bridge.ts`) where user-controlled `request.args` were passed directly to `tool.execute()` without any validation or sanitization. The fix introduces Zod schema validation at line 108, ensuring that tool arguments are parsed against a declared `inputSchema` before execution. This prevents malformed or malicious payloads — including prototype pollution attempts — from reaching the underlying tool implement

high

How Missing Dependabot Cooldown happens in GitHub Actions and how to fix it

A high-severity supply chain vulnerability was discovered in a Dependabot configuration file that lacked cooldown periods for package updates. Without cooldown settings, Dependabot could propose updates to newly published—and potentially malicious—packages immediately after release. The fix adds a 7-day cooldown period to all three package ecosystems (npm, GitHub Actions, and Maven), giving the community time to identify compromised packages before they're automatically proposed.

critical

How command injection happens in Python os.popen() and how to fix it

A critical command injection vulnerability in `spk/itools/src/mounting.py` allowed arbitrary shell command execution through unsanitized iOS device names passed to `os.popen()` and `os.system()` calls. The fix replaced these dangerous functions with `subprocess.run()` using proper argument escaping, eliminating the shell injection attack vector.

high

How CORS credential reflection happens in Hono middleware and how to fix it

A high-severity CORS misconfiguration in Hono's middleware (CVE-2026-54290) allowed any origin to be reflected with credentials when the `origin` option defaulted to wildcard. This vulnerability in the studio frontend could enable attackers to steal authenticated user data through cross-origin requests. The fix upgrades Hono from 4.12.21 to 4.12.25, which properly handles CORS origin validation.