
Chaîne d'exploitation PoC pour CVE-2026-15718 : compilateur baseline wasm de SpiderMonkey, array.fill sans synchronisation -> pointeur invalide -> addrOf/fakeobj -> R/W arbitraire -> RCE
Chaîne d'exploitation proof-of-concept pour CVE-2026-15718, une erreur de compilation par absence de sync() dans le compilateur baseline WebAssembly de Mozilla SpiderMonkey (composant « JavaScript: WebAssembly »). Un unique array.fill sans itération fournit une primitive fakeobj ; la chaîne escalade vers la lecture/écriture arbitraire du processus hôte et l'exécution arbitraire de code natif (posix_spawnp("/bin/sh", ...)).
4441102e62e8 (« Bug 2045443 », une ligne)BaseCompiler::emitArrayFill() () émet la boucle de remplissage de sans appeler au préalable :
js/src/wasm/WasmBaselineCompile.cpparray.fillsync() if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync() vide la pile de valeurs du compilateur baseline (stk_) des registres vers la pile machine. La barrière postérieure imprécise par itération de la boucle de remplissage (emitPostBarrierEdgeImprecise) appelle sync() en interne — mais seulement si la boucle s'exécute. L'étiquette de sortie de la boucle (done) est une jonction de deux chemins :
stk_ est synchronisé — chaque opérande vit dans un emplacement de pile machine ;numElements == 0) : stk_ n'est pas touché — les opérandes vivent encore uniquement dans des registres.Le modèle statique du compilateur après la jonction reflète toujours le chemin synchronisé, donc le code émis après array.fill lit les opérandes depuis des emplacements de pile qu'une exécution sans itération n'a jamais écrits. Un opérande reftype relu de cette façon est un pointeur invalide (périmé) (CWE-763).
La désynchronisation est bidirectionnelle entre deux fonctions wasm identiques au niveau du frame :
n = 1, la boucle s'exécute) : le sync() dans la boucle écrit une valeur dans l'emplacement — une vraie ref (addrOf) ou un i64 contrôlé (mise en place de fakeobj) ;n = 0, zéro itération) : l'emplacement n'est jamais écrit ; la fonction jumelle relit l'emplacement périmé — comme i64 (fuite) ou comme (ref $t) (fakeobj).| Fichier | Étape | Résultat |
|---|---|---|
poc-min.js (+ gen_trigger.py) | démo de désynchronisation | le chemin contrôlé renvoie la valeur correcte ; le chemin zéro itération renvoie un pointeur poubelle lu depuis l'emplacement jamais écrit |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | chaîne complète | addrOf + fakeobj → faux WasmArrayObject → R/W arbitraire → fuite de la base XUL → parcours des imports Mach-O du runtime → funcref forgée → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
Détails des étapes dans chain.js :
plantSpray/plantCmd/plantFun/plantExt + leak — addrOf pour les tableaux wasm, les funcrefs et les objets JS (externref).sprayV + fakeRd/fakeWr — fakeobj ; faux WasmArrayObject (numElements_ @+16, data_ @+24, éléments inline @+40) avec la fenêtre factice conservée dans un tableau hors ligne (alloué par malloc) afin que le GC ne puisse pas la déplacer en cours de balayage.0xfeedfacf) → base XUL.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + table des symboles indirects) pour localiser l'entrée __stubs de _posix_spawnp. Aucun offset codé en dur ; le stub effectue la liaison paresseuse au premier appel.typeDef @+0x40 copié depuis une vraie ref.func, cible d'appel @+0x38 = adresse du stub) puis invocation via call_ref avec posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0).Testé avec un xpcshell (ou le shell js / le navigateur) compilé depuis un arbre antérieur à 152.0.6 (la build de référence ici est Firefox 149.0a1, 2026-01-14) :
# démo minimale de désynchronisation
xpcshell poc/poc-min.js
# chaîne complète (régénère chain.js depuis le template + le générateur)
(cd poc && sh build.sh)
rm -f /tmp/CVE-2026-15718-PWNED
xpcshell poc/chain.js # crée /tmp/CVE-2026-15718-PWNED
ls -l /tmp/CVE-2026-15718-PWNED
Le harnais définit javascript.options.wasm_optimizingjit=false pour épingler le niveau baseline pendant le long balayage Mach-O (le bug ne concerne que le baseline ; dans le navigateur, le déclencheur s'exécute bien sous le seuil de montée de niveau, donc cela n'est pas nécessaire là-bas). tools/xul_slots.py est un validateur hors ligne qui liste les emplacements nommés stub/GOT de XUL (utilisé pour recouper le parcoureur du runtime).
Les offsets concernent macOS arm64, cet arbre/build exact
(WasmArrayObject { +16 numElements, +24 data_, +40 inline },
JSFunction native @ +0x20, WasmFuncRef { +0x38/+0x48 cible d'appel, +0x40
typeDef }). Tous sont redérivés au runtime par les auto-tests de la chaîne ; seuls les agencements de structures sont supposés. Aucun souci de PAC (XUL est un binaire arm64, non arm64e) ; le stub de liaison paresseuse résout la cible au premier appel, donc aucun offset de cache partagé dyld n'est nécessaire.
Ce PoC s'exécute dans un processus shell non sandboxé. Dans une véritable attaque Firefox, la chaîne atterrit dans le processus de contenu, où la sandbox macOS refuse posix_spawnp — un bug distinct d'évasion de la sandbox OS est nécessaire pour une compromission complète (voir par exemple l'analyse de l'étape 2 dans CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box).
javascript.options.wasm=false bloque le vecteur de déclenchement.Pour la recherche en sécurité, l'éducation et les tests défensifs uniquement. La vulnérabilité est corrigée dans les versions actuelles de Firefox/Thunderbird. Ne pas utiliser contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de test.