Die Programmierung in modernen Umgebungen verlangt zunehmend die Nutzung von Multithreading, um Leistung und Effizienz zu verbessern. Gleichzeitig bringt diese Parallelisierung jedoch eine Vielzahl von Herausforderungen mit sich, insbesondere in Bezug auf die Sicherheit und Konsistenz von gemeinsam genutzten Ressourcen. Eine davon ist die Verwendung von Umgebungsvariablen, deren Verwaltung in C-Standardbibliotheken traditionell nicht threadsicher gestaltet ist. Eine spezielle Funktion, die hier im Mittelpunkt steht, ist setenv(). Setenv(), welche dazu dient, Umgebungsvariablen während der Laufzeit eines Programms zu setzen oder zu verändern, gilt in vielen Implementierungen als nicht threadsicher.
Das bedeutet, dass ein paralleler Zugriff durch mehrere Threads unvorhersehbare Nebenwirkungen und sogar Abstürze verursachen kann. Interessanterweise können auch moderne Programmiersprachen wie Rust, die explizit auf Sicherheit und Thread-Sicherheit ausgelegt sind, diese Probleme nicht immer eliminieren, vor allem dann nicht, wenn sie mit traditionellen C-Bibliotheken interagieren. Ein aktuelles Beispiel, das die Brisanz dieses Problems beleuchtet, stammt aus der praktischen Arbeit mit der Netzwerk-I/O-Logik von EdgeDB. Hier wurde ein erheblicher Teil des Codes von Python zu Rust portiert, um von den Vorteilen der Systemsprache zu profitieren. Dabei entstand ein neuer HTTP-Client, der auf Reqwest basierte.
Trotz erfolgreichen Tests auf x86_64-Systemen zeigten sich plötzlich sporadische Abstürze auf ARM64-Linux-Runnern in der CI. Was zunächst wie ein Deadlock oder ein blockierender async Task wirkte, entpuppte sich bei tieferer Diagnose als Absturz in libc, genauer gesagt in der Funktion getenv(), die Umgebungsvariablen abfragt. Die Fehlersuche gestaltete sich schwierig: Der Prozess in einem Docker-Container war abgestürzt, Core Dumps zeigten wenig verwertbare Informationen, und nur mit dem Auspacken der dynamischen Bibliotheken und der korrekten Einrichtung eines GDB-Debugging-Umfelds wurde klar, dass die Speicherzugriffe in getenv() zu illegalen Adressen führten. Die Ursache lag in einem klassischen Race Condition Szenario. Setenv() modifiziert das interne Umgebungsvariablen-Array, eventuell durch Realloc (Reallokation), um Platz für neue Variablen zu schaffen.
Währenddessen versuchte ein anderer Thread mit getenv() auf besagte Struktur zuzugreifen. Aufgrund fehlender Synchronisation wurde das Array zwischenzeitlich überschrieben oder unmöglich referenziert. Resultat war ein Use-After-Free-Fehler oder Speicherzugriffsverletzung. Diese Problematik ist nicht neu. Setenv() war seit jeher nicht für gleichzeitigen Zugriff von Threads designed.
Doch in der Praxis führt diese Tatsache zu schwer diagnostizierbaren Fehlern, vor allem in gemischten Sprachumgebungen und großen Applikationen, wo Rust-Code sich mit C-Bibliotheken und Python-C-Extensions vermischt. Neben der Komplexität, die Multithreading bringt, erschwert die schwache Memory-Model-Dokumentation einiger Plattformen und die Besonderheiten von ARM-Architekturen den Fehlernachweis zusätzlich. ARM-64 etwa ist bekannt für ein lockeres Speicherordnungsmodell, bei dem Speicheränderungen asynchron über Threads hinweg sichtbar werden können. Das macht Fehler wie Race Conditions noch wahrscheinlicher und reproduzierbarer unter bestimmten Umständen. Ein wichtiger Kontext in dem EdgeDB-Team diesen Fehler entdeckte ist die Verwendung des rust-native-tls Backends, das OpenSSL als TLS-Engine integriert.
OpenSSL in Verbindung mit openssl-probe setzt zur Laufzeit die Umgebungsvariablen SSL_CERT_FILE und SSL_CERT_DIR. Leider geschieht dies durch Setzen der Umgebungsvariablen per setenv(), ohne Synchronisierung mit parallelen getenv()-Aufrufen, die zum SSL-Zertifikat Abruf führen können. Genau diese Kombination brachte die Speicherfehler zum Vorschein. Der dadurch ausgelöste Crash kann unter bestimmten Kombinationen von Timing, Anzahl der vorhandenen Umgebungsvariablen, Plattform und Laufzeitbedingungen reproduziert werden. Besonders signifikant ist hier, dass gerade das Zusammenspiel mit Python, das eine globale Interpreter-Sperre (GIL) besitzt, für Synchronisation sorgt, andere Teile des Systems (besonders Rust und native C-Bibliotheken) jedoch keine solche Absicherung bieten.
Die Konsequenzen reichen über EdgeDB hinaus: Alle Anwendungen, die Umgebungsvariablen während der Laufzeit in Multithread- oder asynchronen Kontexten ändern, sind potenziell gefährdet. Selbst sichere Sprachen wie Rust, die internal Schutzmechanismen besitzen, können nicht vor Fehlern schützen, die aus externem Code oder standardisierten C-Funktionen stammen, die nicht threadsicher sind. Lösungsansätze existieren, sind aber nicht trivial umzusetzen. Eine temporäre Maßnahme könnte etwa sein, den gesamten Zugriff auf setenv() und getenv() strikt zu serialisieren – entweder durch globale Mutexes oder die Nutzung vorhandener Sperren wie der Python GIL, wenn ein Python-Prozess beteiligt ist. Alternativ können Umgebungsvariablen statisch oder nur während der Initialisierungsphase gesetzt werden, bevor Multithreading beginnt.
Langfristig sind Bibliotheken und Glibc-Implementierungen gefordert, ihre Funktionen threadsicherer zu machen. Tatsächlich adressiert die glibc-Gemeinschaft derzeit genau dieses Problem, indem sie etwa auf ein Realloc verzichten und stattdessen ältere Environment-Arrays bei Veränderungen inkrementell erweitern oder einfrieren, um Speicherzugriffsprobleme zu vermeiden. Auch die Rust-Community plant mit der 2024er Edition, Funktionen zum Setzen von Umgebungsvariablen als unsafe zu deklarieren, um Entwickler auf die Risiken aufmerksam zu machen und die Verwendung bewusster zu gestalten. Eine weitere praktikable Alternative ist der Wechsel des TLS-Backends. Im Fall von EdgeDB wurde das native-tls Backend mit OpenSSL durch rustls ersetzt.
Rustls ist eine TLS-Engine in reinem Rust, die keine globalen Änderungen an Umgebungsvariablen vornimmt und somit diese spezielle Fehlerquelle ausschließt. Trotz des größeren Footprints in Bezug auf gebündelte TLS-Engines war das der pragmatischere Weg, um Stabilität zu gewinnen. Die Erkenntnisse aus diesem Fall zeigen exemplarisch, dass selbst bewährte Sicherheitsmechanismen moderner Programmiersprachen nicht immer ausreichen, wenn sie mit traditionellen und nicht threadsicheren Compiler-Laufzeitbibliotheken verbunden sind. Das Zusammenspiel heterogener Komponenten erfordert ein tiefes Verständnis von Systemaufrufen, Speicherverwaltung und Synchronisation. Für Entwickler bedeutet das, dass sie bei der Architektur von Multithreaded-Programmen unbedingt auf die Threadsicherheit aller genutzten APIs achten sollten, auch jener, die scheinbar trivial wie Umgebungsvariablen erscheinen.
Das vermeidet schwer erkennbare und langwierige Debuggingprozesse und verbessert die Anwendungsstabilität insbesondere unter Last und in produktiven Umgebungen. Abschließend zeigt das Beispiel EdgeDB eindrucksvoll, dass es in der modernen Softwareentwicklung nicht nur auf die Programmiersprache ankommt, sondern auch auf das sorgfältige Management von Systemressourcen und deren Schnittstellen. Während Rust eine ausgezeichnete Basis für sichere Software bietet, ist es keine Garantie gegen Probleme, die in niedrigeren Ebenen oder Fremdbibliotheken liegen. Ein kritischer Blick auf Funktionalitäten, die mit globalem oder gemeinsamem Zustand operieren, bleibt unabdingbar. Die Weiterentwicklung von Glibc, Rusts Umgebungs-API und verwandter Systeme ist im Fluss und wird in Zukunft solche Probleme hoffentlich deutlich mindern.
Bis dahin sollten Entwickler wie bei EdgeDB pragmatische Lösungen suchen und Multithreading mit Vorsicht behandeln, insbesondere bei Funktionen wie setenv() und getenv().