
Proof-of-Concept für CVE-2026-84118, eine SpiderMonkey-GC-Use-after-Free, die zu Out-of-Bounds-Lesen/-Schreiben und potenzieller Codeausführung führt. Enthält einen deterministischen SEGV-Trigger und Forschung zur Eskalation zu beliebigem Lesen und zur Überwindung von ASLR.
Dies ist einer dieser Fälle, in denen Mozilla eine Critical als High eingestuft hat. Falls das so ist, sollte also vielleicht jemand Firefox dazu bringen, die ursprünglichen Bug-Jäger entsprechend zu bezahlen.
DIE KURZFASSUNG: Man kann ein OOB-R-Primitiv auf einen kontrollierten Zeiger ausführen und dann von diesem Zeiger aus auf- und abwärts navigieren. Wenn man den kontrollierten Zeiger also auf ein Array richtet, kann man lustige Dinge anstellen.
Der PoC enthält nur den SEGV-Teil, der Rest ist ein unterhaltsames Abenteuer, bei dem man in ein Array zeigt und nach Belieben in WASM springt.
Deterministischer Release-Build-SEGV-Proof-of-Concept für Firefox/SpiderMonkey 154.0.1. Behoben in 155 (AtomMarking-Überarbeitung). Mozilla-Schweregrad: HOCH.
/path/to/release/js poc.js
Reproduziert auf 154.0.1: 3/3 auf einer einfachen optimierten Shell (SIGSEGV, Exit 139), 5/5 auf einer Release+gczeal-Shell, bei jedem Lauf identischer Fehler:
SEGV auf Adresse 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, +0x14-Offset
154 vermischt die pro-Zone-Atom-Markierungs-Bitmaps mit der Referenz-Verfolgung. Ein
Symbol, das nur schwach gehalten wird — hier erfasst als schwaches Value-Feld innerhalb
eines Baseline-CacheIR-IC-Stubs — und aus einer nicht eingesammelten Zone referenziert wird,
wird nie neu markiert, wenn die Atomen-Zone in einem zonengebundenen GC eingesammelt wird. Das
Ergebnis ist eine veraltete Kante zu einer ausgeräumten/verschobenen Zelle.
Der Kill-Shot im Release-Build ist ein zweiter, statischer Bug:
MightBeForwarded<JS::Symbol> ist hartkodiert auf false, sodass
IsForwarded false zurückgibt ohne zu prüfen (gc/Marking-inl.h:92-97) und
jeder darauf basierende Zeiger-Fixup-Pfad die Kante stillschweigend überspringt. Wenn die
Atomen-Zone kompaktiert wird, werden starke Kanten korrekt aktualisiert, aber diese
schwache CacheIR-Kante bleibt dauerhaft baumelnd an der alten Adresse des Opfer-Symbols zurück.
(Debug-Builds fangen es als Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94 ab.)
Der PoC hinterlässt nicht nur einen veralteten Zeiger auf wiederverwendbaren Heap — er lässt die zugrunde liegende Seite munmappen, sodass bereits die nächste Dereferenzierung einen Fehler auslöst.
Zone::availableChunks/fullChunks), sodass Atomen-Zonen-Chunks nur
Atomen-Zonen-Arenas enthalten. Das Schicksal eines Chunks kann rein durch
Atom-/Symbol-Lebenszyklen gesteuert werden.c.eval(b) lässt den Top-Level-Call-IC das
Symbol in einem schwachen Stub-Feld erfassen; b = undefined entfernt die letzte starke
Wurzel; das Spray wird entfernt; zwei zonengebundene GCs
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
der leere String plant die Atomen-Zone ein) räumen das tote Spray aus,
kompaktieren das Opfer in die Magneten-Arena, überspringen den Schwach-Kanten-Fixup
und geben die Arena des Opfers frei. Sein Chunk ist nun vollständig leer.gcparam("minEmptyChunkCount", 0) macht jeden leeren Chunk
ablaufbar; während sleep(0.5) führt der BackgroundDecommitTask
-> -> aus — ein echtes
munmap des Opfer-Chunks. (Decommit auf Arena-Ebene ist nur
— lesbare Nullen — die Freigabe ganzer Chunks ist der
einzige munmap-Pfad, weshalb die Chunk-Isolation in Schritt 1 wichtig ist.)Dies ist nicht nur ein DoS. Demonstrierte Eskalationen (PoCs und Logs in den
Forschungsverzeichnissen GC-NDAY/ und CHAIN/):
segv9): nach dem munmap lässt ein
minorgc()-Semispace-Flip die Nursery das freigegebene VA beanspruchen; ein
roher Dense-Double-Spray schreibt ein Angreifer-Qword über das zone-Feld des
veralteten Arena-Headers. Die Ausräumung dereferenziert dann
attacker_ptr + 0x14 — deterministisch 3/3 bei 0x424242424256.js::Zone*, das über einen separaten spekulativen
Seitenkanal geleakt wurde, wurde durch dieses Refill gepflanzt; der Collector konsumierte die
gefälschten Arena-Metadaten ohne Fehler (eine um 1 Bit gestörte Kontrollpflanzung
crasht), was die Komposition ASLR-Besiegung -> UAF-Pflanzung demonstriert.expireEmptyChunkPoolFreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() räumt JIT-Daten aus:
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub liest den veralteten Symbol-Zeiger, dann
führt SweepingTracer::onEdge (gc/Marking.cpp:3195) eine
Chunk-Arithmetik auf der veralteten Adresse aus, um das zone-Feld des
Arena-Headers zu laden. Die Seite ist weg/genullt, also ist zone == nullptr, und
zone->isGCSweeping() liest nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. Beim Fehler enthält rcx den veralteten Symbol-Zeiger und
rdx die mungekappte Chunk-Basis.