GROK-0005
medium harness-verifiedTIFF reader: heap out-of-bounds read when component count (photometric+extrasamples) exceeds SamplesPerPixel
⚠ 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 | Codec / image-format I/O |
| Location | src/lib/codec/formats/TIFFFormat.h · TIFFFormat<T>::readTiffPixels / readImage:1531,1572-1610,1944-2026 |
| Vuln class | oob-read |
| CVE | — |
| CVSS | 6.1 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/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 | heap-buffer-overflow (READ of size 32) |
| Repro | clang-20 + libc++ ASAN build (system libtiff auto-linked); RGB TIFF with SamplesPerPixel patched 3->2; grk_compress -i pocs/grok/GROK-0005/poc.tif -o /tmp/o.jp2 → ASAN heap-buffer-overflow READ in Hwy_deinterleave_i32 (GrkImageSIMD.cpp:614) <- interleave (convert.h:79) <- readTiffPixels (TIFFFormat.h:1610); the 3-component deinterleave reads past the w*tiSpp(=2) scratch buffer. |
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-0005/poc.tif
Writeup
Summary
In the TIFF reader, the per-row scratch buffer is sized from SamplesPerPixel (tiSpp), but the
chunky de-interleave reads width * numcomps interleaved samples, where numcomps is computed
independently as color_channels(photometric) + extrasamples. These two TIFF-tag-derived values
are never cross-checked, so a TIFF declaring colorchannels + extrasamples > SamplesPerPixel
(with SamplesPerPixel ≥ 2, chunky/PLANARCONFIG_CONTIG) makes the de-interleave read past the
scratch buffer.
// readImage(): numcomps = color_channels(photometric) + extrasamples (lines 1944-2026)
// tiSpp read separately (line ~1832); NO check numcomps == tiSpp
// readTiffPixels():
buffer32s = new T[comp[0].w * tiSpp]; // :1531 sized by tiSpp
pixelCount = comp->w * tiSpp; // :1572
targetPlanes = (tiSpp==1) ? 1 : numcomps; // :1575
interleave(buffer32s, planes, w, targetPlanes); // :1610 reads w*numcomps from a w*tiSpp buf
interleave() → hwy_deinterleave_i32 reads src[i*numComps + j]; with numcomps > tiSpp it
reads (numcomps - tiSpp) * w samples per row beyond the w*tiSpp allocation. The over-read
bytes are written into the (validly-allocated) numcomps destination planes (info-leak) until
the read runs off the heap chunk and crashes.
Reproduction
Craft a TIFF with PHOTOMETRIC_RGB (3 color channels), SamplesPerPixel = 3, but an
EXTRASAMPLES tag listing 2 entries → numcomps = 5 > tiSpp = 3, PLANARCONFIG_CONTIG,
8/16-bit. Run grk_compress -i poc.tif -o /tmp/out.jp2 under ASAN (requires codec built with
GRK_BUILD_LIBTIFF).
Root cause
numcomps (photometric color channels + extrasamples) and tiSpp (SamplesPerPixel) are derived
from separate TIFF tags and never reconciled; the scratch buffer is sized by one and walked by the
other.
Verification evidence
Reachability confirmed by source trace: TIFFFormat::readImage (lines 1789-2263) computes
numcomps = extrasamples + colorchannels(photometric) (1944 + switch) independently of tiSpp
(read at 1832) and passes BOTH to readTiffPixels(..., numcomps, tiSpp, ...) (2243) with no
numcomps == tiSpp check anywhere (the only related check is the YCbCr-subsampled-only
numcomps != 3 at 2009, which doesn’t apply to plain chunky RGB). Grok reads
SamplesPerPixel/Photometric/ExtraSamples as independent tags and does its own (absent)
validation, so it does not rely on libtiff to reconcile them. Over-read site: buffer
new T[w*tiSpp] (1531) vs interleave(..., comp->w, numcomps) (1654) → convert.h:90-93 reads
w*numcomps source samples → w*(numcomps - tiSpp) over-read per row. Minimal trigger needs
tiSpp >= 2 (tiSpp==1 forces targetPlanes=1) and chunky (PLANARCONFIG_SEPARATE forces tiSpp=1,
safe).
Verification evidence (ASAN)
VERIFIED (2026-06-16, system libtiff auto-linked):
==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 32
#0 hwy::N_AVX2::LoadU<...> (LoadInterleaved3)
#3 grk::N_AVX2::Hwy_deinterleave_i32(...) GrkImageSIMD.cpp:614
#4 interleave<int>(...) convert.h:79
#5 TIFFFormat<int>::readTiffPixels(...)::lambda TIFFFormat.h:1610
The 3-component (numcomps) LoadInterleaved3 deinterleave reads past the w*tiSpp(=2) scratch
buffer — confirms the numcomps > tiSpp mismatch.
Impact
Heap out-of-bounds read reachable from an untrusted local .tif opened by grk_compress (e.g.
PHOTOMETRIC_RGB + SamplesPerPixel=2 + chunky → numcomps=3 > tiSpp=2), yielding info-disclosure
of adjacent heap into the converted image and/or a crash. Local-untrusted-file reachability, no
non-default config. Severity medium (CVSS 6.1). Fix: validate numcomps == tiSpp (chunky)
before the pixel readers.