
Доказательство концепции для CVE-2026-84118 — use-after-free в сборщике мусора SpiderMonkey, приводящий к чтению/записи за пределами границ и потенциальному выполнению кода. Включает детерминированный триггер SEGV и исследование по эскалации до произвольного чтения и обхода ASLR.
Это один из тех случаев, когда Mozilla пометила Critical как High. Так что, возможно, кому-то стоит пойти и попросить Firefox обновить выплату оригинальным охотникам за багами, если это так.
КРАТКО: Вы можете получить примитив OOB R на управляемом указателе, а затем начать двигаться вверх и вниз от этого указателя. Так что если вы наведёте управляемый указатель на массив, то поздравляю — вы можете делать забавные вещи.
PoC содержит только часть с SEGV, так что остальное — это увлекательное приключение по наведению на массив и переходу в WASM по вашему выбору.
Детерминированный proof-of-concept с SEGV для релизной сборки Firefox/SpiderMonkey 154.0.1. Исправлено в 155 (переработка AtomMarking). Серьёзность по версии Mozilla: HIGH.
/path/to/release/js poc.js
Воспроизведено на 154.0.1: 3/3 на обычной оптимизированной оболочке (SIGSEGV, выход 139), 5/5 на оболочке release+gczeal, идентичный сбой при каждом запуске:
SEGV по адресу 0x000000000014 (READ)
JS::shadow::Zone::gcState() <- zone == nullptr, смещение +0x14
В 154 смешиваются попери-zone битовые карты пометки атомов с отслеживанием ссылок.
Symbol, удерживаемый только слабо — здесь, захваченный как слабое поле Value внутри
IC-стаба Baseline CacheIR — и на который ссылаются из не собираемой зоны,
никогда не помечается повторно, когда зона атомов собирается в GC с областью действия зоны.
Результат — устаревшее ребро к выметенной/перемещённой ячейке.
Убийственный выстрел в релизной сборке — вторая, статическая ошибка:
MightBeForwarded<JS::Symbol> жёстко задано как false, поэтому
IsForwarded возвращает false без проверки (gc/Marking-inl.h:92-97), и
каждый путь исправления указателей, управляемый этим условием, молча пропускает ребро. Когда
зона атомов уплотняется, сильные рёбра обновляются корректно, но это
слабое ребро CacheIR остаётся навсегда висящим по старому адресу Symbol-жертвы.
(Отладочные сборки ловят это как Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94.)
PoC не просто оставляет устаревший указатель на переиспользуемую кучу — он добивается munmap резервной страницы, так что уже следующее разыменование вызывает сбой.
Zone::availableChunks/fullChunks), поэтому чанки зоны атомов содержат только
арены зоны атомов. Судьбой чанка можно управлять исключительно за счёт
времени жизни атомов/symbol'ов.c.eval(b) заставляет IC верхнеуровневого вызова захватить
Symbol в слабом поле стаба; b = undefined убирает последний сильный
корень; распыление сбрасывается; два GC с областью действия зоны
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
пустая строка планирует зону атомов) выметают мёртвое распыление,
уплотняют жертву в арену магнитов, пропускают исправление слабого ребра
и освобождают арену жертвы. Её чанк теперь полностью пуст.gcparam("minEmptyChunkCount", 0) делает каждый пустой чанк
подлежащим истечению; во время sleep(0.5) задача BackgroundDecommitTask выполняет
-> -> — настоящий
munmap чанка жертвы. (Высвобождение на уровне арены — это лишь
— читаемые нули — освобождение целого чанка — это
единственный путь munmap, поэтому изоляция чанков на шаге 1 имеет значение.)Это не просто DoS. Продемонстрированные эскалации (PoC и журналы в
исследовательских каталогах GC-NDAY/ и CHAIN/):
segv9): после munmap
переключение полупространств при minorgc() заставляет nursery захватить освобождённый VA;
сырое распыление плотных double записывает qword атакующего поверх поля zone
устаревшего заголовка арены. Затем выметание разыменовывает
attacker_ptr + 0x14 — детерминированно 3/3 по адресу 0x424242424256.js::Zone*, утёкший через отдельный
спекулятивный побочный канал, был внедрён через этот refill; сборщик потребил
поддельные метаданные арены без сбоя (управляющая посадка с возмущением в 1 бит
вызывает сбой), демонстрируя композицию обхода ASLR -> внедрения UAF.expireEmptyChunkPoolFreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() выметает данные JIT:
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub читает устаревший указатель Symbol, затем
SweepingTracer::onEdge (gc/Marking.cpp:3195) выполняет арифметику
чанков по устаревшему адресу, чтобы загрузить поле zone заголовка арены.
Страница исчезла/обнулена, поэтому zone == nullptr, и
zone->isGCSweeping() читает nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. В момент сбоя rcx содержит устаревший указатель Symbol, а
rdx — базовый адрес munmapped-чанка.