
PoC-Exploitkette für CVE-2026-15718: SpiderMonkey-wasm-Baseline-Compiler array.fill missing-sync -> ungültiger Zeiger -> addrOf/fakeobj -> beliebiges R/W -> RCE
Proof-of-Concept-Exploit-Kette für CVE-2026-15718, eine Fehlkompilierung durch fehlendes sync() im Baseline-Compiler von Mozilla SpiderMonkey für WebAssembly (Komponente „JavaScript: WebAssembly"). Ein einziges array.fill mit null Durchläufen liefert eine Fakeobj-Primitive; die Kette eskaliert zu beliebigem Lesen/Schreiben des Host-Prozesses und beliebiger nativer Codeausführung (posix_spawnp("/bin/sh", ...)).
4441102e62e8 („Bug 2045443", eine Zeile)BaseCompiler::emitArrayFill() () erzeugt die -Füllschleife, ohne vorher aufzurufen:
js/src/wasm/WasmBaselineCompile.cpparray.fillsync() if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync() leert den Wertestapel (stk_) des Baseline-Compilers von den Registern auf den Maschinenstapel. Die unpräzise Post-Barriere der Füllschleife pro Iteration (emitPostBarrierEdgeImprecise) ruft intern sync() auf — aber nur, wenn die Schleife läuft. Das Exit-Label der Schleife (done) ist ein Join zweier Pfade:
stk_ ist synchronisiert — jeder Operand liegt in einem Maschinenstapel-Slot;numElements == 0): stk_ bleibt unberührt — Operanden liegen weiterhin nur in Registern.Das statische Modell des Compilers nach dem Join spiegelt immer den synchronisierten Pfad wider, sodass der nach array.fill erzeugte Code Operanden aus Stapel-Slots liest, die eine Null-Durchlauf-Ausführung nie geschrieben hat. Ein auf diese Weise zurückgelesener Reftype-Operand ist ein ungültiger (veralteter) Zeiger (CWE-763).
Die Desynchronisierung ist bidirektional über zwei frame-identische Wasm-Funktionen:
n = 1, Schleife läuft): das sync() in der Schleife schreibt einen Wert in den Slot — eine echte Ref (addrOf) oder ein kontrolliertes i64 (fakeobj-Setup);n = 0, Null-Durchlauf): der Slot wird nie geschrieben; die Zwillingsfunktion liest den veralteten Slot zurück — als i64 (Leak) oder als (ref $t) (fakeobj).| Datei | Stufe | Ergebnis |
|---|---|---|
poc-min.js (+ gen_trigger.py) | Desync-Demo | Der Kontrollpfad gibt den korrekten Wert zurück; der Null-Durchlauf-Pfad gibt einen Garbage-Zeiger zurück, der aus dem nie geschriebenen Slot gelesen wird |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | vollständige Kette | addrOf + fakeobj → gefälschtes WasmArrayObject → beliebiges R/W → XUL-Basis-Leak → Laufzeit-Mach-O-Import-Walk → gefälschte Funcref → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
Stufendetails in chain.js:
plantSpray/plantCmd/plantFun/plantExt + leak — addrOf für Wasm-Arrays, Funcrefs und JS-Objekte (externref).sprayV + fakeRd/fakeWr — fakeobj; gefälschtes WasmArrayObject (numElements_ @+16, data_ @+24, Inline-Elemente @+40), wobei das Fake-Fenster in einem out-of-line (per malloc) angelegten Array gehalten wird, damit der GC es während des Scans nicht verschieben kann.0xfeedfacf) → XUL-Basis.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + indirekte Symboltabelle), um den __stubs-Eintrag von _posix_spawnp zu finden. Keine fest kodierten Offsets; der Stub führt beim ersten Aufruf Lazy Binding aus.typeDef @+0x40 von einer echten ref.func kopiert, Call-Target @+0x38 = Stub-Adresse) und rufe sie über call_ref mit posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0) auf.Getestet mit einem xpcshell (oder der js-Shell / einem Browser), der aus einem Pre-152.0.6-Baum erstellt wurde (der Referenz-Build hier ist 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
Das Harness setzt javascript.options.wasm_optimizingjit=false, um während des langen Mach-O-Scans die Baseline-Stufe festzunageln (der Bug betrifft nur den Baseline-Compiler; im Browser läuft der Trigger weit unter der Tier-Up-Schwelle, daher ist das dort nicht nötig). tools/xul_slots.py ist ein Offline-Validator, der die benannten Stub-/GOT-Slots von XUL auflistet (zur Gegenprüfung des Laufzeit-Walkers).
Die Offsets gelten für macOS arm64, diesen exakten Baum/Build (WasmArrayObject { +16 numElements, +24 data_, +40 inline }, JSFunction native @ +0x20, WasmFuncRef { +0x38/+0x48 Call-Target, +0x40 typeDef }). Alle werden zur Laufzeit von den Selbsttests der Kette neu abgeleitet; nur die Strukturlayouts werden angenommen. Keine PAC-Bedenken (XUL ist ein arm64-, kein arm64e-Binary); der Lazy-Binding-Stub löst das Target beim ersten Aufruf auf, daher werden keine dyld-Shared-Cache-Offsets benötigt.
Dieser PoC läuft in einem nicht gesandboxten Shell-Prozess. In einem echten Firefox-Angriff landet die Kette im Content-Prozess, wo die macOS-Sandbox posix_spawnp verweigert — für eine vollständige Kompromittierung ist ein separater OS-Sandbox-Escape-Bug nötig (siehe z. B. die Stage-2-Analyse in CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box).
javascript.options.wasm=false blockiert den Trigger-Vektor.Nur für Sicherheitsforschung, Bildung und defensive Tests. Die Schwachstelle ist in den aktuellen Firefox-/Thunderbird-Versionen gepatcht. Nicht gegen Systeme verwenden, die dir nicht gehören oder für die du keine ausdrückliche Testfreigabe hast.