GROK-0014
medium harness-verifiedWavelet: unbounded DWT scratch-pool allocation from attacker tile dimension → memory-exhaustion DoS
⚠ 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 | Wavelet (DWT) |
| Location | src/lib/core/wavelet/WaveletPoolData.cpp · WaveletPoolData::alloc:39-44 |
| Vuln class | dos |
| CVE | — |
| CVSS | 6.1 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/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 | out-of-memory / unbounded allocation (~8.6 GB per thread) |
| Repro | clang-20+libc++ libFuzzer+ASAN build (pocs/grok/fuzz_setup.sh). grk_decompress on a 916-byte crafted J2K (pocs/grok/GROK-0014/poc.j2k) → libFuzzer out-of-memory: malloc(8589937152). With ASAN_OPTIONS=max_allocation_size_mb=2048 the abort stack is: aligned_alloc <- MemoryManager::aligned_malloc (MemManager.h:142) <- grk_aligned_malloc <- WaveletPoolData::alloc (WaveletPoolData.cpp:44) <- WaveletReverse::decompress (WaveletReverse.cpp:2091) <- DecompressScheduler::scheduleT1. |
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-0014/poc.j2k
Writeup
Summary
The inverse-DWT scratch pool sizes its per-thread buffers from maxDim (the larger of the tile
width/height) with no upper bound and no validation against the actual image/tile data size:
size_t buffer_size = maxDim; // WaveletPoolData.cpp:39 maxDim = max(t_width_, t_height_)
auto multiplier = std::max(sizeof(int32_t)*get_PLL_COLS_53(), sizeof(vec4f));
buffer_size *= multiplier; // :41
for(size_t i = 0; i < num_threads; ++i) {
void* horiz_ptr = grk_aligned_malloc(buffer_size); // :44 unbounded, x2, x num_threads
void* vert_ptr = grk_aligned_malloc(buffer_size);
}
maxDim derives from SIZ XTsiz/YTsiz (tile dimensions). SIZ caps the tile count (≤65535) but
not the tile size, so a crafted codestream with a huge tile dimension makes buffer_size enormous
and allocates 2 × num_threads × buffer_size. A tiny input thus drives a multi-gigabyte allocation
→ memory-exhaustion DoS (OOM-kill). This is the runtime confirmation of the WaveletPoolData::alloc
sizing weakness noted during the wavelet audit (its sibling dwt_scratch::alloc has an overflow
guard; this path has none).
Reproduction
VERIFIED via fuzzing. pocs/grok/GROK-0014/poc.j2k (916 bytes), found by the coverage-guided
fuzz harness (pocs/grok/fuzz_setup.sh). grk_decompress -i pocs/grok/GROK-0014/poc.j2k -o /tmp/o.png
→ ~8.6 GB allocation. Under ASAN with max_allocation_size_mb=2048 it aborts cleanly with the stack
below; with no cap it OOM-kills the process. A subsequent 3-hour, 12-worker fork-mode campaign
re-found this site in ~1,160 of 1,171 crash/OOM artifacts (≈99%) — it is by far the most easily
reachable defect in the decoder.
Root cause
DWT scratch-pool buffer size is computed from the attacker-controlled tile dimension with no cap and no check against the bytes actually present, and is multiplied across threads.
Verification evidence
==ERROR: AddressSanitizer: requested allocation size 0x200000a00 (~8.6 GB) exceeds maximum supported size
#0 aligned_alloc
#1 grk::MemoryManager::aligned_malloc(...) util/MemManager.h:142
#3 grk::grk_aligned_malloc(...) util/MemManager.h:325
#4 grk::WaveletPoolData::alloc(...) wavelet/WaveletPoolData.cpp:44
#5 grk::WaveletReverse::decompress() wavelet/WaveletReverse.cpp:2091
#6 grk::DecompressScheduler::scheduleT1(...) scheduling/standard/DecompressScheduler.cpp:362
(libFuzzer also reports it directly: out-of-memory (malloc(8589937152)).)
Impact
Memory-exhaustion denial of service from a tiny untrusted J2K via the default grk_decompress
decode path — large amplification (≈900 bytes → many GB), multiplied by 2 × num_threads. Reliable
OOM-kill; no memory corruption. Local-untrusted-file reachability, no special config. Severity
medium (CVSS 6.1). Fix: bound maxDim/buffer_size against the tile/stream data size (and add
the overflow guard its sibling dwt_scratch::alloc already has).