Heap Buffer Overflow in HAL Filter: How Unvalidated memcpy Sizes Can Sink Your App
Severity: Critical | CWE: CWE-120 (Buffer Copy Without Checking Size of Input) | File:
hal/ndsrvp/src/filter.cpp
Introduction
Deep inside the hardware abstraction layer (HAL) of many image-processing pipelines lives a class of bug that has haunted C and C++ codebases for decades: the unchecked buffer copy. It's quiet, it's fast, and under the right conditions, it's catastrophic.
This week, a critical vulnerability (V-001) was patched in hal/ndsrvp/src/filter.cpp — a component responsible for image filtering operations including bilateral filtering and median blur. The root cause? Multiple calls to memcpy that computed their byte-count arguments from user-influenced values (image dimensions, channel counts, border parameters) without ever verifying that the computed size fit within the destination buffer.
If you write C or C++ code that processes external data — especially image data — this post is for you. Even if you don't, understanding heap buffer overflows is foundational security knowledge that applies across languages and platforms.
The Vulnerability Explained
What Is a Heap Buffer Overflow?
A buffer overflow occurs when a program writes more data into a memory buffer than the buffer was allocated to hold. When this happens on the heap (dynamically allocated memory), adjacent heap metadata and other allocated objects get overwritten — a condition known as heap corruption.
The C standard library function memcpy is a frequent offender:
memcpy(destination, source, n_bytes);
memcpy will copy exactly n_bytes bytes from source to destination. It does not check whether destination is large enough. That responsibility falls entirely on the programmer.
What Was Wrong in filter.cpp?
The vulnerable code appeared in several filter routines — the ndsrvp HAL filter, bilateral filter, and median blur — and followed a pattern like this:
// VULNERABLE: size computed from external inputs, never validated
size_t copy_size = cnes * (rborder - j);
memcpy(dst_buffer, src_ptr, copy_size);
Or similarly:
// VULNERABLE: src_step and height come from image metadata
memcpy(temp_buf, src, src_step * height);
The variables involved — cnes, cn (channel count), rborder, j, src_step, height — are all derived directly or indirectly from image file metadata. An attacker who controls the image file controls these values.
The Math That Kills You
Consider a simplified version of the problem:
dst_bufferis allocated aswidth * height * channelsbytes for a "normal" image.- The copy size is computed as
cn * (rborder - j). - If an attacker crafts an image where
cn = 4and(rborder - j) = 0x40000000, the computed size becomes 1 GB — far exceeding any reasonable allocation.
Even without integer overflow, subtly wrong values can cause the copy to spill just a few hundred bytes past the buffer boundary — enough to overwrite a heap chunk header, a function pointer stored nearby, or security-critical data.
How Could This Be Exploited?
Step 1 — Craft a malicious image. The attacker creates an image file with manipulated metadata: unusual dimensions, an abnormally large channel count, or border parameters that cause arithmetic to produce a large computed size.
Step 2 — Feed it to the application. Any code path that loads and processes this image using the vulnerable filter routines triggers the overflow.
Step 3 — Corrupt heap memory. The oversized memcpy writes past the end of the destination buffer, overwriting adjacent heap allocations.
Step 4 — Achieve impact. Depending on what's adjacent in memory, the attacker may be able to:
- Crash the application (Denial of Service)
- Leak sensitive data from adjacent heap objects (Information Disclosure)
- Overwrite function pointers or vtables to redirect execution (Remote Code Execution)
Real-World Impact
Image processing libraries are ubiquitous. They're embedded in:
- Web servers that resize user-uploaded images
- Mobile apps that apply filters to photos
- Desktop applications that open documents
- IoT devices with camera inputs
- CI/CD pipelines that process build artifacts
A single malicious JPEG, PNG, or raw image file could be the vector. The attacker doesn't need network access beyond the ability to submit a file — a low bar in most applications that accept user content.
The Fix
What Changed
The fix applied to hal/ndsrvp/src/filter.cpp addresses the core problem: computed copy sizes must be validated against the actual size of the destination buffer before calling memcpy.
The corrected pattern looks like this:
// BEFORE (vulnerable)
size_t copy_size = cnes * (rborder - j);
memcpy(dst_buffer, src_ptr, copy_size);
// AFTER (safe)
size_t copy_size = cnes * (rborder - j);
// Validate computed size before copy
CV_Assert(copy_size <= dst_buffer_size);
// or equivalently:
if (copy_size > dst_buffer_size) {
// Handle error: reject input, log, return early
return;
}
memcpy(dst_buffer, src_ptr, copy_size);
For the src_step * height pattern:
// BEFORE (vulnerable)
memcpy(temp_buf, src, src_step * height);
// AFTER (safe)
size_t required = src_step * height;
CV_Assert(required <= temp_buf_allocated_size);
memcpy(temp_buf, src, required);
Why This Works
By asserting (or explicitly checking) that the computed size fits within the allocated buffer before calling memcpy, we ensure that:
- Attacker-controlled values cannot drive the copy past the buffer boundary.
- Malformed inputs are rejected early, before they can cause memory corruption.
- The program fails safely — either by asserting in debug builds (helping developers catch bugs) or by returning an error in production.
Overflow-Safe Size Arithmetic
An additional concern with expressions like cnes * (rborder - j) and src_step * height is integer overflow. On 32-bit platforms (or when using 32-bit types), two large values multiplied together can wrap around to a small number, bypassing a naive size check:
// Dangerous: if src_step=0x10000 and height=0x10001 on 32-bit,
// the product wraps to a small value — check passes, but memcpy still overflows
if (src_step * height <= buf_size) { ... }
The safe approach uses overflow-checked arithmetic:
// Safe multiplication with overflow check
size_t required;
if (__builtin_mul_overflow(src_step, height, &required) || required > buf_size) {
return; // reject
}
memcpy(temp_buf, src, required);
Or use a helper function:
// Using a checked multiply utility
size_t required = checked_mul(src_step, height); // throws/returns error on overflow
Prevention & Best Practices
1. Never Trust External Size Parameters
Any value that originates from a file, network packet, or user input should be treated as untrusted. This includes:
- Image width, height, channel count
- Stride/step values
- Border/padding parameters
- Any computed combination of the above
Rule: Validate all size parameters against known-safe bounds before using them in memory operations.
2. Prefer Safe Alternatives to memcpy
Where possible, replace raw memcpy with bounds-checked alternatives:
| Instead of... | Consider... |
|---|---|
memcpy(dst, src, n) |
memcpy_s(dst, dst_size, src, n) (C11 Annex K) |
| Manual buffer management | std::vector<uint8_t> with .at() or range checks |
| Raw pointer arithmetic | std::span<T> (C++20) with bounds checking |
// C++20 span example — bounds-safe slice operations
std::span<uint8_t> dst_span(dst_buffer, dst_buffer_size);
std::span<const uint8_t> src_span(src_ptr, copy_size);
// This will throw or assert if sizes don't match
std::copy(src_span.begin(), src_span.end(), dst_span.begin());
3. Use Integer Overflow Detection
Always check for overflow in size computations:
#include <limits>
// Safe multiplication
template<typename T>
bool safe_mul(T a, T b, T& result) {
if (a != 0 && b > std::numeric_limits<T>::max() / a) {
return false; // overflow
}
result = a * b;
return true;
}
Modern compilers also offer built-ins:
- GCC/Clang: __builtin_mul_overflow, __builtin_add_overflow
- MSVC: ULongLongMult from <intsafe.h>
4. Enable Compiler and Runtime Protections
Layer your defenses with toolchain features:
| Protection | How to Enable |
|---|---|
| AddressSanitizer (ASan) | -fsanitize=address (GCC/Clang) |
| Stack/Heap Canaries | -fstack-protector-strong |
| FORTIFY_SOURCE | -D_FORTIFY_SOURCE=2 |
| UndefinedBehaviorSanitizer | -fsanitize=undefined |
| Valgrind | Run test suite under valgrind --tool=memcheck |
These won't prevent vulnerabilities from shipping, but they dramatically improve the chance of catching them during development and testing.
5. Fuzz Your Image Parsers
Heap buffer overflows in image processing code are a prime target for fuzzing — automated testing with malformed, random, or mutated inputs.
Tools to consider:
- libFuzzer (built into LLVM) — ideal for in-process fuzzing of parsing code
- AFL++ — coverage-guided fuzzer, excellent for file format targets
- OSS-Fuzz — Google's continuous fuzzing platform for open-source projects
# Example: compile with libFuzzer and ASan, then fuzz
clang++ -fsanitize=fuzzer,address -o fuzz_filter fuzz_filter.cpp filter.cpp
./fuzz_filter corpus/
A well-written fuzzer will find issues like this one in minutes to hours.
6. Code Review Checklist for Memory Operations
When reviewing C/C++ code that handles external data, look for:
- [ ] Every
memcpy/memmove/memset— is the size validated? - [ ] Every size computation involving external data — is overflow possible?
- [ ] Every allocation — is it sized correctly for all possible inputs?
- [ ] Every pointer arithmetic — could it go out of bounds?
- [ ] Every loop bound — is it capped by a known-safe maximum?
Relevant Standards and References
- CWE-120: Buffer Copy Without Checking Size of Input ("Classic Buffer Overflow")
- CWE-122: Heap-based Buffer Overflow
- CWE-190: Integer Overflow or Wraparound
- OWASP: Buffer Overflow
- SEI CERT C Coding Standard: MEM35-C: Allocate sufficient memory for an object
- SEI CERT C: INT30-C: Ensure that unsigned integer operations do not wrap
Conclusion
The vulnerability patched in hal/ndsrvp/src/filter.cpp is a textbook example of CWE-120 — a buffer copy where the size argument is derived from untrusted external data without validation. It's the kind of bug that's easy to write, hard to spot in code review, and potentially devastating in exploitation.
The key takeaways:
- Computed sizes are dangerous. Any
memcpysize that involves external data must be bounds-checked before use. - Integer overflow can defeat naive checks. Use overflow-safe arithmetic when computing sizes.
- Defense in depth matters. ASan, fuzzing, and static analysis catch what human reviewers miss.
- Image processing is high-risk territory. Files are attacker-controlled input. Treat every field in every format as hostile.
Security in systems programming isn't about being perfect — it's about building habits and tooling that make the dangerous patterns visible before they reach production. This fix is a good reminder that even low-level, performance-critical code deserves the same security scrutiny as any public-facing API.
Stay safe, validate your sizes, and fuzz your parsers. 🔒
This vulnerability was identified and patched as part of an automated security review by OrbisAI Security. Automated scanning is one layer of defense — combine it with manual review, fuzzing, and developer education for the strongest security posture.