في ربيع عام 2021 وصل إلى مهندس اسمه Max Kellermann بلاغ دعم فني بسيط: عميل يشكو أن ملف سجلات الدخول Access Log الذي نزله لا يمكن فك ضغطه، فالأداة gzip تقول إن فيه خطأ في قيمة التحقق CRC. أصلح المهندس الملف يدوياً وأغلق البلاغ ونسي الأمر، ثم تكرر البلاغ بعد أشهر، ثم تكرر مرة أخرى، وفي كل مرة كان محتوى الملف سليماً وقيمة التحقق في آخره وحدها خاطئة. هذه الملاحظة الصغيرة، التي كان من السهل جداً تجاهلها، قادته بعد عام من المتابعة إلى ثغرة في نواة Linux تسمح لأي مستخدم عادي بالكتابة في ملفات لا يملك صلاحية الكتابة فيها، وأخذت الثغرة الرقم CVE-2022-0847 المعروف باسم Dirty Pipe، بدرجة خطورة 7.8 من 10، وأعلنت في 7 مارس 2022.
هذا المقال ليس عن الثغرة نفسها بقدر ما هو عن طريقة التفكير التي أوصلت إليها، فهي تحقيق في عطل غامض في البيانات انتهى عند أعمق طبقة في النظام، والأدوات التي استخدمها في كل خطوة موجودة على أي سيرفر Linux، ويستطيع أي مهندس أن يتعلمها. وهو امتداد لطريقة التحقيق التي شرحناها في مقال كيف اكتشف Andres Freund الباب الخلفي في XZ، فكلاهما بدأ من عرض صغير جداً رفض صاحبه أن يتجاهله.
وسوف نناقش في هذا المقال ما يلي:
- العرض الذي بدأ منه كل شيء: ملفات مضغوطة تالفة بقيمة تحقق CRC خاطئة، وكيف ظهر منها نمط.
- خطوات التحقيق كما وصفها بنفسه، مع الأداة المناسبة لكل خطوة والسبب وراءها: قراءة البايتات، ثم إعادة الإنتاج بأصغر برنامج، ثم
git bisectللوصول إلى الالتزام Commit المسبب. - المفاهيم التي تحتاجها لتفهم السبب: الأنبوب Pipe، ونظام
splice، وذاكرة الصفحات Page Cache، والنسخ عند الكتابة Copy-on-Write، باختصار وبرسومنا. - كيف عملت الثغرة بشكل عام، والالتزامان اللذان زرعا الخطأ في 2016 ثم جعلاه قابلاً للاستغلال في 2020، والإصدارات المتأثرة والمصححة.
- تجربة عملية Lab آمنة على Containers: نطبق أدوات التحقيق نفسها على حالات بريئة، بلا أي كود استغلال وبلا أي كتابة في ملف للقراءة فقط.
- الدروس التي تستحق أن تنقلها إلى فريقك.
الصورة التالية تبين خطوات التحقيق التي سوف نمر عليها بالترتيب:

