
Diese Schwachstelle war für den Einsatz bei Pwn2Own 2026 Berlin vorgesehen, wurde jedoch in Version 150.0.3 gepatcht.
Bitfeld-Abkürzung von pcOffset im SpiderMonkey BaselineJIT auf dem Eager-Off-Thread-Baseline-Compile-Pfad, was während der Exception-Unwinding zu einem falschen, aber im gültigen Bereich liegenden Bytecode-pc und anschließend zu einer Type Confusion führt.
RetAddrEntry speichert den Bytecode-Offset pcOffset_ als 28-Bit-Bitfeld. Die in derselben Header-Datei definierte Konstante BaselineMaxScriptLength = 0x0fffffff ist exakt auf den Bereich dieses Bitfelds abgestimmt.
// js/src/jit/BaselineJIT.h:73
static constexpr uint32_t BaselineMaxScriptLength = 0x0fffffffu;
// js/src/jit/BaselineJIT.h:100-105
class RetAddrEntry {
// Offset from the start of the JIT code where call instruction is.
uint32_t returnOffset_;
// The offset of this bytecode op within the JSScript.
uint32_t pcOffset_ : 28;
// js/src/jit/BaselineJIT.h:141-156 (RetAddrEntry constructor)
RetAddrEntry(uint32_t pcOffset, Kind kind, CodeOffset retOffset)
: returnOffset_(uint32_t(retOffset.offset())),
pcOffset_(pcOffset),
kind_(uint32_t(kind)) {
MOZ_ASSERT(returnOffset_ == retOffset.offset(),
"retOffset must fit in returnOffset_");
// The pc offset must fit in at least 28 bits, since we shave off 4 for
// the Kind enum.
MOZ_ASSERT(pcOffset_ == pcOffset);
static_assert(BaselineMaxScriptLength <= (1u << 28) - 1);
MOZ_ASSERT(pcOffset <= BaselineMaxScriptLength);
MOZ_ASSERT(kind < Kind::Invalid);
MOZ_ASSERT(this->kind() == kind, "kind must fit in kind_ bit field");
}
Vor dem Fix wurde diese Obergrenze in Release-Builds nur innerhalb von CanEnterBaselineJIT (js/src/jit/BaselineJIT.cpp) durchgesetzt, also im Warmup-/OSR-Hauptthread-Eintrittspfad. Der Eager-Off-Thread-Baseline-Compile-Pfad durchlief diese Prüfung nicht, sodass ein Skript, dessen Bytecode 256 MB überschreitet, die Baseline-Kompilierung starten konnte, während sein pcOffset durch das 28-Bit-Speichern stillschweigend abgeschnitten wurde (pcOffset & 0x0FFFFFFF). Die beiden informativen MOZ_ASSERTs oben (pcOffset_ == pcOffset und pcOffset <= BaselineMaxScriptLength) sind in Release-Builds No-ops, sodass die Abschneidung unerkannt blieb.
Der Eager-Off-Thread-Baseline-Compile-Pfad:
CompilationStencil::instantiateStencils
-> MaybeDoEagerBaselineCompilations (js/src/frontend/Stencil.cpp:2720)
-> DispatchOffThreadBaselineBatchEager (js/src/jit/BaselineJIT.cpp:386)
-> BaselineCompileTask::runTask (js/src/jit/BaselineCompileTask.cpp:69)
-> BaselineCompile
Vor dem Patch prüfte MaybeDoEagerBaselineCompilations nur auf script->baselineDisabled() und jit::CanBaselineInterpretScript(script). Beides validiert die Skriptlänge nicht, sodass ein überlanges Skript über diesen Pfad die Baseline-Kompilierung erreichen konnte.
// js/src/frontend/Stencil.cpp, MaybeDoEagerBaselineCompilations (pre-patch)
if (script->baselineDisabled()) {
continue;
}
if (!jit::CanBaselineInterpretScript(script)) {
continue;
}
Im Gegensatz dazu erzwang der Hauptthread-Pfad die Grenze (dieser Block wurde später in das gemeinsame CanBaselineCompileScript verschoben):
// js/src/jit/BaselineJIT.cpp, CanEnterBaselineJIT (pre-patch)
if (script->length() > BaselineMaxScriptLength) {
script->disableBaselineCompile();
return Method_CantCompile;
}
Der abgeschnittene pcOffset wird in JSJitFrameIter::baselineScriptAndPc über RetAddrEntry::pc -> JSScript::offsetToPC wieder in einen Bytecode-Zeiger umgewandelt.
// js/src/jit/JSJitFrameIter.cpp:155-160
// address.
uint8_t* retAddr = resumePCinCurrentFrame();
const RetAddrEntry& entry =
script->baselineScript()->retAddrEntryFromReturnAddress(retAddr);
*pcRes = entry.pc(script);
}
// js/src/jit/BaselineJIT.h:162-164 (RetAddrEntry::pc)
jsbytecode* pc(JSScript* script) const {
return script->offsetToPC(pcOffset_);
}
Dieser pc wird an den Exception-Handler HandleExceptionBaseline übergeben. Da der abgeschnittene Offset kleiner als die tatsächliche Skriptlänge ist, gibt offsetToPC einen im gültigen Bereich liegenden, aber falschen pc zurück. Der Fehler ist daher still, anstatt ein offensichtlicher Out-of-Range-Absturz zu sein.
// js/src/jit/JitFrames.cpp:584-591
static void HandleExceptionBaseline(JSContext* cx, JSJitFrameIter& frame,
CommonFrameLayout* prevFrame,
ResumeFromException* rfe) {
MOZ_ASSERT(frame.isBaselineJS());
MOZ_ASSERT(prevFrame);
jsbytecode* pc;
frame.baselineScriptAndPc(nullptr, &pc);
Dieser falsche pc führt während der Exception-Unwinding zu einem falschen Try-Notice-Abgleich (HandleExceptionBaseline greift auf script->trynotes() zu), was ein falsches Lesen eines Stack-Slots zur Folge hat. Ein Slot, der kein JSObject* ist, kann so als ein solcher behandelt und über seine Vtable dispatched werden, was eine Type Confusion erzeugt, die die Grundlage für weitere Ausnutzung bildet.