
OpenAI рассказала, как нашла гонку потоков (race condition), которая 18 лет незаметно жила в GNU libunwind — одной из самых распространенных библиотек для раскрутки стека. Самое абсурдное в этом баге — ширина окна, в котором он срабатывает: буквально одна процессорная инструкция, порядка 100 пикосекунд. На таком масштабе казалось, что он слишком редкий, чтобы вообще проявляться. Но на нагрузке OpenAI это выливалось в больше десятка падений в день.
Все началось с «невозможных» крашей в Rockset — C++-инфраструктуре поиска, которую OpenAI купила в 2024 году и использует в ChatGPT для работы с данными и поиска по перепискам. Обычная функция завершалась и возвращалась не туда: иногда по нулевому адресу, иногда указатель стека оказывался смещен на 8 байт. Так нормальный код просто не падает. Для каждой гипотезы у инженеров находилось опровержение, и баг выглядел невозможным.
Перелом случился, когда команда сменила оптику. Сначала они отлаживали как врач: брали один кор-дамп и пытались поставить диагноз по детальным уликам. Это не работало. Тогда подход поменяли на эпидемиологический — смотреть не на отдельный случай, а на всю популяцию крашей. Скрипт, который скачал и разметил все дампы памяти Rockset за год, написал сам ChatGPT. На чистых данных корреляции проявились мгновенно: то, что считали одним багом, оказалось двумя независимыми. Первый — тихий аппаратный сбой: процессор на одном хосте Azure буквально неправильно считал. Второй — та самая гонка в libunwind.
Сам уязвимый код появился еще в 2007–2008 годах — в первой версии libunwind с поддержкой раскрутки C++-исключений для x86_64 — и все это время спокойно работал. Механизм libunwind можно описать просто. Механизм libunwind можно описать просто. Когда C++ обрабатывает исключение, рантайм «раскручивает» стек и восстанавливает регистры. В этот момент библиотека меняет указатель стека — и ровно на одну инструкцию структура с адресом, куда нужно вернуть управление, оказывается за пределами зоны, которую ядро обещает не трогать. Если именно в этот зазор прилетает системный сигнал, ядро затирает эту память, и адрес возврата превращается в NULL. У OpenAI баг вылез только сейчас, потому что нагрузка Rockset необычна: он кидает очень много исключений, очень часто шлет сигналы, а недавнее изменение заставило обработчик сигнала занимать больше стека. Произведение этих факторов перешло порог видимости.
В качестве лечения OpenAI пересела с GNU libunwind на механизм раскрутки стека из libgcc, а в саму библиотеку отправила воспроизводимый пример и фикс. Но главный вывод инженеры выносят не про ассемблер и не про устройство сигналов. Историю раскрыло не озарение, а качественные данные: пока два разных явления смешивали в одно, найти связное объяснение было невозможно. Как только данные стали лучше, отладка стала простой.
P.S. Поддержать меня можно подпиской на канал «сбежавшая нейросеть», где я рассказываю про ИИ с творческой стороны.
ссылка на оригинал статьи https://habr.com/ru/articles/1054112/