من هو Max Kellermann، وأين ظهر العطل؟
Max Kellermann مهندس في شركة الاستضافة الألمانية CM4all التابعة لـ IONOS، ونشر القصة كاملة في صفحة شرح الثغرة على موقع الشركة، وهي مصدرنا الأساسي في هذا المقال. وبيئة العمل التي ظهر فيها العطل مهمة لفهم القصة: كل خوادم الويب عند الشركة ترسل بيانات كل طلب HTTP إلى خوادم سجلات مركزية، ومهمة ليلية تقسم سجلات اليوم السابق إلى ملف لكل موقع، مضغوط بصيغة gzip. وعندما ينزل صاحب الموقع سجلات شهر كامل، تجمع الخدمة ملفات الأيام المضغوطة وترسلها دون فك ضغطها وإعادة ضغطها، وتستخدم في ذلك نظام splice الذي ينقل البيانات من القرص إلى اتصال الشبكة مباشرة داخل النواة دون نسخها إلى مساحة المستخدم، أي نقل بلا نسخ Zero-copy.
ولاحظ أن هذه التفاصيل ليست استطراداً، فكل عنصر منها، أي الملفات المضغوطة، والأنابيب، ونظام splice، سوف يكون خيطاً في التحقيق. والنقطة الأهم أن العطل ظهر في طبقة البيانات لا في طبقة الأمن، فالمهندس لم يكن يبحث عن ثغرة، وإنما كان يطارد فساداً غامضاً في ملفات العملاء، والثغرة كانت هي التفسير الذي وصل إليه في النهاية.
العرض: قيمة تحقق CRC خاطئة في آخر الملف
صيغة gzip تنتهي بثمانية بايتات: أربعة لقيمة التحقق CRC32 على المحتوى الأصلي، وأربعة لطول المحتوى قبل الضغط، كما تصفها وثيقة RFC 1952. وعند فك الضغط يحسب gzip قيمة التحقق من البيانات ويقارنها بالمخزنة في الآخر، فإذا اختلفتا أعلن خطأ CRC. وهذا بالضبط ما رآه المهندس: المحتوى يفك ضغطه، لكن القيمة المخزنة لا تطابقه.
وعندما جمع الملفات التالفة لاحظ شيئاً لا يحدث في الفساد العشوائي: كلها لها القيمة الخاطئة نفسها. فالفساد العشوائي يعطي قيماً مختلفة كل مرة، أما تطابق القيمة في كل الملفات فيعني أن أحداً كتب هذه البايتات كما هي، لا أنها نتجت عن حساب فاشل. وهنا جاءت لحظة المفتاح، فقد نظر إلى البايتات 50 4b وأدرك أنها رمزا الحرفين «P» و«K» بترميز ASCII، وهي بداية كل ترويسة في صيغة ZIP. البايتات التالفة لم تكن فساداً، بل كانت جزءاً من ترويسة ZIP تسربت إلى داخل الملف.
لكن هذا زاد الأمر غموضاً، فالعملية التي تكتب ملفات gzip لا تعرف شيئاً عن ترويسات ZIP، والعملية الوحيدة التي تنشئ ترويسات «PK» هي خدمة الويب التي تبني ملفات ZIP وقت الطلب، وهذه العملية تعمل بمستخدم مختلف لا يملك أصلاً صلاحية الكتابة على ملفات السجلات، وهي لا تحاول الكتابة فيها، إنما تقرؤها فقط. وقد يتساءل البعض: كيف تكتب عملية بايتات في ملف لا تملك صلاحية الكتابة فيه ولا تحاول ذلك؟ والإجابة هي لب القصة كله، وهي ما استغرق من المهندس شهوراً ليصل إليه.
التحقيق خطوة بخطوة
رسالة Max Kellermann مكتوبة كقصة مرتبة حسب ما اكتشفه، وسوف نعرض خطواتها مع الأداة التي يستخدمها أي مهندس في الموقف نفسه، وما قاله هو مربوطاً بمصدره. وقبل أن نبدأ، لاحظ أن أول ما فعله لم يكن أداة بل عادة: كتب ملاحظة بالعطل وتاريخه ولم يحذف الملفات التالفة، فالملاحظة الأولى وحدها كانت صدفة، والملاحظات المتراكمة هي التي صنعت النمط.
الخطوة الأولى: اجمع الأدلة وابحث عن نمط
عندما عاد العطل مرة بعد مرة، فحص القرص كله بحثاً عن ملفات تالفة، واستغرق الفحص يومين، فظهر نمط واضح كتبه بالأرقام: 37 ملفاً تالفاً خلال ثلاثة أشهر، في 22 يوماً مختلفاً، وأكثرها في آخر يوم من كل شهر، وكلها على السيرفر الأساسي الذي يخدم اتصالات HTTP ويبني ملفات ZIP، أما السيرفر الاحتياطي الذي يشغل نفس مهمة تقسيم السجلات لكنه لا يخدم HTTP فلم يكن فيه أي ملف تالف.
وهذا النمط نفسه كان دليلاً، فالطريقة البسيطة هنا أن تلوم العتاد: ربما ذاكرة RAM معطوبة، أو قرص سيئ. ولكن المهندس استبعد ذلك والسبب أن الأعراض لا تشبه عطل العتاد، إذ إن البيانات على السيرفرين متطابقة إلا في مواضع الفساد، وعطل العتاد لا يختار آخر يوم من الشهر ولا يصيب سيرفراً دون آخر يشغل البرنامج نفسه. فالنمط يشير إلى شيء يفعله السيرفر الأساسي ولا يفعله الاحتياطي، وهو خدمة HTTP وبناء ملفات ZIP. وهذا يعلمنا قاعدة عملية شرحناها في دليل استكشاف الأخطاء: ابحث عما اختلف بين الحالة السليمة والحالة المعطوبة، فالفرق بين السيرفرين هو نصف الإجابة.
الخطوة الثانية: اقرأ البايتات، لا تخمنها
بدل أن يخمن سبب الفساد، قرأ البايتات التالفة بايتاً بايتاً وفسر كل واحد منها: 50 4b هي «PK»، و01 02 رمز ترويسة الدليل المركزي Central Directory في صيغة ZIP، والبايتات بعدها هي رقم النسخة. أي أن ما بدا فساداً عشوائياً كان في الحقيقة بداية ترويسة ZIP صحيحة قصت بعد ثمانية بايتات. وهذه مهارة التحقيق الأولى: لا تصف البيانات التالفة بأنها «خربانة» وتنتقل، بل افهم ما تعنيه كل بايتة، فالبيانات تخبرك من كتبها.
ومن هذه القراءة ربط الخيط الأول: الخدمة التي تبني ZIP تكتب أولاً ترويسة ZIP، ثم تستخدم splice لإرسال كل الملفات المضغوطة، ثم تكتب في النهاية ترويسة الدليل المركزي التي تبدأ بـ 50 4b 01 02، وهي نفسها البايتات التي ظهرت في الملفات التالفة. البيانات التي ترسلها الخدمة على الشبكة تطابق تماماً الفساد الذي يظهر على القرص. وفسر بذلك لماذا يصاب آخر يوم من الشهر غالباً: لأن الخدمة ترسل الأيام بالترتيب، فآخر يوم هو الذي تتبعه ترويسة الدليل المركزي مباشرة.
الخطوة الثالثة: متى تتهم النواة؟
بعد أن استبعد كل ما اعتبره مستحيلاً، أي العتاد والكود في مساحة المستخدم، لم يبق إلا احتمال واحد: أن يكون الخطأ في نواة Linux نفسها. وقد كتب أن اتهام النواة يجب أن يكون الملاذ الأخير، والسبب أنها مشروع بالغ التعقيد يطوره آلاف الأشخاص ومع ذلك مستقر وموثوق إلى حد بعيد، فاحتمال أن يكون الخطأ فيها أقل بكثير من احتمال أن يكون في كودك أنت. وهذه قاعدة مهمة في التحقيق: ابدأ من كودك أنت لأنه الأرجح، ولا تتهم الطبقة التي تحتك إلا بعد أن تستبعد ما فوقها بدليل، لا بالشعور.
الخطوة الرابعة: اصنع أصغر برنامج يكرر العطل
عندما اقتنع أنه خطأ في النواة، كتب برنامجين صغيرين جداً لإعادة إنتاج المشكلة خارج نظامه المعقد: أحدهما يحاكي مقسم السجلات، يكتب مقاطع قصيرة متكررة إلى ملف، والآخر يحاكي بناء ZIP، ينقل بيانات من ذلك الملف إلى أنبوب Pipe باستخدام splice ثم يكتب نصاً قصيراً معروفاً إلى الأنبوب. ولن نضع كود هذين البرنامجين هنا، فهما بذرة إثبات استغلال، ويكفي أن تفهم الفكرة: عملية تقرأ الملف فقط وتكتب نصها إلى أنبوب، لا إلى الملف، ومع ذلك ظهر ذلك النص داخل الملف على القرص. عند هذه النقطة صار العطل مؤكداً أنه خطأ في النواة، وصار قابلاً للتكرار في ثوان.
وهنا جملة من رسالته تستحق أن تعلق على جدار كل فريق:
"All bugs become shallow once they can be reproduced."
أي أن كل عطل يصير سطحياً وسهل الفهم بمجرد أن تستطيع تكراره متى شئت. فأصعب ما في التحقيق ليس فهم السبب، بل الوصول إلى تجربة صغيرة تعيد إنتاج العطل عند الطلب، ومتى ملكتها صار الباقي مسألة وقت. وهذه نفس القاعدة التي شرحناها في دليل استكشاف الأخطاء: فرضية واحدة وتجربة واحدة قابلة للتكرار.
الخطوة الخامسة: أي تغيير أدخل العطل؟ git bisect
التجربة الصغيرة فتحت باباً جديداً: صار يستطيع أن يسأل عن كل إصدار من النواة «هل العطل موجود فيك؟». فتحقق بسرعة أن العطل يصيب نواة 5.10 في Debian Bullseye ولا يصيب نواة 4.19 في Debian Buster. بين هذين الإصدارين، كما يذكر، 185011 التزاماً Commit في مستودع Git للنواة، وفحصها واحداً واحداً مستحيل، ولكن أداة git bisect تبحث فيها بطريقة التنصيف Binary Search: تختار التزاماً في المنتصف وتسألك هل هو سليم أم معطوب، ثم تلغي نصف التاريخ بناء على جوابك، وتكرر حتى تصل إلى أول التزام معطوب، وقد وصلت في 17 خطوة فقط. وسوف نرى git bisect يعمل على مستودع صغير في التجربة العملية.
انتهى التنصيف إلى الالتزام f6dd975583bd بعنوان «pipe: merge anon_pipe_buf*_ops»، وهو التزام من مايو 2020 دخل نواة 5.8، يعيد تنظيم كود خلايا الأنبوب، ويغير طريقة فحص ما إذا كانت الكتابة الجديدة يمكن أن تندمج مع خلية سابقة. ولاحظ أن هذا الالتزام هو الذي كشف العطل لا الذي زرعه، كما سنرى في الخطوة التالية، وهذا فرق مهم يخطئ فيه كثير من التحقيقات: أول التزام يظهر عنده العطل ليس دائماً هو مصدر الخطأ.
الخطوة السادسة: الخطأ الأصلي، علم لم يهيأ
عند قراءة الالتزام الذي وصل إليه التنصيف، وجد المهندس أن الخطأ الحقيقي أقدم منه بسنوات. فالالتزام 241699cd72a8 من سبتمبر 2016، في نواة 4.9، أضاف دالتين تنشئان خلية أنبوب جديدة struct pipe_buffer، لكنهما نسيتا تهيئة حقل الأعلام flags فيها. أي صار ممكناً إنشاء خلية تشير إلى الـ Page Cache بأعلام عشوائية متبقية من استخدام سابق. كان ذلك خطأ تقنياً بلا أثر وقتها، والسبب أن كل الأعلام الموجودة آنذاك لم تكن ذات خطر.
ثم جاء التزام 2020 نفسه الذي وصل إليه التنصيف، فحول فحص الدمج إلى علم لكل خلية اسمه PIPE_BUF_FLAG_CAN_MERGE. ومن هنا صار العلم غير المهيأ خطيراً: إذا بقي هذا العلم مضبوطاً في خلية تشير إلى صفحة في الـ Page Cache، أمكن الكتابة فوق محتوى تلك الصفحة، أي فوق محتوى ملف على القرص، بمجرد الكتابة إلى الأنبوب بطريقة مهيأة خصيصاً. وهذا مثال كلاسيكي على كيف يتحول خطأ نائم لا أثر له إلى ثغرة خطيرة حين يتغير كود آخر بعد سنوات، وهو درس في أن «الخطأ الذي بلا أثر» يستحق الإصلاح، لأن أحداً لا يعرف أي كود مستقبلي سوف يوقظه.
ولمن يريد الخط الكامل، فإن فحص الدمج هذا أعيد تنظيمه ثلاث مرات عبر السنين: أدخله الالتزام 5274f052e7b3 مع نظام splice عام 2006، ثم بدله الالتزام 01e7187b4119 عام 2019 من علم إلى مقارنة مؤشرات، ثم حوله التزام 2020 إلى علم لكل خلية. كل خطوة بدت سليمة وحدها، والمشكلة ظهرت من تقاطع خطأ قديم مع تغيير جديد.
الخطوة السابعة: البلاغ، لمن وبأي ترتيب؟
مثل أي تحقيق أمني، لم تنته القصة بفهم السبب بل بالبلاغ الصحيح. ففي 20 فبراير 2022 أرسل المهندس البلاغ مع الإصلاح إلى فريق أمن نواة Linux، وفي اليوم التالي أعاد إنتاج العطل على هاتف Google Pixel 6 وأبلغ فريق أمن Android. وبناء على نصيحة Linus Torvalds وآخرين، أرسل الإصلاح إلى القائمة البريدية العامة للنواة دون ذكر تفاصيل الثغرة، حتى يدخل الإصلاح بسرعة، ثم أبلغ قائمة توزيعات Linux قبل الإعلان بأيام، وجاء الإعلان العلني في 7 مارس 2022. والدرس أن ترتيب البلاغ جزء من المسؤولية: تبلغ من يصلح أولاً، وتعطي الجهات وقتاً لتجهيز التحديثات، ولا تنشر التفاصيل قبل أن يتوفر الإصلاح للناس.
المفاهيم التي تحتاجها: Pipe وsplice وPage Cache
قبل أن نجمع السبب، نحتاج أربعة مفاهيم، نشرحها باختصار لأن لكل منها تفصيلاً في مقالات أخرى على الموقع.
- الصفحة Page وذاكرة الصفحات Page Cache: أصغر وحدة ذاكرة تديرها النواة هي صفحة Page حجمها 4 كيلوبايت عادة. وعندما تقرأ ملفاً من القرص، تنسخ النواة محتواه إلى صفحات في الذاكرة تسمى الـ Page Cache، فإذا طلب الملف ثانية جاء من الذاكرة بدل القرص، كما تشرح وثيقة إدارة الذاكرة في النواة، وقد رأيناها حية في مقال تحليل أداء سيرفر Linux في 60 ثانية.
- الأنبوب Pipe: قناة اتصال في اتجاه واحد بين عمليتين، طرف يكتب فيها وطرف يقرأ منها، وهو نفس الأنبوب الذي تستخدمه كل يوم بين أمرين على سطر الأوامر كما في مقال سطر الأوامر في Linux. وتنفذ النواة الأنبوب كحلقة من الخلايا
struct pipe_buffer، كل خلية تشير إلى صفحة. وأول كتابة تحجز صفحة، وإذا لم تملأها الكتابة تماماً جاز للكتابة التالية أن تكمل في الصفحة نفسها بدل حجز صفحة جديدة، وهذا هو «الدمج» الذي يحدده علمCAN_MERGE. - نظام splice: أمر في النواة ينقل بيانات بين ملف وأنبوب دون المرور بمساحة المستخدم، كما يشرح دليل splice. والنواة تنفذه بتمرير إشارات Page References لا بنسخ البيانات فعلاً، أي نقل بلا نسخ Zero-copy. وهذا هو موضع الخطر: بعد
spliceمن ملف، تشير خلية الأنبوب إلى صفحة في الـ Page Cache يملكها النظام لا الأنبوب، فيجب ألا تندمج أي كتابة جديدة معها، وإلا كتبت فوق الملف. - النسخ عند الكتابة Copy-on-Write: حيلة تسمح لعدة عمليات بمشاركة الصفحة نفسها ما دامت للقراءة فقط، فإذا أراد أحدها الكتابة تصنع النواة له نسخة خاصة أولاً. وثغرة Dirty Pipe شبيهة بثغرة أقدم اسمها Dirty Cow (CVE-2016-5195) المبنية على خلل في آلية النسخ عند الكتابة، ومن هنا جاء اسمها، لكن Dirty Pipe أسهل استغلالاً وأنظف أثراً كما سنرى.
كيف عملت الثغرة؟
سوف نشرح الفكرة العامة فقط، دون أي خطوة قابلة للاستغلال. الصورة التالية تبين الفرق بين الكتابة إلى أنبوب عادي والكتابة بعد splice من ملف:

