CVE-2026-93241 in Linux
الملخص
بحسب VulDB • 24/09/2026
في نواة لينكس، تم حل الثغرة التالية:
memcg: تجاوز عملية الاسترداد (reclaim) وقاتل OOM للعمليات التي في طور الانهيار بمجرد انتهاء عمل oom_reaper
نرى في Meta حالات حيث تبقى مهمة قُتلت بسبب نفاد الذاكرة (OOM) عالقة في مسار الخروج لعدة ساعات. وفي حالة واحدة محددة، كانت المهمة عالقة لأكثر من 8 ساعات واضطررت إلى إزالة حدود memory.max يدوياً للسماح للعملية بالخروج.
كانت المهمة عبارة عن عملية أحادية وكان لديها ~55 GiB من حد memory.max مع تفعيل zswap. كان لديها ما يقارب 0 anon في الذاكرة و~111 GiB في zswap مضغوطة إلى ~51 GiB في خزان zswap (أي أن كل ذاكرة current كانت تقريباً zswap). لم يتبقَ شيء على قوائم LRUs للاسترداد منه.
عند الفحص الدقيق، لاحظت وجود حوالي 20 ألف خيط من تلك العملية عالقة مع المكدس التالي:
[<0>] mem_cgroup_out_of_memory+0x4e/0xa0
[<0>] charge_memcg+0x8bf/0x990
[<0>] mem_cgroup_swapin_charge_folio+0x4e/0x80
[<0>] __read_swap_cache_async+0x10c/0x260
[<0>] swapin_readahead+0x116/0x3f0
[<0>] do_swap_page+0x13c/0x1ce0
[<0>] handle_mm_fault+0x61d/0x11f0
[<0>] do_user_addr_fault+0x3e7/0x6d0
[<0>] exc_page_fault+0x8f/0x110
[<0>] asm_exc_page_fault+0x22/0x30
[<0>] __get_user_8+0x14/0x20
[<0>] futex_cleanup+0x27/0x1c0
[<0>] futex_exit_release+0x47/0x60
[<0>] do_exit+0x107/0x940
[<0>] do_group_exit+0x81/0xa0
[<0>] get_signal+0x2b1/0x6e0
[<0>] arch_do_signal_or_restart+0x1a/0x1c0
[<0>] exit_to_user_mode_loop+0xa8/0x1c0
[<0>] do_syscall_64+0x152/0x250
[<0>] entry_SYSCALL_64_after_hwframe+0x4b/0x53
بالإضافة إلى ذلك، كان سجل dmesg مليئاً برسائل "نفدت الذاكرة ولا توجد عمليات يمكن قتلها...".
لا أعرف سبب عدم قدرة oom reaper على حصاد/فك ربط العملية. تخميني هو أنه نظراً لأن oom reamer يحاول الحصول على mmap_lock في وضع القراءة لعدد محدود من المرات ثم يستسلم، فقد يكون هناك خيط من تلك العملية كان يمتلك mmap_lock في وضع الكتابة في ذلك الوقت.
كانت شكوكي الأولية هي أن futex_cleanup وخطأ الصفحة (page fault) في النواة يسببان خطأً لا نهائيًا ومحاولات شحن متكررة، لكن هذا تم نفيه في المناقشات السابقة التي حدثت حول مشكلة مماثلة [1].
نظريتي الحالية هي أنه مجرد تسلسل بطيء بسيط خلف oom_lock. وعلى عكس allocator الصفحة، تأخذ كود شحن memcg قفل oom_lock دون استخدام "try". ورغم أن كود OOM الخاص بـ memcg يستخدم mutex_lock_killable()، لاحظ أن استدعاء get_signal() يستهلك SIGKILL (أو sigdelset(SIGKILL)) قبل استدعاء do_group_exit(). لذلك فإن هذا الـ mutex_lock_killable() هو مجرد mutex_lock() هنا. وبالتالي، تنتظر عشرات الآلاف من الخيوط على oom_lock وبشكل فردي تتلقى -EFAULT من get_user() في كود تنظيف futex وتنسحب.
أدت المناقشة من [1] إلى الالتزام a75ffa26122b ("memcg, oom: do not bypass oom killer for dying tasks") الذي يوجه العمليات التي تموت نحو مسار OOM بالضبط بحيث يمكن لـ oom_reaper حصاد mm الخاص بها وتحرير الذاكرة بشكل غير متزامن. لكن المصير (reaper) هو جهد أفضل وحالة واحدة: إذا لم يتمكن من أخذ mmap_lock للقراءة (على سبيل المثال، يحتفظ خيط شقيق به في وضع الكتابة)، فإنه يحدد MMF_OOM_SKIP ولا يحاول مرة أخرى، تاركاً فقط التصريف المتسلسل البطيء جداً المعتمد على oom_lock.
بمجرد تحديد MMF_OOM_SKIP لا يوجد استرداد غير متزامن قادم لـ mm بعد ذلك، لذا فإن المهمة التي تموت والشحن ضدها ليس لديها ما تنتظره: فهي تحرر ذاكرتها فقط بمجرد انتهاء خروجها. ثم يكون تشغيل الاسترداد وقاتل OOM (بدون ضحية) لها أمراً عديم الفائدة، وفعل ذلك لعشرات الآلاف من الخيوط الخارجة هو ما يسلسلهم خلف oom_lock. لذلك، قبل الاسترداد، إذا كان الحالي عملية ضحية لـ OOM انتهى عمل مصيرها، افشل الشحن.
تم إعادة إنتاج المشكلة باستخدام 20 ألف خيط، كل منها يوقف رأس futex قوي على صفحته الخاصة في zswap، وتم قتل المجموعة بأكملها بسبب نفاد الذاكرة (OOM-group-killed) بينما كان شقيقه يحتفظ بـ mmap_lock للكتابة مما جعل المصير يستسلم ويحدد MMF_OOM_SKIP. تم الاختبار على next-20260728 وأظهر الأساس وقت خروج ~90 ثانية، بينما مع التصحيح انخفض وقت الخروج إلى ~3 ثوانٍ.
If you want to get best quality of vulnerability data, you may have to visit VulDB.