GROK-0003
low confirmedMJ2 box parser: headerSize underflow in read_url/read_urn yields heap out-of-bounds read
⚠ Harness reproduced — not real-world verified. Reproduce through the public API, a real application, or a platform decoder before treating this as verified.
Classification
| Target | Grok |
|---|---|
| Component | JP2 file format |
| Location | src/lib/core/fileformat/decompress/FileFormatMJ2Decompress.cpp · FileFormatMJ2Decompress::read_url / read_urn:335-380 |
| Vuln class | oob-read |
| CVE | — |
| CVSS | 3.1 (CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L) |
| Discovered | 2026-06-16 |
Verification
| Evidence | Static only (not real-world verified) |
|---|---|
| Harness fired | ❌ no |
| Protocol | 1.0 |
| Sanitizer | none |
| Crash type | heap-buffer-overflow (read) |
Disclosure
| Reported to | Grok maintainers — support@grokcompression.com (private email; listed as a source-confirmed lead, not yet runtime-verified) |
|---|---|
| Reported | 2026-06-23 |
| Vendor ack | — |
| Embargo until | — |
| Public | 2026-06-24 |
| Patched in | — |
Writeup
Summary
grk_decompress auto-detects the MJ2 format from file magic (offset 0
JP2_RFC3745_MAGIC + offset 20 MJ2_MAGIC, stream/MemStream.cpp:42-47) and routes the file
to FileFormatMJ2Decompress (grok.cpp:198-199) — no special flag required. Inside the MJ2
dref box handler, read_url/read_urn use the unchecked read_version_and_flag and then
subtract 4 from headerSize without a >= 4 guard. A url/urn box with a payload
smaller than 4 bytes underflows headerSize (uint32) to ~4 billion, which then defeats the
bounds check in the subsequent grk_read calls, producing a heap out-of-bounds read.
bool FileFormatMJ2Decompress::read_url(uint8_t* headerData, uint32_t headerSize) {
read_version_and_flag(&headerData, version, flag); // :339 reads 4 bytes, NO size check
headerSize -= 4; // :340 underflows if headerSize < 4
if(flag != 1) {
mj2_url url;
for(uint32_t i = 0; i < 4; ++i)
grk_read(&headerData, &headerSize, url.location_ + i); // :350 bounds-check fooled
}
}
read_version_and_flag (:117-126) calls the no-bytesRemaining grk_read overload
(stream/StreamIO.h:119, passes nullptr → no bounds check), so the 4-byte
version/flag read itself already over-reads when the box payload is < 4 bytes. The checked
grk_read overload (StreamIO.h:106) throws on bytesRemaining < numBytes, but after the
underflow headerSize ≈ 0xFFFFFFFF, so it does not throw and the four (read_url) / eight
(read_urn) 4-byte reads walk past the box buffer.
Contrast read_dref itself (:384) and other MJ2 readers, which correctly use
read_version_and_flag_check (:127, guards *headerSize < 4). read_url/read_urn are the
outliers that skipped the checked variant.
Reproduction
Reachability: read_dref is registered as the MJ2_DREF box handler
(FileFormatMJ2Decompress.cpp:79); it iterates entry_count child boxes and dispatches
MJ2_URL → read_url / MJ2_URN → read_urn (:399-410).
Craft a .mj2 whose header chain reaches a dref box containing one entry that is a url
('url ') or urn box with a declared length giving a payload of 0-3 bytes, and flag != 1.
Run grk_decompress -i poc.mj2 -o /tmp/out.png under ASAN to observe heap-buffer-overflow READ.
Root cause
read_url/read_urn use the unchecked read_version_and_flag and an unguarded
headerSize -= 4, where the rest of the MJ2 parser uses read_version_and_flag_check. The
unsigned underflow turns the downstream length-checked reads into unbounded reads.
Verification evidence
Confirmed by source trace (MJ2 auto-detected; read_version_and_flag is the unchecked overload;
headerSize -= 4 has no guard; the checked grk_read is defeated by the underflowed headerSize).
Runtime finding (2026-06-16, ASAN build): the documented over-read is real but NOT independently
crashable. It is bounded to ≤16 bytes (4 for read_version_and_flag + 4×4 for the url.location_
reads). On the file path the input is mmap-backed and box content is zero-copy, so the over-read
lands in the zero-filled tail page (no fault); on the non-zero-copy path the per-box buffer is a
reused, only-growing allocation (FileFormatJP2Family.cpp:347-356) sized to the largest box
seen (e.g. tkhd=84 B), whose slack absorbs the ≤16 B over-read. So it manifests as reading a few
bytes of stale adjacent buffer into url.location_ (minor), not a crash. Severity lowered to low
accordingly. The crashable defect in the same read_url path (null current_track_) is filed and
verified separately as [[GROK-0013]].
Impact
Heap out-of-bounds read reachable from a single untrusted local file via the default
grk_decompress path (MJ2 is auto-detected from magic), causing a crash/DoS and possible
limited info-disclosure into the parsed url/urn structures. Local-untrusted-file reachability.
Severity medium (CVSS 6.5).
Related (same MJ2 parser, logged for follow-up — see session 2026-06-16-grok)
The MJ2 decompress parser has sibling robustness defects worth fixing together (not separately
filed): read_dref advances only by the 8-byte box header (not the child payload) and passes
the full remaining headerSize to children (:395-406); read_stsd advances by box.length
in the entry_count loop without re-validating it fits (:490-493); read_jp2x does
throw new std::runtime_error(...) — throwing a pointer, which no catch(const&) matches →
std::terminate (DoS) (:189); several table readers (read_stts/read_stsz/read_stco)
take unbounded attacker counts before the per-element checked reads (DoS-ish).