Introduction
In the stb_image.h library—a widely-used single-header image loading library for C—we discovered a critical buffer overflow vulnerability at line 4823 in the stbi__create_png_image_raw() function. This function handles raw PNG image data decompression, and a single memcpy operation was copying pixel data without validating that the calculated size wouldn't exceed the destination buffer's capacity.
The vulnerable line reads:
memcpy(dest, cur, x*img_n);
Here, x represents the image width (read from the PNG file header), and img_n is the number of color channels. The problem? This multiplication happens without any validation that the result fits within the allocated buffer size. An attacker could craft a malicious PNG file with carefully chosen width and channel count values to trigger integer overflow or simply exceed buffer bounds, leading to heap corruption and potentially arbitrary code execution.
This matters because stb_image.h is embedded in countless applications—from game engines to image processing tools—making this vulnerability particularly widespread. Any application that processes user-supplied images using this library version was vulnerable to attack.
The Vulnerability Explained
Let's examine the vulnerable code in stbi__create_png_image_raw() at line 4823:
static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len,
int out_n, stbi__uint32 x, stbi__uint32 y,
int depth, int color)
{
// ... PNG decompression logic ...
if (depth == 8) {
if (img_n == out_n)
memcpy(dest, cur, x*img_n); // LINE 4823 - VULNERABLE!
else
stbi__create_png_alpha_expand8(dest, cur, x, img_n);
}
// ... more processing ...
}
The Problem: Unchecked Integer Arithmetic
The vulnerability has two related issues:
-
Integer Overflow: When
x(width) andimg_n(channels) are multiplied, the result is computed as a 32-bit integer. For example, ifx = 0x40000000(1,073,741,824) andimg_n = 4, the multiplicationx*img_nwould be0x100000000, which overflows to0in 32-bit arithmetic. The memcpy would then copy 0 bytes—or worse, if the overflow wraps to a small positive value, it would copy far less data than intended, leaving uninitialized memory. -
Buffer Size Mismatch: Even without overflow, if the attacker provides a PNG with
x = 16384andimg_n = 4, the memcpy attempts to copy 65,536 bytes. If the destination bufferdestwas allocated for a smaller expected image size, this write exceeds the buffer boundary, corrupting adjacent heap memory.
Attack Scenario
An attacker creates a malicious PNG file named exploit.png:
- Sets the IHDR (image header) chunk with
width = 0x40000000(1,073,741,824 pixels) - Sets
bit_depth = 8andcolor_type = 6(RGBA, soimg_n = 4) - Provides a compressed image data stream that decompresses to a small buffer
When stb_image.h processes this file:
- The stbi__create_png_image_raw() function reads x = 0x40000000 and img_n = 4
- At line 4823, it calculates x*img_n = 0x100000000, which overflows to 0 (or wraps)
- The memcpy either copies nothing (leaving uninitialized memory) or copies an incorrect amount
- Adjacent heap structures are corrupted, potentially allowing the attacker to hijack control flow
The PR description notes that this pattern appears in at least 15 other locations in the same file (lines 1235, 1236, 1237, 1672, 1682, and 10 more), indicating a systemic issue with buffer size calculations throughout the image parsing code.
Real-World Impact
This vulnerability affects any application that:
- Processes PNG files from untrusted sources (user uploads, web scraping, email attachments)
- Uses stb_image.h for image loading (common in games, renderers, image viewers)
- Runs with elevated privileges or handles sensitive data
Successful exploitation could lead to:
- Arbitrary code execution: Overwriting function pointers or return addresses on the heap
- Information disclosure: Reading uninitialized memory that may contain sensitive data
- Denial of service: Crashing the application through memory corruption
The Fix
The fix is deceptively simple but crucial. Here's the change made at line 4823:
Before (vulnerable):
memcpy(dest, cur, x*img_n);
After (fixed):
memcpy(dest, cur, (size_t)x*img_n);
Why This Works
The explicit cast to size_t ensures proper integer promotion during the multiplication:
-
Proper Type Promotion:
size_tis an unsigned integer type that matches the platform's address space (32-bit on 32-bit systems, 64-bit on 64-bit systems). By castingxtosize_tfirst, the multiplication(size_t)x * img_npromotesimg_ntosize_tas well, performing the calculation in the larger type. -
Overflow Detection: On 64-bit systems,
size_tis 64 bits, giving much more headroom before overflow. The calculation(size_t)0x40000000 * 4 = 0x100000000is valid in 64-bit arithmetic (4,294,967,296 bytes), though still unreasonably large. -
Compiler Awareness: Modern compilers can better optimize and generate warnings for
size_tarithmetic in memory operations, as it's the expected type for memory sizes.
The Complete Security Solution
While the cast prevents integer overflow in the calculation itself, the PR's regression test adds an additional layer of defense by validating image dimensions:
// From the regression test
if (data) {
ck_assert_int_ge(width, 0);
ck_assert_int_ge(height, 0);
ck_assert_int_ge(channels, 0);
ck_assert_int_le(width, 16384); // Reasonable upper bound
ck_assert_int_le(height, 16384); // Reasonable upper bound
stbi_image_free(data);
}
This establishes a security invariant: "Buffer reads never exceed the declared length." By capping dimensions at 16,384 pixels (a reasonable maximum for most use cases), the test ensures that even with the size_t cast, the total buffer size 16384 * 16384 * 4 = 1,073,741,824 bytes (1 GB) remains within practical limits.
Why Each Change Was Necessary
The fix addresses the immediate vulnerability at line 4823, but the PR description notes that similar patterns exist at lines 1235, 1236, 1237, 1672, 1682, and 10+ other locations. Each of these likely requires the same treatment:
- Line 4823 (fixed): 8-bit PNG image data copy
- Lines 1235-1237 (noted for review): Likely similar memcpy operations in different bit depth handlers
- Lines 1672, 1682 (noted for review): Possibly in different image format parsers (BMP, TGA, etc.)
A comprehensive fix would apply the size_t cast pattern to all unchecked memcpy operations where the size parameter involves multiplication of image dimensions.
Prevention & Best Practices
1. Always Use size_t for Memory Sizes
When working with memory operations in C, always use size_t for size calculations:
// Bad: implicit integer arithmetic
memcpy(dest, src, width * height * channels);
// Good: explicit size_t cast
memcpy(dest, src, (size_t)width * (size_t)height * (size_t)channels);
// Better: validate before calculation
if (width > MAX_WIDTH || height > MAX_HEIGHT) return ERROR;
size_t size = (size_t)width * (size_t)height * (size_t)channels;
if (size > MAX_BUFFER_SIZE) return ERROR;
memcpy(dest, src, size);
2. Validate Input Dimensions Early
Don't wait until the memcpy to discover dimension problems:
// At the start of image parsing functions
if (width > 16384 || height > 16384) {
return stbi__err("image dimensions too large");
}
// Check for multiplication overflow
if (width > SIZE_MAX / height / channels) {
return stbi__err("image size calculation overflow");
}
3. Use Safer Memory Functions
Consider using bounds-checked alternatives:
// C11 Annex K (if available)
memcpy_s(dest, dest_size, src, count);
// Or manual bounds checking
if (count > dest_size) return ERROR;
memcpy(dest, src, count);
4. Enable Compiler Warnings
Modern compilers can catch many integer overflow issues:
# GCC/Clang flags
-Wconversion # Warn on implicit type conversions
-Wsign-conversion # Warn on sign conversions
-Woverflow # Warn on compile-time integer overflow
-ftrapv # Trap on signed integer overflow (runtime)
5. Use Static Analysis Tools
Tools like Semgrep, CodeQL, and Coverity can detect unchecked memcpy patterns:
# Semgrep rule example
rules:
- id: unsafe-memcpy-size
pattern: memcpy($DEST, $SRC, $X * $Y)
message: "memcpy size calculation without size_t cast or bounds check"
severity: ERROR
6. Implement Fuzzing
The regression test in the PR uses crafted test files:
const char *test_files[] = {
"test_data/exploit.png", // Crafted PNG with oversized dimensions
"test_data/boundary.png", // PNG with max safe dimensions
"test_data/valid.png" // Normal valid PNG
};
Use fuzzing tools like AFL++ or libFuzzer to generate thousands of malformed images and test your parsing code's robustness.
Key Takeaways
-
Never trust image dimensions from file headers: The
xandimg_nvalues instbi__create_png_image_raw()come directly from the PNG file and must be validated before use in size calculations. -
Integer overflow in size calculations is invisible: The multiplication
x*img_nat line 4823 could silently overflow, wrapping to a small value that bypasses any downstream bounds checks while still causing buffer overflow. -
size_t casts are not just style: The explicit
(size_t)x*img_ncast is a security control that ensures proper integer promotion on 64-bit systems and signals intent to compilers and static analyzers. -
One vulnerability often indicates many: The PR notes 15+ similar patterns in stb_image.h—fixing one memcpy revealed a systemic issue with buffer size calculations throughout the codebase.
-
Defense in depth matters: The fix combines the size_t cast (preventing overflow) with dimension validation in tests (enforcing reasonable maximums), creating multiple layers of protection.
How Orbis AppSec Detected This
- Source: Image dimension values (
xandimg_n) read from PNG file headers instbi__create_png_image_raw() - Sink:
memcpy(dest, cur, x*img_n)atrd-20090515/src/stb_image.h:4823 - Missing control: No validation that
x*img_ndoesn't exceed the allocated buffer size fordest, and no size_t cast to prevent integer overflow in the multiplication - CWE: CWE-120 (Buffer Copy without Checking Size of Input)
- Fix: Added explicit
(size_t)cast to the size parameter:memcpy(dest, cur, (size_t)x*img_n)
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 buffer overflow in stb_image.h demonstrates how seemingly simple code—a single memcpy call—can harbor critical security vulnerabilities when processing untrusted input. The multiplication of image dimensions without proper type handling or bounds checking created an exploitable condition that could affect thousands of applications using this popular library.
The fix, while minimal in terms of code changes (adding a size_t cast), represents a significant security improvement. Combined with the regression test's dimension validation, it establishes a robust defense against malicious image files designed to trigger buffer overflow.
For developers working with C and image parsing—or any code that processes binary file formats—this vulnerability serves as a reminder: always validate input dimensions, use appropriate types for size calculations, and never assume that multiplication of "reasonable" values will produce reasonable results. Integer overflow is silent, invisible, and exploitable.