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 Spring Boot EndpointRequest.to() security bypass happens in Java Spring Boot and how to fix it

CVE-2025-22235 is a high-severity vulnerability in Spring Boot where `EndpointRequest.to()` creates an incorrect request matcher when an actuator endpoint is not exposed, potentially allowing unauthorized access to protected endpoints. The fix upgrades Spring Boot from 3.4.4 to 3.4.5 in the anti-corruption-layer service's `pom.xml`. This is particularly dangerous because actuator endpoints can expose sensitive operational data and administrative functions.

medium

How path traversal happens in C file extraction and how to fix it

A path traversal vulnerability in the borpak archive extraction tool allowed attackers to write files to arbitrary locations on the filesystem by crafting malicious .pak archives with `../` sequences in filenames. This medium-severity issue in `tools/borpak/source/borpak.c` could enable system compromise through overwriting critical files like `.bashrc` or cron jobs. The fix implements path validation to ensure extracted files never escape the intended extraction directory.

critical

How SQL injection happens in PostgreSQL dictionary synchronization and how to fix it

A critical SQL injection vulnerability in `zhparser--2.1.sql` allowed attackers to execute arbitrary SQL commands by crafting malicious database names. The vulnerability existed because the dictionary synchronization function constructed COPY commands using string concatenation without proper escaping. This fix implements parameterized queries to safely handle database identifiers.

critical

How command injection happens in Go ffmpeg wrappers and how to fix it

A critical command injection vulnerability was discovered in `drivers/local/util.go` where user-influenced file paths were passed directly to `ffmpeg.Input()` without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a `sanitizeFilePath()` function that validates paths are absolute, clean, and point to

critical

How heap buffer overflow happens in C UART response handling and how to fix it

A critical heap buffer overflow vulnerability was discovered in the AT client response handler (`sm_at_client.c`) where incoming UART data was copied into a fixed-size buffer without verifying available capacity. A compromised modem or malicious UART data could trigger arbitrary heap corruption. The fix replaces an assertion-only guard with proper bounds clamping using `MIN()` to ensure writes never exceed the `at_cmd_resp` buffer allocation.

critical

How unsafe buffer copying happens in C credential storage and how to fix it

A critical vulnerability in `lib/server.c` allowed attackers to trigger out-of-bounds memory reads when copying credentials via unsafe `memcpy()` calls. By replacing `memcpy()` with bounds-safe `strlcpy()`, the fix ensures credentials are safely stored without buffer overruns or null-termination issues.