Back to Blog
critical SEVERITY6 min read

How heap buffer overflow happens in C++ JPEG2000 decoding and how to fix it

A critical heap buffer overflow vulnerability was discovered in the OpenJPEG wrapper for Android (jp2forandroid). The `opj_read_from_byte_array()` function performed memcpy operations without validating that the source offset hadn't exceeded the buffer length, allowing maliciously crafted JPEG2000 images to trigger arbitrary code execution. A simple bounds check before the copy operation now prevents this exploitation path.

O
By Orbis AppSec
Published June 5, 2026Reviewed June 5, 2026

Answer Summary

This is a heap buffer overflow vulnerability (CWE-120) in C++ code that processes JPEG2000 images on Android. The `opj_read_from_byte_array()` function in `openjpg.cpp` used memcpy without checking if the read offset had exceeded the source buffer length. The fix adds a bounds check (`if (p_user_data->offset >= p_user_data->length)`) before the memcpy call to prevent reading beyond allocated memory, and adds integer overflow protection in the malloc allocation.

Vulnerability at a Glance

cweCWE-120
fixAdd offset validation before memcpy and integer overflow check in malloc
riskArbitrary code execution via malicious JP2 images
languageC++
root causeMissing bounds check before memcpy in byte array reader
vulnerabilityHeap Buffer Overflow

Introduction

In the jp2forandroid repository, we discovered a critical heap buffer overflow vulnerability in openjpg.cpp at line 597. The opj_read_from_byte_array() function—responsible for reading bytes from a JPEG2000 image buffer during decoding—contained a dangerous flaw that could allow attackers to execute arbitrary code through specially crafted image files.

The vulnerability exists because the code performs a memcpy() operation using an offset value (p_user_data->offset) without first checking whether that offset has already exceeded the buffer's total length. When processing a malicious JP2 image with manipulated header fields, an attacker could cause the offset to grow beyond the buffer size, leading to out-of-bounds memory reads and heap corruption.

For Android developers working with image processing libraries, this vulnerability demonstrates why every buffer operation—no matter how simple it appears—requires defensive bounds checking.

The Vulnerability Explained

The vulnerable code resided in the opj_read_from_byte_array() function, which serves as a custom stream reader for the OpenJPEG library:

static OPJ_SIZE_T opj_read_from_byte_array (void * p_buffer, OPJ_SIZE_T p_nb_bytes, opj_byte_array_source * p_user_data)
{
    //LOGD("opj_read_from_byte_array started");
    size_t toRead = MIN(p_nb_bytes, p_user_data->length - p_user_data->offset);
    memcpy(p_buffer, p_user_data->data + p_user_data->offset, toRead);
    p_user_data->offset += toRead;
    return toRead;
}

At first glance, the MIN() macro seems protective—it calculates the smaller of the requested bytes and the remaining buffer space. However, there's a critical flaw: what happens when p_user_data->offset is already greater than or equal to p_user_data->length?

The Integer Underflow Problem

When offset >= length, the expression p_user_data->length - p_user_data->offset produces an integer underflow. Since these are unsigned integers, the result wraps around to a massive positive number rather than becoming negative. The MIN() then selects p_nb_bytes (the requested read size), and memcpy() proceeds to read far beyond the allocated buffer.

Attack Scenario

An attacker crafts a JPEG2000 image with manipulated SIZ marker box fields. Here's how the attack unfolds:

  1. Malicious Image Creation: The attacker creates a JP2 file where the SIZ marker declares component dimensions that cause multiple sequential reads
  2. Offset Manipulation: Through carefully crafted header values, the decoder is tricked into advancing p_user_data->offset beyond p_user_data->length
  3. Buffer Overflow Trigger: On the next read operation, the integer underflow causes memcpy() to read arbitrary heap memory
  4. Code Execution: The attacker can corrupt adjacent heap structures, potentially achieving arbitrary code execution

This is particularly dangerous on Android, where a user might simply open a shared image file, triggering the vulnerable code path without any additional interaction.

Additional Vulnerability: Integer Overflow in Allocation

A second issue existed in the imagetoargb() function at line 169:

outImage->pixels = (int *) malloc(sizeof(int) * w * h);

If an attacker provides manipulated width (w) and height (h) values, the multiplication sizeof(int) * w * h could overflow, resulting in a small allocation that's later written with a much larger amount of data.

The Fix

The fix introduces two critical safeguards:

Fix 1: Bounds Check Before Read

static OPJ_SIZE_T opj_read_from_byte_array (void * p_buffer, OPJ_SIZE_T p_nb_bytes, opj_byte_array_source * p_user_data)
{
    //LOGD("opj_read_from_byte_array started");
    if (p_user_data->offset >= p_user_data->length) {
        return (OPJ_SIZE_T)-1;
    }
    size_t toRead = MIN(p_nb_bytes, p_user_data->length - p_user_data->offset);
    memcpy(p_buffer, p_user_data->data + p_user_data->offset, toRead);
    p_user_data->offset += toRead;
    return toRead;
}

