
Cadena de exploit PoC para CVE-2026-15718: array.fill sin sincronizar en el compilador baseline de wasm de SpiderMonkey -> puntero no válido -> addrOf/fakeobj -> R/W arbitrario -> RCE
Cadena de exploits de prueba de concepto para CVE-2026-15718, una mala compilación por falta de sync() en el compilador baseline de WebAssembly de Mozilla SpiderMonkey (componente «JavaScript: WebAssembly»). Un único array.fill de cero iteraciones produce una primitiva fakeobj; la cadena escala a lectura/escritura arbitraria del proceso anfitrión y ejecución arbitraria de código nativo (posix_spawnp("/bin/sh", ...)).
4441102e62e8 («Bug 2045443», una línea)BaseCompiler::emitArrayFill() () emite el bucle de relleno de sin llamar primero a :
js/src/wasm/WasmBaselineCompile.cpparray.fillsync() if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync() vacía la pila de valores (stk_) del compilador baseline, moviendo los registros a la pila de la máquina. La barrera posterior imprecisa de cada iteración del bucle de relleno (emitPostBarrierEdgeImprecise) llama a sync() internamente, pero solo si el bucle se ejecuta. La etiqueta de salida del bucle (done) es un punto de unión de dos caminos:
stk_ está sincronizado; cada operando vive en un slot de la pila de la máquina;numElements == 0): stk_ no se toca; los operandos siguen viviendo solo en registros.El modelo estático del compilador tras la unión siempre refleja el camino sincronizado, por lo que el código emitido después de array.fill lee operandos de slots de pila que una ejecución de cero iteraciones nunca escribió. Un operando reftype leído de esta forma es un puntero no válido (obsoleto) (CWE-763).
La desincronización es bidireccional entre dos funciones wasm con marcos idénticos:
n = 1, el bucle se ejecuta): el sync() interno escribe un valor en el slot — un ref real (addrOf) o un i64 controlado (montaje de fakeobj);n = 0, cero iteraciones): el slot nunca se escribe; la función gemela lee el slot obsoleto — como i64 (fuga) o como (ref $t) (fakeobj).| File | Stage | Result |
|---|---|---|
poc-min.js (+ gen_trigger.py) | demo de desincronización | el camino de control devuelve el valor correcto; el camino de cero iteraciones devuelve un puntero basura leído del slot que nunca se escribió |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | cadena completa | addrOf + fakeobj → WasmArrayObject falso → R/W arbitrario → fuga de la base de XUL → recorrido de importaciones Mach-O en tiempo de ejecución → funcref forjado → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
Detalles de las etapas en chain.js:
plantSpray/plantCmd/plantFun/plantExt + leak — addrOf para arrays wasm, funcrefs y objetos JS (externref).sprayV + fakeRd/fakeWr — fakeobj; WasmArrayObject falso (numElements_ @+16, data_ @+24, elementos inline @+40) con la ventana falsa mantenida en un array fuera de línea (asignado con malloc) para que el GC no pueda moverlo a mitad del escaneo.0xfeedfacf) → base de XUL.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + tabla de símbolos indirectos) para localizar la entrada __stubs de _posix_spawnp. Sin offsets hardcodeados; el stub realiza el lazy binding en la primera llamada.typeDef @+0x40 copiado de un ref.func real, destino de llamada @+0x38 = dirección del stub) e invocarlo mediante call_ref con posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0).Probado con un xpcshell (o el shell js / navegador) compilado a partir de un árbol anterior a 152.0.6 (la build de referencia aquí es 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
El harness establece javascript.options.wasm_optimizingjit=false para fijar el nivel baseline durante el largo escaneo Mach-O (el bug es solo del baseline; en el navegador el trigger se ejecuta muy por debajo del umbral de tier-up, por lo que no es necesario allí). tools/xul_slots.py es un validador offline que lista los slots stub/GOT con nombre de XUL (se utiliza para contrastar el recorrido en tiempo de ejecución).
Los offsets corresponden a macOS arm64, este árbol/build exacto (WasmArrayObject { +16 numElements, +24 data_, +40 inline }, JSFunction nativa @ +0x20, WasmFuncRef { +0x38/+0x48 destino de llamada, +0x40 typeDef }). Todos se re-derivan en tiempo de ejecución mediante los auto-tests de la cadena; solo se asumen los diseños de las estructuras. No hay preocupaciones de PAC (XUL es un binario arm64, no arm64e); el stub de lazy binding resuelve el destino en la primera llamada, por lo que no se necesitan offsets de la caché compartida de dyld.
Este PoC se ejecuta en un proceso de shell sin sandbox. En un ataque real a Firefox, la cadena aterriza dentro del proceso de contenido, donde la sandbox de macOS deniega posix_spawnp; se necesita un bug independiente de escape de la sandbox del SO para un compromiso completo (véase, p. ej., el análisis de la etapa 2 en CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box).
javascript.options.wasm=false bloquea el vector del trigger.Solo para investigación de seguridad, educación y pruebas defensivas. La vulnerabilidad está parcheada en las versiones actuales de Firefox/Thunderbird. No lo utilices contra sistemas que no poseas o para los que no tengas autorización explícita de prueba.