HeadlinesBriefing HeadlinesBriefing.com

Ошибка времени выполнения Go: сбой во встроенной Linux-системе 32 бит

Hacker News •
×

Наша повседневная работа обычно вращается вокруг тем Linux и безопасности, углубляясь в стек программного обеспечения. Тем не менее, значительно чаще, чем вы могли бы подумать, мы оказываемся отлаживающими приложения, которые работают намного выше в стеке. Иногда такая проблема имеет свои корни в ядре Linux, иногда в другом месте. В этом блоге мы показываем, как мы нашли и исправили ошибку внутри времени выполнения Go. Недавно клиент сообщил, что приложение, написанное на Go, время от времени падает на одной из их встроенных Linux-систем. Сбой всегда был одним и тем же фатальным ошибкой с подписью: runtime: netpoll: eventfd ready for 5. Сначала мы предположили, что само приложение содержит ошибку. Но после более тщательного изучения ошибки стало очевидно, что это больше похоже на внутреннее предположение в механизме netpoll в Go, которое больше не выполняется. Сообщение об ошибке pochodит из 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. Эта проблема описывает точно ту фатальную ошибку, которую мы наблюдали. Более того, она произошла также на встроенной Linux-системе ARM 32 бит! Сообщавшие также отметили, что сбой происходит в приложениях, которые работают длительное время. Это также совпадало с описанием нашего клиента. Бинго! Эта проблема была открыта и нерешена с марта 2025 года. Сопровождающие Go также отклонили одну попытку исправить проблему. Из комментариев к проблеме мы узнали, что эта фатальная ошибка появлялась только на системах Linux 32 бит ARM и i386, со всеми версиями ядра. Некоторые ядра были довольно старыми, другие — недавними. Ни один из сообщавших не наблюдал это на системе x86_64 или arm64. Сначала мы подозревали, что epoll ведет себя иначе на ARM 32 бит или i386. Мы быстро отвергли эту идею, потому что epoll является общим базовым кодом в ядре. Почему он должен возвращать ложный набор событий только на ARM 32 бит или i386? Тем не менее, факт того, что ошибка проявлялась только на системах 32 бит, продолжал беспокоить нас. Как следующий шаг, мы проанализировали использование epoll в коде netpoll Go. С помощью LLM мы прошли через src/runtime/netpoll_epoll.go, сосредоточившись на подводных камнях 32 бит, таких как преобразования целых чисел. Анализ показал, что код netpoll в Go использует поле data структуры epoll_event.

Linux epoll может хранить в ядре cookie размером 8 байт и возвращать его как часть срабатывающего события в пользовательское пространство. Приложения используют этот cookie для прикрепления метаданных к событию, например, чтобы различать источники событий. struct epoll_event { __poll_t events; /* ev. Events in Go netpoll */ __u64 data; /* ev. Data in Go netpoll */ }. Глубоко внутри времени выполнения Go, основной обработчик событий должен знать, относится ли событие к файловым дескрипторам события или сокета. Он определяет это, сравнивая ev.

Data с адресом своего внутреннего объектa файловый дескриптор события: if *(**uintptr)(unsafe. Pointer(&ev. Data)) == &netpoll Event Fd {...}. Читая дальше по коду, мы увидели, что ev.

Data содержит либо «сырой» указатель на netpoll Event Fd, либо «помеченный» указатель на объект poll Desc на сокет. Метка указателя — это счетчик, fdseq, который различает переиспользуемые объекты poll Desc. До сих пор всё было хорошо. Смешивание «сырых» и «помеченных» указателей в одном поле выглядело подозрительно. Но как это связано со сбоем, было ещё не совсем ясно. Изучение того, как Go размещает помеченные указатели в памяти, наконец-то раскрыло коренную причину. На системах 32 бит указатель занимает 4 байта, тогда как поле data ожидает 8 байт. Это несоответствие размеров приводит к обрезанию при хранении значения указателя. Верхние 4 байта обнуляются во время преобразования. Когда время выполнения позже сравнивает обрезанный указатель с исходным адресом файловый дескриптор события, то...

Источник: Hacker News · Сводку подготовил HeadlinesBriefing