Unsere tägliche Arbeit dreht sich in der Regel um Linux- und Sicherheitsthemen, tief in der Software-Stack. Dennoch, häufiger als man denken könnte, landen wir dabei, Anwendungen zu debuggen, die viel höher im Stack laufen. Manchmal hat ein solches Problem seine Wurzeln im Linux-Kernel, manchmal anderswo.
In diesem Blog-Beitrag zeigen wir, wie wir einen Fehler im Go-Laufzeitumfeld gefunden und behoben haben. Kürzlich meldete ein Kunde, dass eine in Go geschriebene Anwendung auf einem ihrer eingebetteten Linux-Systeme zeitweise abstürzt. Der Absturz zeigte immer denselben fatalen Fehler mit der Signatur: runtime: netpoll: eventfd ready for 5.
Anfangs gingen wir davon aus, dass die Anwendung selbst fehlerhaft sei. Doch nach näherer Betrachtung des Fehlers sah es eher nach einer inneren Annahme im Go-netpoll-Mechanismus aus, die nicht mehr gilt. Die Fehlermeldung stammt aus netpoll() in src/runtime/netpoll_epoll.go: wenn ev.
Events != linux. EPOLLIN { throw("runtime: netpoll: eventfd ready for something unexpected") }. Auf diesem Codepfad erwartet der netpoll-Code, dass EPOLLIN das einzige auslösende Ereignis ist, aber er erhielt 5, was EPOLLIN|EPOLLOUT entspricht.
Warum würde epoll plötzlich mehr als EPOLLIN melden, wenn der Code nur EPOLLIN angefordert hätte? Bevor wir tiefer gruben, haben wir die Fehlermeldung in eine Suchmaschine eingegeben, in der Hoffnung, dass jemand anderes das gleiche Problem zuvor hatte. Dies führte uns direkt zu einem Bericht im Issue-Tracker des Go-Projekts: runtime: netpoll: eventfd ready for something unexpected. Dieses Issue beschreibt genau den fatalen Fehler, den wir beobachtet haben.
Außerdem trat es auch auf einem 32-Bit-ARM-Eingebetteten-Linux-System auf! Die Berichterstatter bemerkten außerdem, dass der Absturz in Anwendungen auftritt, die lange laufen. Dies stimmte auch mit der Beschreibung unseres Kunden überein. Bingo! Dieses Issue war seit März 2025 offen und ungelöst.
Auch die Go-Maintainer hatten einen Versuch abgelehnt, das Problem zu beheben. Aus den Kommentaren zum Issue erfuhren wir, dass dieser fatale Fehler ausschließlich auf 32-Bit-ARM- und i386-Linux-Systemen aufgetreten ist, unabhängig von der Kernel-Version. Einige Kernel waren eher alt, andere eher neu.
Kein Berichterstatter hat es auf einem x86_64- oder arm64-System gesehen. Anfangs vermuteten wir, dass epoll sich auf 32-Bit-ARM oder i386 anders verhält. Wir verwarfen diese Idee schnell, da epoll generischer Kerncode im Kernel ist.
Warum würde es nur auf 32-Bit-ARM oder i386 ein spurisches Ereignismeldung zurückgeben? Trotzdem beschäftigte uns die Tatsache, dass der Fehler ausschließlich auf 32-Bit-Systemen auftrat. Als nächsten Schritt untersuchten wir die Verwendung von epoll im netpoll-Code von Go. Mit Hilfe eines LLM gingen wir durch src/runtime/netpoll_epoll.go und konzentrierten uns auf 32-Bit-Fallen wie Ganzzahlkonvertierungen.
Die Untersuchung zeigte, dass der Go-netpoll-Code das data-Feld der struct epoll_event verwendet. Linux epoll kann im Kernel ein 8-Byte-Cookie speichern und es als Teil des auslösenden Ereignisses an den Userspace zurückgeben. Anwendungen verwenden dieses Cookie, um Metadaten an ein Ereignis anzuhängen, beispielsweise um verschiedene Ereignisquellen zu unterscheiden. struct epoll_event { __poll_t events; /* ev.
Events in Go netpoll */ __u64 data; /* ev. Data in Go netpoll */ }. Tief im Go-Laufzeitumfeld muss der Haupt-Ereignishandler wissen, ob ein Ereignis zu einem Ereignis-Dateideskriptor oder zu einem Socket-Dateideskriptor gehört.
Er entscheidet dies, indem er ev. Data mit der Adresse seines internen Ereignis-Dateideskriptor-Objekts vergleicht: if *(**uintptr)(unsafe. Pointer(&ev.
Data)) == &netpoll Event Fd {...}. Weiter im Code sahen wir, dass ev. Data entweder ein roher Zeiger auf netpoll Event Fd oder ein getaggter Zeiger auf ein pro-Socket-poll-Desc-Objekt enthält.
Der Zeigertag ist ein Zähler, fdseq, der wiederverwendete poll-Desc-Objekte unterscheidet. Bis dahin schien alles in Ordnung zu sein. Das Mischen von rohen und getaggten Zeigern im selben Feld schien uns verdächtig.
Doch wie dies mit dem Absturz zusammenhängt, war noch nicht klar. Die Untersuchung, wie Go getaggte Zeiger im Speicher anordnet, enthüllte schließlich die Ursache. Auf 32-Bit-Systemen belegt ein Zeiger 4 Bytes, während das data-Feld 8 Bytes erwartet.
Diese Größenunterschied führt dazu, dass beim Speichern eines Zeigerwerts eine Abschneidung erfolgt. Die oberen 4 Bytes werden während der Konvertierung auf null gesetzt. Wenn das Laufzeitumfeld später den abgeschnittenen Zeiger mit der ursprünglichen Adresse des Ereignis-Dateideskriptors vergleicht, dann...
Quelle: Hacker News · Zusammengefasst von HeadlinesBriefing