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-0005

medium harness-verified

TIFF reader: heap out-of-bounds read when component count (photometric+extrasamples) exceeds SamplesPerPixel

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 (READ of size 32)
Topmost entry point unknown — establish reachability
Verified through no real consumer named
Real-world impact 6.1 CVSS · medium

Classification

TargetGrok
ComponentCodec / image-format I/O
Locationsrc/lib/codec/formats/TIFFFormat.h · TIFFFormat<T>::readTiffPixels / readImage:1531,1572-1610,1944-2026
Vuln classoob-read
CVE
CVSS6.1 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H)
Discovered2026-06-16

Verification

Evidence Harness reproduced (not real-world verified)
Harness fired✅ yes
Protocol1.0
Sanitizerasan
Crash typeheap-buffer-overflow (READ of size 32)
Reproclang-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 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-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.

References