HeadlinesBriefing HeadlinesBriefing.com

Error en tiempo de ejecución de Go: fallo en Linux embebido de 32 bits

Hacker News •
×

Nuestro trabajo diario suele girar en torno a temas de Linux y seguridad, profundizando en la pila de software. Sin embargo, con más frecuencia de lo que podría pensar, terminamos depurando aplicaciones que se ejecutan mucho más arriba en la pila. A veces dicho problema tiene sus raíces en el kernel de Linux, a veces en otro lugar.

En esta entrada del blog mostramos cómo encontramos y corregimos un error dentro del tiempo de ejecución de Go. Recientemente un cliente informó que una aplicación escrita en Go se bloquea de vez en cuando en uno de sus sistemas Linux embebidos. El fallo siempre era el mismo error fatal con la firma: runtime: netpoll: eventfd ready for 5.

Al principio asumimos que la aplicación misma tenía un error. Pero después de inspeccionar el error más de cerca, parecía mucho más una suposición interna en el mecanismo netpoll de Go que ya no se cumple. El mensaje de error proviene de netpoll() en src/runtime/netpoll_epoll.go: if ev.

Events != linux. EPOLLIN { throw("runtime: netpoll: eventfd ready for something unexpected") }. En esta ruta de código, el código netpoll espera que EPOLLIN sea el único evento de activación, pero obtuvo 5, que es EPOLLIN|EPOLLOUT. ¿Por qué epoll de repente informaría más que EPOLLIN si el código solo solicitó EPOLLIN? Antes de profundizar, lanzamos el mensaje de error a un motor de búsqueda, esperando que alguien más hubiera enfrentado el mismo problema antes.

Esto nos llevó directamente a un informe en el rastreador de problemas del proyecto Go: runtime: netpoll: eventfd ready for something unexpected. El problema describe exactamente el error fatal que vimos. También en un sistema Linux embebido de 32 bits ARM! Los informantes también señalaron que el fallo ocurre en aplicaciones que se ejecutan durante mucho tiempo.

Eso también coincidió con la descripción de nuestro cliente. ¡Bingo! El problema había estado abierto y sin resolver desde marzo de 2025. Los mantenedores de Go también habían rechazado un intento de corregir el problema. A partir de los comentarios en el problema aprendimos que el error fatal solo apareció alguna vez en sistemas Linux de 32 bits ARM e i386, con todo tipo de versiones de kernel.

Algunos kernels eran bastante antiguos, otros recientes. Ningún informante lo vio en un sistema x86_64 o arm64. Inicialmente sospechamos que epoll se comporta de manera diferente en ARM de 32 bits o i386.

Descartamos esta idea rápidamente porque epoll es código núcleo genérico en el kernel. ¿Por qué devolvería un conjunto de eventos espurio solo en ARM de 32 bits o i386? Aún así, el hecho de que el error apareciera solo en sistemas de 32 bits nos molestaba. Como siguiente paso, revisamos el uso de epoll en el código netpoll de Go. Con la ayuda de un LLM, revisamos src/runtime/netpoll_epoll.go, enfocándonos en trampas de 32 bits como las conversiones de enteros.

La revisión reveló que el código netpoll de Go utiliza el campo data de la estructura epoll_event. Linux epoll puede almacenar una cookie de 8 bytes en el kernel y devolverla como parte del evento de activación al espacio de usuario. Las aplicaciones utilizan esta cookie para adjuntar metadatos a un evento, por ejemplo, para distinguir diferentes fuentes de eventos. struct epoll_event { __poll_t events; /* ev.

Events in Go netpoll */ __u64 data; /* ev. Data in Go netpoll */ }; Profundizando en el tiempo de ejecución de Go, el manejador principal de eventos necesita saber si un evento pertenece a un descriptor de archivo de evento o a un descriptor de archivo de socket. Lo determina comparando ev.

Data con la dirección de su objeto interno de descriptor de archivo de evento: if *(**uintptr)(unsafe. Pointer(&ev. Data)) == &netpoll Event Fd {...}.

Leyendo más adelante en el código vimos que ev. Data contiene ya sea un puntero crudo a netpoll Event Fd o un puntero etiquetado a un objeto poll Desc por socket. La etiqueta del puntero es un contador, fdseq, que distingue objetos poll Desc reciclados.

Hasta ahora todo bien. Mezclar punteros crudos y etiquetados en el mismo campo nos pareció sospechoso. Pero cómo esto se relaciona con el fallo aún no estaba claro.

Examinar cómo Go dispone los punteros etiquetados en la memoria finalmente reveló la causa raíz. En sistemas de 32 bits, un puntero ocupa 4 bytes mientras que el campo data espera 8 bytes. Esta discrepancia de tamaño causa truncamiento al almacenar un valor de puntero.

Los 4 bytes superiores se establecen en cero durante la conversión. Cuando el tiempo de ejecución posteriormente compara el puntero truncado con la dirección original del descriptor de archivo de evento, el...

Fuente: Hacker News · Resumido por HeadlinesBriefing