
CVE-2026-2763 के लिए प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, जो Mozilla के JavaScript इंजन में use-after-free है, जो एक सीमित 1-बिट राइट प्रिमिटिव का प्रदर्शन करता है जो out-of-bounds read/write की ओर ले जाता है।
यह भेद्यता for-in सिंटैक्स के कार्यान्वयन से उत्पन्न होती है।
एक for-in लूप का स्कोप बाइटकोड की एक जोड़ी द्वारा दर्शाया जाता है: JSOP::Iter और JSOP::EndIter।
CASE(Iter) {
MOZ_ASSERT(REGS.stackDepth() >= 1);
HandleValue val = REGS.stackHandleAt(-1);
JSObject* iter = ValueToIterator(cx, val); // [1]
if (!iter) {
goto error;
}
REGS.sp[-1].setObject(*iter); // [2]
}
...
CASE(EndIter) {
MOZ_ASSERT(REGS.stackDepth() >= 2);
CloseIterator(®S.sp[-2].toObject());
REGS.sp -= 2; // [3]
}
स्कोप प्रवेश पर, [1] पर ValueToIterator को एक PropertyIteratorObject बनाने के लिए कॉल किया जाता है। इंटरप्रेटर फिर इसे [2] पर इंटरप्रेटर स्टैक पर संग्रहीत (पुश) करता है। जब स्कोप समाप्त होता है, तो PropertyIteratorObject को [3] पर हटा दिया जाता है।
static PropertyIteratorObject* GetIteratorImpl(JSContext* cx, HandleObject obj,
bool wantIndices,
bool forObjectKeys) {
...
PropertyIteratorObject* iterobj = CreatePropertyIterator(
cx, obj, keys, supportsIndices, indicesPtr, cacheableProtoChainLength,
ownPropertyCount, forObjectKeys); // [1]
if (!iterobj) {
return nullptr;
}
if (!forObjectKeys) {
RegisterEnumerator(cx, iterobj->getNativeIterator(), obj); // [2]
}
...
return iterobj;
}
static inline void RegisterEnumerator(JSContext* cx, NativeIterator* ni,
HandleObject obj) {
ni->initObjectBeingIterated(*obj);
// Register non-escaping native enumerators (for-in) with the current
// context.
ni->link(cx->compartment()->enumeratorsAddr());
MOZ_ASSERT(!ni->isActive());
ni->markActive();
}
ValueToIterator के अंदर, ऑब्जेक्ट निर्माण ऊपर दिखाए अनुसार किया जाता है। [1] पर इटरेटर ऑब्जेक्ट आवंटित करने के बाद, इसे [2] पर एक वैश्विक लिंक्ड सूची में जोड़ा जाता है। इस बिंदु पर, PropertyIteratorObject का एकमात्र संदर्भ इंटरप्रेटर स्टैक पर रखा जाता है।
void js::CloseIterator(JSObject* obj) {
if (!obj->is<PropertyIteratorObject>()) {
return;
}
// Remove iterator from the active list, which is a stack. The shared iterator
// used for for-in with null/undefined is immutable and unlinked.
NativeIterator* ni = obj->as<PropertyIteratorObject>().getNativeIterator();
if (ni->isEmptyIteratorSingleton()) {
return;
}
ni->unlink();
MOZ_ASSERT(ni->isActive());
ni->markInactive();
ni->clearObjectBeingIterated();
// Reset the enumerator; it may still be in the cached iterators for
// this thread and can be reused.
ni->resetPropertyCursorForReuse();
}
जब स्कोप समाप्त होता है, तो CloseIterator को आमंत्रित किया जाता है और इटरेटर को पहले से लिंक की गई सूची से अलग कर देता है, जिससे स्कोप के बाहर PropertyIteratorObject तक किसी भी पहुंच को रोका जा सके।
हालाँकि, क्योंकि yield को सही ढंग से संभाला नहीं जाता है, CloseIterator चलने से पहले लूप स्कोप के बाहर कोड निष्पादित करना संभव है।
इस स्थिति में, चूंकि PropertyIteratorObject का एकमात्र मजबूत संदर्भ इंटरप्रेटर स्टैक पर है, यदि yield निष्पादित किया जाता है और लौटाया गया जनरेटर भी संदर्भित नहीं है, तो जब GC चलता है तो PropertyIteratorObject को एकत्र (मुक्त) किया जा सकता है।
static bool SuppressDeletedPropertyHelper(JSContext* cx, HandleObject obj,
Handle<JSLinearString*> str) {
NativeIteratorListIter iter(obj->compartment()->enumeratorsAddr());
while (!iter.done()) {
NativeIterator* ni = iter.next();
if (!SuppressDeletedProperty(cx, ni, obj, str)) { // [1]
return false;
}
}
return true;
}
यह फ़ंक्शन तब कॉल किया जाता है जब delete का उपयोग किया जाता है। यह वैश्विक लिंक्ड सूची को पार करता है और हटाई गई संपत्ति के लिए इटरेटर ऑब्जेक्ट्स को अपडेट करता है। [1] पर, एक कॉलबैक आमंत्रित किया जा सकता है; यदि यह GC को ट्रिगर करता है, तो एक असंदर्भित PropertyIteratorObject को मुक्त किया जा सकता है। इसका फाइनलाइज़र संबंधित ni ऑब्जेक्ट को भी मुक्त कर देगा, जिससे उपयोग-के-बाद-मुक्त (UAF) हो जाता है।
static bool SuppressDeletedProperty(JSContext* cx, NativeIterator* ni,
HandleObject obj,
Handle<JSLinearString*> str) {
...
// Check whether id is still to come.
Rooted<JSLinearString*> idStr(cx);
IteratorProperty* cursor = ni->nextProperty();
for (; cursor < ni->propertiesEnd(); ++cursor) {
idStr = cursor->asString();
// Common case: both strings are atoms.
if (idStr->isAtom() && str->isAtom()) {
if (idStr != str) {
continue;
}
} else {
if (!EqualStrings(idStr, str)) {
continue;
}
}
...
cursor->markDeleted(); // [1]
ni->markHasUnvisitedPropertyDeletion();
return true;
}
return true;
}
एक NativeIterator का आकार लक्ष्य ऑब्जेक्ट पर गुणों की संख्या के माध्यम से नियंत्रित किया जा सकता है, और cursor उस प्रॉपर्टी स्लॉट का पॉइंटर है जिसे हटाए गए के रूप में चिह्नित किया जाएगा। चूंकि [1] प्रभावी रूप से एक एकल बिट को OR करता है, यह एक सीमित प्रिमिटिव उत्पन्न करता है: एक मनमाने (8-बाइट संरेखित) पते पर 1-बिट लेखन।
NativeIterator GC आवंटक के माध्यम से आवंटित नहीं किया जाता है; इसके बजाय, इसे js::MallocArena से आवंटित किया जाता है। परिणामस्वरूप, आप नियंत्रित GC-हीप ऑब्जेक्ट्स को इसके निकट रखने के लिए विशिष्ट JS-ऑब्जेक्ट स्प्रे के साथ आगे नहीं बढ़ सकते।
js::UniquePtr<ImmutableScriptData> js::ImmutableScriptData::new_(
FrontendContext* fc, uint32_t codeLength, uint32_t noteLength,
uint32_t numResumeOffsets, uint32_t numScopeNotes, uint32_t numTryNotes) {
auto size = sizeFor(codeLength, noteLength, numResumeOffsets, numScopeNotes,
numTryNotes);
if (!size.isValid()) {
ReportAllocationOverflow(fc);
return nullptr;
}
// Allocate contiguous raw buffer.
void* raw = fc->getAllocator()->pod_malloc<uint8_t>(size.value());
MOZ_ASSERT(uintptr_t(raw) % alignof(ImmutableScriptData) == 0);
if (!raw) {
return nullptr;
}
// Constuct the ImmutableScriptData. Trailing arrays are uninitialized but
// GCPtrs are put into a safe state.
UniquePtr<ImmutableScriptData> result(new (raw) ImmutableScriptData(
codeLength, noteLength, numResumeOffsets, numScopeNotes, numTryNotes));
if (!result) {
return nullptr;
}
// Sanity check
MOZ_ASSERT(result->endOffset() == size.value());
return result;
}
js::MallocArena में आवंटित एक उपयोगी ऑब्जेक्ट ImmutableScriptData है, जो JS बाइटकोड संग्रहीत करता है।
static MOZ_ALWAYS_INLINE void InitElemArrayOperation(JSContext* cx,
jsbytecode* pc,
Handle<ArrayObject*> arr,
HandleValue val) {
MOZ_ASSERT(JSOp(*pc) == JSOp::InitElemArray);
// The dense elements must have been initialized up to this index. The JIT
// implementation also depends on this.
uint32_t index = GET_UINT32(pc); // [1]
MOZ_ASSERT(index < arr->getDenseCapacity());
MOZ_ASSERT(index == arr->getDenseInitializedLength());
// Bump the initialized length even for hole values to ensure the
// index == initLength invariant holds for later InitElemArray ops.
arr->setDenseInitializedLength(index + 1); // [2]
if (val.isMagic(JS_ELEMENTS_HOLE)) {
arr->initDenseElementHole(index);
} else {
arr->initDenseElement(index, val);
}
}
यह JS ऐरे को प्रारंभ करते समय उपयोग किया जाने वाला बाइटकोड हैंडलर है।
यह बाइटकोड स्ट्रीम से 4-बाइट मान पढ़ता है और इसका उपयोग [2] पर ऐरे की InitializedLength सेट करने के लिए करता है।
यदि 1-बिट लेखन [1] पर बाइटकोड पर लागू किया जाता है, तो InitializedLength को ऐरे की वास्तविक क्षमता से परे सेट करना संभव हो जाता है। वहां से, शोषण OOB रीड/राइट प्रिमिटिव का लाभ उठाकर आगे बढ़ता है।