How buffer overflow happens in C sprintf() and how to fix it
The Vulnerability in Context
In the ArrowTest() function of main/main.c, a critical buffer overflow vulnerability was lurking in embedded firmware code that renders graphical arrows on an LCD display. The function accepts width and height parameters from callers and uses them to calculate screen coordinates for drawing. However, these coordinates were being formatted into a small 24-byte buffer using sprintf() without any size checking—a classic recipe for heap corruption.
The vulnerability exists because an attacker with control over width or height values (perhaps through a compromised network peer on the same LAN segment, given this is embedded firmware) could provide large values like 999999, causing the formatted output to exceed the buffer boundary and overwrite adjacent heap memory.
The Vulnerability Explained
Let's examine the vulnerable code from lines 147, 155, and 162 in main/main.c:
// Line 147 - VULNERABLE
sprintf((char *)ascii, "%d,0", width-1);
// Line 155 - VULNERABLE
sprintf((char *)ascii, "0,%d", height-1);
// Line 162 - VULNERABLE
sprintf((char *)ascii, "%d,%d", width-1, height-1);
The problem is clear when you understand the buffer constraints:
char ascii[24]; // Fixed 24-byte buffer
The sprintf() function has no knowledge of the buffer size and will write as many bytes as needed to format the output. If width or height are large values, the formatted string can easily exceed 24 bytes.
Attack scenario: Suppose an attacker controls the width and height parameters through a network message or configuration file. They provide:
- width = 2147483647 (INT_MAX)
- height = 2147483647
The formatted string "2147483646,2147483646" is 20 characters—still within bounds. But consider:
- width = 999999999999 (12 digits)
- height = 999999999999
The formatted string "999999999998,999999999998" is 25 characters, exceeding the 24-byte buffer by 1 byte. This single byte overflow corrupts the next heap structure, potentially leading to:
- Heap metadata corruption
- Use-after-free vulnerabilities
- Arbitrary code execution if the heap layout is predictable
In embedded firmware contexts, exploiting heap corruption is often more reliable than in general-purpose systems because memory layouts are more predictable and there are fewer mitigations like ASLR.
The Fix
The fix is straightforward but critical: replace all three sprintf() calls with snprintf() and explicitly pass the buffer size:
- sprintf((char *)ascii, "%d,0",width-1);
+ snprintf((char *)ascii, sizeof(ascii), "%d,0",width-1);
- sprintf((char *)ascii, "0,%d",height-1);
+ snprintf((char *)ascii, sizeof(ascii), "0,%d",height-1);
- sprintf((char *)ascii, "%d,%d",width-1, height-1);
+ snprintf((char *)ascii, sizeof(ascii), "%d,%d",width-1, height-1);
What changed:
- sprintf() → snprintf(): The size-aware variant
- Added second parameter: sizeof(ascii) = 24 bytes
- Format string and arguments remain unchanged
How this solves the problem:
snprintf() guarantees that it will write at most sizeof(ascii) bytes (including the null terminator). If the formatted output would exceed this limit, snprintf() truncates the string and null-terminates it safely. The buffer is protected from overflow.
For example, with the attack scenario above:
- snprintf() attempts to format "999999999998,999999999998"
- It detects this would require 26 bytes (25 characters + 1 null terminator)
- It truncates to 23 characters: "999999999998,9999999" (fits in 24-byte buffer)
- No overflow occurs
The trade-off is that the coordinate string is truncated, but this is far preferable to heap corruption and potential code execution.
Prevention & Best Practices
1. Never use sprintf() with dynamic input
The C standard library provides safer alternatives:
- snprintf(): Size-limited, returns number of characters that would have been written
- asprintf(): Dynamically allocates buffer (GNU extension)
- strncpy(): For string copying with size limits
2. Use compiler warnings and static analysis
Enable compiler warnings that flag unsafe functions:
gcc -Wall -Wformat -Wformat-overflow -Wformat-truncation main.c
These flags catch many sprintf/strcpy issues at compile time.
3. Apply the "sizeof() pattern"
Always use sizeof() when working with fixed buffers:
char buffer[256];
// Good:
snprintf(buffer, sizeof(buffer), format, args);
// Bad:
snprintf(buffer, 256, format, args); // Magic number, breaks if buffer size changes
4. Validate input ranges
Even with snprintf(), consider validating that width and height values are within expected ranges:
if (width < 0 || width > MAX_WIDTH || height < 0 || height > MAX_HEIGHT) {
// Reject invalid values
return ERROR;
}
5. Use static analysis tools
Tools like:
- Semgrep: Can detect sprintf() without size limits
- Clang Static Analyzer: Finds buffer overflow patterns
- Orbis AppSec: Automatically detects and fixes these issues
CWE Reference: This vulnerability is classified as:
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
OWASP Reference: Falls under OWASP A06:2021 – Vulnerable and Outdated Components when unsafe C functions are used in production code.
Key Takeaways
- sprintf() is never safe with dynamic input: Always use snprintf() with explicit buffer size limits in production code
- The ArrowTest() function now enforces buffer boundaries: All three formatting calls at lines 147, 155, and 162 are now protected by snprintf() with sizeof(ascii)
- Embedded firmware is particularly vulnerable: With predictable memory layouts and limited mitigations, heap overflow exploitation is more reliable than in general-purpose systems
- sizeof() is your friend: Using sizeof(buffer) instead of hardcoded numbers prevents future buffer size changes from reintroducing vulnerabilities
- Truncation is acceptable: snprintf() truncating output is far better than heap corruption; the LCD will display partial coordinates rather than crashing
How Orbis AppSec Detected This
- Source: The
widthandheightfunction parameters to ArrowTest() in main/main.c - Sink: Three sprintf() calls at lines 147, 155, and 162 that write to the 24-byte
asciibuffer without bounds checking - Missing control: No validation of parameter values and no size-limiting wrapper around the formatting function
- CWE: CWE-120 (Buffer Copy without Checking Size of Input)
- Fix: Replaced all sprintf() calls with snprintf(ascii, sizeof(ascii), ...) to enforce 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
Buffer overflow vulnerabilities in C formatting functions represent a critical security risk, especially in embedded firmware where exploitation is more reliable due to predictable memory layouts. The transition from sprintf() to snprintf() in the ArrowTest() function demonstrates that fixing these issues is often as simple as adding two parameters—the buffer size and sizeof().
This vulnerability highlights why security scanning tools are essential for C codebases: unsafe functions can easily slip past code review and testing because they only fail under specific input conditions. By adopting snprintf() as a standard practice, validating input ranges, and using static analysis tools, developers can prevent entire classes of buffer overflow vulnerabilities before they reach production.
For teams maintaining embedded firmware and systems code, treating sprintf() as a code smell and automatically flagging it for review is a practical defense strategy that catches these issues early.