OpenAI превратила отладку крахов в эпидемиологическое исследование и исправила 18-летнюю уязвимость в GNU libunwind

OpenAIOpenAI
Инженеры OpenAI, исследуя загадочные сбои в сервисе Rockset, разработали метод «эпидемиологической отладки»: они проанализировали все core-дампы за год, разделили ошибки на два класса и нашли корень — 18-летнюю гонку в _Ux86_64_setcontext библиотеки GNU libunwind, которая проявлялась из-за интенсивной отправки сигналов таймером.
Инженеры OpenAI потратили недели на объяснение таинственных крахов в Rockset — сервисе на C++, обеспечивающем поиск и работу с данными в ChatGPT. Они заметили, что функции возвращают неверные адреса памяти, а стековый указатель смещается на 8 байт. Команда переключилась с анализа отдельных сбоев на «эпидемиологическую отладку»: построила конвейер для автоматического изучения каждого core-дампа из продакшена за последний год. ChatGPT написал скрипт, который извлекал начальные данные из каждого дампа, выделял регистры, фильтровал ложные срабатывания и классифицировал крахи как «возврат нулевого указателя» или «ошибка выравнивания стека». Анализ выявил, что видимые однотипными сбои на самом деле делятся на две разные группы. Ошибки выравнивания стека происходили только в одной зоне Azure, имели чёткую дату начала и никогда не возникали на долго работающих узлах. Выяснилось, что физический хост содержал неисправный CPU — он не перегревался и не выдавал машинных исключений, но тихо производил неверные математические операции. После вывода хоста из эксплуатации крахи с ошибкой стека исчезли. Оставшиеся падения из-за нулевого указателя удалось объяснить исключениями C++. Оказалось, что причина — 18-летняя гонка в функции _Ux86_64_setcontext библиотеки GNU libunwind. При раскрутке стека исключений libunwind создаёт на стеке структуру ucontext_t, заполняет регистры и вызывает _Ux86_64_setcontext для передачи управления обработчику. Проблема в том, что функция обновляет указатель стека (%rsp) до того, как завершит чтение счётчика команд (%rip) из старой структуры. После изменения %rsp структура больше не находится в активной части стека и не защищена красной зоной ядра. Если сигнал попадает в окно между обновлением %rsp и чтением %rip, ядро строит свой сигнальный фрейм поверх структуры, повреждая счётчик команд — происходит переход на NULL или мусорный адрес. Окно конкуренции составляет всего одну инструкцию (~100 пикосекунд). В обычных программах оно почти не проявляется, но Rockset использует timer_create для отправки SIGUSR2 каждые несколько миллисекунд CPU-времени для лёгкого учёта запросов — такая частота сигналов превратила теоретическую гонку в реальные сбои. Команда отправила исправление в GNU libunwind (перестановка инструкций: сначала чтение %rip, потом обновление %rsp) и показала, что другие раскрутчики (libgcc) не имеют этой проблемы. Главный вывод инженеров: важнейшим шагом было построение качественного набора данных — без него две разные ошибки смешались бы в одну неразрешимую проблему.
Сокращения
CPU = Central Processing Unit — центральный процессор
Источник: InfoQ 中国 — оригинал

Наши прошлые публикации по теме

Есть свежие новости