في أنبوب عادي يكتب فيه برنامج، تحجز أول كتابة صفحة يملكها الأنبوب، والعلم CAN_MERGE مضبوط، فالكتابة التالية تكمل في الصفحة نفسها، وهذا سليم لأنه لا ملف وراء تلك الصفحة. أما بعد splice من ملف، فتشير خلية الأنبوب إلى صفحة في الـ Page Cache يملكها النظام ويقف وراءها ملف على القرص، ولذلك يجب أن يكون العلم مطفأ حتى لا تندمج أي كتابة تالية مع هذه الصفحة. لكن بسبب خطأ 2016، لم يكن حقل الأعلام يهيأ عند إنشاء الخلية، فبقي العلم مضبوطاً من استخدام سابق للأنبوب، فتدخل الكتابة التالية إلى صفحة الـ Page Cache وتكتب فوق محتوى الملف.
وهذا يفسر فساد السجلات بالضبط: الخدمة تكتب بيانات إلى الأنبوب، ثم تنقل ملفات مضغوطة كثيرة بـ splice فتنشئ خلايا تشير إلى الـ Page Cache، وبعضها يرث العلم مضبوطاً عشوائياً، فإذا جاءت كتابة ترويسة الدليل المركزي 50 4b 01 02 على خلية ورثت العلم، دخلت إلى صفحة آخر ملف مضغوط بدل أن تذهب إلى الأنبوب. وقد يتساءل البعض: لماذا تفسد ثمانية بايتات فقط؟ والإجابة أن كل الترويسة تنسخ إلى الصفحة، لكن العملية لا تزيد حجم الملف، فلا يكتب إلا في المساحة غير المستعملة في آخر الصفحة، وكانت ثمانية بايتات.
وهنا تظهر خاصية مقلقة شرحها المهندس: هذا التغيير لا يصل إلى القرص غالباً، والسبب أن الكتابة في الـ Page Cache بهذه الطريقة لا تجعل الصفحة «متسخة» Dirty، والنواة لا تكتب الصفحة إلى القرص إلا إذا اعتبرتها متسخة. فما لم تكتب عملية أخرى في الملف فعلاً، يبقى التغيير في الذاكرة فقط ويختفي بعد إعادة التشغيل أو عند إخلاء الصفحة من الذاكرة. ولذلك كان الأثر الوحيد الظاهر في قصة CM4all هو فساد عابر في ملفات ينزلها العملاء من الذاكرة، لا فساد دائم على القرص، وهذا ما جعل التحقيق أصعب.
وأما لماذا صارت ثغرة أمنية لا مجرد عطل بيانات، فلأن المهاجم، حين فهم الآلية، لا يحتاج أن ينتظر عملية تكتب الملف، ولا يتقيد بتوقيت، إذ يستطيع أن يكتب في الـ Page Cache لأي ملف يملك صلاحية قراءته، بشروط بسيطة في الموضع والحجم. والكتابة إلى أنبوب لا تفحص أي صلاحيات، والـ Page Cache قابل للكتابة دائماً من النواة، ولذلك تعمل الثغرة حتى على ملفات غير قابلة للتعديل Immutable وعلى أنظمة ملفات مركبة للقراءة فقط. ولن نذكر خطوات الاستغلال، فهي في المصدر الأصلي لمن يحتاجها لأغراض الدفاع، والمهم هنا أن تفهم لماذا ظهرت الأعراض التي رآها المحقق.
الإصلاح والإصدارات المتأثرة
الإصلاح بسيط إلى درجة مدهشة بالنظر إلى خطورة الثغرة، وهو الالتزام 9d2231c5d74e بعنوان «lib/iov_iter: initialize flags in new pipe_buffer»، ويضيف سطرين فقط يضعان buf->flags = 0 في الدالتين اللتين كانتا تنشئان خلية أنبوب دون تهيئة الأعلام، أي يصلح الخطأ الأصلي من 2016 مباشرة. ويذكر الالتزام نفسه في سطر Fixes: أنه يصلح الالتزام 241699cd72a8، وهو تأكيد من مطوري النواة أنفسهم على أن مصدر الخطأ هو تغيير 2016 لا تغيير 2020.
ويصف تعريف الثغرة في NVD السبب بأنه نقص في تهيئة حقل flags في الدالتين copy_page_to_iter_pipe وpush_pipe، ما يسمح لمستخدم محلي عادي بالكتابة في صفحات الـ Page Cache المدعومة بملفات للقراءة فقط ومن ثم رفع صلاحياته. وفيما يخص الإصدارات:
- المتأثرة: نواة Linux من الإصدار 5.8 فصاعداً، إذ إن علم
PIPE_BUF_FLAG_CAN_MERGEالذي جعل الخطأ قابلاً للاستغلال دخل في 5.8. أما الإصدارات الأقدم من 5.8 فليست متأثرة بهذا المسار. - المصححة: صدرت الإصدارات المستقرة بالإصلاح في 23 فبراير 2022، وهي
5.16.11و5.15.25و5.10.102فما أحدث، وكل فرع أحدث من 5.17 يحمل الإصلاح أصلاً.
ولاحظ أن فرع 5.8 نفسه كان قد انتهى دعمه، فمن بقي عليه لم يحصل على إصلاح على فرعه، والحل هو الانتقال إلى فرع مدعوم مصحح، وسوف نكتب سكربتاً صغيراً في التجربة يحكم على رقم أي نواة.
الاستجابة: من تأثر فعلاً؟
خطورة Dirty Pipe أنها لا تحتاج إلا حساب مستخدم عادي على النظام، ولذلك كانت خطيرة بوجه خاص على الأنظمة متعددة المستخدمين وعلى الهواتف. وفيما يلي ما فعلته كل جهة بحسب مصادرها الرسمية:
- وكالة CISA الأمريكية: أضافت الثغرة في 25 أبريل 2022 إلى قائمة الثغرات المستغلة المعروفة KEV تحت اسم Dirty Pipe، مع تصنيفها خللاً في التهيئة من نوع CWE-665، وأعطت الجهات الحكومية مهلة للإصلاح. ووجودها في هذه القائمة يعني أن الاستغلال في العالم الحقيقي مؤكد لا نظري.
- Android وGoogle: دمجت Google الإصلاح في نواة Android في 24 فبراير 2022 عبر مراجعة الكود المعلنة، ثم أدرجت الثغرة في نشرة أمان Android لمايو 2022، وأشارت إلى أنها قد تكون تحت استغلال محدود وموجه. وتأثر الأجهزة التي تعمل بنواة 5.8 فأحدث، ومنها هواتف حديثة مثل Pixel 6 وGalaxy S22.
- Red Hat: يوضح سجل Red Hat للثغرة أن الإصدارات التي تستخدم نواة 5.8 فأحدث متأثرة، وأن Red Hat Enterprise Linux 8 و9 كانا متأثرين لأنهما يشحنان نوى حديثة، مع درجة خطورة مهمة Important.
- التوزيعات عموماً: نشرت كل التوزيعات الكبرى تحديثات نواة خلال أيام من الإعلان، لأن الإصلاح صغير وواضح وسهل الدمج في كل الفروع المدعومة.
والصورة التالية تجمع الخط الزمني من زرع الخطأ في الكود إلى الإصلاح والإعلان:

