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

medium harness-verified

Wavelet: unbounded DWT scratch-pool allocation from attacker tile dimension → memory-exhaustion DoS

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 out-of-memory / unbounded allocation (~8.6 GB per thread)
Topmost entry point unknown — establish reachability
Verified through no real consumer named
Real-world impact 6.1 CVSS · medium

Classification

TargetGrok
ComponentWavelet (DWT)
Locationsrc/lib/core/wavelet/WaveletPoolData.cpp · WaveletPoolData::alloc:39-44
Vuln classdos
CVE
CVSS6.1 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H)
Discovered2026-06-16

Verification

Evidence Harness reproduced (not real-world verified)
Harness fired✅ yes
Protocol1.0
Sanitizerasan
Crash typeout-of-memory / unbounded allocation (~8.6 GB per thread)
Reproclang-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 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-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).

References