عادة ما يدور عملنا اليومي حول مواضيع لينكس والأمان، متعمقين في كومة البرمجيات. ومع ذلك، أكثر مما قد تعتقد، نجد أنفسنا نصلح تطبيقات تعمل على مستويات أعلى بكثير. أحيانًا يكون جذر هذه المشكلة في نواة لينكس، وأحيانًا في مكان آخر. في هذه المشاركة في المدونة نوضح كيف وجدنا وصححنا خطأ داخل وقت تشغيل Go. récemment، أبلغ عميل أن تطبيقًا مكتوبًا بلغة Go يتعطل من وقت لآخر على أحد أنظمة لينكس المدمجة الخاصة به. كان التعطل دائمًا نفس الخطأ الفatal بالتوقيع: runtime: netpoll: eventfd ready for 5. في البداية افترضنا أن التطبيق نفسه يحتوي على خطأ. لكن بعد فحص الخطأ عن كثب، بدا أنه أكثر تشابهًا بفرضية داخلية في آلية netpoll في Go لم تعد صحيحة. تأتي رسالة الخطأ من netpoll() في src/runtime/netpoll_epoll.go: إذا كان ev. Events != linux. EPOLLIN { throw("runtime: netpoll: eventfd ready for something unexpected") }. في هذا المسار البرمجي، يتوقع كود netpoll أن يكون EPOLLIN هو الحدث المُشغّل الوحيد، لكنه حصل على 5، وهو EPOLLIN|EPOLLOUT. لماذا سيُبلغ epoll فجأة عن أكثر من EPOLLIN إذا كان الكود يطلب فقط EPOLLIN؟ قبل التعمق أكثر، ألقينا رسالة الخطأ في محرك بحث، آملين أن يكون شخص آخر قد واجه نفس المشكلة من قبل. هذا قادنا مباشرة إلى تقرير في مُتتبع مشكلات مشروع Go: runtime: netpoll: eventfd ready for something unexpected. يصف هذا المشكل بالضبط الخطأ الفatal الذي رأيناه. أيضًا على نظام لينكس مدمج ARM من 32 بت! كما لاحظ المبلغون أن التعطل يحدث في التطبيقات التي تعمل لفترة طويلة. وهذا يتطابق أيضًا مع وصف عميلنا.bingo! كان هذا المشكل مفتوحًا ولم يُحل منذ مارس 2025. كما رفضmantainers Go أيضًا محاولة واحدة لتصحيح المشكلة. من التعليقات على المشكلة تعلمنا أن الخطأ الفatal لم يظهر أبدًا إلا على أنظمة لينكس ARM وi386 من 32 بت، مع جميع إصدارات النواة. بعض النوى كانت قديمة جدًا، وبعضها حديث. لم يلاحظ أي مبلغ ذلك على نظام x86_64 أو arm64. في البداية اشتبهنا أن epoll يتصرف بشكل مختلف على ARM من 32 بت أو i386. لكننا رفضنا هذه الفكرة بسرعة لأن epoll هو كود نواة أساسي.
Generic. لماذا سيُعيد مجموعة أحداث زائفة فقط على ARM من 32 بت أو i386؟ ومع ذلك، فإن حقيقة أن الخطأ ظهر فقط على أنظمة 32 بت كانت لا تزال تزعجنا. كخطوة تالية، راجعنا استخدام epoll في كود netpoll الخاص بـ Go. بمساعدة نموذج لغوي كبير، مررنا عبر src/runtime/netpoll_epoll.go، مع التركيز على مخاطر 32 بت مثل تحويلات الأعداد الصحيحة. كشف المراجعة أن كود netpoll في Go يستخدم حقل data في بنية epoll_event. يمكن لـ Linux epoll تخزين ملف تعريف ارتباط (cookie) بطول 8 بايت في النواة وإعادته كجزء من الحدث المُشغّل إلى مساحة المستخدم. تستخدم التطبيقات هذا الملف التعريفي لربط البيانات الوصفية بالحدث، على سبيل المثال لتمييز مصادر الأحداث المختلفة. struct epoll_event { __poll_t events; /* ev. Events in Go netpoll */ __u64 data; /* ev. Data in Go netpoll */ }; بعمق في وقت تشغيل Go، يحتاج معالج الحدث الرئيسي إلى معرفة ما إذا كان الحدث ينتمي إلى ملف描述符 evento أو ملف描述符 socket. يحدد ذلك من خلال مقارنة ev.
Data بعنوان كائن ملف描述符 الحدث الداخلي الخاص به: if *(**uintptr)(unsafe. Pointer(&ev. Data)) == &netpoll Event Fd {...}. عند القراءة الإضافية في الكود رأينا أن ev.
Data يحتوي إما على مؤشر خام إلى netpoll Event Fd أو على مؤشر مُوسوم إلى كائن poll Desc لكل socket. وسم المؤشر هو عداد، fdseq، يميز كائنات poll Desc المعاد تدويرها. حتى الآن كل شيء على ما يرام. خلط المؤشرات الخام والمؤشرات المُوسومة في نفس الحقل بدا لنا مريبًا. لكن كيف يرتبط هذا بالتعطل لم يكن واضحًا بعد. فحص كيفية وضع Go للمؤشرات المُوسومة في الذاكرة كشف أخيرًا عن السبب الجذري. على أنظمة 32 بت، يشغل المؤشر 4 بايت بينما يتوقع حقل data 8 بايت. هذا عدم التطابق في الحجم يسبب截断 عند تخزين قيمة المؤشر. يتم تعيين البايتات العليا إلى الصفر أثناء التحويل. عندما يقارن وقت التشغيل لاحقًا المؤشر المقطوع مع العنوان الأصلي لملف描述符 الحدث، فإن...
المصدر: Hacker News · لخّصه HeadlinesBriefing