Repository navigation
fix(jpeg2000): decode subsampled HTJ2K components correctly - #5643
Conversation
6dbae55 to
b0b387c
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The decoder can corrupt color-transformed images and hang on zero-height components, while offset and extent calculations lack targeted regression coverage.
3 open issues
What changed in this PR
This PR updates OpenImageIO’s OpenJPH decoder to handle subsampled HTJ2K components without hanging and reconstruct them at full image resolution.
Changes:
- Decode subsampled components in planar order and expand samples using offset-aware mapping.
- Derive image dimensions from the image extent rather than component 0.
- Add subsampling regression cases and document the decoder behavior.
| File | Description |
|---|---|
testsuite/htj2k/run.py |
Adds valid subsampling cases and a corrupt-file hang regression. |
testsuite/htj2k/ref/out.txt |
Records expected dimensions, pixel hashes, and completion output. |
src/jpeg2000.imageio/jpeg2000input.cpp |
Implements per-component decoding, sample expansion, and canvas sizing. |
.agents/skills/oiio-input-security-audit/references/format-notes.md |
Documents the subsampling fix and OpenJPEG fallback limitations. |
.agents/skills/oiio-input-security-audit/references/bug-catalog.md |
Records the component-size mismatch and its decoding consequences. |
Review effort: Balanced
b0b387c to
7a70cbf
Compare
|
Addressed the copilot comments and pushed an amentment. |
There was a problem hiding this comment.
🟡 Changes recommended
The regressions lack coverage for MCT files with five or more components, and the corrupt-fixture test can mask decoder crashes by hiding the exit status and forcing success.
2 open issues
1 resolved since last review
Review effort: Balanced
88ef76e to
3a709f5
Compare
The OpenJPH HTJ2K path assumed every component is full resolution, pulling one full-width line per component for each image row. Subsampled components put it out of step with the decoder, and OpenJPH spins indefinitely in codestream::pull(). Found by the fuzzer. Each component is now pulled at its own reconstructed size, and each sample is replicated over the full-resolution pixels it covers (the same nearest-sample mapping as the OpenJPEG path, but honoring the image offset). The image size comes from the image extent rather than from component 0, which is smaller when subsampled. OpenJPH's pull order is kept: color-transformed files are decoded interleaved, which requires every component to have the same subsampling, so unequal ones are rejected; other files are pulled component by component. Every pulled line is checked for the expected component and width. A component that is empty despite a nonempty image (offset and extent rounding to the same sample) made pull() spin and is rejected from the header. A tile-component with no columns, from tiles narrower than the component's horizontal subsampling, crashes OpenJPH before 0.27.0, which added a zero-width guard to tile::pull; such files are rejected only when built against an older OpenJPH. Tile rows with no samples are skipped by every version. Keep in mind that our security policy basically doesn't recognize vulnerabilities that are only problems with old versions of dependencies. The valid subsampled test fixtures come from a committed generator (make_subsampled_htj2k.cpp) that codes a known pattern losslessly, so the expected pixels can be checked independently of the reader; the empty-component fixture is patched by make_malformed_htj2k.py. They cover 4:2:0 at odd sizes, 10-bit, tiles with image and tile offsets, non-power-of-2 factors, a subsampled first component, color transforms with three and five components, four components with their own alpha subsampling, and tiles holding no rows or no columns of a component. The version-dependent outcomes have their own output file and refs. Assisted-by: Claude Code / Claude Opus 5.5 Signed-off-by: Larry Gritz <lg@larrygritz.com>
3a709f5 to
2f19626
Compare
|
Any final objections on this? |
|
No objections, cleared multiple rounds of tedious robot reviews. Merging. |
…oftwareFoundation#5643) The OpenJPH HTJ2K path assumed every component is full resolution, pulling one full-width line per component for each image row. Subsampled components put it out of step with the decoder, and OpenJPH spins indefinitely in codestream::pull(). Found by the fuzzer. Each component is now pulled at its own reconstructed size, and each sample is replicated over the full-resolution pixels it covers (the same nearest-sample mapping as the OpenJPEG path, but honoring the image offset). The image size comes from the image extent rather than from component 0, which is smaller when subsampled. OpenJPH's pull order is kept: color-transformed files are decoded interleaved, which requires every component to have the same subsampling, so unequal ones are rejected; other files are pulled component by component. Every pulled line is checked for the expected component and width. A component that is empty despite a nonempty image (offset and extent rounding to the same sample) made pull() spin and is rejected from the header. A tile-component with no columns, from tiles narrower than the component's horizontal subsampling, crashes OpenJPH before 0.27.0, which added a zero-width guard to tile::pull; such files are rejected only when built against an older OpenJPH. Tile rows with no samples are skipped by every version. Keep in mind that our security policy basically doesn't recognize vulnerabilities that are only problems with old versions of dependencies. The valid subsampled test fixtures come from a committed generator (make_subsampled_htj2k.cpp) that codes a known pattern losslessly, so the expected pixels can be checked independently of the reader; the empty-component fixture is patched by make_malformed_htj2k.py. They cover 4:2:0 at odd sizes, 10-bit, tiles with image and tile offsets, non-power-of-2 factors, a subsampled first component, color transforms with three and five components, four components with their own alpha subsampling, and tiles holding no rows or no columns of a component. The version-dependent outcomes have their own output file and refs. Assisted-by: Claude Code / Claude Opus 5.5 Signed-off-by: Larry Gritz <lg@larrygritz.com>


The OpenJPH HTJ2K path assumed every component is full resolution, pulling one full-width line per component for each image row. Subsampled components put it out of step with the decoder, and OpenJPH spins indefinitely in codestream::pull(). Found by the fuzzer.
Each component is now pulled at its own reconstructed size, and each sample is replicated over the full-resolution pixels it covers (the same nearest-sample mapping as the OpenJPEG path, but honoring the image offset). The image size comes from the image extent rather than from component 0, which is smaller when subsampled. OpenJPH's pull order is kept: color-transformed files are decoded interleaved, which requires every component to have the same subsampling, so unequal ones are rejected; other files are pulled component by component. Every pulled line is checked for the expected component and width.
A component that is empty despite a nonempty image (offset and extent rounding to the same sample) made pull() spin and is rejected from the header. A tile-component with no columns, from tiles narrower than the component's horizontal subsampling, crashes OpenJPH before 0.27.0, which added a zero-width guard to tile::pull; such files are rejected only when built against an older OpenJPH. Tile rows with no samples are skipped by every version. Keep in mind that our security policy basically doesn't recognize vulnerabilities that are only problems with old versions of dependencies.
The valid subsampled test fixtures come from a committed generator (make_subsampled_htj2k.cpp) that codes a known pattern losslessly, so the expected pixels can be checked independently of the reader; the empty-component fixture is patched by make_malformed_htj2k.py. They cover 4:2:0 at odd sizes, 10-bit, tiles with image and tile offsets, non-power-of-2 factors, a subsampled first component, color transforms with three and five components, four components with their own alpha subsampling, and tiles holding no rows or no columns of a component. The version-dependent outcomes have their own output file and refs.
Assisted-by: Claude Code / Claude Opus 5.5