GROK-0010
high harness-verifiedDecompress strip composite: first-tile-row buffer under-allocated vs interior tile-row height → heap OOB write
⚠ 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 | Core utilities |
| Location | src/lib/core/util/GrkImage.h · compositePlanar (alloc in CodeStreamDecompress::activateScratch):GrkImage.h:957-963; CodeStreamDecompress.cpp:2407-2420,343-351,315 |
| Vuln class | oob-write |
| CVE | — |
| CVSS | 7 (CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H) |
| Discovered | 2026-06-16 |
Verification
| Evidence | Harness reproduced (not real-world verified) |
|---|---|
| Harness fired | ✅ yes |
| Protocol | 1.0 |
| Sanitizer | asan |
| Crash type | heap-buffer-overflow (WRITE of size 32) |
| Repro | clang-20 + libc++ ASAN build. Make tiled J2K (odd YTOsiz=1, odd tile height=3, 2 tile rows): grk_compress -i in.pgm -o poc.j2k -t 32,3 -d 0,1 -T 0,1. Then grk_decompress -i poc.j2k -o /tmp/out.pgm -r 1 → ASAN heap-buffer-overflow WRITE in GrkImage::compositePlanar<short> (GrkImage.h:959) <- CodeStreamDecompress.cpp:315 <- TileCompletion::complete. Default decompress output path (band callback); no env vars. |
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-0010/poc.j2k
Writeup
Summary
In the streaming band-callback decode path, the per-strip composite buffer is allocated once,
sized for the first tile-row’s reduced height H0, but the drain loop then mutates
comp->h to each subsequent tile-row’s height HN without reallocating. When the tile origin
YTOsiz is not a multiple of dy·2^reduce, ceil-rounding makes an interior row HN > H0, so
compositePlanar writes HN rows into the H0-sized buffer → heap OOB write.
// activateScratch (band-callback branch): buffer sized for first slated tile-row
H0 = ceildivpow2(ceildiv(ty0_ + t_height_, dy), reduce) - ceildivpow2(ceildiv(ty0_, dy), reduce);
allocData(comp); // CodeStreamDecompress.cpp:2407-2420 -> stride*H0
// drain lambda, per subsequent tile-row N: enlarges comp->h, NO realloc
comp->h = HN; // :350
scratchImage_->composite(tileImage); // :315 -> compositePlanar
// compositePlanar: destCompRect uses the enlarged comp->h, intersection does NOT clamp to H0
for(j=0; j<destWin.height(=HN); ++j) { memcpy(dest + j*stride, ...); } // GrkImage.h:957-963
For ty0_ not aligned to dy·2^reduce (e.g. YTOsiz=1, YTsiz=3, reduce=1 → H0=1, HN=2), rows
j>=H0 write past the stride*H0 allocation. No allocated-height clamp / empty() guard exists in
compositePlanar/compositeInterleaved.
Reproduction (VERIFIED — band-callback path is the default)
The band-callback (incremental strip) output is set by default in grk_decompress
(GrkDecompress.cpp:1152) whenever output is to a file, post-processing is a no-op, there is no
decode region, and the format supportsIncrementalBandWrite() (PGM/PNM/TIFF…). It is NOT
env-gated (contrast GROK-0008).
grk_compress -i in.pgm -o poc.j2k -t 32,3 -d 0,1 -T 0,1 # YOsiz=YTOsiz=1 (odd), tile h=3 (odd), 2 rows
grk_decompress -i poc.j2k -o /tmp/out.pgm -r 1 # reduce=1 -> H0=1 < H1=2 -> OOB write
PoC: pocs/grok/GROK-0010/poc.j2k (gen: pocs/grok/gen_grok0010_poc.sh). Trigger requires reduce >= 1
(the -r flag) — or, equivalently, a chroma-subsampled component (dy > 1) at reduce 0, since the
mismatch comes from the ceildiv(.,dy) / ceildivpow2(.,reduce) rounding. Hence AC:H (the victim
must decode at reduced resolution, a common thumbnail/preview operation).
Root cause
The strip buffer is sized for the first tile-row only, but comp->h is advanced per tile-row
without reallocation, and the composite copy is not clamped to the allocated height.
Verification evidence
VERIFIED under AddressSanitizer (2026-06-16) on the stock grk_decompress (no env vars):
==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 32
#0 __asan_memcpy
#1 grk::GrkImage::compositePlanar<short>(...) util/GrkImage.h:959
#2 CodeStreamDecompress::decompress(...)::$_1::operator() CodeStreamDecompress.cpp:315
#10 grk::TileCompletion::complete(unsigned short) TileCompletion.h:159
WRITE of size 32 = the 32-px tile row written into the first-tile-row-sized strip buffer; the
band-callback composite path is the default. Confirms the H0 < HN under-allocation exactly as
predicted.
Impact
Attacker-controlled heap out-of-bounds write of decoded pixel rows past the strip buffer,
reachable on the default grk_decompress file-output path (band-callback composite) from a
crafted J2K with misaligned tile origin + odd tile height, when decoded at reduced resolution
(-r >= 1) — or with a chroma-subsampled component at full resolution. A controllable heap write is
an exploitation-grade primitive; the reduce/subsampling precondition (a common preview/thumbnail
operation) is the only mitigant, hence AC:H. Local-untrusted-file. Severity high (CVSS 7.0).
Fix: size the strip buffer to the max reduced tile-row height (or realloc on advance) and clamp
compositePlanar/compositeInterleaved to the allocated height with an empty()/valid() guard.