مهندسان OpenAI هفتهها در تلاش برای توضیح خرابیهای مرموز در Rockset، سرویس زیرساخت دادههای C++ که پشتیبان جستجو و پلاگینهای داده چتجیپیتی است، بودند. توابع به آدرسهای حافظه اشتباه برمیگشتند و نشانگرهای پشته در حین اجرا ۸ بایت جابهجا میشدند. هر فرضیهای با شواهد قوی رد میشد و به نظر میرسید که این باگ غیرممکن است. اما داستان جالبتر از اینها بود: آنچه یک باگ تصور میشد، در واقع دو باگ کاملاً نامرتبط بود که به طور تصادفی همزمان ظاهر شده بودند.
چالش: خرابیهای مرموز Rockset
تیم OpenAI ابتدا تصور میکرد که با یک نقص نرمافزاری پیچیده روبرو است. اما هر تلاش برای تحلیل عمیقتر هر خرابی فردی به بنبست میرسید. کلید کشف حقیقت در تغییر رویکرد از تحلیل جزءنگر به تحلیل کلنگر نهفته بود. تیم این روش را اشکالزدایی همهگیرشناسی (Epidemiological Debugging) نامید.
رویکرد جدید: اشکالزدایی همهگیرشناسی
به جای بررسی موردی هر کرش، تیم خط لولهای خودکار ساخت تا تمام دامپهای هسته (core dumps) تولیدی از سال گذشته را تحلیل کند. آنها با کمک ChatGPT اسکریپتی نوشتند که پیشوند هر فایل هسته را دانلود کرده، ثباتها را استخراج، موارد مثبت کاذب را فیلتر و هر خرابی را به یکی از دستههای «بازگشت به صفر» (return-to-null)، «پشته ناهمتراز» (misaligned-stack) یا «سایر» برچسبگذاری میکرد. اجرای موازی این اسکریپت روی تمام دامپهای یک ساله Rockset، همبستگیهای شگفتانگیزی را آشکار کرد.
دو باگ جداگانه: خرابی سختافزاری و باگ نرمافزاری
آنچه یک سندرم واحد به نظر میرسید، در واقع دو جمعیت خرابی با امضاهای کاملاً متفاوت بود: خرابیهای «پشته ناهمتراز» و خرابیهای «بازگشت به صفر».
منشأ سختافزاری: CPU معیوب
خرابیهای پشته ناهمتراز همه از یک منطقه خاص Azure میآمدند، تاریخ شروع مشخصی داشتند و هرگز روی گرههای طولانیمدت ظاهر نمیشدند. تیم آنها را به یک میزبان فیزیکی منفرد ردیابی کرد که در آن CPU بیصدا نتایج نادرست تولید میکرد – نه داغ شدن بیش از حد و نه استثنای ماشین. با حذف آن میزبان، این دسته از خرابیها کاملاً ناپدید شد.
باگ ۱۸ ساله در GNU libunwind
پس از جداسازی خرابیهای سختافزاری، خرابیهای «بازگشت به صفر» قابل بررسی شدند. تیم در ابتدا باز کردن استثنای C++ را رد کرده بود، زیرا موارد نقض در مسیرهای کدی که از استثنا استفاده نمیکردند نیز دیده میشد. اما آن موارد نقض همگی از خوشه فساد سختافزاری بودند. پس از حذف آن آلودگی، هر خرابی باقیمانده در حین باز کردن استثنا رخ میداد.
علت ریشهای، یک وضعیت رقابتی (race condition) در تابع _Ux86_64_setcontext کتابخانه GNU libunwind بود که ۱۸ سال وجود داشت. در حین باز کردن استثنای C++، libunwind یک ساختار ucontext_t روی پشته میساخت و سپس تابع مذکور را برای انتقال کنترل به هندلر پاکسازی فراخوانی میکرد. مشکل آنجا بود که این تابع ابتدا نشانگر پشته (%rsp) را برای اشاره به قاب جدید بهروزرسانی میکرد و سپس نشانگر دستورالعمل (%rip) را از ساختار قدیمی میخواند. لحظهای که %rsp تغییر میکرد، ساختار از حفاظت «منطقه قرمز» (red zone) هسته خارج میشد و اگر سیگنالی در همان پنجره باریک (حدود ۱۰۰ پیکوثانیه) میرسید، هسته قاب سیگنال خود را روی ساختار میساخت و نشانگر دستورالعمل را خراب میکرد. در نتیجه تابع به NULL یا حافظه آشغال میپرید.
در برنامههای معمولی این پنجره رقابتی بسیار نادر است، اما Rockset از timer_create برای ارسال سیگنال SIGUSR2 هر چند میلیثانیه استفاده میکرد که فرکانس تحویل سیگنال را به شدت افزایش میداد و این خطای نظری را به خرابی عملیاتی تبدیل میکرد.
رفع باگ و درسهای آموخته شده
تیم OpenAI یک رفع (patch) و یک تولیدکننده مجدد مستقل (self-contained reproducer) به مخزن بالادستی GNU libunwind ارسال کرد و تأیید کرد که بازکنندههای دیگر مانند libgcc این مشکل را ندارند. رفع با جابهجایی ترتیب دستورالعملها، خواندن %rip را قبل از بهروزرسانی %rsp انجام میدهد و پنجره رقابتی را کاملاً از بین میبرد.
خلاصه تیم از این تجربه ارزش تکرار دارد:
مهمترین گام، خواندن اسمبلی هوشمندانه یا دانش عمیق جزئیات نبود؛ بلکه ساختن یک مجموعه داده با کیفیت بالا بود. در نبود این مجموعه، ما دو پدیده متمایز را در یک داستان ترکیب میکردیم. وقتی دادههای جمعیت دقیق و کاملی داشتیم، ساختار مشکل آشکار شد.
این داستان برای هر تیمی که با خرابیهای تولیدی مقاوم در برابر توضیح دست و پنجه نرم میکند، درس مهمی دارد: بررسی کنید که آیا چندین باگ را با هم ترکیب میکنید؟ علائمی که با هر فرضیه ناسازگارند ممکن است با دو فرضیه مختلف که به طور تصادفی مخلوط شدهاند سازگار باشند. سریعترین مسیر برای دیدن ساختار، جمعآوری دادههای جمعیتی کامل و دقیق است.





