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

medium harness-verified

BMP reader: heap out-of-bounds read in RLE8/RLE4 decoders (input pointer never bounded by biSizeImage)

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 1)
Topmost entry point unknown — establish reachability
Verified through no real consumer named
Real-world impact 6.5 CVSS · medium

Classification

TargetGrok
ComponentCodec / image-format I/O
Locationsrc/lib/codec/formats/BMPFormat.h · BMPFormat<T>::readRle8Data / readRle4Data:643-718
Vuln classoob-read
CVE
CVSS6.5 (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 1)
Reproclang-20 + libc++ ASAN build; grk_compress -i pocs/grok/GROK-0002/poc.bmp -o /tmp/o.jp2 → ASAN: heap-buffer-overflow READ size 1 at BMPFormat.h:661 (readRle8Data *pixels_ptr++), 0 bytes after the 4-byte 'pixels' alloc (new uint8_t[biSizeImage] at BMPFormat.h:650).

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

Writeup

Summary

The BMP RLE8/RLE4 decoders (BMPFormat.h:643 / :721) read the compressed RLE stream into a heap buffer sized by the attacker-controlled biSizeImage, then walk it with *pixels_ptr++ without ever checking pixels_ptr against the end of the buffer (pixels + biSizeImage). The decode loop terminates only on y >= height or an explicit end-of-bitmap byte (0x01); neither is guaranteed, so a crafted/truncated RLE stream reads past the heap allocation.

auto pixels = new uint8_t[infoHeader_.biSizeImage];          // :650
if(!read(pixels, infoHeader_.biSizeImage)) goto cleanup;     // :651
pixels_ptr = pixels;
while(y < height) {
  int c = *pixels_ptr++;                 // :661  no bound check
  if(c) { uint8_t c1 = *pixels_ptr++;    // :665  no bound check
          ... }
  else { c = *pixels_ptr++;              // :674  no bound check
         ... case 0x02 MOVE: c=*pixels_ptr++; ...; c=*pixels_ptr++;   // :687/:689
         ... default run: c1=*pixels_ptr++; ...; if(c&1) pixels_ptr++; // :698/:704
  }
}

Every output write is guarded (x < width && pix < beyond, lines 666/696), so this is an out-of-bounds read, not a write. The written != width*height check at line 709 fires only after the loop, far too late.

Reproduction

Reachability: BMPFormat<T>::readImage (BMPFormat.h:800) validates width/height nonzero and the stride/bmpStride*height allocation, but never relates biSizeImage to width*height, then dispatches biCompression==1 → readRle8Data (:926) / ==2 → readRle4Data (:931).

Craft a .bmp:

  • Valid "BM" file header + a valid 40-byte BITMAPINFOHEADER (biSize==40).
  • biBitCount = 8, biCompression = 1 (RLE8) — or biBitCount = 4, biCompression = 2.
  • biWidth/biHeight large (e.g. 64×64) so the row counter never reaches height.
  • biSizeImage small (e.g. 2, or even 0), with an RLE payload that contains no end-of-bitmap (0x00 0x01) marker — e.g. a single run opcode. With biSizeImage==0, new uint8_t[0] then *pixels_ptr++ at line 661 over-reads immediately.

Run grk_compress -i poc.bmp -o /tmp/out.jp2 under ASAN to observe heap-buffer-overflow READ.

Root cause

The RLE decoders trust that the encoded stream is self-terminating and large enough, bounding only the output pointer. There is no input-side bound (pixels_ptr < pixels + biSizeImage) on any of the *pixels_ptr++ reads, and biSizeImage is fully attacker-controlled and unrelated to the declared image dimensions.

Verification evidence

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

==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 1
  #0 BMPFormat<int>::readRle8Data(...)  BMPFormat.h:661   (int c = *pixels_ptr++)
  #1 BMPFormat<int>::readImage(...)      BMPFormat.h:926
  #2 grk::loadInputImage<int>(...)       GrkCompress.cpp:296
0x... is located 0 bytes after 4-byte region  (allocated at BMPFormat.h:650: new uint8_t[biSizeImage])

The PoC: 8-bit RLE8 BMP, biSizeImage=4, RLE payload with no end-of-bitmap → the decode loop reads *pixels_ptr++ past the 4-byte pixels heap buffer, exactly as predicted. (Note: the initial crafted PoC omitted the required 8-bit palette and errored at the palette read before reaching readRle8Data; the fixed PoC adds a 2-entry palette — see pocs/grok/gen_grok_pocs.py.)

Impact

Heap out-of-bounds read of attacker-influenced extent, reachable from a single untrusted local .bmp opened by grk_compress. Primary impact is a crash/DoS (read past the heap allocation); limited info-disclosure is possible only for the small fraction of over-read bytes that land in the bounded output before pix reaches beyond. Local-untrusted-file reachability, no special config. Severity medium (CVSS 6.5). Mirrors historical OpenJPEG BMP-RLE over-read CVEs.

References