
CVE-2026-8389 के लिए तकनीकी विश्लेषण और प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, फायरफॉक्स में एक SpiderMonkey BaselineJIT टाइप कन्फ्यूजन भेद्यता, जिसे बाइटकोड ट्रंकेशन और अपवाद अनवाइंडिंग हेरफेर के साथ प्रदर्शित किया गया है।
यह भेद्यता Pwn2Own 2026 बर्लिन में उपयोग के लिए लक्षित थी, लेकिन संस्करण 150.0.3 में इसे पैच कर दिया गया।
SpiderMonkey BaselineJIT pcOffset बिटफ़ील्ड ट्रंकेशन, ईगर ऑफ-थ्रेड बेसलाइन-कंपाइल पथ पर, जो अपवाद अनरैपिंग के दौरान गलत लेकिन सीमा के भीतर बाइटकोड pc की ओर ले जाता है, और उसके बाद एक प्रकार भ्रम (टाइप कन्फ्यूजन) उत्पन्न करता है।
RetAddrEntry बाइटकोड ऑफसेट pcOffset_ को 28-बिट बिटफ़ील्ड के रूप में संग्रहीत करता है। उसी हेडर में परिभाषित BaselineMaxScriptLength = 0x0fffffff स्थिरांक का आकार ठीक उस बिटफ़ील्ड रेंज से मेल खाने के लिए है।
// 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");
}
फिक्स से पहले, यह ऊपरी सीमा केवल CanEnterBaselineJIT (js/src/jit/BaselineJIT.cpp) के अंदर रिलीज़ बिल्ड में लागू की जाती थी, जो वार्मअप/OSR मुख्य-थ्रेड एंट्री पथ है। ईगर ऑफ-थ्रेड बेसलाइन-कंपाइल पथ उस जाँच से नहीं गुजरता था, इसलिए एक स्क्रिप्ट जिसका बाइटकोड 256 MB से अधिक है, बेसलाइन कंपाइलेशन को साफ़ कर सकता है जबकि इसका pcOffset 28-बिट स्टोर (pcOffset & 0x0FFFFFFF) द्वारा चुपचाप छोटा (ट्रंकेट) हो जाता है। ऊपर दिए गए दो सूचनात्मक MOZ_ASSERT (pcOffset_ == pcOffset और pcOffset <= BaselineMaxScriptLength) रिलीज़ बिल्ड में कोई कार्य नहीं करते, इसलिए ट्रंकेशन का पता नहीं चलता।
ईगर ऑफ-थ्रेड बेसलाइन-कंपाइल पथ:
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
पैच से पहले, MaybeDoEagerBaselineCompilations केवल script->baselineDisabled() और jit::CanBaselineInterpretScript(script) पर गेट किया गया था। दोनों में से कोई भी स्क्रिप्ट की लंबाई को मान्य नहीं करता, इसलिए एक अत्यधिक लंबी स्क्रिप्ट इस पथ के माध्यम से बेसलाइन कंपाइलेशन तक पहुँच गई।
// js/src/frontend/Stencil.cpp, MaybeDoEagerBaselineCompilations (pre-patch)
if (script->baselineDisabled()) {
continue;
}
if (!jit::CanBaselineInterpretScript(script)) {
continue;
}
इसके विपरीत, मुख्य-थ्रेड पथ ने सीमा लागू की (यह ब्लॉक बाद में साझा CanBaselineCompileScript में स्थानांतरित कर दिया गया):
// js/src/jit/BaselineJIT.cpp, CanEnterBaselineJIT (pre-patch)
if (script->length() > BaselineMaxScriptLength) {
script->disableBaselineCompile();
return Method_CantCompile;
}
ट्रंकेटेड pcOffset को JSJitFrameIter::baselineScriptAndPc में, RetAddrEntry::pc -> JSScript::offsetToPC के माध्यम से बाइटकोड पॉइंटर में वापस बदल दिया जाता है।
// 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_);
}
यह pc अपवाद हैंडलर HandleExceptionBaseline को दिया जाता है। चूँकि ट्रंकेटेड ऑफसेट वास्तविक स्क्रिप्ट लंबाई से छोटा होता है, offsetToPC एक सीमा के भीतर लेकिन गलत pc लौटाता है, इसलिए विफलता एक स्पष्ट सीमा-से-बाहर क्रैश के बजाय चुपचाप (साइलेंट) होती है।
// 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);
यह गलत pc अपवाद अनरैपिंग के दौरान गलत ट्राई-नोट मिलान (try-note matching) को संचालित करता है (HandleExceptionBaseline script->trynotes() पर निर्भर करता है), जिससे एक गलत स्टैक-स्लॉट रीड होता है। एक स्लॉट जो JSObject* नहीं है, उसे फिर एक के रूप में माना जा सकता है और उसके vtable के माध्यम से डिस्पैच किया जा सकता है, जिससे एक प्रकार भ्रम उत्पन्न होता है जो आगे शोषण का आधार है।