Cazando un bug del runtime de Go en sistemas embebidos de 32 bits

Fuentes: Hunting Down a Go Runtime Bug on 32-bit Embedded Systems

Un proveedor de servicios gestionados, especializado en Linux y seguridad,investigó los crashesesporádicos que un cliente sufría en una aplicación escrita en Go sobre un sistema Linux embebido ARM de 32 bits. La aplicación lanzaba de forma intermitente un error fatal del runtime: "netpoll: eventfd ready for something unexpected", acompañado por un stack trace de Go.

El mensaje procede de la función netpoll() del runtime de Go, que espera recibir únicamente el evento EPOLLIN al sondear el eventfd, pero recibía también EPOLLOUT. Tras descartar que el problema estuviera en el comportamiento de epoll en el kernel y revisar el código del runtime con ayuda de un LLM, el equipo descubrió la raíz del fallo: Go almacena dos tipos de punteros en el campo de 8 bytes ev.Data de struct epoll_event. Para sockets se usa un puntero etiquetado (tagged pointer) que combina una dirección de 4 bytes con un contador fdseq en los otros 4 bytes, mientras que para el eventfd se guarda un puntero sin etiquetar.

En sistemas little-endian de 32 bits, al convertir ev.Data a uintptr —4 bytes— solo se leen los bytes bajos, donde el contador fdseq coincide con la dirección de netpollEventFd una vez que se reciclan millones de objetos pollDesc. Cuando esto ocurre, netpoll confunde un descriptor de socket con el eventfd y la asunción de que el evento es solo EPOLLIN deja de cumplirse. En sistemas de 64 bits o big-endian de 32 bits la comparación cubre los 8 bytes y el aliasing no se produce.

El equipo reprodujo el bug con un caso de prueba aislado que provocaba el crash en pocos minutos y propuso un parche al proyecto Go: en lugar de almacenar el puntero sin etiquetar al eventfd, se almacena un pollDesc etiquetado con valor nil, de forma que ev.Data contiene siempre un puntero etiquetado y desaparece el aliasing.