
Proof-of-concept for CVE-2026-84118, a SpiderMonkey GC use-after-free leading to out-of-bounds read/write and potential code execution. Includes deterministic SEGV trigger and research on escalation to arbitrary read and ASLR defeat.
This is one of those where Mozilla labeled a Critical as a High. So someone might want to go have Firefox update the original bug hunters payment if that's the case.
THE TLDR: You can do an OOB R primitive on a controlled pointer and then start going up and down from that pointer. So if you point the controlled pointer at an array then congrats you can do fun things.
The POC only has the SEGV part, so the rest is a fun adventure into pointing into an array and jumping into WASM of your choosing.
Deterministic release-build SEGV proof-of-concept for Firefox/SpiderMonkey 154.0.1. Fixed in 155 (AtomMarking rework). Mozilla severity: HIGH.
/path/to/release/js poc.js
Reproduced on 154.0.1: 3/3 on a plain optimized shell (SIGSEGV, exit 139), 5/5 on a release+gczeal shell, identical fault every run:
SEGV on address 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, +0x14 offset
154 conflates per-zone atom mark bitmaps with reference tracking. A
Symbol held only weakly — here, captured as a weak Value field inside
a Baseline CacheIR IC stub — and referenced from an uncollected zone is
never re-marked when the atoms zone is collected in a zone-scoped GC. The
result is a stale edge to a swept/relocated cell.
The release-build kill shot is a second, static bug:
MightBeForwarded<JS::Symbol> is hardcoded false, so
IsForwarded returns false without checking (gc/Marking-inl.h:92-97) and
every pointer-fixup path gated on it silently skips the edge. When the
atoms zone is compacted, strong edges are updated correctly, but this
weak CacheIR edge is left permanently dangling at the victim Symbol's old
address. (Debug builds catch it as Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94.)
The PoC does not just leave a stale pointer to reusable heap — it gets the backing page munmapped, so the very next dereference faults.
Zone::availableChunks/fullChunks), so atoms-zone chunks contain only
atoms-zone arenas. A chunk's fate can be controlled purely by
atom/symbol lifetimes.c.eval(b) makes the top-level call IC capture the
Symbol in a weak stub field; b = undefined drops the last strong
root; the spray is dropped; two zone-scoped GCs
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
the empty string schedules the atoms zone) sweep the dead spray,
compact the victim into the magnet arena, skip the weak-edge fixup,
and free the victim's arena. Its chunk is now completely empty.gcparam("minEmptyChunkCount", 0) makes every empty chunk
expirable; during sleep(0.5) the BackgroundDecommitTask runs
-> -> — a true
munmap of the victim chunk. (Arena-level decommit is only
— readable zeros — whole-chunk release is the
only munmap path, which is why chunk isolation in step 1 matters.)This is not just a DoS. Demonstrated escalations (PoCs and logs in the
GC-NDAY/ and CHAIN/ research directories):
segv9): after the munmap, a
minorgc() semispace flip makes the nursery claim the freed VA; a raw
dense-double spray writes an attacker qword over the stale arena
header's zone field. The sweep then dereferences
attacker_ptr + 0x14 — deterministic 3/3 at 0x424242424256.js::Zone* leaked via a separate speculative
side channel was planted through this refill; the collector consumed the
forged arena metadata without faulting (a 1-bit-perturbed control
plant crashes), demonstrating ASLR-defeat -> UAF-plant composition.expireEmptyChunkPoolFreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() sweeps JIT data:
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub reads the stale Symbol pointer, then
SweepingTracer::onEdge (gc/Marking.cpp:3195) performs chunk
arithmetic on the stale address to load the arena header's zone
field. The page is gone/zeroed, so zone == nullptr, and
zone->isGCSweeping() reads nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. At the fault, rcx holds the stale Symbol pointer and
rdx the munmapped chunk base.