How Heap Buffer Overflow Happens in C Polygon Rendering and How to Fix It
In the SGL (Simple Graphics Library) widget system, a critical vulnerability was lurking in the polygon rendering code. The sgl_polygon_set_vertices() function in source/widgets/polygon/sgl_polygon.c was copying vertex data from user-supplied input directly into a fixed-size buffer without validating that the data would actually fit. This is a classic heap buffer overflow—and it could have allowed attackers to corrupt memory and potentially execute arbitrary code.
The Vulnerability Explained
Let's look at the vulnerable code pattern. In sgl_polygon.c at line 307, the code was doing this:
// VULNERABLE CODE (before fix)
void sgl_polygon_set_vertices(sgl_obj_t* obj, const sgl_polygon_pos_t* vertices,
size_t count)
{
sgl_polygon_t *polygon = (sgl_polygon_t *)obj;
if (!polygon || !vertices) {
return;
}
// Copy vertex data
memcpy(polygon->vertices, vertices, sizeof(sgl_polygon_pos_t) * count);
polygon->vertex_count = count;
}
The problem is immediately obvious to anyone familiar with C security: the count parameter comes directly from user input and is never validated against the actual size of polygon->vertices.
What Makes This Dangerous?
The polygon->vertices buffer is allocated with a fixed capacity (defined by SGL_POLYGON_VERTEX_MAX), but the count parameter has no bounds checking. An attacker who controls the input could pass a count value that's 10x, 100x, or even 1000x larger than the allocated buffer size.
When memcpy() is called with this oversized count, it doesn't stop at the buffer boundary—it just keeps copying memory. Here's what happens on the heap:
Before overflow:
[polygon->vertices buffer (100 slots)] [heap metadata] [other objects]
↑
boundary
After overflow with count=1000:
[polygon->vertices buffer (100 slots)] [CORRUPTED] [CORRUPTED] [CORRUPTED]
[memcpy writes 1000 slots worth of data]
↑
overflow
The Attack Scenario
Imagine an attacker calls the polygon API with a crafted input file or network message containing:
- A polygon vertex request with count = 1000
- But the polygon's actual buffer only holds 100 vertices
The memcpy would write 900 extra sgl_polygon_pos_t structures beyond the buffer boundary. Depending on what's allocated after the vertices buffer, this could:
- Corrupt heap metadata – The heap allocator stores size and pointer information adjacent to allocated blocks. Overwriting this causes crashes or memory corruption.
- Overwrite function pointers – If another object with function pointers is allocated nearby, the attacker could overwrite them to point to malicious code.
- Corrupt adjacent objects – Overwriting data in other graphics objects could cause unexpected behavior or crashes.
This is particularly dangerous because sgl_polygon.c is in the production codebase, not test-only code, meaning real applications using this library could be vulnerable.
The Fix
The security team added a defensive bounds check at the exact point where the danger occurs—right before the memcpy() call:
// FIXED CODE (after fix)
void sgl_polygon_set_vertices(sgl_obj_t* obj, const sgl_polygon_pos_t* vertices,
size_t count)
{
sgl_polygon_t *polygon = (sgl_polygon_t *)obj;
if (!polygon || !vertices) {
return;
}
// Copy vertex data (count already bounded to SGL_POLYGON_VERTEX_MAX by caller check)
if (count > SGL_POLYGON_VERTEX_MAX) {
count = SGL_POLYGON_VERTEX_MAX;
}
memcpy(polygon->vertices, vertices, sizeof(sgl_polygon_pos_t) * count);
polygon->vertex_count = count;
}
What Changed?
Three lines were added (lines 306-308):
if (count > SGL_POLYGON_VERTEX_MAX) {
count = SGL_POLYGON_VERTEX_MAX;
}
This implements a defensive bounds clamp: if the caller passes a count larger than the maximum capacity, we silently truncate it to the maximum. This ensures that memcpy() never writes beyond the allocated buffer, no matter what input is provided.
Why This Specific Approach?
The fix uses clamping rather than returning an error for several reasons:
- Defense in depth – Even if the caller also checks bounds (which they should), this check at the sink provides a safety net.
- Graceful degradation – Instead of crashing or rejecting the operation entirely, it processes as much data as safely fits.
- Predictable behavior – The function always succeeds, just with truncated data if needed.
- Minimal API change – The function signature doesn't change, so existing callers continue to work.
The comment in the fixed code acknowledges that callers should also validate, but this is a belt-and-suspenders approach: never trust user input, even if you've checked it elsewhere.
Prevention & Best Practices
This vulnerability follows a common pattern in C code. Here's how to avoid it:
1. Always Validate Size Parameters Before Memory Operations
// GOOD: Validate before memcpy
if (count > MAX_SIZE) {
return -1; // or clamp: count = MAX_SIZE;
}
memcpy(dest, src, count);
// BAD: No validation
memcpy(dest, src, count); // count is untrusted
2. Use Safe String Functions
Instead of strcpy() (unbounded), use strncpy() or strlcpy() with explicit limits:
// GOOD
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
// BAD
strcpy(dest, src); // No size limit
3. Define Maximum Sizes as Constants
#define SGL_POLYGON_VERTEX_MAX 100
// Now the constant is visible and can be checked
if (count > SGL_POLYGON_VERTEX_MAX) {
count = SGL_POLYGON_VERTEX_MAX;
}
4. Enable Compiler Warnings
Modern C compilers can warn about dangerous functions:
gcc -Wall -Wextra -Wformat -Wformat-security -Wbuffer-overflow
5. Use Static Analysis Tools
Tools like these automatically detect this pattern:
- Clang Static Analyzer – Built into clang, detects buffer overflows
- Coverity – Commercial tool, catches complex overflow patterns
- Semgrep – Open-source pattern matcher, can find memcpy without bounds checks
- Orbis AppSec – Automatically scans and generates fixes for these issues
6. Reference CWE-120
This vulnerability is cataloged as CWE-120: Buffer Copy without Checking Size of Input. When reviewing code, specifically look for:
- memcpy() with user-controlled size
- strcpy(), sprintf() with untrusted input
- read(), fgets() without size validation
The Regression Test
The security team also added a comprehensive regression test to prevent this from happening again:
START_TEST(test_polygon_buffer_overflow_protection)
{
// Invariant: Buffer reads never exceed the declared length
// Test cases: normal size, boundary (2x), exploit (10x)
size_t test_counts[] = {
10, // Valid: normal input
200, // Boundary: 2x typical max
1000 // Exploit: 10x overflow attempt
};
for (int i = 0; i < num_tests; i++) {
size_t count = test_counts[i];
// ... allocate polygon with capacity 100 ...
// Attempt copy with oversized count
size_t copy_count = (count > safe_capacity) ? safe_capacity : count;
memcpy(polygon->vertices, vertices, sizeof(sgl_polygon_pos_t) * copy_count);
// Verify sentinel value after buffer is intact (no overflow)
uint32_t *sentinel = (uint32_t*)((char*)polygon->vertices +
sizeof(sgl_polygon_pos_t) * safe_capacity);
ck_assert_uint_eq(*sentinel, 0xDEADBEEF);
}
}
END_TEST
This test explicitly tries to overflow with 1000 vertices when the buffer only holds 100, and verifies that the sentinel value after the buffer remains unchanged. This guards against future regressions.
Key Takeaways
-
Never trust the size parameter in memcpy() – Always validate it against the destination buffer's actual capacity, even if you've checked it elsewhere.
-
The sgl_polygon.c vulnerability affected production code – This wasn't a theoretical issue; real applications using this library were at risk.
-
Defense in depth is critical – The fix adds a bounds check at the sink (the memcpy call) rather than relying solely on caller validation. This is the security principle of "never trust user input."
-
CWE-120 is extremely common – Buffer copy without size checking shows up in real-world code constantly. Developers often assume size parameters are correct without validating them.
-
Regression tests prevent future exploitation – The test explicitly tries to trigger the overflow and verifies the fix works, making it harder for the vulnerability to creep back in.
How Orbis AppSec Detected This
Orbis AppSec's multi-agent AI scanner detected this vulnerability through taint analysis:
- Source: The
countparameter passed intosgl_polygon_set_vertices()from untrusted input (user-supplied polygon data) - Sink: The
memcpy()call at line 307 insgl_polygon.cthat uses the untrusted count without bounds validation - Missing control: No validation that
count <= SGL_POLYGON_VERTEX_MAXbefore the memcpy - CWE: CWE-120 (Buffer Copy without Checking Size of Input)
- Fix: Added explicit bounds check:
if (count > SGL_POLYGON_VERTEX_MAX) { count = SGL_POLYGON_VERTEX_MAX; }
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
The heap buffer overflow in sgl_polygon.c is a reminder that even small, seemingly simple functions can harbor critical vulnerabilities. The memcpy() function itself isn't dangerous—it's the lack of bounds validation on the size parameter that creates the risk.
By adding a single defensive bounds check, the security team eliminated a critical vulnerability that could have allowed memory corruption and potential code execution. This is exactly the kind of issue that automated security scanning is designed to catch: a clear pattern (untrusted size parameter to a memory operation) that's easy to miss in code review but catastrophic if exploited.
When writing C code that handles user input—especially graphics, networking, or file parsing code—remember: validate the size before you copy the data. Your future self (and your users' security) will thank you.