- Die C-Funktionen
setenv() und unsetenv() können in Programmen mit Threads nicht sicher verwendet werden
- Diese Funktionen verändern globalen Zustand und können mit
getenv()-Aufrufen aus anderen Threads in Konflikt geraten
- Solche Konflikte treten auch in anderen Sprachen auf, die C-Standardbibliotheksfunktionen verwenden, etwa Go mit
os.Setenv und Rust mit std::env::set_var()
- Es dauerte zwei Tage, das zugehörige Problem in einem Go-Programm nachzuverfolgen und als Bug zu melden
- Der Grund ist, dass der DNS-Resolver von Go intern
getaddrinfo() verwendet und dieses wiederum getenv() aufruft
- Das Problem ist allerdings schon sehr alt. Bereits 2017 gab es einen entsprechenden Artikel, an dessen Ende stand: „Wir sehen uns in 5 Jahren, 2022!“ – und 2023 traf man sich erneut
- Es handelt sich um einen Mangel des POSIX-Standards, der den C-Standard erweitert hat, sodass Umgebungsvariablen verändert werden können
- Der frustrierendste Teil ist, dass viele Menschen, die Standards beeinflussen oder C-Bibliotheken pflegen können, das nicht als Problem ansehen
- Der Grund ist, dass in der Spezifikation klar steht, dass
setenv() nicht zusammen mit Threads verwendet werden kann
- Wenn jemand das dennoch tut und es crasht, sei das also seine eigene Schuld
- Also sollten wir wohl „die Spezifikationen aller Funktionen sorgfältig lesen, keine von anderen geschriebene Software verwenden und keine Threads benutzen“
- In moderner Software ist das jedoch eine unrealistische Annahme
- Stattdessen sollte man versuchen, APIs zu entwerfen, die schwerer kaputtzumachen sind und sich mit Veränderungen im Ökosystem weiterentwickeln
- C und die Standardbibliothek spielen weiterhin eine wichtige Rolle als Fundament der meisten Software; deshalb sollte man Wege finden, sie zu verbessern – oder Wege, sie loszuwerden
Warum ist setenv() nicht thread-safe?
getenv() gibt ein char* zurück, das die Anwendung später nicht freigeben muss
- Während ein Thread diesen Pointer verwendet, kann ein anderer Thread mit
setenv() oder unsetenv() dieselbe Umgebungsvariable ändern
- Der C-Standard enthält nur
getenv(), aber die meisten Implementierungen folgen dem POSIX-Standard und enthalten auch Funktionen zum Ändern der Umgebung
putenv() fügt der Menge der Umgebungsvariablen ein char* hinzu; wenn die Anwendung den Speicher nach der Rückkehr von putenv() verändert, ändert sich damit auch die Umgebungsvariable
environ ist ein NULL-terminiertes Pointer-Array (char**), das Anwendungen lesen und zuweisen können; der Zugriff auf dieses Array ist nicht thread-safe
Wie Umgebungsvariablen implementiert werden
- Wenn eine Anwendung eine bestehende Variable überschreibt, muss die Implementierung entscheiden, wie sie damit umgeht
- glibc und Solaris/Illumos geben Umgebungsvariablen niemals frei; dadurch sind die von
getenv() zurückgegebenen Werte unveränderlich und können thread-safe verwendet werden
- musl sowie FreeBSD/Apple geben Umgebungsvariablen frei; wenn ein anderer Thread nach einem
setenv()-Aufruf einen von getenv() zurückgegebenen Pointer verwendet, kann das zu Abstürzen führen
- Das zweite Problem ist, sicherzustellen, dass die Menge der Umgebungsvariablen thread-safe aktualisiert wird; genau dadurch entstehen in glibc Abstürze
Warum Programme Umgebungsvariablen verwenden
- Umgebungsvariablen sind nützlich, um gemeinsam genutzte Bibliotheken oder Sprach-Runtimes zu konfigurieren, die in anderen Programmen eingebettet sind
- Nutzer können Konfigurationen ändern, ohne dass die Programmautorin oder der Programmautor diese explizit weiterreichen muss
- Viele Bibliotheken rufen
getenv() auf, und Programme müssen diese Variablen ändern, um die verwendeten Bibliotheken zu konfigurieren
Dieses Problem sollte gelöst werden, und zwar zum Beispiel so
- Meiner Meinung nach ist es absurd, dass dieses Problem seit so langer Zeit bekannt ist
- Tausende Stunden wurden damit verschwendet, das Problem zu debuggen oder mögliche Workarounds zu diskutieren
- Mögliche Wege zur Lösung
- Eine thread-safe Implementierung wie bei Illumos/Solaris schaffen
- Das hat gewisse Grenzen:
setenv() verursacht Speicherlecks, und wenn ein Programm putenv() oder environ verwendet, ist es weiterhin nicht sicher
- Trotzdem wäre das eine Verbesserung gegenüber den aktuellen Implementierungen unter Linux und Apple
- Zweitens könnte man eine neue API hinzufügen, die per Design thread-safe ist und alle Umgebungsvariablen abfragen kann, ähnlich wie Microsofts
getenv_s()
- Die bevorzugte Lösung wäre, beides zu tun
- Das würde die Wahrscheinlichkeit von Problemen in bestehenden Programmen und Bibliotheken verringern und zugleich einen Weg bieten, Probleme in neuerem Code oder in Sprachen wie Go und Rust ganz zu vermeiden
- Eine Funktion hinzufügen, die – ähnlich wie
getenv_s() – eine einzelne Umgebungsvariable in einen vom Nutzer bereitgestellten Puffer kopiert
- Eine thread-safe API hinzufügen, mit der man über alle Umgebungsvariablen iterieren oder alle Variablen kopieren kann
getenv() als veraltet kennzeichnen und stattdessen eine neue thread-safe getenv()-Funktion empfehlen
putenv() als veraltet kennzeichnen und stattdessen setenv() empfehlen
environ als veraltet kennzeichnen und stattdessen Funktionen für Umgebungsvariablen empfehlen
- Die Implementierung von Umgebungsvariablen so aktualisieren, dass sie thread-safe ist
3 Kommentare
„Weil in der Spezifikation ausdrücklich steht, dass
setenv()nicht zusammen mit Threads verwendet werden darf“ ==> Bei der Verwendung einer API oder eines SDK gehört es zu den absoluten Grundlagen, die Spezifikation sorgfältig zu prüfen. Das wirkt auf mich wie erzwungene Nutzung.Das Problem ist, eine Funktion zu verwenden, die von Anfang an schlecht entworfen wurde.
....