HeadlinesBriefing HeadlinesBriefing.com

Bug em tempo de execução do Go: falha Linux embarcado 32 bits

Hacker News •
×

Nosso trabalho diário geralmente gira em torno de tópicos de Linux e segurança, aprofundando-se na pilha de software. Ainda assim, com mais frequência do que você poderia pensar, acabamos depurando aplicativos que são executados muito mais alto na pilha. Às vezes, esse problema tem suas raízes no kernel do Linux, às vezes em outro lugar. Neste post do blog, mostramos como encontramos e corrigimos um bug dentro do tempo de execução do Go.

Recentemente, um cliente relatou que um aplicativo escrito em Go trava de vez em quando em um de seus sistemas Linux embarcados. A falha sempre era o mesmo erro fatal com a assinatura: runtime: netpoll: eventfd ready for 5. Inicialmente, assumimos que o próprio aplicativo tinha um bug.

Porém, após examinarmos o erro com mais atenção, ele parecia muito mais uma suposição interna no mecanismo netpoll do Go que não se sustentava mais. A mensagem de erro vem de netpoll() em src/runtime/netpoll_epoll.go: se ev. Events != linux.

EPOLLIN { throw("runtime: netpoll: eventfd ready for something unexpected") }. Nesse caminho de código, o código netpoll espera que EPOLLIN seja o único evento de disparo, mas recebeu 5, que é EPOLLIN|EPOLLOUT. Por que o epoll de repente relataria mais do que EPOLLIN se o código solicitasse apenas EPOLLIN? Antes de aprofundarmos, enviamos a mensagem de erro para um mecanismo de busca, na esperança de que alguém mais já tivesse enfrentado o mesmo problema antes.

Isso nos levou diretamente a um relatório no rastreador de problemas do projeto Go: runtime: netpoll: eventfd ready for something unexpected. Esse problema descreve exatamente o erro fatal que vimos. Além disso, ocorreu também em um sistema Linux embarcado ARM de 32 bits! Os relatantes também observaram que a falha ocorre em aplicativos que são executados por longos períodos.

Isso também correspondeu à descrição do nosso cliente. Bingo! O problema estava aberto e sem solução desde março de 2025. Os mantenedores do Go também haviam rejeitado uma tentativa de corrigir o problema.

A partir dos comentários no problema, aprendemos que o erro fatal só apareceu alguma vez em sistemas Linux de 32 bits ARM e i386, com todas as versões de kernel. Alguns kernels eram bastante antigos, outros recentes. Nenhum relatante o viu em um sistema x86_64 ou arm64.

Inicialmente, suspeitamos que o epoll se comporte de forma diferente em ARM de 32 bits ou i386. Descartamos essa ideia rapidamente porque o epoll é código de núcleo genérico no kernel. Por que ele retornaria um conjunto de eventos espúrio apenas em ARM de 32 bits ou i386? Ainda assim, o fato de o erro aparecer apenas em sistemas de 32 bits nos incomodava.

Como próximo passo, analisamos o uso de epoll no código netpoll do Go. Com a ajuda de um LLM, examinamos src/runtime/netpoll_epoll.go, focando em armadilhas de 32 bits, como conversões de inteiros. A análise revelou que o código netpoll do Go utiliza o campo data da estrutura epoll_event.

O Linux epoll pode armazenar um cookie de 8 bytes no kernel e retorná-lo como parte do evento de disparo para o espaço do usuário. As aplicações usam esse cookie para anexar metadados a um evento, por exemplo, para distinguir diferentes fontes de eventos. struct epoll_event { __poll_t events; /* ev. Events in Go netpoll */ __u64 data; /* ev.

Data in Go netpoll */ }. Aprofundando no tempo de execução do Go, o manipulador principal de eventos precisa saber se um evento pertence a um descritor de arquivo de evento ou a um descritor de arquivo de socket. Ele determina isso comparando ev.

Data com o endereço de seu objeto interno de descritor de arquivo de evento: if *(**uintptr)(unsafe. Pointer(&ev. Data)) == &netpoll Event Fd {...}.

Lendo mais adiante no código, vimos que ev. Data contém ou um ponteiro bruto para netpoll Event Fd ou um ponteiro marcado para um objeto poll Desc por socket. A marca do ponteiro é um contador, fdseq, que distingue objetos poll Desc reciclados.

Até agora, tudo bem. Misturar ponteiros brutos e marcados no mesmo campo nos pareceu suspeito. Mas como isso se relaciona com a falha ainda não estava claro.

Examinar como o Go organiza os ponteiros marcados na memória finalmente revelou a causa raiz. Em sistemas de 32 bits, um ponteiro ocupa 4 bytes enquanto o campo data espera 8 bytes. Essa discrepância de tamanho causa truncamento ao armazenar um valor de ponteiro.

Os 4 bytes superiores são definidos como zero durante a conversão. Quando o tempo de execução posteriormente compara o ponteiro truncado com o endereço original do descritor de arquivo de evento, o...

Fonte: Hacker News · Resumido por HeadlinesBriefing