Skip to content

J2KDecoder crashes with "memory access out of bounds" for J2K images with component subsampling (XRsiz/YRsiz > 1) #69

Description

@sha07

Summary

J2KDecoder::decode() throws a WebAssembly RuntimeError: memory access out of bounds when decoding a J2K/J2C codestream whose SIZ marker declares component subsampling (XRsiz/YRsiz > 1). This affects real-world digital mammography (MG) images, which commonly use XRsiz=YRsiz=2.

Sample base64-encoded j2c MG image: mg-j2c-base64.txt

Package version: @cornerstonejs/codec-openjpeg v1.3.0
Affected file: packages/openjpeg/src/J2KDecoder.hpp


Root cause

After opj_decode() returns, decode_i() populates frameInfo_ from the raw SIZ reference-grid fields:

// J2KDecoder.hpp (current)
frameInfo_.width  = image->x1;   // Xsiz — reference-grid extent
frameInfo_.height = image->y1;   // Ysiz — reference-grid extent

For images with component subsampling, image->x1 / image->y1 are the reference-grid dimensions (e.g. 4095×5625), not the actual decoded pixel dimensions. OpenJPEG already computes the correct component dimensions in image->comps[0].w / .h = ceil((x1−x0) / XRsiz) (e.g. 2048×2813 for XRsiz=YRsiz=2).

Using the wrong (oversized) dimensions causes two bugs:

  1. getFrameInfo() returns incorrect dimensions — callers receive the reference-grid extent instead of the actual pixel count, so they misinterpret the decoded buffer.
  2. Heap out-of-bounds write / WASM crash — calculateSizeAtDecompositionLevel() uses frameInfo_.width to compute destinationSize, allocating a buffer sized for x1 × y1 pixels (e.g. ~44 MB). The subsequent pixel-copy loop then indexes image->comps[0].data[y * x1], which runs far past the end of the component data (only comps [0].w × comps[0].h int32 values were allocated by OpenJPEG). In the WASM build this is caught by linear-memory bounds checking and throws:

RuntimeError: memory access out of bounds


Reproduction

Any J2K codestream with XRsiz=YRsiz=2 in the SIZ marker triggers the crash. A minimal example with the digital mammography image format:

const decoder = new codec.J2KDecoder();
decoder.getEncodedBuffer(mg_j2k_bytes.length).set(mg_j2k_bytes);
decoder.decode(); // ← throws "memory access out of bounds"

The SIZ marker of such a codestream looks like:

Xsiz = 4095 (reference-grid width)
Ysiz = 5625 (reference-grid height)
XRsiz = 2 (component subsampling factor)
YRsiz = 2
→ actual component pixels: ceil(4095/2) × ceil(5625/2) = 2048 × 2813


Fix

Replace image->x1 / image->y1 with image->comps[0].w / image->comps[0].h:

// J2KDecoder.hpp — proposed fix
frameInfo_.width = image->comps[0].w; // ceil((x1 - x0) / XRsiz)
frameInfo_.height = image->comps[0].h; // ceil((y1 - y0) / YRsiz)

image->comps[i].w and .h are set correctly by OpenJPEG during opj_decode() per ISO 15444-1 §B.2, and already account for the subsampling factor. This makes getFrameInfo() return the actual decoded pixel dimensions and fixes the out-of-bounds access.

The one-line change has been tested against:

• A real MG J2K with XRsiz=YRsiz=2 (reference grid 4095×5625, component 2048×2813): decodes correctly, frameInfo.width=2048, frameInfo.height=2813.
• Existing CT, MR, and other images with XRsiz=YRsiz=1: no regression.


References

• ISO 15444-1 §B.2: component sample count = ceil((Xsiz − XOsiz) / XRsiz)
• OpenJPEG sets image->comps[i].w correctly in opj_j2k_update_image_dimensions()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions