
Cadeia de exploit PoC para CVE-2026-15718: compilador baseline wasm do SpiderMonkey array.fill missing-sync -> ponteiro inválido -> addrOf/fakeobj -> R/W arbitrário -> RCE
Cadeia de exploits de prova de conceito para CVE-2026-15718, uma má compilação por falta de sync() no compilador baseline do WebAssembly do SpiderMonkey da Mozilla (componente "JavaScript: WebAssembly"). Um único array.fill de zero iterações gera uma primitiva fakeobj; a cadeia evolui para leitura/escrita arbitrária do processo hospedeiro e execução arbitrária de código nativo (posix_spawnp("/bin/sh", ...)).
4441102e62e8 ("Bug 2045443", uma linha)BaseCompiler::emitArrayFill() () emite o laço de preenchimento do sem chamar primeiro:
js/src/wasm/WasmBaselineCompile.cpparray.fillsync() if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync() esvazia a pilha de valores do compilador baseline (stk_), movendo os registradores para a pilha de máquina. A pós-barreira imprecisa por iteração do laço de preenchimento (emitPostBarrierEdgeImprecise) chama sync() internamente — mas somente se o laço executar. O rótulo de saída do laço (done) é um ponto de junção de dois caminhos:
stk_ foi sincronizado — todo operando reside em um slot da pilha de máquina;numElements == 0): stk_ não foi tocado — operandos ainda residem apenas em registradores.O modelo estático do compilador após a junção sempre reflete o caminho sincronizado, então o código emitido depois de array.fill lê operandos de slots da pilha que uma execução de zero iterações nunca escreveu. Um operando reftype lido dessa forma é um ponteiro inválido (obsoleto) (CWE-763).
A dessincronização é bidirecional entre duas funções wasm com molduras idênticas:
n = 1, laço executa): o sync() dentro do laço escreve um valor no slot — uma ref real (addrOf) ou um i64 controlado (setup do fakeobj);n = 0, zero iterações): o slot nunca é escrito; a função gêmea lê o slot obsoleto de volta — como i64 (vazamento) ou como (ref $t) (fakeobj).| Arquivo | Estágio | Resultado |
|---|---|---|
poc-min.js (+ gen_trigger.py) | demonstração da dessincronização | o caminho de controle retorna o valor correto; o caminho de zero iterações retorna um ponteiro-lixo lido do slot nunca escrito |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | cadeia completa | addrOf + fakeobj → WasmArrayObject falso → R/W arbitrário → vazamento da base da XUL → varredura de imports Mach-O em tempo de execução → funcref forjado → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
Detalhes dos estágios em chain.js:
plantSpray/plantCmd/plantFun/plantExt + leak — addrOf para arrays wasm, funcrefs e objetos JS (externref).sprayV + fakeRd/fakeWr — fakeobj; WasmArrayObject falso (numElements_ @+16, data_ @+24, elementos inline @+40) com a janela falsa mantida em um array fora de linha (allocado via malloc) para que o GC não possa movê-la durante a varredura.0xfeedfacf) → base da XUL.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + tabela de símbolos indireta) para localizar a entrada __stubs de _posix_spawnp. Sem offsets fixos; o stub executa lazy binding na primeira chamada.typeDef @+0x40 copiado de um ref.func real, alvo da chamada @+0x38 = endereço do stub) e invoque-o via call_ref com posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0).Testado com um xpcshell (ou o shell js / navegador) compilado a partir de uma árvore anterior à 152.0.6 (a build de referência aqui é Firefox 149.0a1, 14/01/2026):
# demonstração mínima da dessincronização
xpcshell poc/poc-min.js
# cadeia completa (regenera chain.js a partir do template + gerador)
(cd poc && sh build.sh)
rm -f /tmp/CVE-2026-15718-PWNED
xpcshell poc/chain.js # cria /tmp/CVE-2026-15718-PWNED
ls -l /tmp/CVE-2026-15718-PWNED
O harness define javascript.options.wasm_optimizingjit=false para fixar o tier baseline durante a longa varredura do Mach-O (o bug é exclusivo do baseline; no navegador, o gatilho executa bem abaixo do limite de tier-up, então isso não é necessário lá). tools/xul_slots.py é um validador offline que lista os slots nomeados de stub/GOT da XUL (usado para verificar cruzadamente o walker em tempo de execução).
Os offsets são para macOS arm64, nesta árvore/build exata (WasmArrayObject { +16 numElements, +24 data_, +40 inline }, JSFunction native @ +0x20, WasmFuncRef { +0x38/+0x48 alvo da chamada, +0x40 typeDef }). Todos eles são re-derivados em tempo de execução pelos autotestes da cadeia; apenas os layouts das estruturas são assumidos. Sem preocupações com PAC (XUL é um binário arm64, não arm64e); o stub de lazy binding resolve o alvo na primeira chamada, então não são necessários offsets de dyld shared cache.
Este PoC executa em um processo de shell sem sandbox. Em um ataque real ao Firefox, a cadeia termina dentro do processo de conteúdo, onde a sandbox do macOS nega posix_spawnp — é necessário um bug separado de escape da sandbox do SO para um comprometimento total (veja, por exemplo, a análise de estágio 2 em CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box).
javascript.options.wasm=false bloqueia o vetor de gatilho.Apenas para pesquisa de segurança, educação e testes defensivos. A vulnerabilidade está corrigida nas versões atuais do Firefox/Thunderbird. Não use contra sistemas que você não possui ou para os quais não tem autorização explícita para testar.