
إثبات مفهوم لاستغلال CVE-2026-2763، وهو استخدام بعد التحرير في محرك جافاسكريبت الخاص بمؤسسة موزيلا، يوضح بدائية كتابة مقيدة بمقدار 1-بت تؤدي إلى قراءة/كتابة خارج الحدود.
تنشأ الثغرة من تنفيذ صيغة 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]
}
عند دخول النطاق، يتم استدعاء ValueToIterator عند [1] لإنشاء 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]، يمكن استدعاء دالة استدعاء راجعة (callback)؛ إذا أدى ذلك إلى تشغيل جامع القمامة (GC)، يمكن تحرير PropertyIteratorObject غير المُشار إليه. كما سيقوم المُنهي (finalizer) الخاص به بتحرير كائن 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 بايت).
لا يتم تخصيص NativeIterator عبر مُخصّص جامع القمامة (GC allocator)؛ بدلاً من ذلك، يتم تخصيصه من js::MallocArena. نتيجة لذلك، لا يمكنك المتابعة باستخدام رشّ كائنات JS نموذجي لوضع كائنات كومة GC خاضعة للتحكم بجواره.
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 بايت من تدفق البايت كود ويستخدمها لتعيين InitializedLength للمصفوفة عند [2].
إذا تم تطبيق كتابة البت الواحد على البايت كود عند [1]، يصبح من الممكن تعيين InitializedLength بما يتجاوز السعة الفعلية للمصفوفة. من هناك، يتابع الاستغلال بالاستفادة من بدائيات القراءة/الكتابة خارج الحدود (OOB).