-
Notifications
You must be signed in to change notification settings - Fork 25
fix(openjpeg): correct the buffer-stream skip signature and decoded image dimensions #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
c7e6e22
fix: use x1-x0 / y1-y0 for correct image dimensions + add opj_end_dec…
ahmedezzat85 e38e98e
ci: add GitHub Actions workflow to build openjpeg WASM and commit dis…
ahmedezzat85 f82b6c2
ci: fix artifact copy paths to build/src/ + force-track dist/ + trigg…
ahmedezzat85 72a0ca6
ci: fix copy paths to build/extern/openjpeg/bin/ (matching build.sh) …
ahmedezzat85 41d9418
ci: fix exit code 128 - use GITHUB_TOKEN for push + add token to chec…
ahmedezzat85 e66f60c
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] a9e0ded
fix: add -s ALLOW_TABLE_GROWTH=1 to fix indirect call signature misma…
ahmedezzat85 f4dd4a0
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] 12ca69f
fix: add ALLOW_TABLE_GROWTH=1 to openjpegjs and openjpegjs_decode tar…
ahmedezzat85 f7c9cd1
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] cbe3bd3
fix: add build version string to force recompile with ALLOW_TABLE_GRO…
ahmedezzat85 0dc3d16
debug: add verbose decode logs + ASSERTIONS=2 + SAFE_HEAP to trace er…
ahmedezzat85 e73d211
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] bd75102
fix: add EMULATE_FUNCTION_POINTER_CASTS=1 to all 4 targets to fix ind…
ahmedezzat85 98ed415
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] 18f6c7a
cleanup: remove debug instrumentation, restore clean J2KDecoder bindings
ahmedezzat85 d2c4cfd
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] 27b28c5
fix: add EMULATE_FUNCTION_POINTER_CASTS=1 to resolve indirect call si…
ahmedezzat85 0c9dc15
fix: correct image dimensions and add opj_end_decompress in J2KDecoder
ahmedezzat85 a52abe4
chore: remove openjpeg info_callback to silence verbose tile decode logs
ahmedezzat85 3683c6d
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] 2e5b571
restore: J2KDecoder.hpp and jslib-decode.cpp to intended versions
ahmedezzat85 173e5d5
ci: update openjpeg WASM dist artifacts [skip ci]
github-actions[bot] e4b419e
Merge remote-tracking branch 'origin/main' into pr63
wayfarer3130 14e4c79
revert: drop the CI workflow, committed wasm and .gitignore changes
wayfarer3130 f21c945
revert: restore the release build flags in openjpeg CMakeLists
wayfarer3130 4768e8e
fix(openjpeg): derive decoded size from x1-x0 / y1-y0, and end decomp…
ahmedezzat85 17c6b48
fix(openjpeg): match opj_stream_skip_fn's signature in the buffer stream
wayfarer3130 e89a261
fix(openjpeg): correct the openjpeg include paths in the C++ test CMa…
John-Skinner 5512b97
test(openjpeg): cover the skip callback with a generated JP2 fixture
wayfarer3130 9c9a735
ci: never run the release workflow in a fork
wayfarer3130 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| // Builds a JP2 (box-wrapped) file around a bare J2K codestream. | ||
| // | ||
| // Why this is generated rather than committed as a fixture: the point of the | ||
| // JP2 wrapper here is to make openjpeg delegate a skip to our own | ||
| // opj_skip_from_buffer callback in BufferStream.hpp, and that only happens for | ||
| // a skip LARGER than the stream's internal buffer. opj_stream_read_data always | ||
| // refills a full OPJ_J2K_STREAM_CHUNK_SIZE (1MB) chunk, and | ||
| // opj_stream_read_skip serves anything within m_bytes_in_buffer directly, so | ||
| // the skipped box has to be over 1MB. That makes the file too big to want in | ||
| // git, and it is trivially reproducible from a codestream we already ship. | ||
| // | ||
| // Run directly to write one out, e.g. to hand to a bug report: | ||
| // node test/helpers/jp2.mjs test/fixtures/j2k/CT1.j2k /tmp/CT1-boxed.jp2 | ||
|
|
||
| import { readFileSync, writeFileSync } from "node:fs" | ||
| import { pathToFileURL } from "node:url" | ||
|
|
||
| // opj_stream_default_create's buffer size (openjpeg.h OPJ_J2K_STREAM_CHUNK_SIZE). | ||
| export const STREAM_CHUNK_SIZE = 0x100000 | ||
|
|
||
| // Comfortably over one chunk, so the skip cannot be served from the buffer no | ||
| // matter how much of it the header reads happen to have consumed. | ||
| export const DEFAULT_FILLER_BYTES = STREAM_CHUNK_SIZE + 4096 | ||
|
|
||
| function box(type, ...contents) { | ||
| const content = Buffer.concat(contents) | ||
| const header = Buffer.alloc(8) | ||
| header.writeUInt32BE(content.length + 8, 0) | ||
| header.write(type, 4, 4, "ascii") | ||
| return Buffer.concat([header, content]) | ||
| } | ||
|
|
||
| function u32(value) { | ||
| const b = Buffer.alloc(4) | ||
| b.writeUInt32BE(value, 0) | ||
| return b | ||
| } | ||
|
|
||
| function u16(value) { | ||
| const b = Buffer.alloc(2) | ||
| b.writeUInt16BE(value, 0) | ||
| return b | ||
| } | ||
|
|
||
| /** | ||
| * Reads the SIZ marker segment of a raw J2K codestream, so the JP2 image | ||
| * header we synthesise describes the codestream it actually wraps. | ||
| */ | ||
| export function parseSiz(codestream) { | ||
| if (codestream.readUInt16BE(0) !== 0xff4f || codestream.readUInt16BE(2) !== 0xff51) { | ||
| throw new Error("not a raw J2K codestream (expected SOC then SIZ)") | ||
| } | ||
|
|
||
| const xsiz = codestream.readUInt32BE(8) | ||
| const ysiz = codestream.readUInt32BE(12) | ||
| const xosiz = codestream.readUInt32BE(16) | ||
| const yosiz = codestream.readUInt32BE(20) | ||
| const numComponents = codestream.readUInt16BE(40) | ||
|
|
||
| // Ssiz is (bitdepth - 1) with the top bit set when signed — the same | ||
| // encoding JP2's ihdr BPC field uses, so it can be copied across verbatim. | ||
| const ssiz = codestream.readUInt8(42) | ||
|
|
||
| return { | ||
| width: xsiz - xosiz, | ||
| height: ysiz - yosiz, | ||
| numComponents, | ||
| ssiz, | ||
| bitsPerSample: (ssiz & 0x7f) + 1, | ||
| isSigned: (ssiz & 0x80) !== 0, | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Wraps a raw J2K codestream in the minimum set of JP2 boxes, with a filler | ||
| * box in front of the codestream that openjpeg has no handler for and will | ||
| * therefore skip (jp2.c, the unknown-box branch of opj_jp2_read_header). | ||
| * | ||
| * @param {Buffer} codestream a bare .j2k codestream | ||
| * @param {{fillerBytes?: number}} [options] | ||
| * @returns {Buffer} a JP2 file | ||
| */ | ||
| export function wrapInJp2(codestream, { fillerBytes = DEFAULT_FILLER_BYTES } = {}) { | ||
| const siz = parseSiz(codestream) | ||
|
|
||
| const signature = box("jP ", Buffer.from([0x0d, 0x0a, 0x87, 0x0a])) | ||
| const fileType = box("ftyp", Buffer.from("jp2 ", "ascii"), u32(0), Buffer.from("jp2 ", "ascii")) | ||
|
|
||
| const ihdr = box( | ||
| "ihdr", | ||
| u32(siz.height), | ||
| u32(siz.width), | ||
| u16(siz.numComponents), | ||
| Buffer.from([ | ||
| siz.ssiz, | ||
| 7, // C: compression type, always 7 | ||
| 0, // UnkC: colourspace is known | ||
| 0, // IPR: no intellectual property rights box | ||
| ]) | ||
| ) | ||
| // METH=1 (enumerated), PREC=0, APPROX=0, then EnumCS. | ||
| const enumCs = siz.numComponents >= 3 ? 16 /* sRGB */ : 17 /* greyscale */ | ||
| const colr = box("colr", Buffer.from([1, 0, 0]), u32(enumCs)) | ||
|
|
||
| // A 'free' box is exactly this: padding with no defined meaning. openjpeg | ||
| // has no handler for it, so it takes the skip path we are trying to reach. | ||
| const filler = box("free", Buffer.alloc(fillerBytes)) | ||
|
|
||
| return Buffer.concat([ | ||
| signature, | ||
| fileType, | ||
| box("jp2h", ihdr, colr), | ||
| filler, | ||
| box("jp2c", codestream), | ||
| ]) | ||
| } | ||
|
|
||
| // CLI: node test/helpers/jp2.mjs <in.j2k> <out.jp2> [fillerBytes] | ||
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { | ||
| const [input, output, fillerBytes] = process.argv.slice(2) | ||
| if (!input || !output) { | ||
| console.error("usage: node test/helpers/jp2.mjs <in.j2k> <out.jp2> [fillerBytes]") | ||
| process.exit(1) | ||
| } | ||
| const jp2 = wrapInJp2(readFileSync(input), { | ||
| fillerBytes: fillerBytes ? Number(fillerBytes) : undefined, | ||
| }) | ||
| writeFileSync(output, jp2) | ||
| console.log(`wrote ${output} (${jp2.length} bytes)`) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 6157
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 50377
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 39349
🌐 Web query:
OpenJPEG j2k.c cp_reduce image->comps[w] x0 x1 reduced component dimensions💡 Result:
In OpenJPEG, the parameter cp_reduce (often set via the -r option in tools like opj_decompress) specifies the number of highest-resolution levels to be discarded during decoding [1][2]. When this reduction is applied, the image dimensions are effectively divided by 2 to the power of the reduction factor [1][2]. Internally, the OpenJPEG library handles these adjustments through functions such as opj_image_comp_header_update, which recalculates component metadata including dimensions (w, h) and offsets (x0, y0) based on the target resolution factor [3]. Specifically: 1. Component Dimensions: The width (w) and height (h) of each image component are updated using ceiling division of the grid coordinates, followed by a power-of-two division corresponding to the specified reduction factor [3]. l_width = opj_uint_ceildivpow2(l_comp_x1 - l_comp_x0, l_img_comp->factor); l_height = opj_uint_ceildivpow2(l_comp_y1 - l_comp_y0, l_img_comp->factor); [3] 2. Component Offsets (x0, x1): The offsets are recalculated relative to the reduced grid [3]. The x0 and y0 values are adjusted to represent the component's origin on the reduced-resolution reference grid [3]. 3. cp_reduce Constraints: The library enforces that the requested reduction factor does not exceed the number of available decomposition levels defined in the codestream's tiles [4][1]. Attempting a reduction larger than what is available will result in an error [4]. This mechanism allows for efficient decoding of lower-resolution versions of an image without decompressing the full-resolution data, significantly saving memory and processing time [5].
Citations:
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 39242
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 420
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 1758
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 349
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 1974
🏁 Script executed:
Repository: cornerstonejs/codecs
Length of output: 1826
Other (CWE-125): Out-of-bounds Read
Reachability: External · Exploitability: Moderate
Use OpenJPEG’s decoded component dimensions for reduced output.
When
decompositionLevel > 0and component subsampling is present,calculateSizeAtDecompositionLevel()can exceedimage->comps[0].wor.h. The copy loops then read beyond the allocated component buffers. SetsizeAtDecompositionLevelfromimage->comps[0].wand.h.🤖 Prompt for AI Agents
Source: MCP tools