
Preuve de concept pour CVE-2026-84118, une utilisation après libération du ramasse-miettes (GC) de SpiderMonkey menant à une lecture/écriture hors limites et à une exécution de code potentielle. Inclut un déclencheur SEGV déterministe et des recherches sur l'escalade vers une lecture arbitraire et la neutralisation de l'ASLR.
C'est l'un de ces cas où Mozilla a qualifié de « High » une faille « Critical ». Donc si c'est le cas, quelqu'un voudra peut-être aller demander à Firefox de mettre à jour le paiement des chasseurs de bugs d'origine.
EN BREF : Vous pouvez réaliser une primitive de lecture hors limites (OOB R) sur un pointeur contrôlé, puis commencer à monter et descendre à partir de ce pointeur. Donc si vous pointez le pointeur contrôlé vers un tableau, félicitations, vous pouvez faire des choses amusantes.
Le POC ne contient que la partie SEGV, le reste est une aventure amusante qui consiste à pointer dans un tableau et à sauter dans le WASM de votre choix.
Preuve de concept déterministe de SEGV en build release pour Firefox/SpiderMonkey 154.0.1. Corrigé dans la 155 (refonte d'AtomMarking). Sévérité Mozilla : HIGH.
/path/to/release/js poc.js
Reproduit sur 154.0.1 : 3/3 sur un shell optimisé simple (SIGSEGV, exit 139), 5/5 sur un shell release+gczeal, faute identique à chaque exécution :
SEGV à l'adresse 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, offset +0x14
La 154 confond les bitmaps de marquage des atomes par zone avec le suivi des références. Un
Symbole détenu uniquement faiblement — ici, capturé comme champ Value faible à l'intérieur
d'un stub IC CacheIR Baseline — et référencé depuis une zone non collectée n'est
jamais re-marqué lorsque la zone des atomes est collectée lors d'un GC limité à une zone. Le
résultat est une arête obsolète vers une cellule balayée/relocalisée.
Le coup fatal en build release est un second bug statique :
MightBeForwarded<JS::Symbol> est codé en dur à false, donc
IsForwarded renvoie false sans vérifier (gc/Marking-inl.h:92-97) et
chaque chemin de correction de pointeur qui en dépend ignore silencieusement l'arête. Lorsque la
zone des atomes est compactée, les arêtes fortes sont mises à jour correctement, mais cette
arête CacheIR faible reste définitivement pendante à l'ancienne adresse du Symbole victime. (Les builds debug la détectent comme Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94.)
Le PoC ne se contente pas de laisser un pointeur obsolète vers un tas réutilisable — il fait munmapper la page sous-jacente, de sorte que la toute prochaine déréférence provoque une faute.
Zone::availableChunks/fullChunks), donc les chunks de la zone des atomes ne contiennent que des
arènes de la zone des atomes. Le sort d'un chunk peut être contrôlé purement par les
durées de vie des atomes/symboles.c.eval(b) fait capturer le Symbole par l'IC d'appel de niveau supérieur dans un champ de stub faible ; b = undefined supprime la dernière racine
forte ; le spray est supprimé ; deux GC limités à une zone
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
la chaîne vide planifie la zone des atomes) balayent le spray mort,
compactent la victime dans l'arène des aimants, sautent la correction de l'arête faible,
et libèrent l'arène de la victime. Son chunk est désormais complètement vide.gcparam("minEmptyChunkCount", 0) rend chaque chunk vide
expirable ; pendant , la BackgroundDecommitTask exécute
-> -> — un véritable
munmap du chunk de la victime. (La désactivation au niveau de l'arène n'est que
— des zéros lisibles — la libération de chunk entier est le
seul chemin munmap, c'est pourquoi l'isolation des chunks à l'étape 1 est importante.)Ce n'est pas qu'un simple DoS. Escalades démontrées (PoCs et journaux dans les
répertoires de recherche GC-NDAY/ et CHAIN/) :
segv9) : après le munmap, un
basculement de semi-espace minorgc() fait réclamer l'AV libéré par la nursery ; un
spray brut de doubles denses écrit un qword d'attaquant sur le champ zone
de l'en-tête d'arène obsolète. Le balayage déréférence alors
attacker_ptr + 0x14 — déterministe 3/3 à 0x424242424256.js::Zone* divulgué via un canal
secondaire spéculatif distinct a été planté via ce réapprovisionnement ; le collecteur a consommé les
métadonnées d'arène forgées sans faute (une plantation de contrôle perturbée d'1 bit
crashe), démontrant la composition contournement-d'ASLR -> plantation-UAF.sleep(0.5)expireEmptyChunkPoolFreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() final balaie les données JIT :
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub lit le pointeur de Symbole obsolète, puis
SweepingTracer::onEdge (gc/Marking.cpp:3195) effectue une
arithmétique de chunk sur l'adresse obsolète pour charger le champ zone
de l'en-tête d'arène. La page a disparu/est zéroée, donc zone == nullptr, et
zone->isGCSweeping() lit nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. Lors de la faute, rcx contient le pointeur de Symbole obsolète et
rdx la base du chunk munmappé.