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

high harness-verified

BMP reader: stack buffer overflow in readInfoHeader (biSize read into fixed buffer before validation)

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 of size 512)
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/BMPFormat.h · BMPFormat<T>::readInfoHeader:537-541
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 of size 512)
Reproclang-20 + libc++ ASAN build (-DGRK_ENABLE_ASAN=ON); grk_compress -i pocs/grok/GROK-0001/poc.bmp -o /tmp/o.jp2 → ASAN: stack-buffer-overflow WRITE size 512 in fread <- FileStandardIO::read (FileStandardIO.cpp:83) <- BMPFormat<int>::readInfoHeader (BMPFormat.h:540), overflowing 'temp' (line 538).

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-0001/poc.bmp

Writeup

Summary

grk_compress accepts an untrusted input image and auto-dispatches by extension/magic to the BMP reader. BMPFormat<T>::readInfoHeader (src/lib/codec/formats/BMPFormat.h:537) uses the attacker-controlled DIB-header-size field biSize to size a read() into a fixed 124-byte stack buffer before validating biSize, giving an attacker-controlled stack buffer overflow with attacker-controlled length and content.

// readFileHeader() — biSize read raw from file offset 14, NO validation:
get_int(&temp_ptr, &infoHeader->biSize);                 // BMPFormat.h:528

// readInfoHeader():
const size_t len_initial = infoHeader->biSize - sizeof(uint32_t);   // :537  (size_t)
uint8_t temp[sizeof(GRK_BITMAPINFOHEADER)];               // :538  124 bytes
if(!read(temp, len_initial))                              // :540  fread(temp,1,len_initial,fh)
  return false;
switch(infoHeader->biSize) { ...valid sizes... default: return false; }  // :543 — TOO LATE

read() resolves to ImageFormat::readFileStandardIO::readfread(buf, 1, len, fh) (fileio/FileStandardIO.cpp:83) with no clamp of len to the destination buffer. The switch that rejects unknown biSize values runs only after the read.

Two trigger classes:

  • biSize ∈ {0,1,2,3}len_initial = biSize - 4 underflows the size_t to ~0xFFFF…FFFCfread pours the entire remaining file into temp[124] (unbounded, fully attacker-controlled overflow).
  • biSize > 124 but not a known header constant (e.g. 200) → len_initial = 196 > 124 → a 72-byte stack smash before the switch ever rejects the value.

Reproduction

Reachability (all auto, no special flags): GrkCompressloadInputImage<int32_t>BMPFormat<int32_t>::readImage (BMPFormat.h:800) → readFileHeader (:825) → readInfoHeader (:827).

Craft a .bmp:

  • Bytes 0-1: "BM" (bfType == 19778, required by readFileHeader).
  • Bytes 2-13: any bfSize/reserved/bfOffBits.
  • Bytes 14-17 (biSize): 00 00 00 00 (value 0) — or C8 00 00 00 (200).
  • Then ≥ ~200 bytes of attacker-controlled padding (for the 124-byte buffer to be overrun; for the biSize==0 case, as many bytes as desired — the whole tail lands on the stack).

Run grk_compress -i poc.bmp -o /tmp/out.jp2 (build with -fsanitize=address to observe the stack-buffer-overflow WRITE).

Root cause

The DIB header size is consumed as a read length before being validated against the set of legal header sizes, and the underlying read() performs an unclamped fread into a fixed-size stack buffer. Both the underflow (small biSize) and the over-length (biSize > sizeof(GRK_BITMAPINFOHEADER)) cases escape the only sanity check, which is ordered after the copy.

Verification evidence

VERIFIED under AddressSanitizer (2026-06-16, clang-20 + libc++ ASAN build, -DGRK_ENABLE_ASAN=ON). grk_compress -i pocs/grok/GROK-0001/poc.bmp -o /tmp/o.jp2:

==ERROR: AddressSanitizer: stack-buffer-overflow ... WRITE of size 512
  #0 fread
  #1 FileStandardIO::read(unsigned char*, unsigned long)  FileStandardIO.cpp:83
  #2 BMPFormat<int>::readInfoHeader(...)                  BMPFormat.h:540
  #3 BMPFormat<int>::readImage(...)                       BMPFormat.h:827
  #4 grk::loadInputImage<int>(...)                        GrkCompress.cpp:296
  ...
This frame has 3 object(s):
    [160, 284) 'temp' (line 538) <== Memory access at offset 284 overflows this variable

The 512-byte fread (attacker-controlled length & content, here from biSize=0) overflows the 124-byte stack temp exactly as predicted. sizeof(GRK_BITMAPINFOHEADER) == 124.

Impact

Stack buffer overflow writable with attacker-controlled length and content, reachable from a single untrusted local .bmp opened by grk_compress (no non-default config). This is a classic return-address-overwrite primitive (stack canaries/ASLR notwithstanding) with RCE potential, and a guaranteed crash/DoS at minimum. Reachability is local-untrusted-file (user converts a received image). Severity high (CVSS 7.8); treat as the top-priority fix on this target. Same bug family as historical OpenJPEG BMP-reader CVEs.

References