GROK-0002
medium harness-verifiedBMP reader: heap out-of-bounds read in RLE8/RLE4 decoders (input pointer never bounded by biSizeImage)
⚠ 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/BMPFormat.h · BMPFormat<T>::readRle8Data / readRle4Data:643-718 |
| Vuln class | oob-read |
| CVE | — |
| CVSS | 6.5 (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 1) |
| Repro | clang-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 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-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-byteBITMAPINFOHEADER(biSize==40). biBitCount = 8,biCompression = 1(RLE8) — orbiBitCount = 4,biCompression = 2.biWidth/biHeightlarge (e.g. 64×64) so the row counter never reachesheight.biSizeImagesmall (e.g.2, or even0), with an RLE payload that contains no end-of-bitmap (0x00 0x01) marker — e.g. a single run opcode. WithbiSizeImage==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.