تجربة عملية: نطبق أدوات التحقيق على حالات بريئة
الآن نطبق أدوات هذا التحقيق بأنفسنا، لكن على حالات آمنة تماماً. لن نشغل أي كود استغلال، ولن نكتب في أي ملف للقراءة فقط، ولن ننزل أي «أداة فحص» من الإنترنت. سوف نرى بدل ذلك كيف تعمل الأدوات التي يستخدمها أي محقق: كيف ينقل splice البيانات بين ملف وأنبوب، وكيف نراقب الـ Page Cache، وكيف نكتشف ملف gzip تالفاً بخطأ CRC، وكيف يجد git bisect التزاماً أدخل عطلاً، وكيف نحكم على رقم نواة هل هو في النطاق المتأثر.
lab-dirtypipe-، ولا يفتح أي منفذ على جهازك. كل الملفات التي نتعامل معها ننشئها نحن ونملكها، وكل كتابة في التجربة في ملف نملك صلاحية الكتابة فيه. لا تنزل أي سكربت فحص لـ Dirty Pipe من الإنترنت وتشغله بصلاحيات root على سيرفر حقيقي، والسبب أن هذا بالضبط ما يستغله المهاجمون بعد كل ثغرة مشهورة، والفحص الآمن هو قراءة رقم النواة كما سنفعل في آخر التجربة.تجهيز ال Container
نشغل Container من Debian 13.1 بإصدار مثبت، ثم نثبت فيه أدوات التحقيق:
docker run -d --name lab-dirtypipe-lab debian:13.1 sleep infinity
docker exec -it lab-dirtypipe-lab bash
apt-get update
apt-get install -y python3 gzip git util-linux util-linux-extra vmtouch coreutils
python3 --version; git --version; fincore --version | head -1; uname -rوالمخرج سوف يكون كما يلي:
Python 3.13.5
git version 2.47.3
fincore from util-linux 2.41.5
5.15.167.4-microsoft-standard-WSL2لاحظ أن النواة التي تظهر هي نواة المضيف، فال Container يشارك نواة الجهاز الذي يشغل Docker، وهنا نواة WSL2 رقمها 5.15.167.4، وهي أحدث من 5.15.25 المصححة، وسوف نحكم عليها آلياً في آخر التجربة.
كيف ينقل splice البيانات بين ملف وأنبوب؟
أول أداة في القصة كانت splice، ولفهمها نكتب برنامجاً صغيراً بلغة Python ينقل بايتات من ملف نملكه إلى أنبوب بـ os.splice، ثم يقرؤها من الأنبوب. الملف يفتح للقراءة فقط، ولا نكتب فيه شيئاً:
#!/usr/bin/env python3
# splice_demo.py: ينقل بيانات من ملف نملكه إلى أنبوب بـ os.splice بلا نسخ، ثم يقرؤها من الأنبوب.
# الملف يفتح للقراءة فقط ولا نكتب فيه شيئاً.
import os
path = "/work/own.txt"
fd = os.open(path, os.O_RDONLY) # الملف يفتح للقراءة فقط
r, w = os.pipe() # أنبوب: r طرف القراءة، w طرف الكتابة
moved = os.splice(fd, w, 16, offset_src=0) # ننقل 16 بايتاً من الملف إلى الأنبوب، النواة تمرر إشارة إلى صفحة
print("splice moved bytes :", moved)
print("read from pipe :", os.read(r, 16).decode())
os.close(fd); os.close(r); os.close(w)نحفظ الملف في /work/splice_demo.py، وننشئ ملفاً نصياً نملكه ثم نشغل البرنامج:
mkdir -p /work
printf "HELLO-FROM-FILE-ARABROOT-LAB\n" > /work/own.txt
python3 /work/splice_demo.py
cat /work/own.txtوالمخرج سوف يكون كما يلي:
splice moved bytes : 16
read from pipe : HELLO-FROM-FILE-
HELLO-FROM-FILE-ARABROOT-LABفي المخرج أعلاه لاحظ التالي:
- البرنامج نقل 16 بايتاً من الملف إلى الأنبوب ثم قرأها، دون أن ينسخ محتوى الملف إلى متغير في مساحة المستخدم، وهذا هو النقل بلا نسخ Zero-copy الذي يجعل
spliceسريعاً، ويشرح تفاصيله دليل splice. - الملف بقي كما هو، لأننا فتحناه للقراءة فقط ولم نكتب فيه. هذا هو الجانب الآمن من
splice، والخطر في القصة الحقيقية كان في خطوة تالية هي الكتابة إلى الأنبوب بعدsplice، وهي ما لن نكررها.
مراقبة الـ Page Cache قبل القراءة وبعدها
في القصة كان الفساد يحدث في الـ Page Cache لا على القرص، لذلك من المهم أن نرى هذه الذاكرة بأعيننا. ننشئ ملفاً كبيراً، ثم نخرجه من الذاكرة بأداة vmtouch، ونقيس كم من صفحاته محمل في الذاكرة بأداة fincore قبل القراءة وبعدها:
cd /work
dd if=/dev/urandom of=big.bin bs=1M count=200 status=none
sync; vmtouch -e big.bin >/dev/null # أخرج الملف من الـ Page Cache
echo "== fincore قبل القراءة =="
fincore big.bin
grep -E "^Cached:" /proc/meminfo
echo "== نقرأ الملف كاملاً مرة واحدة =="
cat big.bin > /dev/null
echo "== fincore بعد القراءة =="
fincore big.bin
grep -E "^Cached:" /proc/meminfo
echo "== vmtouch يبين الصفحات المحملة =="
vmtouch big.binوالمخرج سوف يكون كما يلي:
== fincore قبل القراءة ==
RES PAGES SIZE FILE
0B 0 200M big.bin
Cached: 7555648 kB
== نقرأ الملف كاملاً مرة واحدة ==
== fincore بعد القراءة ==
RES PAGES SIZE FILE
200M 51200 200M big.bin
Cached: 7760340 kB
== vmtouch يبين الصفحات المحملة ==
Files: 1
Directories: 0
Resident Pages: 51200/51200 200M/200M 100%
Elapsed: 0.005187 secondsفي المخرج أعلاه لاحظ التالي:
- قبل القراءة كان
fincoreيعرض 0 صفحة من الملف في الذاكرة، وبعد قراءته مرة واحدة صارت كل صفحاته 51200 صفحة في الذاكرة، وهي 200 ميجابايت مقسومة على 4 كيلوبايت للصفحة، ويشرح الأداة دليل fincore. - قيمة
Cachedفي/proc/meminfoارتفعت نحو 200 ميجابايت بعد القراءة، وهذا هو الـ Page Cache يكبر، وقد شرحنا أن هذه الذاكرة ليست مستهلكة وتتخلى عنها النواة عند الحاجة في مقال تحليل أداء سيرفر Linux. - الصفحة Page هي وحدة الـ Page Cache، وهي نفسها الوحدة التي كانت الثغرة تكتب فيها، فرؤيتها بهذه الأدوات تجعل الآلية ملموسة بدل أن تبقى مجردة.
اكتشاف ملف gzip تالف بخطأ CRC
العرض الأول في القصة كان خطأ CRC في ملف gzip. ننشئ ملف سجلات ونضغطه، ونرى أن gzip -t يمر عليه، ثم نفسد نحن بايتة واحدة من قيمة التحقق في آخره، فيعلن gzip -t خطأ CRC، تماماً كالعرض في القصة:
cd /work
for i in $(seq 1 500); do echo "2022-02-19 request $i GET /index.html 200"; done > access.log
gzip -k -n access.log
echo "== ملف gzip سليم يمر في اختبار السلامة =="
gzip -t access.log.gz && echo "gzip -t: OK"
echo "== آخر 8 بايتات: CRC32 ثم الطول، بترتيب little-endian =="
sz=$(stat -c%s access.log.gz)
od -An -tx1 -j $((sz-8)) access.log.gz
# نفسد بايتة واحدة من قيمة التحقق المخزنة، في نسخة نملكها
cp access.log.gz crc.gz
printf '\xff' | dd of=crc.gz bs=1 seek=$((sz-8)) count=1 conv=notrunc status=none
echo "== gzip -t الآن يعلن خطأ CRC، نفس عرض القصة =="
gzip -t crc.gz; echo "exit code: $?"
od -An -tx1 -j $((sz-8)) crc.gzوالمخرج سوف يكون كما يلي:
== ملف gzip سليم يمر في اختبار السلامة ==
gzip -t: OK
== آخر 8 بايتات: CRC32 ثم الطول، بترتيب little-endian ==
69 d4 98 cd 90 53 00 00
== gzip -t الآن يعلن خطأ CRC، نفس عرض القصة ==
gzip: crc.gz: invalid compressed data--crc error
exit code: 1
ff d4 98 cd 90 53 00 00في المخرج أعلاه لاحظ التالي:
- الأمر
gzip -tيفك الضغط في الذاكرة ويقارن قيمة التحقق المحسوبة بالمخزنة، فإذا اختلفتا أعلنcrc errorوأعاد رمز خروج 1، وهذا هو العرض الذي بدأت منه القصة، والصيغة موصوفة في وثيقة RFC 1952. - غيرنا بايتة واحدة في قيمة التحقق المخزنة فقط، والبيانات المضغوطة لم تتغير، تماماً كما كان فساد القصة في بايتات قليلة في آخر الملف مع سلامة المحتوى. وهذا يفسر لماذا بدا المحتوى سليماً وقيمة التحقق وحدها خاطئة.
- الدرس العملي: اجعل فحص سلامة ملفاتك المضغوطة آلياً بـ
gzip -tفي مهامك الدورية، فهو أرخص طريقة لاكتشاف فساد مبكر قبل أن يصل إلى العميل.
إيجاد التزام أدخل عطلاً بـ git bisect
الأداة التي أوصلت المهندس إلى الالتزام المسبب كانت git bisect. نبنيها على مستودع صغير من أحد عشر التزاماً، أحدها يدخل «عطلاً» في سكربت بسيط، ثم نترك git bisect يجده آلياً. أولاً نبني المستودع:
cd /work
rm -rf toy && mkdir toy && cd toy
git init -q
git config user.email [email protected]; git config user.name "ArabRoot Lab"
printf '#!/bin/sh\n# good behaviour: echo the two args joined\necho "$1$2"\n' > merge.sh
chmod +x merge.sh
git add merge.sh; git commit -q -m "c0: initial merge works"
for i in 1 2 3 4 5; do echo "# tweak $i" >> notes.txt; git add notes.txt; git commit -q -m "c$i: unrelated change"; done
# العطل: إعادة تنظيم تسقط الوسيط الثاني بالخطأ
sed -i 's/echo "$1$2"/echo "$1"/' merge.sh
git add merge.sh; git commit -q -m "c6: refactor merge (regression hides here)"
for i in 7 8 9 10; do echo "# tweak $i" >> notes.txt; git add notes.txt; git commit -q -m "c$i: unrelated change"; done
git rev-list --count HEADثم نكتب اختباراً يعيد 0 إذا كان السكربت يدمج الوسيطين، و1 إذا لم يفعل، ونشغل git bisect run بين آخر التزام وعشرة التزامات قبله، فيجد أول التزام معطوب بنفسه:
cat > /work/test.sh <<'EOF'
#!/bin/sh
cd /work/toy
[ "$(./merge.sh AA BB)" = "AABB" ]
EOF
chmod +x /work/test.sh
git bisect start HEAD HEAD~10
git bisect run /work/test.sh
git bisect resetوالمخرج سوف يكون كما يلي:
running '/work/test.sh'
Bisecting: 2 revisions left to test after this (roughly 1 step)
[cb86d1618a0e4f2f38017bbbdbaefefa9afe2405] c7: unrelated change
running '/work/test.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4b160f42a22b865fe1bdc68d7548e553650fb0ba] c6: refactor merge (regression hides here)
running '/work/test.sh'
4b160f42a22b865fe1bdc68d7548e553650fb0ba is the first bad commit
commit 4b160f42a22b865fe1bdc68d7548e553650fb0ba
Author: ArabRoot Lab <[email protected]>
Date: Thu Oct 8 15:33:54 2026 +0000
c6: refactor merge (regression hides here)
merge.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commitفي المخرج أعلاه لاحظ التالي:
- وجدت الأداة الالتزام
c6في خطوتين فقط بين أحد عشر التزاماً، بفضل التنصيف Binary Search: كل اختبار يلغي نصف التاريخ المتبقي، كما يشرح دليل git bisect. وهذا هو السبب في أن بحث المهندس احتاج 17 خطوة فقط بين 185 ألف التزام، لأن عدد الخطوات يكبر لوغاريتمياً لا خطياً مع عدد الالتزامات. - الخيار
git bisect runيجعل البحث آلياً تماماً: تعطيه سكربتاً يعيد 0 للسليم وغير الصفر للمعطوب، فيشغله على كل التزام يختاره حتى يصل. وهذا يحول ساعات من الاختبار اليدوي إلى دقائق، وقد شرحنا أساسيات Git في مقال إدارة النسخ وGit. - أول التزام معطوب ليس دائماً مصدر الخطأ الجذري، كما رأينا في القصة حيث كشف التزام 2020 خطأ 2016، لكن
git bisectيوصلك إلى نقطة واضحة تبدأ منها القراءة.
هل نواتك في النطاق المتأثر؟
الفحص الآمن لـ Dirty Pipe لا يحتاج أي سكربت من الإنترنت، إنما رقم النواة فقط. نكتب سكربتاً يقرأ الرقم من uname -r أو من وسيط، ويحكم عليه حسب النطاق: متأثرة من 5.8، ومصححة في 5.10.102 و5.15.25 و5.16.11 وما بعدها. اقرأه قبل تشغيله، فهو يقرأ ولا ينزل شيئاً:
#!/bin/sh
# check-dirtypipe.sh: يحكم على رقم نواة حسب نطاق Dirty Pipe (CVE-2022-0847).
# متأثرة: Linux 5.8 فأحدث. مصححة في 5.10.102 و5.15.25 و5.16.11 (و5.17 فأحدث).
# يقرأ رقماً فقط، من الوسيط أو من uname -r، ولا ينزل ولا يشغل شيئاً.
rel=${1:-$(uname -r)}
set -- $(echo "$rel" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' | tr '.' ' ')
maj=$1 min=$2 pat=$3
key=$(( maj*1000000 + min*1000 + pat ))
verdict="not affected (older than 5.8)"
if [ "$key" -ge 5008000 ]; then
case "$min.$maj" in
10.5) [ "$pat" -ge 102 ] && verdict="patched (5.10.102+)" || verdict="VULNERABLE (5.10 before .102)" ;;
15.5) [ "$pat" -ge 25 ] && verdict="patched (5.15.25+)" || verdict="VULNERABLE (5.15 before .25)" ;;
16.5) [ "$pat" -ge 11 ] && verdict="patched (5.16.11+)" || verdict="VULNERABLE (5.16 before .11)" ;;
*) if [ "$key" -ge 5017000 ]; then verdict="patched (5.17+)"; else verdict="VULNERABLE (no stable fix on this branch)"; fi ;;
esac
fi
printf '%-34s -> %s\n' "$rel" "$verdict"نحفظه في /work/check-dirtypipe.sh، ثم نشغله على النواة الحالية وعلى أرقام أمثلة:
echo "== النواة الحالية =="
sh /work/check-dirtypipe.sh
echo "== أرقام أمثلة =="
for v in 4.19.0-amd64 5.8.0-generic 5.10.92-generic 5.10.102-generic 5.15.167.4-microsoft-standard-WSL2 6.1.0-amd64; do
sh /work/check-dirtypipe.sh "$v"
doneوالمخرج سوف يكون كما يلي:
== النواة الحالية ==
5.15.167.4-microsoft-standard-WSL2 -> patched (5.15.25+)
== أرقام أمثلة ==
4.19.0-amd64 -> not affected (older than 5.8)
5.8.0-generic -> VULNERABLE (no stable fix on this branch)
5.10.92-generic -> VULNERABLE (5.10 before .102)
5.10.102-generic -> patched (5.10.102+)
5.15.167.4-microsoft-standard-WSL2 -> patched (5.15.25+)
6.1.0-amd64 -> patched (5.17+)في المخرج أعلاه لاحظ التالي:
- النواة الحالية 5.15.167 أحدث من 5.15.25 المصححة، فالحكم «مصححة»، وهذا هو الفحص كله: رقم النواة لا غير.
- الرقم 5.8.0 متأثر لكن بلا إصلاح على فرعه لأن 5.8 انتهى دعمه، والحل الانتقال إلى فرع مدعوم مصحح، لا انتظار تحديث على فرع ميت.
- إذا كنت تفحص أسطولاً من السيرفرات أو Docker Images، فأدوات فحص الاعتماديات والثغرات تعرف هذه الثغرة من قواعد بياناتها، كما شرحنا في مقال فحص ال Docker Images مع Trivy، وهي أنسب من سكربت يدوي عندما يكبر العدد.
حذف التجربة
نحذف ال Container بالاسم من جهازك، وهذا يزيل معه كل الملفات التي أنشأناها بداخله:
docker rm -f lab-dirtypipe-labماذا نتعلم كمهندسين؟
- خذ فساد البيانات الغريب على محمل الجد: ملف مضغوط بقيمة تحقق خاطئة هو شيء يسهل إصلاحه يدوياً وإغلاق البلاغ، لكن خلفه كانت ثغرة في نواة Linux. القاعدة العملية: إذا رأيت فساداً لا تفسره، فلا تكتف بإصلاح العرض، واكتب ملاحظة بتاريخه حتى لو لم يكن عندك وقت للتحقيق الآن.
- احفظ الأدلة واجمعها: المهندس لم يحذف الملفات التالفة، ففحص القرص كله بحثاً عن نمط، والنمط (آخر يوم من الشهر، سيرفر دون آخر) كان نصف الحل. وهذا جوهر التحقيق الرقمي الذي شرحناه في طريقة «ما الذي اختلف؟» في دليل استكشاف الأخطاء.
- اقرأ البيانات، لا تصفها بأنها خربانة: البايتات
50 4bلم تكن فساداً عشوائياً بل حرفي «PK»، وفهم كل بايتة هو ما كشف أن مصدر الكتابة خدمة ZIP. البيانات تخبرك من كتبها إذا قرأتها. - اصنع أصغر تجربة تعيد العطل: «كل عطل يصير سطحياً بمجرد أن تستطيع تكراره». الانتقال من نظام إنتاج معقد إلى برنامجين صغيرين هو ما حول اللغز إلى خطأ مؤكد وقابل للتنصيف.
- استخدم git bisect: عندما يظهر عطل بين إصدارين، لا تقرأ آلاف الالتزامات، بل اجعل التنصيف يصل إلى الالتزام المسبب في خطوات لوغاريتمية، ويفضل أن تعطيه سكربت اختبار آلياً بـ
git bisect run. - أول التزام معطوب ليس دائماً الخطأ الجذري: هنا كشف تغيير 2020 خطأً نائماً من 2016. ولذلك «الخطأ الذي بلا أثر» يستحق الإصلاح، لأن أحداً لا يعرف أي تغيير مستقبلي سوف يوقظه.
- البلاغ المسؤول جزء من العمل: أبلغ من يصلح أولاً، أعط الجهات وقتاً لتجهيز التحديثات قبل الإعلان العلني، ولا تنشر التفاصيل قبل توفر الإصلاح. وهذه نفس قاعدة الإفصاح التي رأيناها في تحقيق XZ.
- قلل من يعمل بصلاحيات واسعة على سيرفرك: خطورة Dirty Pipe أنها تحتاج حساب مستخدم عادي فقط، فكل حساب على نظامك باب محتمل، وتحديث النواة فور صدور الإصلاح هو خط الدفاع الأول، وقد شرحنا تحديث السيرفر وتأمينه في مقال تأمين سيرفر VPS.
الخلاصة
- بدأ اكتشاف ثغرة Dirty Pipe من عرض صغير: ملفات سجلات مضغوطة بصيغة gzip بقيمة تحقق CRC خاطئة في آخرها، مع سلامة المحتوى.
- قاد النمط (آخر يوم من الشهر، السيرفر الأساسي فقط) وقراءة البايتات (
50 4bأي «PK»، بداية ترويسة ZIP) المهندس Max Kellermann إلى استنتاج أن المشكلة في نواة Linux. - سلسلة التحقيق: اجمع الأدلة وابحث عن نمط، اقرأ البايتات، اصنع أصغر برنامج يعيد العطل، ثم
git bisectللوصول إلى الالتزام المسبب في 17 خطوة. - السبب خطأ من 2016 (التزام
241699cd72a8) ترك حقل الأعلام في خلية الأنبوب دون تهيئة، وصار قابلاً للاستغلال بتعديل 2020 (التزامf6dd975583bd) الذي أدخل علمPIPE_BUF_FLAG_CAN_MERGE، فأمكنت الكتابة فوق صفحات الـ Page Cache لملفات للقراءة فقط. - الثغرة أخذت الرقم CVE-2022-0847 بدرجة 7.8، وتصيب نواة Linux من 5.8، وأصلحت في 5.16.11 و5.15.25 و5.10.102 فأحدث، وأدرجتها CISA في قائمة الثغرات المستغلة، وأصلحتها Google في نواة Android.
- الطريقة أهم من القصة: خذ الفساد الغريب على محمل الجد، اقرأ البيانات، اصنع تجربة صغيرة قابلة للتكرار، ثم نصف تاريخ التغييرات للوصول إلى السبب.
وإذا أردت أن تكمل من هنا، فهذه المقالات تشرح الأدوات والطريقة التي مر ذكرها:
دليلكيف اكتشف Andres Freund الباب الخلفي في XZخطوات التحقيق والأوامر التي قادت مطوراً لاحظ نصف ثانية زائدة في دخول SSH إلى باب خلفي في مكتبة XZ، وكيف تكررها على سيرفرك.
دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.
دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.
دليلسطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفركيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.
دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.
دليلالذاكرة للمبرمجين: كيف تعمل ال Stack وال Heapلماذا تتغير قائمة لم تطلب تغييرها، وكيف تتسرب الذاكرة في لغة فيها جامع قمامة Garbage Collector، بأمثلة حقيقية من عدة لغات.
دليلفحص ال Docker Images وملفات البنية ككود IaCماذا يرى فاحص الثغرات داخل ال Docker Image وما الذي يفوته، وكيف تختار Base Image أصغر وتجعل الفحص بوابة في خط CI.
دليلتأمين سيرفر VPS على Ubuntu من أول دخولقبل أن تثبت أي تطبيق على سيرفرك الجديد، هذه الخطوات تغلق أبرز الثغرات وأولها الدخول بمفتاح SSH بدل كلمة المرور.سجل التحديثات (Changelog)
- أكتوبر 2026: كتابة المقال اعتماداً على شرح Max Kellermann للثغرة على dirtypipe.cm4all.com، والتزام الإصلاح والتزامات الانحدار على git.kernel.org، وتعريف NVD، وقائمة CISA للثغرات المستغلة، ونشرة أمان Android وسجل Red Hat، مع تجربة عملية على Debian 13.1 ورسوم توضيحية من عملنا.