> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# كيف اكتشف Max Kellermann ثغرة Dirty Pipe
- URL: https://arabroot.io/articles/كيف-اكتشف-max-kellermann-ثغرة-dirty-pipe/
- Published: 2026-10-08T22:00:00.000Z
- Updated: 2026-10-09T12:56:48.000Z
- Description: كيف قادت ملفات gzip تالفة مهندساً إلى ثغرة في نواة Linux تسمح بالكتابة في ملفات للقراءة فقط، وكيف تتبعها بأدواته.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, قصص وتحقيقات, لينكس والسيرفرات, DevOps وCI/CD, الاستضافة الذاتية

في ربيع عام 2021 وصل إلى مهندس اسمه Max Kellermann بلاغ دعم فني بسيط: عميل يشكو أن ملف سجلات الدخول Access Log الذي نزله لا يمكن فك ضغطه، فالأداة gzip تقول إن فيه خطأ في قيمة التحقق CRC. أصلح المهندس الملف يدوياً وأغلق البلاغ ونسي الأمر، ثم تكرر البلاغ بعد أشهر، ثم تكرر مرة أخرى، وفي كل مرة كان محتوى الملف سليماً وقيمة التحقق في آخره وحدها خاطئة. هذه الملاحظة الصغيرة، التي كان من السهل جداً تجاهلها، قادته بعد عام من المتابعة إلى ثغرة في نواة Linux تسمح لأي مستخدم عادي بالكتابة في ملفات لا يملك صلاحية الكتابة فيها، وأخذت الثغرة الرقم [CVE-2022-0847](https://nvd.nist.gov/vuln/detail/CVE-2022-0847?ref=arabroot.io) المعروف باسم Dirty Pipe، بدرجة خطورة 7.8 من 10، وأعلنت في 7 مارس 2022.

هذا المقال ليس عن الثغرة نفسها بقدر ما هو عن طريقة التفكير التي أوصلت إليها، فهي تحقيق في عطل غامض في البيانات انتهى عند أعمق طبقة في النظام، والأدوات التي استخدمها في كل خطوة موجودة على أي سيرفر Linux، ويستطيع أي مهندس أن يتعلمها. وهو امتداد لطريقة التحقيق التي شرحناها في [مقال كيف اكتشف Andres Freund الباب الخلفي في XZ](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-andres-freund-%D8%A7%D9%84%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D8%AE%D9%84%D9%81%D9%8A-%D9%81%D9%8A-xz/)، فكلاهما بدأ من عرض صغير جداً رفض صاحبه أن يتجاهله.

وسوف نناقش في هذا المقال ما يلي:

- العرض الذي بدأ منه كل شيء: ملفات مضغوطة تالفة بقيمة تحقق CRC خاطئة، وكيف ظهر منها نمط.
- خطوات التحقيق كما وصفها بنفسه، مع الأداة المناسبة لكل خطوة والسبب وراءها: قراءة البايتات، ثم إعادة الإنتاج بأصغر برنامج، ثم `git bisect` للوصول إلى الالتزام Commit المسبب.
- المفاهيم التي تحتاجها لتفهم السبب: الأنبوب Pipe، ونظام `splice`، وذاكرة الصفحات Page Cache، والنسخ عند الكتابة Copy-on-Write، باختصار وبرسومنا.
- كيف عملت الثغرة بشكل عام، والالتزامان اللذان زرعا الخطأ في 2016 ثم جعلاه قابلاً للاستغلال في 2020، والإصدارات المتأثرة والمصححة.
- تجربة عملية Lab آمنة على Containers: نطبق أدوات التحقيق نفسها على حالات بريئة، بلا أي كود استغلال وبلا أي كتابة في ملف للقراءة فقط.
- الدروس التي تستحق أن تنقلها إلى فريقك.

الصورة التالية تبين خطوات التحقيق التي سوف نمر عليها بالترتيب:

![ثماني بطاقات مرقمة من اليمين إلى اليسار: 1 العرض، ملف سجل مضغوط لا يفك ضغطه وgzip يعطي خطأ CRC والمحتوى يبدو سليماً، 2 نمط، 37 ملفاً في 3 أشهر أغلبها في آخر يوم من الشهر وعلى السيرفر الأساسي فقط، 3 قراءة، البايتات هي ترويسة ZIP وPK من خدمة أخرى لا تملك صلاحية الكتابة، 4 فرضية، لا بد أنها ثغرة في النواة بعد استبعاد العتاد والكود في مساحة المستخدم، 5 إعادة إنتاج، برنامجان صغيران يكرران العطل بـ splice من ملف إلى Pipe ثم كتابة تظهر في الملف، 6 bisect، 17 خطوة إلى الالتزام المسبب بـ git bisect بين v4.19 وv5.10، 7 السبب، علم لم يهيأ في Pipe Buffer وخطأ من 2016 صار قابلاً للاستغلال بتعديل 2020، 8 البلاغ، فريق أمن النواة ثم Android ثم الإعلان برقم CVE-2022-0847، وتحتها حلقة: ملاحظة غريبة ثم فرضية ثم إعادة إنتاج ثم مكان أصغر للبحث](https://arabroot.io/content/images/2026/10/dirty-pipe-investigation-01-investigation.webp)

خطوات تحقيق Max Kellermann كما وصفها بنفسه، وكل خطوة ملاحظة ثم فرضية ثم مكان أصغر للبحث

## من هو Max Kellermann، وأين ظهر العطل؟

Max Kellermann مهندس في شركة الاستضافة الألمانية CM4all التابعة لـ IONOS، ونشر القصة كاملة في [صفحة شرح الثغرة على موقع الشركة](https://dirtypipe.cm4all.com/?ref=arabroot.io)، وهي مصدرنا الأساسي في هذا المقال. وبيئة العمل التي ظهر فيها العطل مهمة لفهم القصة: كل خوادم الويب عند الشركة ترسل بيانات كل طلب HTTP إلى خوادم سجلات مركزية، ومهمة ليلية تقسم سجلات اليوم السابق إلى ملف لكل موقع، مضغوط بصيغة gzip. وعندما ينزل صاحب الموقع سجلات شهر كامل، تجمع الخدمة ملفات الأيام المضغوطة وترسلها دون فك ضغطها وإعادة ضغطها، وتستخدم في ذلك نظام `splice` الذي ينقل البيانات من القرص إلى اتصال الشبكة مباشرة داخل النواة دون نسخها إلى مساحة المستخدم، أي نقل بلا نسخ Zero-copy.

ولاحظ أن هذه التفاصيل ليست استطراداً، فكل عنصر منها، أي الملفات المضغوطة، والأنابيب، ونظام `splice`، سوف يكون خيطاً في التحقيق. والنقطة الأهم أن العطل ظهر في طبقة البيانات لا في طبقة الأمن، فالمهندس لم يكن يبحث عن ثغرة، وإنما كان يطارد فساداً غامضاً في ملفات العملاء، والثغرة كانت هي التفسير الذي وصل إليه في النهاية.

## العرض: قيمة تحقق CRC خاطئة في آخر الملف

صيغة gzip تنتهي بثمانية بايتات: أربعة لقيمة التحقق CRC32 على المحتوى الأصلي، وأربعة لطول المحتوى قبل الضغط، كما تصفها [وثيقة RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952?ref=arabroot.io). وعند فك الضغط يحسب gzip قيمة التحقق من البيانات ويقارنها بالمخزنة في الآخر، فإذا اختلفتا أعلن خطأ CRC. وهذا بالضبط ما رآه المهندس: المحتوى يفك ضغطه، لكن القيمة المخزنة لا تطابقه.

وعندما جمع الملفات التالفة لاحظ شيئاً لا يحدث في الفساد العشوائي: كلها لها القيمة الخاطئة نفسها. فالفساد العشوائي يعطي قيماً مختلفة كل مرة، أما تطابق القيمة في كل الملفات فيعني أن أحداً كتب هذه البايتات كما هي، لا أنها نتجت عن حساب فاشل. وهنا جاءت لحظة المفتاح، فقد نظر إلى البايتات `50 4b` وأدرك أنها رمزا الحرفين «P» و«K» بترميز ASCII، وهي بداية كل ترويسة في صيغة ZIP. البايتات التالفة لم تكن فساداً، بل كانت جزءاً من ترويسة ZIP تسربت إلى داخل الملف.

لكن هذا زاد الأمر غموضاً، فالعملية التي تكتب ملفات gzip لا تعرف شيئاً عن ترويسات ZIP، والعملية الوحيدة التي تنشئ ترويسات «PK» هي خدمة الويب التي تبني ملفات ZIP وقت الطلب، وهذه العملية تعمل بمستخدم مختلف لا يملك أصلاً صلاحية الكتابة على ملفات السجلات، وهي لا تحاول الكتابة فيها، إنما تقرؤها فقط. وقد يتساءل البعض: كيف تكتب عملية بايتات في ملف لا تملك صلاحية الكتابة فيه ولا تحاول ذلك؟ والإجابة هي لب القصة كله، وهي ما استغرق من المهندس شهوراً ليصل إليه.

## التحقيق خطوة بخطوة

رسالة Max Kellermann مكتوبة كقصة مرتبة حسب ما اكتشفه، وسوف نعرض خطواتها مع الأداة التي يستخدمها أي مهندس في الموقف نفسه، وما قاله هو مربوطاً بمصدره. وقبل أن نبدأ، لاحظ أن أول ما فعله لم يكن أداة بل عادة: كتب ملاحظة بالعطل وتاريخه ولم يحذف الملفات التالفة، فالملاحظة الأولى وحدها كانت صدفة، والملاحظات المتراكمة هي التي صنعت النمط.

### الخطوة الأولى: اجمع الأدلة وابحث عن نمط

عندما عاد العطل مرة بعد مرة، فحص القرص كله بحثاً عن ملفات تالفة، واستغرق الفحص يومين، فظهر نمط واضح كتبه بالأرقام: 37 ملفاً تالفاً خلال ثلاثة أشهر، في 22 يوماً مختلفاً، وأكثرها في آخر يوم من كل شهر، وكلها على السيرفر الأساسي الذي يخدم اتصالات HTTP ويبني ملفات ZIP، أما السيرفر الاحتياطي الذي يشغل نفس مهمة تقسيم السجلات لكنه لا يخدم HTTP فلم يكن فيه أي ملف تالف.

وهذا النمط نفسه كان دليلاً، فالطريقة البسيطة هنا أن تلوم العتاد: ربما ذاكرة RAM معطوبة، أو قرص سيئ. ولكن المهندس استبعد ذلك والسبب أن الأعراض لا تشبه عطل العتاد، إذ إن البيانات على السيرفرين متطابقة إلا في مواضع الفساد، وعطل العتاد لا يختار آخر يوم من الشهر ولا يصيب سيرفراً دون آخر يشغل البرنامج نفسه. فالنمط يشير إلى شيء يفعله السيرفر الأساسي ولا يفعله الاحتياطي، وهو خدمة HTTP وبناء ملفات ZIP. وهذا يعلمنا قاعدة عملية شرحناها في [دليل استكشاف الأخطاء: ابحث عما اختلف بين الحالة السليمة والحالة المعطوبة](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#what-changed)، فالفرق بين السيرفرين هو نصف الإجابة.

### الخطوة الثانية: اقرأ البايتات، لا تخمنها

بدل أن يخمن سبب الفساد، قرأ البايتات التالفة بايتاً بايتاً وفسر كل واحد منها: `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."

أي أن كل عطل يصير سطحياً وسهل الفهم بمجرد أن تستطيع تكراره متى شئت. فأصعب ما في التحقيق ليس فهم السبب، بل الوصول إلى تجربة صغيرة تعيد إنتاج العطل عند الطلب، ومتى ملكتها صار الباقي مسألة وقت. وهذه نفس القاعدة التي شرحناها في [دليل استكشاف الأخطاء: فرضية واحدة وتجربة واحدة قابلة للتكرار](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#hypothesis).

### الخطوة الخامسة: أي تغيير أدخل العطل؟ git bisect

التجربة الصغيرة فتحت باباً جديداً: صار يستطيع أن يسأل عن كل إصدار من النواة «هل العطل موجود فيك؟». فتحقق بسرعة أن العطل يصيب نواة 5.10 في Debian Bullseye ولا يصيب نواة 4.19 في Debian Buster. بين هذين الإصدارين، كما يذكر، 185011 التزاماً Commit في مستودع Git للنواة، وفحصها واحداً واحداً مستحيل، ولكن أداة `git bisect` تبحث فيها بطريقة التنصيف Binary Search: تختار التزاماً في المنتصف وتسألك هل هو سليم أم معطوب، ثم تلغي نصف التاريخ بناء على جوابك، وتكرر حتى تصل إلى أول التزام معطوب، وقد وصلت في 17 خطوة فقط. وسوف نرى `git bisect` يعمل على مستودع صغير في [التجربة العملية](#lab-bisect).

انتهى التنصيف إلى الالتزام [f6dd975583bd](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f6dd975583bd8ce088400648fd9819e4691c8958&ref=arabroot.io) بعنوان «pipe: merge anon\_pipe\_buf\*\_ops»، وهو التزام من مايو 2020 دخل نواة 5.8، يعيد تنظيم كود خلايا الأنبوب، ويغير طريقة فحص ما إذا كانت الكتابة الجديدة يمكن أن تندمج مع خلية سابقة. ولاحظ أن هذا الالتزام هو الذي كشف العطل لا الذي زرعه، كما سنرى في الخطوة التالية، وهذا فرق مهم يخطئ فيه كثير من التحقيقات: أول التزام يظهر عنده العطل ليس دائماً هو مصدر الخطأ.

### الخطوة السادسة: الخطأ الأصلي، علم لم يهيأ

عند قراءة الالتزام الذي وصل إليه التنصيف، وجد المهندس أن الخطأ الحقيقي أقدم منه بسنوات. فالالتزام [241699cd72a8](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=241699cd72a8489c9446ae3910ddd243e9b9061b&ref=arabroot.io) من سبتمبر 2016، في نواة 4.9، أضاف دالتين تنشئان خلية أنبوب جديدة `struct pipe_buffer`، لكنهما نسيتا تهيئة حقل الأعلام `flags` فيها. أي صار ممكناً إنشاء خلية تشير إلى الـ Page Cache بأعلام عشوائية متبقية من استخدام سابق. كان ذلك خطأ تقنياً بلا أثر وقتها، والسبب أن كل الأعلام الموجودة آنذاك لم تكن ذات خطر.

ثم جاء التزام 2020 نفسه الذي وصل إليه التنصيف، فحول فحص الدمج إلى علم لكل خلية اسمه `PIPE_BUF_FLAG_CAN_MERGE`. ومن هنا صار العلم غير المهيأ خطيراً: إذا بقي هذا العلم مضبوطاً في خلية تشير إلى صفحة في الـ Page Cache، أمكن الكتابة فوق محتوى تلك الصفحة، أي فوق محتوى ملف على القرص، بمجرد الكتابة إلى الأنبوب بطريقة مهيأة خصيصاً. وهذا مثال كلاسيكي على كيف يتحول خطأ نائم لا أثر له إلى ثغرة خطيرة حين يتغير كود آخر بعد سنوات، وهو درس في أن «الخطأ الذي بلا أثر» يستحق الإصلاح، لأن أحداً لا يعرف أي كود مستقبلي سوف يوقظه.

ولمن يريد الخط الكامل، فإن فحص الدمج هذا أعيد تنظيمه ثلاث مرات عبر السنين: أدخله الالتزام [5274f052e7b3](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5274f052e7b3dbd81935772eb551dfd0325dfa9d&ref=arabroot.io) مع نظام `splice` عام 2006، ثم بدله الالتزام [01e7187b4119](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=01e7187b41191376cee8bea8de9f907b001e87b4&ref=arabroot.io) عام 2019 من علم إلى مقارنة مؤشرات، ثم حوله التزام 2020 إلى علم لكل خلية. كل خطوة بدت سليمة وحدها، والمشكلة ظهرت من تقاطع خطأ قديم مع تغيير جديد.

### الخطوة السابعة: البلاغ، لمن وبأي ترتيب؟

مثل أي تحقيق أمني، لم تنته القصة بفهم السبب بل بالبلاغ الصحيح. ففي 20 فبراير 2022 أرسل المهندس البلاغ مع الإصلاح إلى [فريق أمن نواة Linux](https://docs.kernel.org/process/security-bugs.html?ref=arabroot.io)، وفي اليوم التالي أعاد إنتاج العطل على هاتف Google Pixel 6 وأبلغ فريق أمن Android. وبناء على نصيحة Linus Torvalds وآخرين، أرسل الإصلاح إلى القائمة البريدية العامة للنواة دون ذكر تفاصيل الثغرة، حتى يدخل الإصلاح بسرعة، ثم أبلغ قائمة توزيعات Linux قبل الإعلان بأيام، وجاء الإعلان العلني في 7 مارس 2022\. والدرس أن ترتيب البلاغ جزء من المسؤولية: تبلغ من يصلح أولاً، وتعطي الجهات وقتاً لتجهيز التحديثات، ولا تنشر التفاصيل قبل أن يتوفر الإصلاح للناس.

## المفاهيم التي تحتاجها: Pipe وsplice وPage Cache

قبل أن نجمع السبب، نحتاج أربعة مفاهيم، نشرحها باختصار لأن لكل منها تفصيلاً في مقالات أخرى على الموقع.

- **الصفحة Page وذاكرة الصفحات Page Cache:** أصغر وحدة ذاكرة تديرها النواة هي صفحة Page حجمها 4 كيلوبايت عادة. وعندما تقرأ ملفاً من القرص، تنسخ النواة محتواه إلى صفحات في الذاكرة تسمى الـ Page Cache، فإذا طلب الملف ثانية جاء من الذاكرة بدل القرص، كما تشرح [وثيقة إدارة الذاكرة في النواة](https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html?ref=arabroot.io)، وقد رأيناها حية في [مقال تحليل أداء سيرفر Linux في 60 ثانية](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#page-cache).
- **الأنبوب Pipe:** قناة اتصال في اتجاه واحد بين عمليتين، طرف يكتب فيها وطرف يقرأ منها، وهو نفس الأنبوب الذي تستخدمه كل يوم بين أمرين على سطر الأوامر كما في [مقال سطر الأوامر في Linux](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#redirection). وتنفذ النواة الأنبوب كحلقة من الخلايا `struct pipe_buffer`، كل خلية تشير إلى صفحة. وأول كتابة تحجز صفحة، وإذا لم تملأها الكتابة تماماً جاز للكتابة التالية أن تكمل في الصفحة نفسها بدل حجز صفحة جديدة، وهذا هو «الدمج» الذي يحدده علم `CAN_MERGE`.
- **نظام splice:** أمر في النواة ينقل بيانات بين ملف وأنبوب دون المرور بمساحة المستخدم، كما يشرح [دليل splice](https://man7.org/linux/man-pages/man2/splice.2.html?ref=arabroot.io). والنواة تنفذه بتمرير إشارات Page References لا بنسخ البيانات فعلاً، أي نقل بلا نسخ Zero-copy. وهذا هو موضع الخطر: بعد `splice` من ملف، تشير خلية الأنبوب إلى صفحة في الـ Page Cache يملكها النظام لا الأنبوب، فيجب ألا تندمج أي كتابة جديدة معها، وإلا كتبت فوق الملف.
- **النسخ عند الكتابة Copy-on-Write:** حيلة تسمح لعدة عمليات بمشاركة الصفحة نفسها ما دامت للقراءة فقط، فإذا أراد أحدها الكتابة تصنع النواة له نسخة خاصة أولاً. وثغرة Dirty Pipe شبيهة بثغرة أقدم اسمها [Dirty Cow (CVE-2016-5195)](https://nvd.nist.gov/vuln/detail/CVE-2016-5195?ref=arabroot.io) المبنية على خلل في آلية النسخ عند الكتابة، ومن هنا جاء اسمها، لكن Dirty Pipe أسهل استغلالاً وأنظف أثراً كما سنرى.

## كيف عملت الثغرة؟

سوف نشرح الفكرة العامة فقط، دون أي خطوة قابلة للاستغلال. الصورة التالية تبين الفرق بين الكتابة إلى أنبوب عادي والكتابة بعد `splice` من ملف:

![لوحتان: على اليمين أنبوب عادي والاندماج مسموح، عملية تكتب بـ write إلى خلايا الأنبوب وعلمها CAN_MERGE مضبوط، وأول كتابة تحجز صفحة يملكها الأنبوب فالكتابة التالية تكمل فيها، دمج آمن لأنه لا ملف وراء الصفحة. على اليسار بعد splice من ملف ويجب ألا يندمج، ملف للقراءة فقط يدخل بـ splice فتشير خلية الأنبوب إلى صفحة يملكها الـ Page Cache خلف ملف على القرص، ثم write يكتب بيانات جديدة والعلم بقي مضبوطاً فالكتابة تدخل الصفحة وتفسد الملف، لأن علم PIPE_BUF_FLAG_CAN_MERGE لم يهيأ عند إنشاء الخلية فبقي من استخدام سابق](https://arabroot.io/content/images/2026/10/dirty-pipe-investigation-02-pipe-pagecache.webp)

الكتابة إلى أنبوب عادي تندمج بأمان، أما الكتابة بعد 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](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9d2231c5d74e13b2a0546fee6737ee4446017903&ref=arabroot.io) بعنوان «lib/iov\_iter: initialize flags in new pipe\_buffer»، ويضيف سطرين فقط يضعان `buf->flags = 0` في الدالتين اللتين كانتا تنشئان خلية أنبوب دون تهيئة الأعلام، أي يصلح الخطأ الأصلي من 2016 مباشرة. ويذكر الالتزام نفسه في سطر `Fixes:` أنه يصلح الالتزام `241699cd72a8`، وهو تأكيد من مطوري النواة أنفسهم على أن مصدر الخطأ هو تغيير 2016 لا تغيير 2020.

ويصف [تعريف الثغرة في NVD](https://nvd.nist.gov/vuln/detail/CVE-2022-0847?ref=arabroot.io) السبب بأنه نقص في تهيئة حقل `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 نفسه كان قد انتهى دعمه، فمن بقي عليه لم يحصل على إصلاح على فرعه، والحل هو الانتقال إلى فرع مدعوم مصحح، وسوف نكتب سكربتاً صغيراً في [التجربة](#lab-version) يحكم على رقم أي نواة.

## الاستجابة: من تأثر فعلاً؟

خطورة Dirty Pipe أنها لا تحتاج إلا حساب مستخدم عادي على النظام، ولذلك كانت خطيرة بوجه خاص على الأنظمة متعددة المستخدمين وعلى الهواتف. وفيما يلي ما فعلته كل جهة بحسب مصادرها الرسمية:

- **وكالة CISA الأمريكية:** أضافت الثغرة في 25 أبريل 2022 إلى [قائمة الثغرات المستغلة المعروفة KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog?ref=arabroot.io) تحت اسم Dirty Pipe، مع تصنيفها خللاً في التهيئة من نوع CWE-665، وأعطت الجهات الحكومية مهلة للإصلاح. ووجودها في هذه القائمة يعني أن الاستغلال في العالم الحقيقي مؤكد لا نظري.
- **Android وGoogle:** دمجت Google الإصلاح في نواة Android في 24 فبراير 2022 عبر [مراجعة الكود المعلنة](https://android-review.googlesource.com/c/kernel/common/+/1998671?ref=arabroot.io)، ثم أدرجت الثغرة في [نشرة أمان Android لمايو 2022](https://source.android.com/docs/security/bulletin/2022-05-01?ref=arabroot.io)، وأشارت إلى أنها قد تكون تحت استغلال محدود وموجه. وتأثر الأجهزة التي تعمل بنواة 5.8 فأحدث، ومنها هواتف حديثة مثل Pixel 6 وGalaxy S22.
- **Red Hat:** يوضح [سجل Red Hat للثغرة](https://access.redhat.com/security/cve/CVE-2022-0847?ref=arabroot.io) أن الإصدارات التي تستخدم نواة 5.8 فأحدث متأثرة، وأن Red Hat Enterprise Linux 8 و9 كانا متأثرين لأنهما يشحنان نوى حديثة، مع درجة خطورة مهمة Important.
- **التوزيعات عموماً:** نشرت كل التوزيعات الكبرى تحديثات نواة خلال أيام من الإعلان، لأن الإصلاح صغير وواضح وسهل الدمج في كل الفروع المدعومة.

والصورة التالية تجمع الخط الزمني من زرع الخطأ في الكود إلى الإصلاح والإعلان:

![خط زمني في عمودين. الجذور في الكود: 2006 إضافة نظام splice وخلايا أنبوب تشير إلى الـ Page Cache بلا دمج، 22 سبتمبر 2016 التزام 241699cd72a8 يضيف دالتين تنشئان خلية أنبوب دون تهيئة حقل flags خطأ بلا أثر وقتها، 23 يناير 2019 التزام 01e7187b4119 يحول علم can_merge إلى مقارنة مؤشرات، 20 مايو 2020 التزام f6dd975583bd في نواة 5.8 يحوله إلى علم لكل خلية CAN_MERGE فصار الخطأ قابلاً للاستغلال. العرض ثم التحقيق: 29 أبريل 2021 أول بلاغ عن ملف تالف، نوفمبر 2021 إلى يناير 2022 تكرار العطل وظهور نمط آخر يوم من الشهر والسيرفر الأساسي فقط، 19 فبراير 2022 برنامجان صغيران يعيدان الإنتاج ثم git bisect يصل للالتزام المسبب. البلاغ والإصلاح: 20 فبراير بلاغ لفريق أمن النواة، 21 فبراير إعادة الإنتاج على Pixel 6 وبلاغ لأمن Android، 23 فبراير إصدارات مستقرة بالإصلاح 5.16.11 و5.15.25 و5.10.102، 7 مارس الإعلان العلني برقم CVE-2022-0847 المسمى Dirty Pipe](https://arabroot.io/content/images/2026/10/dirty-pipe-investigation-03-timeline.webp)

العلم غير المهيأ عاش منذ 2016، وصار قابلاً للاستغلال في 2020، ثم اكتشف وأصلح في فبراير ومارس 2022

## تجربة عملية: نطبق أدوات التحقيق على حالات بريئة

الآن نطبق أدوات هذا التحقيق بأنفسنا، لكن على حالات آمنة تماماً. لن نشغل أي كود استغلال، ولن نكتب في أي ملف للقراءة فقط، ولن ننزل أي «أداة فحص» من الإنترنت. سوف نرى بدل ذلك كيف تعمل الأدوات التي يستخدمها أي محقق: كيف ينقل `splice` البيانات بين ملف وأنبوب، وكيف نراقب الـ Page Cache، وكيف نكتشف ملف gzip تالفاً بخطأ CRC، وكيف يجد `git bisect` التزاماً أدخل عطلاً، وكيف نحكم على رقم نواة هل هو في النطاق المتأثر.

⚠️

التجربة كلها داخل Container واحد يبدأ اسمه بـ `lab-dirtypipe-`، ولا يفتح أي منفذ على جهازك. كل الملفات التي نتعامل معها ننشئها نحن ونملكها، وكل كتابة في التجربة في ملف نملك صلاحية الكتابة فيه. لا تنزل أي سكربت فحص لـ Dirty Pipe من الإنترنت وتشغله بصلاحيات root على سيرفر حقيقي، والسبب أن هذا بالضبط ما يستغله المهاجمون بعد كل ثغرة مشهورة، والفحص الآمن هو قراءة رقم النواة كما سنفعل في آخر التجربة.

### تجهيز ال Container

نشغل Container من Debian 13.1 بإصدار مثبت، ثم نثبت فيه أدوات التحقيق:

```bash
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
```

والمخرج سوف يكون كما يلي:

```text
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`، ثم يقرؤها من الأنبوب. الملف يفتح للقراءة فقط، ولا نكتب فيه شيئاً:

```python
#!/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`، وننشئ ملفاً نصياً نملكه ثم نشغل البرنامج:

```bash
mkdir -p /work
printf "HELLO-FROM-FILE-ARABROOT-LAB\n" > /work/own.txt
python3 /work/splice_demo.py
cat /work/own.txt
```

والمخرج سوف يكون كما يلي:

```text
splice moved bytes : 16
read from pipe     : HELLO-FROM-FILE-
HELLO-FROM-FILE-ARABROOT-LAB
```

في المخرج أعلاه لاحظ التالي:

- البرنامج نقل 16 بايتاً من الملف إلى الأنبوب ثم قرأها، دون أن ينسخ محتوى الملف إلى متغير في مساحة المستخدم، وهذا هو النقل بلا نسخ Zero-copy الذي يجعل `splice` سريعاً، ويشرح تفاصيله [دليل splice](https://man7.org/linux/man-pages/man2/splice.2.html?ref=arabroot.io).
- الملف بقي كما هو، لأننا فتحناه للقراءة فقط ولم نكتب فيه. هذا هو الجانب الآمن من `splice`، والخطر في القصة الحقيقية كان في خطوة تالية هي الكتابة إلى الأنبوب بعد `splice`، وهي ما لن نكررها.

### مراقبة الـ Page Cache قبل القراءة وبعدها

في القصة كان الفساد يحدث في الـ Page Cache لا على القرص، لذلك من المهم أن نرى هذه الذاكرة بأعيننا. ننشئ ملفاً كبيراً، ثم نخرجه من الذاكرة بأداة `vmtouch`، ونقيس كم من صفحاته محمل في الذاكرة بأداة `fincore` قبل القراءة وبعدها:

```bash
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
```

والمخرج سوف يكون كما يلي:

```text
== 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](https://man7.org/linux/man-pages/man1/fincore.1.html?ref=arabroot.io).
- قيمة `Cached` في `/proc/meminfo` ارتفعت نحو 200 ميجابايت بعد القراءة، وهذا هو الـ Page Cache يكبر، وقد شرحنا أن هذه الذاكرة ليست مستهلكة وتتخلى عنها النواة عند الحاجة في [مقال تحليل أداء سيرفر Linux](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#page-cache).
- الصفحة Page هي وحدة الـ Page Cache، وهي نفسها الوحدة التي كانت الثغرة تكتب فيها، فرؤيتها بهذه الأدوات تجعل الآلية ملموسة بدل أن تبقى مجردة.

### اكتشاف ملف gzip تالف بخطأ CRC

العرض الأول في القصة كان خطأ CRC في ملف gzip. ننشئ ملف سجلات ونضغطه، ونرى أن `gzip -t` يمر عليه، ثم نفسد نحن بايتة واحدة من قيمة التحقق في آخره، فيعلن `gzip -t` خطأ CRC، تماماً كالعرض في القصة:

```bash
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
```

والمخرج سوف يكون كما يلي:

```text
== ملف 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](https://datatracker.ietf.org/doc/html/rfc1952?ref=arabroot.io).
- غيرنا بايتة واحدة في قيمة التحقق المخزنة فقط، والبيانات المضغوطة لم تتغير، تماماً كما كان فساد القصة في بايتات قليلة في آخر الملف مع سلامة المحتوى. وهذا يفسر لماذا بدا المحتوى سليماً وقيمة التحقق وحدها خاطئة.
- الدرس العملي: اجعل فحص سلامة ملفاتك المضغوطة آلياً بـ `gzip -t` في مهامك الدورية، فهو أرخص طريقة لاكتشاف فساد مبكر قبل أن يصل إلى العميل.

### إيجاد التزام أدخل عطلاً بـ git bisect

الأداة التي أوصلت المهندس إلى الالتزام المسبب كانت `git bisect`. نبنيها على مستودع صغير من أحد عشر التزاماً، أحدها يدخل «عطلاً» في سكربت بسيط، ثم نترك `git bisect` يجده آلياً. أولاً نبني المستودع:

```bash
cd /work
rm -rf toy && mkdir toy && cd toy
git init -q
git config user.email lab@arabroot.io; 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` بين آخر التزام وعشرة التزامات قبله، فيجد أول التزام معطوب بنفسه:

```bash
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
```

والمخرج سوف يكون كما يلي:

```text
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 <lab@arabroot.io>
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](https://git-scm.com/docs/git-bisect?ref=arabroot.io). وهذا هو السبب في أن بحث المهندس احتاج 17 خطوة فقط بين 185 ألف التزام، لأن عدد الخطوات يكبر لوغاريتمياً لا خطياً مع عدد الالتزامات.
- الخيار `git bisect run` يجعل البحث آلياً تماماً: تعطيه سكربتاً يعيد 0 للسليم وغير الصفر للمعطوب، فيشغله على كل التزام يختاره حتى يصل. وهذا يحول ساعات من الاختبار اليدوي إلى دقائق، وقد شرحنا أساسيات Git في [مقال إدارة النسخ وGit](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#history).
- أول التزام معطوب ليس دائماً مصدر الخطأ الجذري، كما رأينا في القصة حيث كشف التزام 2020 خطأ 2016، لكن `git bisect` يوصلك إلى نقطة واضحة تبدأ منها القراءة.

### هل نواتك في النطاق المتأثر؟

الفحص الآمن لـ Dirty Pipe لا يحتاج أي سكربت من الإنترنت، إنما رقم النواة فقط. نكتب سكربتاً يقرأ الرقم من `uname -r` أو من وسيط، ويحكم عليه حسب النطاق: متأثرة من 5.8، ومصححة في 5.10.102 و5.15.25 و5.16.11 وما بعدها. اقرأه قبل تشغيله، فهو يقرأ ولا ينزل شيئاً:

```bash
#!/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`، ثم نشغله على النواة الحالية وعلى أرقام أمثلة:

```bash
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
```

والمخرج سوف يكون كما يلي:

```text
== النواة الحالية ==
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](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AD%D8%A7%D9%88%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A8%D9%86%D9%8A%D8%A9-%D9%83%D9%83%D9%88%D8%AF-%D9%85%D8%B9-trivy/#vendor-advisories)، وهي أنسب من سكربت يدوي عندما يكبر العدد.

### حذف التجربة

نحذف ال Container بالاسم من جهازك، وهذا يزيل معه كل الملفات التي أنشأناها بداخله:

```bash
docker rm -f lab-dirtypipe-lab
```

## ماذا نتعلم كمهندسين؟

- **خذ فساد البيانات الغريب على محمل الجد:** ملف مضغوط بقيمة تحقق خاطئة هو شيء يسهل إصلاحه يدوياً وإغلاق البلاغ، لكن خلفه كانت ثغرة في نواة Linux. القاعدة العملية: إذا رأيت فساداً لا تفسره، فلا تكتف بإصلاح العرض، واكتب ملاحظة بتاريخه حتى لو لم يكن عندك وقت للتحقيق الآن.
- **احفظ الأدلة واجمعها:** المهندس لم يحذف الملفات التالفة، ففحص القرص كله بحثاً عن نمط، والنمط (آخر يوم من الشهر، سيرفر دون آخر) كان نصف الحل. وهذا جوهر التحقيق الرقمي الذي شرحناه في طريقة «ما الذي اختلف؟» في [دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#method).
- **اقرأ البيانات، لا تصفها بأنها خربانة:** البايتات `50 4b` لم تكن فساداً عشوائياً بل حرفي «PK»، وفهم كل بايتة هو ما كشف أن مصدر الكتابة خدمة ZIP. البيانات تخبرك من كتبها إذا قرأتها.
- **اصنع أصغر تجربة تعيد العطل:** «كل عطل يصير سطحياً بمجرد أن تستطيع تكراره». الانتقال من نظام إنتاج معقد إلى برنامجين صغيرين هو ما حول اللغز إلى خطأ مؤكد وقابل للتنصيف.
- **استخدم git bisect:** عندما يظهر عطل بين إصدارين، لا تقرأ آلاف الالتزامات، بل اجعل التنصيف يصل إلى الالتزام المسبب في خطوات لوغاريتمية، ويفضل أن تعطيه سكربت اختبار آلياً بـ `git bisect run`.
- **أول التزام معطوب ليس دائماً الخطأ الجذري:** هنا كشف تغيير 2020 خطأً نائماً من 2016\. ولذلك «الخطأ الذي بلا أثر» يستحق الإصلاح، لأن أحداً لا يعرف أي تغيير مستقبلي سوف يوقظه.
- **البلاغ المسؤول جزء من العمل:** أبلغ من يصلح أولاً، أعط الجهات وقتاً لتجهيز التحديثات قبل الإعلان العلني، ولا تنشر التفاصيل قبل توفر الإصلاح. وهذه نفس قاعدة الإفصاح التي رأيناها في [تحقيق XZ](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-andres-freund-%D8%A7%D9%84%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D8%AE%D9%84%D9%81%D9%8A-%D9%81%D9%8A-xz/#step-report).
- **قلل من يعمل بصلاحيات واسعة على سيرفرك:** خطورة Dirty Pipe أنها تحتاج حساب مستخدم عادي فقط، فكل حساب على نظامك باب محتمل، وتحديث النواة فور صدور الإصلاح هو خط الدفاع الأول، وقد شرحنا تحديث السيرفر وتأمينه في [مقال تأمين سيرفر VPS](https://arabroot.io/articles/%D8%AA%D8%A3%D9%85%D9%8A%D9%86-%D8%AE%D8%A7%D8%AF%D9%85-vps-%D9%85%D9%86-%D8%A3%D9%88%D9%84-%D8%AF%D8%AE%D9%88%D9%84/#update-system).

## الخلاصة

- بدأ اكتشاف ثغرة 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.
- الطريقة أهم من القصة: خذ الفساد الغريب على محمل الجد، اقرأ البيانات، اصنع تجربة صغيرة قابلة للتكرار، ثم نصف تاريخ التغييرات للوصول إلى السبب.

وإذا أردت أن تكمل من هنا، فهذه المقالات تشرح الأدوات والطريقة التي مر ذكرها:

[![](https://arabroot.io/content/images/2026/10/xz-investigation-cover-cover.webp)دليلكيف اكتشف Andres Freund الباب الخلفي في XZخطوات التحقيق والأوامر التي قادت مطوراً لاحظ نصف ثانية زائدة في دخول SSH إلى باب خلفي في مكتبة XZ، وكيف تكررها على سيرفرك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-andres-freund-%D8%A7%D9%84%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D8%AE%D9%84%D9%81%D9%8A-%D9%81%D9%8A-xz/)[![](https://arabroot.io/content/images/2026/10/troubleshooting-guide-cover-cover.webp)دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/linux-performance-cover-cover.webp)دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/linux-cli-cover-cover.webp)دليلسطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفركيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/git-basics-cover-cover.webp)دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final\_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/memory-basics-cover-cover.webp)دليلالذاكرة للمبرمجين: كيف تعمل ال Stack وال Heapلماذا تتغير قائمة لم تطلب تغييرها، وكيف تتسرب الذاكرة في لغة فيها جامع قمامة Garbage Collector، بأمثلة حقيقية من عدة لغات.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86-stack-%D9%88-heap-%D9%88%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/container-scanning-cover-cover.webp)دليلفحص ال Docker Images وملفات البنية ككود IaCماذا يرى فاحص الثغرات داخل ال Docker Image وما الذي يفوته، وكيف تختار Base Image أصغر وتجعل الفحص بوابة في خط CI.](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AD%D8%A7%D9%88%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A8%D9%86%D9%8A%D8%A9-%D9%83%D9%83%D9%88%D8%AF-%D9%85%D8%B9-trivy/)[![](https://arabroot.io/content/images/2026/10/secure-vps-cover-cover-2.webp)دليلتأمين سيرفر VPS على Ubuntu من أول دخولقبل أن تثبت أي تطبيق على سيرفرك الجديد، هذه الخطوات تغلق أبرز الثغرات وأولها الدخول بمفتاح SSH بدل كلمة المرور.](https://arabroot.io/articles/%D8%AA%D8%A3%D9%85%D9%8A%D9%86-%D8%AE%D8%A7%D8%AF%D9%85-vps-%D9%85%D9%86-%D8%A3%D9%88%D9%84-%D8%AF%D8%AE%D9%88%D9%84/)

## سجل التحديثات (Changelog)

- أكتوبر 2026: كتابة المقال اعتماداً على شرح Max Kellermann للثغرة على dirtypipe.cm4all.com، والتزام الإصلاح والتزامات الانحدار على git.kernel.org، وتعريف NVD، وقائمة CISA للثغرات المستغلة، ونشرة أمان Android وسجل Red Hat، مع تجربة عملية على Debian 13.1 ورسوم توضيحية من عملنا.