
Prova de conceito para CVE-2026-84118, um use-after-free no GC do SpiderMonkey que leva a leitura/escrita fora dos limites e potencial execução de código. Inclui gatilho determinístico de SEGV e pesquisa sobre escalada para leitura arbitrária e derrota do ASLR.
Este é um daqueles casos em que a Mozilla classificou um Critical como High. Então alguém pode querer ir atualizar o Firefox e cobrar o pagamento dos caçadores de bugs originais, se for esse o caso.
O RESUMO: Você pode fazer uma primitiva R de OOB em um ponteiro controlado e então começar a subir e descer a partir desse ponteiro. Então, se você apontar o ponteiro controlado para um array, parabéns, você pode fazer coisas divertidas.
O PoC só tem a parte do SEGV, então o resto é uma aventura divertida de apontar para um array e pular para o WASM de sua escolha.
Prova de conceito determinística de SEGV em build de release para Firefox/SpiderMonkey 154.0.1. Corrigido na 155 (reformulação do AtomMarking). Severidade Mozilla: HIGH.
/path/to/release/js poc.js
Reproduzido na 154.0.1: 3/3 em um shell otimizado simples (SIGSEGV, exit 139), 5/5 em um shell release+gczeal, falha idêntica a cada execução:
SEGV no endereço 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, offset +0x14
A 154 confunde os bitmaps de marcação de átomos por zona com o rastreamento de referências. Um
Symbol mantido apenas fracamente — aqui, capturado como um campo Value fraco dentro
de um stub IC CacheIR de Baseline — e referenciado a partir de uma zona não coletada nunca é
remarcado quando a zona de átomos é coletada em um GC com escopo de zona. O
resultado é uma aresta obsoleta para uma célula varrida/realocada.
O golpe fatal no build de release é um segundo bug estático:
MightBeForwarded<JS::Symbol> é codificado como false, então
IsForwarded retorna false sem verificar (gc/Marking-inl.h:92-97) e
todo caminho de correção de ponteiro condicionado a ele silenciosamente ignora a aresta. Quando a
zona de átomos é compactada, as arestas fortes são atualizadas corretamente, mas esta
aresta CacheIR fraca fica permanentemente pendurada no endereço antigo do Symbol vítima.
(Builds de debug detectam isso como Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94.)
O PoC não apenas deixa um ponteiro obsoleto para heap reutilizável — ele faz com que a página de suporte seja desmapeada (munmap), então a próxima desreferência já causa falha.
Zone::availableChunks/fullChunks), então chunks da zona de átomos contêm apenas
arenas da zona de átomos. O destino de um chunk pode ser controlado puramente por
ciclos de vida de átomos/símbolos.c.eval(b) faz o IC de chamada de nível superior capturar o
Symbol em um campo de stub fraco; b = undefined remove a última raiz
forte; o spray é descartado; dois GCs com escopo de zona
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
a string vazia agenda a zona de átomos) varrem o spray morto,
compactam a vítima na arena dos ímãs, pulam a correção da aresta fraca
e liberam a arena da vítima. Seu chunk agora está completamente vazio.gcparam("minEmptyChunkCount", 0) faz com que todo chunk vazio seja
expirável; durante sleep(0.5) a BackgroundDecommitTask executa
-> -> — um munmap
real do chunk da vítima. (O decommit em nível de arena é apenas
— zeros legíveis — a liberação de chunk inteiro é o
único caminho de munmap, e é por isso que o isolamento de chunks na etapa 1 importa.)Isto não é apenas um DoS. Escalonamentos demonstrados (PoCs e logs nos
diretórios de pesquisa GC-NDAY/ e CHAIN/):
segv9): após o munmap, uma
inversão de semispace de minorgc() faz o nursery reivindicar o VA liberado; um
spray bruto de doubles densos escreve um qword do atacante sobre o campo zone
do cabeçalho da arena obsoleta. A varredura então desreferencia
attacker_ptr + 0x14 — determinístico 3/3 em 0x424242424256.js::Zone* real vazado por meio de um canal lateral
especulativo separado foi plantado através deste refill; o coletor consumiu os
metadados de arena forjados sem causar falha (uma planta de controle perturbada
em 1 bit crasha), demonstrando a composição derrota-de-ASLR -> planta-de-UAF.expireEmptyChunkPoolFreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() final varre dados JIT:
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub lê o ponteiro Symbol obsoleto e então
SweepingTracer::onEdge (gc/Marking.cpp:3195) realiza aritmética de
chunk no endereço obsoleto para carregar o campo zone do cabeçalho da arena.
A página se foi/foi zerada, então zone == nullptr, e
zone->isGCSweeping() lê nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. No momento da falha, rcx contém o ponteiro Symbol obsoleto e
rdx a base do chunk desmapeado.