
إثبات المفهوم لـ CVE-2026-84118، وهو ثغرة استخدام بعد التحرير (use-after-free) في جامع القمامة (GC) الخاص بـ SpiderMonkey تؤدي إلى قراءة/كتابة خارج الحدود واحتمال تنفيذ كود. يتضمن مشغّل خطأ تجزئة (SEGV) حتميًا وبحثًا حول التصعيد إلى قراءة عشوائية وكسر ASLR.
هذه إحدى الحالات التي صنّفتها Mozilla على أنها حرجة (Critical) بينما كانت في الواقع عالية الخطورة (High). لذا قد يرغب شخص ما في مطالبة Firefox بتحديث مكافأة مكتشفي الثغرة الأصليين إذا كان الأمر كذلك.
الخلاصة المختصرة: يمكنك تنفيذ بدائية قراءة/كتابة خارج الحدود (OOB R) على مؤشر مُتحكَم فيه ثم البدء بالتحرك صعودًا وهبوطًا من ذلك المؤشر. لذا إذا وجّهت المؤشر المُتحكَم فيه إلى مصفوفة، فتهانينا يمكنك فعل أشياء ممتعة.
الـ POC يحتوي فقط على جزء الـ SEGV، لذا فإن الباقي مغامرة ممتعة في توجيه المؤشر داخل مصفوفة والقفز إلى WASM من اختيارك.
إثبات مفهوم حتمي لانهيار SEGV في إصدار الإصدار (release) لمتصفح Firefox/SpiderMonkey 154.0.1. تم إصلاحه في الإصدار 155 (إعادة هيكلة AtomMarking). خطورة Mozilla: عالية (HIGH).
/path/to/release/js poc.js
تمت إعادة الإنتاج على الإصدار 154.0.1: 3/3 على غلاف مُحسَّن عادي (SIGSEGV، خروج 139)، 5/5 على غلاف release+gczeal، نفس الخطأ في كل تشغيل:
SEGV على العنوان 0x000000000014 (قراءة)
JS::shadow::Zone::gcState() <- zone == nullptr، إزاحة +0x14
الإصدار 154 يخلط بين خرائط بتات العلامات (mark) للذرات لكل منطقة (per-zone) مع تتبع المراجع (reference). رمز (Symbol) محتفظ به بشكل ضعيف فقط — هنا، مُلتقَط كحقل Value ضعيف داخل كعب IC الخاص بـ Baseline CacheIR — ومشار إليه من منطقة غير مجمَّعة (uncollected zone) لا يتم إعادة تمييزه أبدًا عندما يتم جمع منطقة الذرات في GC بنطاق منطقة (zone-scoped GC). النتيجة هي حافة قديمة (stale edge) إلى خلية تم كنسها/نقلها.
الضربة القاتلة في إصدار الإصدار هي خطأ ثانٍ ثابت:
MightBeForwarded<JS::Symbol> مُرمَّز بشكل ثابت على false، لذا
IsForwarded تُرجع false بدون فحص (gc/Marking-inl.h:92-97) و
كل مسار إصلاح مؤشر يعتمد عليها يتخطى الحافة بصمت. عندما تكون منطقة الذرات مضغوطة (compacted)، يتم تحديث الحواف القوية بشكل صحيح، لكن هذه
الحافة الضعيفة لـ CacheIR تُترك معلقة بشكل دائم على العنوان القديم لرمز الضحية. (إصدارات التصحيح تلتقطها كـ Assertion failure: !t->isForwarded(), gc/Marking-inl.h:94.)
الـ PoC لا يكتفي بترك مؤشر قديم إلى كومة قابلة لإعادة الاستخدام — بل يجعل الصفحة الداعمة غير مُعيَّنة (munmapped)، بحيث يسبب أي إلغاء مرجعية تالٍ خطأ.
Zone::availableChunks/fullChunks)، لذا فإن قطع منطقة الذرات تحتوي فقط على
حاويات (arenas) لمنطقة الذرات. يمكن التحكم في مصير القطعة بشكل كامل بواسطة
أعمار الذرات/الرموز.c.eval(b) يجعل كعب IC للاستدعاء على المستوى الأعلى يلتقط
الرمز في حقل كعب ضعيف؛ b = undefined يُسقط آخر جذر قوي؛
يتم إسقاط الرش؛ عمليتا GC بنطاق منطقة
(gc("zone"), schedulezone(c) + schedulezone("") + gc("zone") —
السلسلة الفارغة تُجدول منطقة الذرات) تكنسان الرش الميت،
وتضغطان الضحية في حاوية المغناطيس، تتخطيان إصلاح الحافة الضعيفة،
وتحرران حاوية الضحية. قطعتها الآن فارغة تمامًا.gcparam("minEmptyChunkCount", 0) يجعل كل قطعة فارغة
قابلة للانتهاء؛ أثناء sleep(0.5) تقوم مهمة BackgroundDecommitTask بتشغيل
expireEmptyChunkPool -> -> — إلغاء تعيين حقيقي
لقطعة الضحية. (إلغاء الالتزام على مستوى الحاوية هو فقط
— أصفار قابلة للقراءة — تحرير القطعة بالكامل هو
مسار munmap الوحيد، ولهذا فإن عزل القطع في الخطوة 1 مهم.)هذا ليس مجرد DoS. تصعيدات مُثبتة (PoCs وسجلات في
دليلي البحث GC-NDAY/ و CHAIN/):
segv9): بعد الـ munmap،
انقلاب نصف مساحة minorgc() يجعل الحضانة (nursery) تطالب بالعنوان الافتراضي المحرر؛
رش مصفوفة كثيفة مزدوجة يكتب كلمة رباعية (qword) يختارها المهاجم فوق حقل zone
لرأس الحاوية القديم. ثم يقوم الكنس بإلغاء مرجعية
attacker_ptr + 0x14 — حتمي 3/3 عند 0x424242424256.js::Zone* حقيقي تم تسريبه عبر قناة جانبية تخمينية
منفصلة تم زرعه من خلال هذا التعبئة؛ المستهلك (المجمِّع) استهلك
بيانات الحاوية المزورة بدون التسبب بخطأ (زرع تحكم مضطرب ببت واحد
ينهار)، مما يُثبت تكوين هزيمة ASLR -> زرع UAF.FreeChunkPoolUnmapPagesmadvise(MADV_DONTNEED)gc() النهائية تكنس بيانات JIT:
sweepJitDataOnMainThread -> ICEntry::traceWeak ->
TraceWeakCacheIRStub يقرأ مؤشر الرمز القديم، ثم
SweepingTracer::onEdge (gc/Marking.cpp:3195) يقوم بحساب القطع
على العنوان القديم لتحميل حقل zone لرأس الحاوية. الصفحة
اختفت/صُفِّرت، لذا zone == nullptr، و
zone->isGCSweeping() يقرأ nullptr + 0x14 (shadow::Zone::gcState)
-> SIGSEGV. عند الخطأ، يحمل rcx مؤشر الرمز القديم و
rdx قاعدة القطعة غير المُعيَّنة.