GROK-0006
high harness-verifiedTileProcessor: use-after-free of an LRU-evicted Tile on re-decompress (reinitForReDecompress)
⚠ Harness reproduced — not real-world verified. Reproduce through the public API, a real application, or a platform decoder before treating this as verified.
grk_decompresspublic API Classification
| Target | Grok |
|---|---|
| Component | Tile processor |
| Location | src/lib/core/tile_processor/TileProcessor.cpp · TileProcessor::reinitForReDecompress / release / getMCT:85,281-303,924-961 |
| Entry point | grk_decompress public API grk_decompress → CodeStreamDecompress::decompress → TileProcessor::reinitForReDecompress → TileProcessor::init → TileComponent::initReachable from untrusted input via the public decompress API, but gated on a non-default decode mode: GRK_TILE_CACHE_LRU (or swath) eviction + re-decompress of the evicted tile, on a multi-component MCT image. Hence CVSS AC:H. |
| Vuln class | use-after-free |
| CVE | — |
| CVSS | 7 (CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/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-use-after-free (READ of size 8, freed 464-byte Tile) |
| Repro | clang-20+libc++ ASAN build. Harness pocs/grok/harness_grok0006.cpp: init with GRK_TILE_CACHE_LRU + max_active_tiles=1, decode multi-tile MCT J2K (pocs/grok/GROK-0006/poc.j2k), set_progression_state(tile 0, different layer count -> dirty), decode again. ASAN heap-use-after-free: freed by Tile::~Tile <- TileProcessor::release (TileProcessor.cpp:930) <- TileCache::evictLRU (TileCache.h:347); used by TileComponent::init (TileComponent.h:281) <- TileProcessor::init (TileProcessor.cpp:401) during reinitForReDecompress. |
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-0006/poc.j2k
Writeup
Summary
TileProcessor::mct_ is constructed once, capturing the current tile_ pointer
(TileProcessor.cpp:85, mct_(new Mct(tile_, ...)); Mct stores a raw Tile* tile_,
point_transform/mct.h:124). When the tile cache releases a tile (release(strategy) /
releaseForSwath), it does delete tile_; tile_ = nullptr (lines 930/959) but never updates
mct_ — so mct_->tile_ now dangles. On re-decompress, reinitForReDecompress allocates a
new tile_ (line 288) but never rebuilds mct_ — mct_->tile_ still points at the freed
original tile. The subsequent multi-component (MCT) inverse pass dereferences getMCT()->tile_,
reading and writing the freed Tile and its component buffers.
// constructor:
mct_(new Mct(tile_, headerImage_, tcp_)), // :85 Mct captures tile_ (raw ptr, mct.h:124)
// release(strategy) — for LRU / swath (anything except GRK_TILE_CACHE_ALL):
delete tile_; tile_ = nullptr; // :930 frees tile_, mct_ untouched → mct_->tile_ dangles
// (releaseForSwath: same at :959-960)
// reinitForReDecompress():
if(tile_) return true;
tile_ = new Tile(headerImage_->numcomps); // :288 new tile, mct_ NOT rebuilt
... init(); // mct_->tile_ STILL points at the freed old tile
Mct* TileProcessor::getMCT(void) { return mct_; } // :919-921 hands out stale mct_
// MCT inverse pass: getMCT()->...->tile_->comps_[compno]... → UAF read+write
Reproduction
Preconditions (all reachable, but not the default whole-image decode):
- A tile-cache mode that calls
releasewithoutGRK_TILE_CACHE_ALL(LRU eviction viaTileCache.h:347 → release(GRK_TILE_CACHE_LRU), orreleaseForSwath), so a decoded tile’stile_is freed while theTileProcessor/mct_live on. - Re-decompression of that evicted tile (
CodeStreamDecompress.cppre-decode/region/swath path →reinitForReDecompress). - MCT active: COD
mct = 1(or 2) with ≥3 components (attacker-controlled via SIZ/COD).
Decode a multi-component MCT image in region/swath or LRU-capped mode that forces eviction then
re-request of a tile. Build with ASAN to observe heap-use-after-free in the MCT pass
(point_transform/mct.cpp, e.g. the tile_->comps_[...] accesses).
Root cause
Mct holds a raw, non-owning Tile* snapshotted at TileProcessor construction. The tile’s
lifetime is independently managed by the cache (release) and by reinitForReDecompress, neither
of which re-points or rebuilds mct_. There is no invariant tying mct_->tile_ to the current
tile_.
Verification evidence
Confirmed by source trace: mct_ built once with tile_ (line 85, raw ptr at mct.h:124);
release/releaseForSwath free tile_ without touching mct_ (930/959); reinitForReDecompress
makes a new tile_ without rebuilding mct_ (288); getMCT() returns the stale mct_ (921).
VERIFIED under AddressSanitizer (2026-06-16) via the API harness pocs/grok/harness_grok0006.cpp
(init GRK_TILE_CACHE_LRU + max_active_tiles=1; decode pocs/grok/GROK-0006/poc.j2k — a 4-tile, 3-comp
MCT/ICT image; grk_decompress_set_progression_state(tile 0, 1 layer) to mark it dirty; decode again):
==ERROR: AddressSanitizer: heap-use-after-free READ of size 8 (40 bytes into a freed 464-byte Tile)
freed by:
Tile::~Tile() Tile.h:50
grk::TileProcessor::release(unsigned int) TileProcessor.cpp:930 (delete tile_)
grk::TileCache::evictLRU() TileCache.h:347
grk::TileCache::put(...) / getTileProcessor CodeStreamDecompress.cpp:2448
used by (pass-2 re-decode):
grk::TileComponent::init(...) TileComponent.h:281
grk::TileProcessor::init() TileProcessor.cpp:401
grk::TileProcessor::prepareForDecompression()::lambda TileProcessor.cpp:903 (reinit path)
The LRU eviction frees the Tile at the exact predicted line (TileProcessor.cpp:930), and the
re-decompress (reinitForReDecompress → init) then reads the freed Tile. (The crash lands in
TileComponent::init during the re-init, i.e. a stale per-Tile reference is dereferenced even
before the MCT pass — so the dangling-Tile problem is broader than just mct_->tile_; mct_ is
also stale per the source trace above. Same root cause and fix.)
Impact
Heap use-after-free (read and write) of a freed Tile and its component data during the MCT
inverse transform, reachable from untrusted input once tile caching / re-decompression is in use
(region/swath/LRU decode) on a multi-component MCT image. A write-capable UAF is an
exploitation-grade primitive (rubric: UAF reachable with untrusted input ⇒ high). Reachability is
gated on a non-default decode mode (hence AC:H). Severity high (CVSS 7.0). Fix: rebuild or
re-point mct_ in reinitForReDecompress (and null/refresh it in release), or have Mct read
tile_ from the owning TileProcessor rather than snapshotting it.