
Catena di exploit PoC per CVE-2026-15718: compilatore baseline wasm di SpiderMonkey, array.fill con sync mancante -> puntatore non valido -> addrOf/fakeobj -> R/W arbitraria -> RCE
Catena di exploit proof-of-concept per CVE-2026-15718, una miscompilazione per mancata sync() nel compilatore WebAssembly baseline di Mozilla SpiderMonkey (componente "JavaScript: WebAssembly"). Un singolo array.fill a zero iterazioni produce una primitiva fakeobj; la catena scala fino a lettura/scrittura arbitraria del processo host ed esecuzione arbitraria di codice nativo (posix_spawnp("/bin/sh", ...)).
4441102e62e8 ("Bug 2045443", una sola riga)BaseCompiler::emitArrayFill() () genera il ciclo di riempimento di senza chiamare prima :
js/src/wasm/WasmBaselineCompile.cpparray.fillsync() if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync() trasferisce lo stack dei valori del compilatore baseline (stk_) dai registri allo stack macchina. La post-barriera imprecisa per iterazione del ciclo di riempimento (emitPostBarrierEdgeImprecise) chiama sync() internamente — ma solo se il ciclo viene eseguito. L'etichetta di uscita del ciclo (done) è la confluenza di due percorsi:
stk_ è sincronizzato — ogni operando vive in uno slot dello stack macchina;numElements == 0): stk_ non viene toccato — gli operandi vivono ancora solo nei registri.Il modello statico del compilatore dopo la confluenza riflette sempre il percorso sincronizzato, quindi il codice generato dopo array.fill legge gli operandi da slot dello stack che un'esecuzione a zero iterazioni non ha mai scritto. Un operando di tipo reftype riletto in questo modo è un puntatore non valido (obsoleto) (CWE-763).
La desincronizzazione è bidirezionale tra due funzioni wasm dal frame identico:
n = 1, il ciclo viene eseguito): la sync() interna al ciclo scrive un valore nello slot — un ref reale (addrOf) o un i64 controllato (setup di fakeobj);n = 0, a zero iterazioni): lo slot non viene mai scritto; la funzione gemella rilegge lo slot obsoleto — come i64 (leak) o come (ref $t) (fakeobj).| File | Fase | Risultato |
|---|---|---|
poc-min.js (+ gen_trigger.py) | demo della desincronizzazione | il percorso di controllo restituisce il valore corretto; il percorso a zero iterazioni restituisce un puntatore spazzatura letto dallo slot mai scritto |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | catena completa | addrOf + fakeobj → WasmArrayObject fake → R/W arbitrario → leak della base di XUL → percorrimento runtime delle importazioni Mach-O → funcref contraffatto → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
Dettagli delle fasi in chain.js:
plantSpray/plantCmd/plantFun/plantExt + leak — addrOf per gli array wasm, i funcref e gli oggetti JS (externref).sprayV + fakeRd/fakeWr — fakeobj; WasmArrayObject fake (numElements_ @+16, data_ @+24, elementi inline @+40) con la finestra dei fake mantenuta in un array out-of-line (allocato con malloc()) così che il GC non possa spostarla durante la scansione.0xfeedfacf) → base di XUL.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + tabella dei simboli indiretta) per individuare la entry __stubs di _posix_spawnp. Nessun offset hardcoded; lo stub esegue il lazy binding alla prima chiamata.typeDef @+0x40 copiato da un ref.func reale, call target @+0x38 = indirizzo dello stub), invocato tramite call_ref con posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0).Testato con uno xpcshell (o la shell js / il browser) compilato da un albero pre-152.0.6 (la build di riferimento qui è Firefox 149.0a1, 2026-01-14):
# minimal desync demo
xpcshell poc/poc-min.js
# full chain (regenerates chain.js from the template + generator)
(cd poc && sh build.sh)
rm -f /tmp/CVE-2026-15718-PWNED
xpcshell poc/chain.js # creates /tmp/CVE-2026-15718-PWNED
ls -l /tmp/CVE-2026-15718-PWNED
L'harness imposta javascript.options.wasm_optimizingjit=false per fissare il tier baseline durante la lunga scansione Mach-O (il bug riguarda solo il baseline; nel browser il trigger gira ben al di sotto della soglia di tier-up, quindi non serve lì). tools/xul_slots.py è un validatore offline che elenca gli slot stub/GOT con nome di XUL (usato per la verifica incrociata del walker a runtime).
Gli offset valgono per macOS arm64, questo esatto albero/build (WasmArrayObject { +16 numElements, +24 data_, +40 inline }, JSFunction native @ +0x20, WasmFuncRef { +0x38/+0x48 call target, +0x40 typeDef }). Tutti vengono ricalcolati a runtime dai self-test della catena; si assume solo il layout delle strutture. Nessun problema di PAC (XUL è un binario arm64, non arm64e); lo stub di lazy binding risolve la destinazione alla prima chiamata, quindi non servono offset della dyld shared cache.
Questo PoC viene eseguito in un processo shell senza sandbox. In un attacco Firefox reale la catena finisce nel content process, dove la sandbox di macOS blocca posix_spawnp — serve un bug separato di fuga dalla sandbox del sistema operativo per una compromissione completa (vedi ad es. l'analisi dello stage 2 in CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box).
javascript.options.wasm=false blocca il vettore del trigger.Solo per ricerca sulla sicurezza, formazione e test difensivi. La vulnerabilità è corretta nelle versioni attuali di Firefox/Thunderbird. Non utilizzare contro sistemi che non possiedi o per i quali non hai un'autorizzazione esplicita al test.