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

high harness-verified

JPEG reader: stack buffer overflow on CMYK/YCCK (4-component) JPEG — fixed [3] arrays indexed by output_components

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 stack-buffer-overflow (WRITE)
Topmost entry point unknown — establish reachability
Verified through no real consumer named
Real-world impact 7.8 CVSS · high

Classification

TargetGrok
ComponentCodec / image-format I/O
Locationsrc/lib/codec/formats/JPEGFormat.h · JPEGFormat<T>::jpegtoimage:133,139,235-244,300-303,343-347
Vuln classoob-write
CVE
CVSS7.8 (CVSS:3.1/AV:L/AC:L/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 typestack-buffer-overflow (WRITE)
Reproclang-20 + libc++ ASAN build (system libjpeg auto-linked via find_package); grk_compress -i pocs/grok/GROK-0004/poc.jpg -o /tmp/o.jp2 → ASAN stack-buffer-overflow WRITE at JPEGFormat<int>::jpegtoimage (JPEGFormat.h:237) — the cmptparm[j] loop with a 4-component CMYK JPEG (output_components==4) overruns the [3] stack array.

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-0004/poc.jpg

Writeup

Summary

grk_compress decodes an untrusted JPEG via libjpeg and copies its planes into a grk_image. JPEGFormat<T>::jpegtoimage declares fixed 3-element stack arrays for the component params and plane pointers, but then loops up to cinfo.output_components, which is 4 for a CMYK/YCCK JPEG (libjpeg’s default out_color_space for an Adobe 4-channel file). The only header gate is bps != 8; nothing rejects >3 components. The 4th iteration writes one element past each stack array, and the de-interleave then writes through the clobbered/garbage plane pointer for every pixel.

T* planes[3];                                    // :133  3-element stack array
grk_image_comp cmptparm[3]; /* mono or RGB */    // :139  3-element stack array
...
bps = cinfo.data_precision;
if(bps != 8) { ...error... }                     // :219  ONLY guard; no component-count check
decompress_num_comps = (uint16_t)cinfo.output_components;   // :226  (=4 for CMYK)
...
for(int j = 0; j < cinfo.output_components; j++) {          // :235  j reaches 3
  cmptparm[j].prec = (uint8_t)bps; ... cmptparm[j].w = w;   // :237-243  cmptparm[3] OOB write
}
image_ = grk_image_new(decompress_num_comps, &cmptparm[0], ...);  // :246  reads cmptparm[3] OOB
...
for(int j=0;j<decompress_num_comps;j++) planes[j] = ...;   // :300-303 planes[3] OOB write
interleave(buffer32s, planes, w, decompress_num_comps);    // :343-347 wild write via planes[3]

libjpeg never has its out_color_space overridden here (Step 4 is a no-op, :205-209), so a CMYK/YCCK input yields output_components == 4 and cmptparm/planes are overrun.

Reproduction

Reachability: GrkCompressloadInputImage<int32_t>JPEGFormat<int32_t>::jpegtoimage (requires the codec built with JPEG support — GRK_BUILD_JPEG defaults ON).

Create a CMYK (or YCCK) baseline 8-bit JPEG (e.g. ImageMagick convert in.png -colorspace CMYK cmyk.jpg, which embeds an Adobe APP14 marker → libjpeg reports 4 components). Run grk_compress -i cmyk.jpg -o /tmp/out.jp2.

PoC build note (this environment could not build the C++23 codec for a runtime ASAN repro — clang-20 here lacks <concepts>/libc++; needs libc++-20-dev or gcc ≥ 13). Build with -fsanitize=address to observe the stack-buffer-overflow WRITE at the cmptparm[j]/planes[j] loop.

Root cause

Component-param and plane-pointer arrays are hard-coded [3] (“mono or RGB”) while the loop bound and grk_image_new count come straight from libjpeg’s output_components, which is 4 for 4-channel JPEGs. No output_components <= 3 guard, and out_color_space is left at libjpeg’s default instead of being forced to RGB/grayscale.

Verification evidence

VERIFIED under AddressSanitizer (2026-06-16, system libjpeg auto-linked via find_package(JPEG)). grk_compress -i pocs/grok/GROK-0004/poc.jpg -o /tmp/o.jp2 (a 32×32 CMYK JPEG, output_components==4):

==ERROR: AddressSanitizer: stack-buffer-overflow ... WRITE of size 1
  #0 JPEGFormat<int>::jpegtoimage(...)   JPEGFormat.h:237   (cmptparm[j].prec = ...)
  #1 JPEGFormat<int>::readImage(...)     JPEGFormat.h:687
  #2 grk::loadInputImage<int>(...)       GrkCompress.cpp:326
Address ... in stack of frame JPEGFormat<int>::jpegtoimage (JPEGFormat.h:132)

The 4th component (j==3) writes past the 3-element cmptparm/planes stack arrays exactly as predicted.

Impact

Stack buffer overflow (write) plus a wild write through an attacker-influenced plane pointer, reachable from a single untrusted local CMYK/YCCK .jpg opened by grk_compress (default codec config). Controllable stack corruption with RCE potential; guaranteed crash at minimum. Local-untrusted-file reachability. Severity high (CVSS 7.8). Fix: reject output_components > 3, or force cinfo.out_color_space = JCS_RGB/JCS_GRAYSCALE before jpeg_start_decompress.

References