PUBLIC MIRROR A read-only public view of Anvil. Only publicly-disclosed findings are shown; the Playbook, techniques, sessions and embargoed research are hidden.

← Findings

GROK-0010

high harness-verified

Decompress strip composite: first-tile-row buffer under-allocated vs interior tile-row height → heap OOB write

Harness reproduced Crashes in a custom/AI-generated harness under a sanitizer. A TEST ARTIFACT — not real-world verification.

⚠ Harness reproduced — not real-world verified. Reproduce through the public API, a real application, or a platform decoder before treating this as verified.

Crash heap-buffer-overflow (WRITE of size 32)
Topmost entry point unknown — establish reachability
Verified through no real consumer named
Real-world impact 7 CVSS · high

Classification

TargetGrok
ComponentCore utilities
Locationsrc/lib/core/util/GrkImage.h · compositePlanar (alloc in CodeStreamDecompress::activateScratch):GrkImage.h:957-963; CodeStreamDecompress.cpp:2407-2420,343-351,315
Vuln classoob-write
CVE
CVSS7 (CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H)
Discovered2026-06-16

Verification

Evidence Harness reproduced (not real-world verified)
Harness fired✅ yes
Protocol1.0
Sanitizerasan
Crash typeheap-buffer-overflow (WRITE of size 32)
Reproclang-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 toGrok maintainers — support@grokcompression.com (private email; repo has no Private Vulnerability Reporting / SECURITY.md)
Reported2026-06-23
Vendor ack
Embargo until
Public2026-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=1H0=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.

References