
Цепочка эксплойта PoC для CVE-2026-15718: компилятор базового уровня wasm в SpiderMonkey, отсутствие синхронизации в array.fill -> недопустимый указатель -> addrOf/fakeobj -> произвольное чтение/запись -> RCE
Цепочка эксплойта уровня proof-of-concept для CVE-2026-15718 — ошибочная компиляция из-за пропущенного sync() в базовом компиляторе WebAssembly движка Mozilla SpiderMonkey (компонент «JavaScript: WebAssembly»). Один-единственный array.fill с нулевым числом итераций даёт примитив fakeobj; цепочка развивается до произвольного чтения/записи в процессе хоста и выполнения произвольного нативного кода (posix_spawnp("/bin/sh", ...)).
4441102e62e8 («Bug 2045443», одна строка)BaseCompiler::emitArrayFill() () генерирует цикл заполнения , не вызывая предварительно :
js/src/wasm/WasmBaselineCompile.cpparray.fillsync() if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync() сбрасывает стек значений базового компилятора (stk_) из регистров в машинный стек. Вызываемая на каждой итерации цикла заполнения неточная пост-барьерная проверка (emitPostBarrierEdgeImprecise) вызывает sync() внутри себя — но только если цикл выполняется. Метка выхода из цикла (done) является точкой соединения двух путей:
stk_ синхронизирован — каждый операнд находится в слоте машинного стека;numElements == 0): stk_ не тронут — операнды по-прежнему живут только в регистрах.Статическая модель компилятора после точки соединения всегда отражает синхронизированный путь, поэтому код, сгенерированный после array.fill, читает операнды из стековых слотов, которые при нулевой итерации никогда не записывались. Операнд типа reftype, прочитанный таким образом, является недопустимым (устаревшим) указателем (CWE-763).
Рассинхронизация двунаправленная между двумя wasm-функциями с одинаковыми фреймами:
n = 1, цикл выполняется): sync() внутри цикла записывает значение в слот — настоящий ref (addrOf) или контролируемый i64 (подготовка fakeobj);n = 0, нулевая итерация): слот никогда не записывается; функция-близнец читает устаревший слот — как i64 (утечка) или как (ref $t) (fakeobj).| File | Stage | Result |
|---|---|---|
poc-min.js (+ gen_trigger.py) | демо рассинхронизации | контролируемый путь возвращает корректное значение; путь с нулевой итерацией возвращает мусорный указатель, прочитанный из никогда не записывавшегося слота |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | полная цепочка | addrOf + fakeobj → поддельный WasmArrayObject → произвольное R/W → утечка базового адреса XUL → обход импортов Mach-O в рантайме → подделанный funcref → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
Детали этапов в chain.js:
plantSpray/plantCmd/plantFun/plantExt + leak — addrOf для wasm-массивов, funcref-ов и JS-объектов (externref).sprayV + fakeRd/fakeWr — fakeobj; поддельный WasmArrayObject (numElements_ @+16, data_ @+24, инлайн-элементы @+40), при этом поддельное окно хранится во внешнем (malloc'd) массиве, чтобы GC не мог переместить его во время сканирования.0xfeedfacf) → базовый адрес XUL.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + таблица косвенных символов) для поиска записи _posix_spawnp в секции __stubs. Никаких захардкоженных смещений; стаб выполняет ленивое связывание при первом вызове.typeDef @+0x40 скопирован из настоящего ref.func, цель вызова @+0x38 = адрес стаба) и вызываем его через call_ref с posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0).Протестировано с xpcshell (или оболочкой js / браузером), собранным из дерева до 152.0.6 (в качестве эталонной сборки здесь используется 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
Харнесс устанавливает javascript.options.wasm_optimizingjit=false, чтобы зафиксировать базовый тир во время длительного сканирования Mach-O (баг присутствует только в базовом компиляторе; в браузере триггер срабатывает задолго до порога повышения тира, так что там это не требуется). tools/xul_slots.py — офлайн-валидатор, который перечисляет именованные слоты стабов/GOT в XUL (используется для перекрёстной проверки рантайм-обходчика).
Смещения приведены для macOS arm64, именно этого дерева/сборки (WasmArrayObject { +16 numElements, +24 data_, +40 inline }, нативный вход JSFunction @ +0x20, WasmFuncRef { +0x38/+0x48 цель вызова, +0x40 typeDef }). Все они заново вычисляются в рантайме самотестами цепочки; предполагается только расположение полей в структурах. PAC не актуален (XUL — arm64-бинарник, не arm64e); стаб с ленивым связыванием разрешает цель при первом вызове, так что смещения из dyld shared cache не нужны.
Этот PoC выполняется в процессе оболочки без песочницы. В реальной атаке на Firefox цепочка срабатывает внутри контентного процесса, где песочница macOS запрещает posix_spawnp — для полной компрометации требуется отдельная уязвимость для побега из OS-песочницы (см., например, анализ стадии 2 в CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box).
javascript.options.wasm=false блокирует вектор срабатывания.Только для исследований в области безопасности, обучения и защитного тестирования. Уязвимость исправлена в текущих выпусках Firefox/Thunderbird. Не используйте против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.