OpenAI разобрала сбой в инфраструктуре ChatGPT и в итоге нашла ошибку GNU libunwind, которая прожила в коде 18 лет. История важна не только из-за громкого имени компании: она показывает, что продакшен-падения иногда нельзя понять по одному core dump, и для русскоязычных команд это прямой урок по SRE, отладке C++ и работе с сигналами в Linux.
Инцидент произошел в Rockset — C++-сервисе обработки данных, который используется в инфраструктуре ChatGPT для поиска и data plugins, сообщает InfoQ. Инженеры неделями пытались объяснить странные крэши: функции будто возвращались по ложным адресам, а указатель стека в середине выполнения оказывался смещен на 8 байт. Картина выглядела почти мистической: каждая версия казалась правдоподобной, но на каждую находилось сильное опровержение.
Один симптом, два разных источника
Перелом наступил не тогда, когда команда глубже закопалась в отдельные дампы памяти, а когда она сменила сам метод расследования. Вместо точечной ручной отладки OpenAI собрала конвейер для массового анализа всех production core dump за предыдущий год. Скрипт, который, по словам команды, помог написать ChatGPT, скачивал префикс каждого файла, вытаскивал регистры, отбрасывал известные ложные срабатывания и размечал аварии по типам: возврат в NULL, смещенный стек и прочее. После параллельного прогона по всей годовой выборке стало видно главное: инженеры пытались объяснить не один дефект, а сразу два несвязанных сбоя, которые просто совпали по времени.
Первая группа падений со смещением стека оказалась привязана к одной Azure-региональной зоне, имела четкую дату начала и не встречалась на долго живущих узлах. В итоге след вывел на один физический хост, где процессор тихо выдавал неверные результаты. Не перегрев, не machine-check exception, не красивая аппаратная авария, а именно молчаливая порча вычислений. После вывода этого хоста из эксплуатации соответствующие крэши исчезли полностью. Для любой команды, которая верит, что железо либо работает, либо падает с флагом в руках, это неприятное, но полезное напоминание: иногда сервер просто врет.
Когда аппаратный шум убрали из выборки, вторая часть истории наконец стала внятной. Оставшиеся падения всегда происходили во время unwinding C++-исключений. Раньше инженеры исключали эту версию, потому что видели как будто бы контрпримеры — аварии в участках кода, где исключения не использовались. Но эти контрпримеры, как выяснилось, принадлежали совсем другой популяции сбоев, связанной с дефектным хостом. Как только данные очистили, ошибка GNU libunwind начала смотреть прямо в лицо.
Гонка длиной в одну инструкцию
Корневая причина сидела в функции _Ux86_64_setcontext библиотеки GNU libunwind. Во время unwinding библиотека собирает на стеке структуру ucontext_t, заполняет нужное состояние регистров, а затем передает управление cleanup-обработчику. Проблема была в порядке инструкций: функция обновляла указатель стека %rsp раньше, чем дочитывала указатель инструкции %rip из старой структуры. В этот момент структура уже переставала быть частью активного стека и лишалась защиты kernel red zone. Если ровно в этом окне прилетал сигнал, ядро раскладывало свой signal frame поверх той же области памяти и портила значение %rip. Дальше процесс прыгал в NULL или в мусорный адрес.
Технически это окно было шириной в одну инструкцию — примерно 100 пикосекунд на современных частотах. В обычной программе такой дефект мог бы не выстрелить вообще никогда. Но у Rockset была особенность: сервис использовал timer_create и регулярно посылал SIGUSR2 каждые несколько миллисекунд CPU-времени для легковесного учета ресурсов на запрос. То есть система сама создавала заметно больше событий доставки сигналов, чем большинство приложений. Теоретическая гонка превратилась в вполне практический production crash. Это важный нюанс для разработчиков высоконагруженных систем: редкая гонка остается редкой ровно до тех пор, пока вы не начинаете дергать сигналы с индустриальным энтузиазмом.
OpenAI отправила исправление и самодостаточный воспроизводимый пример в апстрим GNU libunwind, а также отдельно проверила, что другие механизмы unwinding, в частности libgcc, этой проблемой не страдают. Сам фикс выглядит почти обидно простым: чтение %rip переставили раньше обновления %rsp, и уязвимое окно исчезло. Но вся история интересна не ассемблерной акробатикой, а способом мышления. Команда OpenAI прямо формулирует вывод: ключом стало не тонкое знание деталей, а качественный набор данных. Пока инженеры смешивали две разные причины в одну историю, логика ломалась на каждом шаге. Как только появились полные размеченные данные по всей популяции сбоев, структура проблемы стала очевидной.
Для рынка это еще один сигнал, что зрелая эксплуатация все сильнее напоминает эпидемиологию, а не героическую ночную отладку одного инцидента. Чем сложнее стек, чем больше сигналов, таймеров, рантаймов и облачной инфраструктуры, тем выше шанс, что под одинаковым симптомом живут разные поломки. И если ошибка GNU libunwind смогла прятаться 18 лет, маскируясь под аппаратную аномалию на одном хосте Azure, то главный вопрос для остальных команд звучит проще и жестче: достаточно ли у вас данных, чтобы отличить один редкий баг от двух редких багов, которые неудачно встретились в одном дашборде?