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

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.