GROK-0007
high harness-verifiedMJ2: sample offset/size from STCO/STSZ used as a raw file pointer with no bounds check → 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 · readHeader / decompressSampleInternal:724-737,762-785 |
| Vuln class | oob-read |
| CVE | — |
| CVSS | 7.1 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:H) |
| Discovered | 2026-06-16 |
Verification
| Evidence | Harness reproduced (not real-world verified) |
|---|---|
| Harness fired | ✅ yes |
| Protocol | 1.0 |
| Sanitizer | asan |
| Crash type | SEGV / out-of-bounds read (wild, basePtr + 0xFFFFFFFF) |
| Repro | clang-20 + libc++ ASAN build; grk_decompress -i pocs/grok/GROK-0007/poc.mj2 → ASAN SEGV (READ) in grk_read (StreamIO.h:53) <- FileFormatMJ2Decompress::readHeader (FileFormatMJ2Decompress.cpp:730); rdx=0xffffffff is the bogus STCO offset. |
Disclosure
| Reported to | Grok maintainers — support@grokcompression.com (private email; repo has no Private Vulnerability Reporting / SECURITY.md) |
|---|---|
| Reported | 2026-06-23 |
| Vendor ack | — |
| Embargo until | — |
| Public | 2026-06-24 |
| Patched in | — |
PoC: pocs/grok/GROK-0007/poc.mj2
Writeup
Summary
grk_decompress auto-detects MJ2 from file magic and parses its sample tables. Each video
sample’s byte offset (sample.offset_, accumulated from the STCO chunk-offset table in
stco_decompact) and size (sample.samples_size_, from STSZ) are used as an absolute pointer
into the whole-file buffer with no validation that the offset/size lie within the file. A
crafted STCO/STSZ makes the parser read arbitrarily far past the input buffer.
Two reachable sites, both with the same root defect:
// readHeader() — runs on the STANDARD parse, before any decompress:
stream_->seek(0);
auto basePtr = stream_->currPtr();
for(const auto& sample : current_track_->samples_) {
auto ptr = basePtr + sample.offset_; // :726 offset_ unvalidated (attacker STCO)
uint32_t len;
grk_read(ptr, &len); // :730 unchecked 4-byte read at basePtr+offset_
...
}
// decompressSampleInternal():
auto samplePtr = basePtr + sample.offset_; // :765
grk_read(samplePtr, &boxLen); // :770 unchecked read
...
auto j2kData = samplePtr + boxHeaderSize;
auto j2kLen = sample.samples_size_ - boxHeaderSize; // :782 size unbounded vs file
auto subStream = memStreamCreate(j2kData, j2kLen, ...); // :785 J2K substream reads OOB
grk_read(ptr, &val) (the 2-arg form) is an unchecked memcpy (stream/StreamIO.h:124). A
grep of the file confirms sample.offset_ is never compared against numBytesLeft() / the
stream length anywhere. The only related check (readHeader:731) compares the box length read
at the bogus pointer to samples_size_ — which the attacker also controls — so it does not
bound the offset.
Reproduction
Craft a .mj2 (magic: JP2_RFC3745_MAGIC at 0 + MJ2_MAGIC at 20 → auto-routed to
FileFormatMJ2Decompress) with a valid moov/trak/mdia/minf/stbl chain whose STCO chunk
offset (and/or STSZ sample size) points beyond the file (e.g. offset_ = 0xFFFFFFFF). Run
grk_decompress -i poc.mj2 -o /tmp/out.png. The readHeader loop (line 726/730) over-reads
first; decompressSampleInternal adds an attacker-length J2K substream over OOB memory.
(Runtime ASAN repro not produced in this env — the C++23 codec can’t be built here, see session.)
Root cause
MJ2 sample table offsets/sizes are treated as trusted absolute file positions. There is no
“offset_ + samples_size_ <= stream length” validation between the table parse and the pointer
arithmetic.
Verification evidence
VERIFIED under AddressSanitizer (2026-06-16). grk_decompress -i pocs/grok/GROK-0007/poc.mj2 (a
256-byte MJ2: ftyp(mjp2) + moov/trak/tkhd + mdia/minf/stbl/{stts,stsz,stsc,stco}, with the STCO
chunk offset = 0xFFFFFFFF):
==ERROR: AddressSanitizer: SEGV on unknown address ... (READ memory access)
#0 grk::grk_read<unsigned int>(...) StreamIO.h:53
#1 grk::grk_read<unsigned int>(...) StreamIO.h:127
#2 grk::FileFormatMJ2Decompress::readHeader(...) FileFormatMJ2Decompress.cpp:730
#3 grk::GrkDecompress::main(...) GrkDecompress.cpp:1439
rdx = 0x00000000ffffffff (= sample.offset_, the unvalidated STCO offset)
The grk_read(basePtr + sample.offset_, &len) at line 730 reads basePtr + 0xFFFFFFFF → wild OOB
read (SEGV here; a smaller in-range-ish offset yields a heap over-read that leaks into the J2K
substream). Confirms the missing offset/size validation.
Impact
Heap out-of-bounds read at an attacker-chosen offset, reachable from a single untrusted local
.mj2 via the default grk_decompress path (MJ2 auto-detected). Causes a crash/DoS, and the
attacker-length J2K substream over out-of-bounds memory can decode adjacent heap into the output
image (info-disclosure). Local-untrusted-file reachability. Severity high (CVSS 7.1). Fix:
validate sample.offset_ + sample.samples_size_ <= stream length (and offset_ + 4 before the
box-length probe) before any pointer use.