How buffer overflow happens in C sprintf() and how to fix it
Introduction
In the ARB generator component (src/GL/arbgenerator.c), a critical vulnerability was lurking in plain sight. The generateVariablePre() function uses an 11-byte stack buffer to format integer values, but at line 52 and line 68, it was calling sprintf() without any size checking. This is a textbook buffer overflow vulnerability—the kind that has plagued C developers for decades. An attacker who can control the size of array variables processed by this generator could overflow the buffer, potentially corrupting the stack and executing arbitrary code.
The Vulnerability Explained
Let's look at the exact vulnerable code from the diff:
char buf[11]; // Assume 32-bits array address, should never overflow...
sprintf(buf, "%d", varPtr->size); // VULNERABLE: No size limit!
The problem is immediately apparent: sprintf() has no knowledge of the buffer size. It will happily write as many bytes as the format string and arguments require, completely ignoring the 11-byte limit of buf.
Consider what happens when varPtr->size is a large integer:
- An int can be up to 2,147,483,647 (2^31 - 1)
- Formatted as a string with "%d", that's 10 characters plus a null terminator = 11 bytes—exactly the buffer size
- But what if varPtr->size is negative? The format string would produce a minus sign, making it 12 bytes
- What if the size value is corrupted or attacker-controlled? It could produce a much longer string
The same issue appears again at line 68:
sprintf(buf, "%zd", i); // VULNERABLE: No size limit!
Here, i is a size_t being formatted with "%zd". On 64-bit systems, a size_t can produce up to 20 characters—far exceeding the 11-byte buffer.
How This Could Be Exploited
Since this is a CLI tool, exploitation requires the attacker to control command-line arguments or input files. An attacker could craft a specially-formed input file that:
- Causes
varPtr->sizeto be set to a negative number or extremely large value - Triggers the
sprintf()call ingenerateVariablePre() - Overflows the 11-byte
bufinto adjacent stack memory - Corrupts the return address or other critical stack data
- Gains control of program execution
For example, if the input file specifies an array with size -2147483648, the format string "%d" would produce the string "-2147483648" (11 characters), exactly filling the buffer. But if the parsing logic has an off-by-one error or if negative values are sign-extended differently on some systems, the overflow becomes possible.
Real-World Impact
This vulnerability affects the ARB (OpenGL ARB extension) code generation pipeline. Any tool or library that uses arbgenerator.c to process untrusted ARB shader definitions could be compromised. The impact ranges from denial of service (crash) to remote code execution, depending on how the buffer overflow is leveraged.
The Fix
The fix is straightforward but critical: replace sprintf() with snprintf() and provide the buffer size as a parameter.
Before (Vulnerable)
char buf[11];
sprintf(buf, "%d", varPtr->size);
APPEND_OUTPUT2(buf)
After (Fixed)
char buf[11];
snprintf(buf, sizeof(buf), "%d", varPtr->size);
APPEND_OUTPUT2(buf)
The key changes in the diff:
- Line 52:
sprintf(buf, "%d", varPtr->size)→snprintf(buf, sizeof(buf), "%d", varPtr->size) - Line 68:
sprintf(buf, "%zd", i)→snprintf(buf, sizeof(buf), "%zd", i)
How This Solves the Problem
snprintf() accepts a size parameter (sizeof(buf) = 11) that enforces a hard limit on how many bytes can be written. If the formatted output would exceed 10 characters (11 minus 1 for the null terminator), snprintf() truncates the output and null-terminates it safely.
For example:
- sprintf(buf, "%d", -2147483648) → writes 12 bytes, overflows buffer ❌
- snprintf(buf, 11, "%d", -2147483648) → writes exactly 10 bytes + null terminator, safe ✓
The security improvement is concrete: no matter what value is passed, the buffer cannot be overflowed. The worst-case scenario with snprintf() is truncation, not memory corruption.
Prevention & Best Practices
1. Never Use Unbounded String Functions
The C standard library provides dangerous functions that should be avoided entirely in production code:
| Dangerous | Safe Alternative | Why |
|---|---|---|
sprintf() |
snprintf() |
Accepts size limit |
strcpy() |
strncpy() or strlcpy() |
Accepts size limit |
strcat() |
strncat() or strlcat() |
Accepts size limit |
gets() |
fgets() |
Accepts size limit |
2. Always Use sizeof() for Buffer Sizes
char buffer[256];
snprintf(buffer, sizeof(buffer), "Format: %s", data); // ✓ Correct
snprintf(buffer, 256, "Format: %s", data); // ⚠ Error-prone
snprintf(buffer, strlen(data), "Format: %s", data); // ❌ Wrong
Using sizeof(buffer) ensures the size matches the actual buffer, even if the buffer is later resized.
3. Enable Compiler Warnings
Modern compilers can warn about unsafe functions:
gcc -Wall -Wextra -Wformat -Wformat-security -Wno-format-nonliteral arbgenerator.c
clang -Wall -Wextra -Wformat -Wformat-security arbgenerator.c
4. Use Static Analysis Tools
Tools like Semgrep, Clang Static Analyzer, and Coverity can automatically detect these patterns:
semgrep --config=p/security-audit src/GL/arbgenerator.c
5. Reference Security Standards
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
- OWASP: Buffer Overflow Prevention Cheat Sheet
Key Takeaways
- Never use
sprintf()withoutsnprintf(): The 11-byte buffer inarbgenerator.ccould be overflowed by any input producing more than 10 characters of output. sizeof(buffer)is your friend: Always pass the actual buffer size to bounded functions; it's the compiler's way of helping you stay safe.- Negative numbers and edge cases matter: The vulnerability in
arbgenerator.ccould be triggered by negative array sizes or largesize_tvalues that produce longer formatted strings. - Static analysis catches what code review misses: The Semgrep rule
utils.custom.buffer-overflow-strcpyflagged this exact pattern automatically, demonstrating the value of automated security scanning. - Defense in depth: Even if input validation exists elsewhere, using
snprintf()provides a critical second layer of protection against buffer overflow.
How Orbis AppSec Detected This
Source: The varPtr->size and loop index i values, which are derived from parsed input data in the ARB generator, are user-influenced.
Sink: The sprintf(buf, "%d", varPtr->size) call at line 52 and sprintf(buf, "%zd", i) at line 68 in src/GL/arbgenerator.c, which write directly to a fixed 11-byte stack buffer without bounds checking.
Missing control: No size parameter limits how many bytes sprintf() can write; the buffer size is not enforced by the function call.
CWE: CWE-120 (Buffer Copy without Checking Size of Input)
Fix: Replace sprintf() with snprintf() and pass sizeof(buf) as the maximum size parameter, ensuring writes never exceed the buffer boundary.
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 overflow vulnerabilities in C are not relics of the past—they remain a critical security risk in modern codebases. The arbgenerator.c vulnerability demonstrates how easily these issues can slip past code review: a single missing size parameter in a format function call can compromise the entire application.
The fix is simple and effective: use snprintf() instead of sprintf(). This single change enforces a hard boundary on buffer writes, eliminating the overflow risk entirely. By adopting this practice across your codebase and leveraging static analysis tools to catch violations automatically, you can significantly reduce your attack surface and build more secure software.
Remember: in C, size matters. Always specify it.
References
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
- OWASP: Buffer Overflow Prevention Cheat Sheet
- C Standard Library: snprintf() documentation
- Semgrep Rule: Buffer Overflow Detection
- fix: use bounded strlcpy/snprintf in arbgenerator.c...