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

high harness-verified

TileProcessor: use-after-free of an LRU-evicted Tile on re-decompress (reinitForReDecompress)

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-use-after-free (READ of size 8, freed 464-byte Tile)
Topmost entry point grk_decompresspublic API
Verified through no real consumer named
Real-world impact 7 CVSS · high

Classification

TargetGrok
ComponentTile processor
Locationsrc/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::init
Reachable 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 classuse-after-free
CVE
CVSS7 (CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H)
Discovered2026-06-16

Verification

Evidence Harness reproduced (not real-world verified)
Harness fired✅ yes
Protocol1.0
Sanitizerasan
Crash typeheap-use-after-free (READ of size 8, freed 464-byte Tile)
Reproclang-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 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-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):

  1. A tile-cache mode that calls release without GRK_TILE_CACHE_ALL (LRU eviction via TileCache.h:347 → release(GRK_TILE_CACHE_LRU), or releaseForSwath), so a decoded tile’s tile_ is freed while the TileProcessor/mct_ live on.
  2. Re-decompression of that evicted tile (CodeStreamDecompress.cpp re-decode/region/swath path → reinitForReDecompress).
  3. 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 (reinitForReDecompressinit) 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.

References