The new check if (p_user_data->offset >= p_user_data->length) ensures that:
- We never attempt to read when the offset has reached or exceeded the buffer length
- The subtraction length - offset is always safe (no underflow possible)
- The function returns an error code (-1) to signal the end of available data

Fix 2: Integer Overflow Protection in Allocation

outImage->pixels = (w > 0 && h > 0 && h <= SIZE_MAX / (sizeof(int) * w)) 
    ? (int *) malloc(sizeof(int) * w * h) 
    : NULL;

This change validates that:
- Both dimensions are positive
- The multiplication won't overflow SIZE_MAX
- If validation fails, NULL is returned instead of allocating a dangerously small buffer

Before and After Comparison

Aspect Before After
Offset validation None Explicit bounds check
Integer underflow Possible Prevented
Allocation overflow Possible Checked against SIZE_MAX
Error handling Silent corruption Returns error code

Prevention & Best Practices

Defensive Coding for Buffer Operations

  1. Always validate offsets before arithmetic: Check that your offset is within bounds before using it in any calculation
  2. Use safe integer arithmetic: Consider using compiler built-ins like __builtin_add_overflow() or libraries that detect overflow
  3. Validate all size parameters from untrusted input: Image dimensions, chunk sizes, and length fields in file formats are attacker-controlled

Code Review Checklist

When reviewing C/C++ code that handles buffers:

  • [ ] Is every memcpy(), memmove(), or strcpy() preceded by a bounds check?
  • [ ] Are all arithmetic operations checked for overflow before use in allocations?
  • [ ] Do offset variables get validated before use in pointer arithmetic?
  • [ ] Are error conditions properly handled (not silently ignored)?

Tools for Detection

  • AddressSanitizer (ASan): Compile with -fsanitize=address to detect buffer overflows at runtime
  • Static analyzers: Tools like Coverity, PVS-Studio, or clang-tidy can identify missing bounds checks
  • Fuzzing: Use AFL or libFuzzer with malformed image inputs to trigger edge cases

Key Takeaways

  • Never trust that previous operations kept offsets valid: The opj_read_from_byte_array() function assumed the offset would always be less than length—a dangerous assumption when processing untrusted data
  • Integer underflow is as dangerous as overflow: The expression length - offset with unsigned integers can produce massive values when offset exceeds length
  • Image parsing is a high-risk area: JPEG2000, PNG, and other image formats have complex structures that attackers frequently exploit
  • Multi-step attacks are common: This vulnerability required manipulating header fields to first advance the offset, then trigger the overflow—exactly the "2-step chain" the scanner identified
  • Bounds checks must come before arithmetic, not after: Placing the validation after the MIN() calculation would still allow the underflow to occur

How Orbis AppSec Detected This

  • Source: Untrusted data from JP2 image file header fields (SIZ marker box dimensions)
  • Sink: memcpy(p_buffer, p_user_data->data + p_user_data->offset, toRead) in openjpg.cpp:601
  • Missing control: No validation that p_user_data->offset < p_user_data->length before the subtraction operation
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Added bounds check if (p_user_data->offset >= p_user_data->length) before the memcpy operation and integer overflow protection in the malloc call

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 heap buffer overflow in opj_read_from_byte_array() demonstrates how a seemingly simple oversight—failing to validate an offset before subtraction—can create a critical security vulnerability. The fix required just three lines of code, but those three lines prevent attackers from exploiting malicious JPEG2000 images to execute arbitrary code on Android devices.

When working with native code that processes untrusted input, remember: every buffer operation is a potential attack surface. Validate early, validate often, and never assume that previous operations have left your state in a safe condition.

References

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes or reads data beyond the boundaries of a dynamically allocated memory buffer, potentially corrupting adjacent memory and enabling code execution.

How do you prevent heap buffer overflow in C++?

Validate all buffer lengths before copy operations, use safe functions like memcpy_s, check for integer overflow in size calculations, and always verify that offsets don't exceed buffer bounds.

What CWE is heap buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers classic buffer overflow vulnerabilities where data is copied without validating the input size against destination capacity.

Is using MIN() enough to prevent buffer overflow?

No, MIN() alone isn't sufficient if the offset used in the calculation can exceed the total length, resulting in integer underflow. You must validate the offset is within bounds first.

Can static analysis detect heap buffer overflow?

Yes, static analysis tools can detect many buffer overflow patterns, especially when memcpy is called without preceding bounds checks on offset values or when integer overflow is possible in size calculations.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.