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:
- The buffer is dynamically allocated —
buf->iov_basepoints to heap memory - The overflow is controllable — an attacker controls the input data via stdin
- The function is in a hot loop — marked
__hot, it's called repeatedly during data loading
Attack Scenario
An attacker exploiting this vulnerability would:
- Craft an input file with lines precisely sized to fill the buffer to its
iov_lenboundary - Redirect this file as stdin to
mdbx_load:./mdbx_load < malicious_input.dat - The
fgets()call writes one byte past the heap allocation - Depending on the heap allocator's metadata layout, this corrupts heap management structures or adjacent data
- 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 mostn - 1chars and appends\0- If you have a buffer of size
N, you passNtofgets()—notN + 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()receivesbuf->iov_lenas the sizefgets()writes at mostbuf->iov_len - 1data bytes + 1 null byte =buf->iov_lentotal bytes- This exactly fills the available space without overflow
- The subsequent
buf->iov_len *= 2doubling 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
+ 1in anfgets()size parameter created a critical heap overflow inmdbx_load.c's hot-pathreadline()function — off-by-one errors in C are never "just one byte" - The
fgets()contract already accounts for the null terminator — passingbuf->iov_len + 1double-compensates and overflows; pass the exact buffer size - Dynamic buffer tracking with
iov_lenrequires extreme precision — whenbuf->iov_len *= 2doubles the size for reallocation, the size passed tofgets()must always reflect current available space, not future space - Local CLI tools are not exempt from security review —
mdbx_loadprocesses 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 inmdbx_load.c - Sink:
fgets((char *)c1, (int)buf->iov_len + 1, stdin)atmdbx_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+ 1arithmetic error bypasses the natural safety offgets() - CWE: CWE-120 (Buffer Copy without Checking Size of Input)
- Fix: Removed the erroneous
+ 1from thefgets()size parameter, changing(int)buf->iov_len + 1to(int)buf->iov_lento 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.