
Prueba de concepto para CVE-2026-84118, un use-after-free en el GC de SpiderMonkey que conduce a una lectura/escritura fuera de límites y a una posible ejecución de código. Incluye un desencadenante determinista de SEGV e investigación sobre la escalada a lectura arbitraria y la anulación de ASLR.
Este es uno de esos casos en los que Mozilla etiquetó una Critical como High. Así que alguien podría querer que Firefox actualice el pago a los cazadores de bugs originales si ese es el caso.
EL TLDR: Puedes hacer una primitiva R OOB sobre un puntero controlado y luego empezar a subir y bajar desde ese puntero. Así que si apuntas el puntero controlado a un array, enhorabuena, puedes hacer cosas divertidas.
El POC solo tiene la parte del SEGV, así que el resto es una aventura divertida de apuntar a un array y saltar al WASM que elijas.
Prueba de concepto determinista de SEGV en build de release para Firefox/SpiderMonkey 154.0.1. Corregido en 155 (rework de AtomMarking). Severidad Mozilla: HIGH.
/path/to/release/js poc.js
Reproducido en 154.0.1: 3/3 en un shell optimizado simple (SIGSEGV, exit 139), 5/5 en un shell de release+gczeal, fallo idéntico en cada ejecución:
SEGV en la dirección 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, offset +0x14
154 confunde los bitmaps de marcado de átomos por zona con el seguimiento de referencias. Un
Symbol mantenido solo débilmente — aquí, capturado como un campo Value débil dentro
de un stub IC CacheIR de Baseline — y referenciado desde una zona no recolectada nunca
se vuelve a marcar cuando la zona de átomos se recolecta en un GC con ámbito de zona. El
resultado es un borde obsoleto hacia una celda barrida/desplazada.
El golpe letal en build de release es un segundo bug estático:
MightBeForwarded<JS::Symbol> está hardcodeado como false, así que
IsForwarded devuelve false sin comprobar (gc/Marking-inl.h:92-97) y
cada ruta de corrección de punteros que depende de ello omite silenciosamente el borde. Cuando la
zona de átomos se compacta, los bordes fuertes se actualizan correctamente, pero este
borde CacheIR débil queda permanentemente colgando en la dirección antigua del Symbol víctima.
(Los builds de debug lo detectan como Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94.)
El PoC no solo deja un puntero obsoleto a heap reutilizable — consigue que la página subyacente sea desmapeada (munmapped), de modo que la siguiente desreferencia falla.
Zone::availableChunks/fullChunks), así que los chunks de la zona de átomos contienen solo
arenas de la zona de átomos. El destino de un chunk puede controlarse puramente por
las vidas de átomos/symbols.c.eval(b) hace que el IC de llamada de nivel superior capture el
Symbol en un campo de stub débil; b = undefined suelta la última raíz
fuerte; el spray se suelta; dos GCs con ámbito de zona
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
la cadena vacía programa la zona de átomos) barren el spray muerto,
compactan la víctima en la arena de imanes, omiten la corrección del borde débil,
y liberan la arena de la víctima. Su chunk ahora está completamente vacío.gcparam("minEmptyChunkCount", 0) hace que cada chunk vacío sea
expirable; durante sleep(0.5) la BackgroundDecommitTask ejecuta
-> -> — un munmap
real del chunk víctima. (El decommit a nivel de arena es solo
— ceros legibles — la liberación de chunks completos es la
única ruta de munmap, por eso el aislamiento de chunks del paso 1 importa.)Esto no es solo un DoS. Escaladas demostradas (PoCs y logs en los
directorios de investigación GC-NDAY/ y CHAIN/):
segv9): después del munmap, un
cambio de semiespacio de minorgc() hace que la nursery reclame la VA liberada; un
spray crudo de dobles densos escribe un qword del atacante sobre el campo zone
del header de la arena obsoleta. El barrido entonces desreferencia
attacker_ptr + 0x14 — determinista 3/3 en 0x424242424256.js::Zone* real filtrado a través de un canal lateral
especulativo separado fue plantado mediante este relleno; el recolector consumió los
metadatos de arena falsificados sin fallar (una planta de control perturbada en 1 bit
crashea), demostrando la composición derrota-de-ASLR -> planta-de-UAF.expireEmptyChunkPoolFreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() final barre datos JIT:
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub lee el puntero Symbol obsoleto, y luego
SweepingTracer::onEdge (gc/Marking.cpp:3195) realiza aritmética de
chunks sobre la dirección obsoleta para cargar el campo zone del header de la arena.
La página ha desaparecido/está a ceros, así que zone == nullptr, y
zone->isGCSweeping() lee nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. En el fallo, rcx contiene el puntero Symbol obsoleto y
rdx la base del chunk desmapeado.