Die C-Standardbibliothek war nicht thread-sicher, und selbst sicheres Rust konnte den Crash nicht verhindern
(edgedb.com)- Während EdgeDB Netzwerk-I/O von Python nach Rust verlagerte, wirkte ein neuer auf
reqwestbasierender HTTP-Fetch-Test nur auf ARM64-CI so, als würde er hängen, tatsächlich stürzte er aber ab - Die Analyse des Core Dumps zeigte, dass die Problemstelle nicht der neue HTTP-Code war, sondern innerhalb von
getenv()inlibc; beim Durchlaufen des Arrays von Umgebungsvariablen scheiterte ein Lesezugriff auf den ungültigen Pointer0x220 - Ein anderer Thread setzte im
openssl-probe-PfadSSL_CERT_FILEundSSL_CERT_DIR; dabei allokiertesetenv()environneu, während gleichzeitig ein Python-Fehlerpfadgetenv()aufrief, was zu einer Race Condition führte - Im Rust-Code gab es kein explizites
unsafe, aber währendstd::env::set_var()die globale Umgebung veränderte, synchronisierte Rusts interner Lock nicht mit anderen Runtimes oder direktenlibc-Aufrufen - Die Lösung war, unter Linux vom
rust-native-tls-/openssl-Backend vonreqwestaufrustlsumzusteigen; auch Rust Edition 2024 und glibc entwickeln sich in eine Richtung, die diese Problemklasse reduziert
Crash, der nur auf ARM64-CI sichtbar wurde
- EdgeDB entwickelte eine neue HTTP-Fetch-Funktion, während ein großer Teil des Netzwerk-I/O-Codes von Python nach Rust portiert wurde
- Als HTTP-Client-Bibliothek wurde
reqwestverwendet; lokal und auf x86_64-CI-Runnern liefen die Tests durch, auf ARM64-CI-Runnern schlugen sie jedoch sporadisch fehl - Zunächst sah es so aus, als würde der Test-Runner unendlich hängen; in den CI-Logs blieb ein Test ohne Fehlermeldung stundenlang als laufend stehen und lief schließlich in ein Timeout
- Die erste Hypothese war ein Unterschied im Speichermodell zwischen Intel und ARM64
- Intel hat ein vergleichsweise strenges Speichermodell mit einer globalen Ordnung, auf die sich alle Prozessoren bei Speicherschreibvorgängen einigen
- ARM hat ein schwächer geordnetes Speichermodell, bei dem Schreibvorgänge in verschiedenen Threads in unterschiedlicher Reihenfolge sichtbar werden können
Verfolgung des Core Dumps in der Docker-CI-Umgebung
- Die nächtlichen CI-Maschinen liefen auf Amazon AWS, sodass man sich als echter root-Benutzer außerhalb des Containers anmelden und
dmesgsowie Systemlogs einsehen konnte - Innerhalb und außerhalb des Containers wurde nach der PID gesucht, die zu hängen schien, doch der betreffende Prozess existierte nicht; damit war klar, dass es sich nicht um einen Deadlock, sondern um einen Crash handelte
- Da Docker-Container einen Prozess-Namespace verwenden, wurde der Core Dump an den Docker-Host weitergereicht; in
journalctlfand sich ein Core Dump despython3-Prozesses - Als der Core Dump zunächst mit
gdbgeöffnet wurde, war der Backtrace nutzlos, weil die.so-Dateien aus dem Container fehlten - Nach dem Kopieren von
/lib,/usrusw. aus dem Container und dem Setzen vonsolib-absolute-prefixingdbließ sich bestätigen, dass der Crash ingetenv()auslibc.so.6auftrat
Der kaputte Pointer einer Umgebungsvariablen, den getenv() las
- Der vollständige Backtrace verlief über
getenv()→__dcigettext()→strerror_r()→strerror()→PyErr_SetFromErrnoWithFilenameObjects() - Nicht der neue HTTP-Code stürzte direkt ab; vielmehr rief Python beim Erzeugen einer errno-basierten Exception über einen gettext-bezogenen Pfad
getenv()auf - Die Implementierung von
getenv()in GLIBC 2.17 durchläuft POSIX-environalschar **-Liste und prüft Pointer auf Umgebungsvariablen-Strings bis zum abschließendenNULL-Pointer - Aus Disassembly und Registerzustand ergab sich, dass
getenv()beim Lesen eines Bytes von Adressex19 = 0x220abstürzte0x220war offensichtlich keine gültige Speicheradresse- Bei der Prüfung von
environselbst wirkte die aktuelle Liste der Umgebungsvariablen konsistent
Ursache: Race Condition zwischen setenv() und getenv()
setenv()ist keine Funktion, die sich in Multithread-Umgebungen sicher aufrufen lässt; Probleme mit seltsamen Crashes inlibc-getenv()wurden bereits mehrfach wiederentdeckt- Der Abgleich von Disassembly und C-Code zeigte, dass
x20dem Pointerepentsprach, der durch dasenviron-Array lief - Zum Zeitpunkt des Crashs war
x200x248b5000, währendenvironaktuell auf0x28655750zeigte, also rund 60 MB weiter hinten lag - Beim Vergleich des Speichers rund um das alte Umgebungsarray mit dem aktuellen
environtrat der letzte Unterschied bei den EinträgenSSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crtundSSL_CERT_DIR=/etc/ssl/certsauf - Ein anderer Thread hatte während eines
setenv()-Aufrufsenvironverschoben, undgetenv()las weiter aus dem alten Umgebungsarray — ein Use-after-free
Verbindung zu openssl-probe und TLS-Backend
- In einem älteren Issue zu
rust-native-tlsfand sich der Hinweis, dassopenssl-probedie UmgebungsvariablenSSL_CERT_FILEundSSL_CERT_DIRsetzt, um Systemzertifikate zu finden - Unter Linux wird dieser Pfad verwendet, wenn das
openssl-Backend vonrust-native-tlsgenutzt wird - Der problematische
openssl-probe-Code setzte diese beiden Umgebungsvariablen ohne explizitesunsafeviaenv::set_var()SSL_CERT_FILESSL_CERT_DIR
- Durch diese Kombination interagierte Rust-Code ohne
unsafeungünstig mit derlibc-Nutzung innerhalb desselben Prozesses und verursachte den Crash
Warum der Fehler auf ARM64 Linux reproduzierbar war
- Dieser Crash tritt nur auf, wenn
setenv()das Umgebungsarray perreallocverschiebt und gleichzeitig ein anderer Threadgetenv()aufruft - Für die Reproduktion mussten mehrere Bedingungen zugleich erfüllt sein
- Die Anzahl der Umgebungsvariablen musste so liegen, dass
reallocausgelöst wurde - Ein nicht zusammenhängender I/O-Fehler musste von
asyncioabgefangen werden - Der Python-Fehlerpfad musste genau in diesem Moment
getenv()aufrufen, um die UmgebungsvariableLANGUAGEzu lesen
- Die Anzahl der Umgebungsvariablen musste so liegen, dass
- Der fehlerhafte Wert
0x220lag gemessen in 64-Bit-Wörtern nahe an der früheren Größe der Umgebung0x220 / 8 = 68- Offenbar wurde dieser Wert an die Stelle des abschließenden
NULLim alten Umgebungsblock geschrieben; vermutlich kennzeichnete er die Größe eines freien Blocks im System-malloc
- Weil so viele Voraussetzungen zusammenkommen mussten, war es fast schon Glück, dass sich der Fehler auf einer einzelnen Plattform recht gut reproduzieren ließ
Hinweise aus dem ARM64-Disassembly
- Im Disassembly von
getenv()war zunächst verwirrend, dass nicht klar sichtbar war, an welcher Stelle sichx20änderte - Der Schlüssel war der Pre-Index-Adressierungsmodus von AArch64
ldr x19, [x20, #8]!arbeitet wie folgtx19 = *(x20 + 8)x20 = x20 + 8
- Wegen dieses Adressierungsmodus lief
x20durch das Array der Pointer auf Umgebungsvariablen, auch wenn es nicht explizit auf der linken Seite geschrieben wurde
Angewandter Fix und Veränderungen in verwandten Projekten
- Letztlich fiel die Entscheidung, unter Linux vom
rust-native-tls-/openssl-Backend vonreqwestaufrustlszu migrieren - Ursprünglich war das native-TLS-Backend gewählt worden, um während der Portierung des Python-Codes nach Rust nicht zwei TLS-Engines gleichzeitig mitzuliefern
- Nach diesem Vorfall wurde entschieden, dass zwei TLS-Engines kurzfristig akzeptabel sind
- Als Alternative wäre es möglich gewesen, den ersten Aufruf von
try_init_ssl_cert_env_vars()auszuführen, während Pythons GIL gehalten wird- Rust besitzt einen internen Lock, der Race Conditions beim Lesen und Schreiben der Umgebung zwischen Rust-Code verhindert
- Er verhindert jedoch keine direkten
libc-Aufrufe aus Code anderer Sprachen - Wenn die GIL gehalten wird, lässt sich zumindest die Race Condition mit Python-Threads vermeiden
- Das Rust-Projekt kennt dieses Problem bereits und plant, die Environment-Setter-Funktionen in Edition 2024 als
unsafezu markieren - Auch das glibc-Projekt hat kürzlich Änderungen aufgenommen, die
reallocvermeiden und stattdessen alte Umgebungsarrays leaken, um die Thread-Sicherheit vongetenv()zu erhöhen
4 Kommentare
setenvist nicht threadsicher, und C möchte das nicht ändernDie Funktion
setenvmacht schon wieder Ärger.Ich würde den Titel als „Die Thread-Unsicherheit der C-Stdlib kann nicht einmal das ach so sichere Rust retten“ schreiben. :)
Ich habe es definitiv verstanden.
Meinungen auf Hacker News
Der wichtigste Punkt hier ist, dass die Funktionen zum Setzen von Umgebungsvariablen in der nächsten Rust-Edition unsafe werden
Mit etwas Glück wirkt sich das bis auf die Crates aus, die solche Crashes verursachen; inzwischen wurde upstream ein Issue eröffnet: https://github.com/alexcrichton/openssl-probe/issues/30
getenvundsetenvbzw.unsetenvkönnen nicht sicher aus unterschiedlichen Threads aufgerufen werdenDie einzige verlässliche Lösung scheint zu sein, diese Funktionen so zu ändern, dass sie zwingend einen Mutex nehmen
Leider sieht das Ökosystem anders aus: https://github.com/seanmonstar/reqwest/blob/master/Cargo.tom...
____UNSAFE_payattention__nevermindthatthisappears50timesinthisfile___zu ignorierenIn Web-Frameworks ist es ähnlich: Vue hat die Direktive
v-html, React hatdangerouslySetInnerHTML; in dieser Hinsicht ist Vue meiner Ansicht nach eindeutig besserset_varundremove_varaus der Rust-Standardbibliothek werden in der nächsten Edition, der Edition 2024, korrekt einenunsafe {}-Block verlangenDie Dokumentation erwähnt die Sicherheitsprobleme inzwischen ebenfalls, aber diese Funktionen überhaupt als safe anzubieten war ein Fehler – ein Fehler, den auch höherstufige Sprachen gemacht haben
https://doc.rust-lang.org/stable/std/env/fn.set_var.html
In glibc gibt es einen Patch, der
getenvin weiteren Fällen sicher macht, in denen die Umgebung geändert wird; da C aber weiterhin direkten Zugriff aufenvironerlaubt, kann es bei Änderungen nie vollständig sicher werden: https://github.com/bminor/glibc/commit/7a61e7f557a97ab597d6f...Pro aktiver Umgebungsvariable entsteht amortisiert ein Speicherleck konstanter Größe, aber bei der Variablen selbst gibt es ohnehin schon ein solches Leck, das sogar von der Länge abhängt und auch nicht mehr verwendete Werte umfasst
Es wird sicher pathologische Nutzungsmuster geben, bei denen der Speicher deshalb auch in API-konformen Programmen unbegrenzt wächst
Um Programme zu reparieren, die durch Nutzung der API aus mehreren Threads gegen die Regeln verstoßen, wird in Programme, die sich an die API halten, offenbar der Bug unbegrenzten Speicherwachstums eingeführt. Das ist interessant, aber unangenehm – eher Pragmatismus als Dogma
unsafeverlangt werden mussSelbst wenn Maintainer der C-Standardbibliothek dagegen sind,
setenvmultithread-sicher zu machen, sollte zumindest eine neue thread-sichere API definiert werden – ob innerhalb von POSIX oder indem man erst einen De-facto-Standard schafft, den POSIX später übernimmtMit der Zeit, die dafür aufgewendet wird zu erklären, warum man nichts tun kann, hätte man dieses Problem beheben können; der alte
setenvhätte ersetzt und in vielen Softwareprojekten als veraltet markiert und entfernt werden könnenAngesichts der Tatsache, dass glibc Änderungen vornimmt, die dieses Problem faktisch beseitigen sollen, wirkt auch die Aussage des Musl-Maintainers wenig überzeugend, dass es sich in Musl nicht beheben lasse
extern char **environ;existiertSolange
environöffentlich zugänglich ist, gibt es nicht einmal die Garantie, dasssetenvundgetenvüberhaupt verwendet werden. Denn beide sind nicht verpflichtendWenn man
environentfernen könnte, wäre es ziemlich einfach,setenvundgetenvthread-sicher zu machen. Wenn man es nicht entfernen kann, ist es unmöglich; dennoch kann man argumentieren, dass thread-sicheresetenvundgetenveine Verbesserung wären, auch wenn es keine vollständige Lösung istexec()-Funktionen, die keine Umgebung als Argument entgegennehmen oderPATHdurchsuchen, um eine ausführbare Datei zu finden, werden wohl eine Sperre brauchenUnter Linux auf Bugs rund um Umgebungsvariablen zu stoßen, wirkt fast wie ein Initiationsritus; auf anderen Unix-Systemen scheint das merkwürdigerweise weniger problematisch zu sein.
Linus und der Kernel beheben POSIX-Bugs pragmatisch so, dass sie in der Praxis nicht auslösen; bei glibc ist es etwas amüsant, wie weit man selbst Jahrzehnte, nachdem Leute versucht haben, das Problem wenigstens etwas abzumildern, noch hinterherhinkt.
Klar gibt es allerlei Ärgernisse wie
TZ, aber wenn mangetenv_r()bereitgestellt, es mitsetenv()synchronisiert und bei der Verwendung vongetenv()schon beim Kompilieren und Linken gewarnt hätte, wäre ein großer Teil der Probleme verschwunden.Man hätte sogar noch weiter gehen und die Umgebungs-Pointer per Copy-on-Write (COW) schreibgeschützt halten können.
Stattdessen wurde das Problem auf einzelne Anwendungen abgewälzt, was ein großer Fehler ist, weil Anwendungsentwickler kaum wissen können, was ihre Abhängigkeiten tun. Genau in so einer Lage war ich vor langer Zeit auch, und damals sagte der Closed-Source-Bibliotheksanbieter, ich solle doch aufhören, diesen Spielzeug-Unix-Klon Linux zu verwenden.
Das Problem ist nicht die Implementierung, sondern die API selbst.
setenv(),unsetenv(),putenv()und besondersenvironsind in Multithread-Programmen grundsätzlich unsicher.Auch
getenv_r()kann einen nicht vollständig retten. Denn während ein Thread den alten Wert einer Umgebungsvariable in den bereitgestellten Puffer kopiert, kann ein anderer Threadsetenv()aufrufen.Den Fall, dass man mit
getenv()einen Wert erhält und ein anderer Thread danachsetenv()aufruft und diesen Speicher ungültig macht, behebtgetenv_r()zwar, aber es gibt keine Möglichkeit, andere API-brechende Aufrufe zu verhindern.libc kann manche Probleme abmildern, indem sie innerhalb von
getenv()/setenv()/putenv()/unsetenv()einen Mutex hält, aber weiterhin kann libc nicht garantieren, dass ein vongetenv()zurückgegebener Wert lange genug gültig bleibt, damit der aufrufende Code ihn verwenden kann.Es gibt auch keine gute Möglichkeit, direkten Zugriff auf
environsicher zu machen. Man könnteenvironzwar thread-lokal machen, aber dann könnten die Umgebungsansichten der einzelnen Threads dauerhaft auseinanderlaufen, und die Ergebnisse einesgetenv_r()-Aufrufs könnten von einer direkten Prüfung vonenvironabweichen.Abwärtskompatibilität hier zu wahren, ist wirklich schwierig, und schon das Hinzufügen eines Mutex zum Schutz der Funktionen könnte die Bedeutung bestehender Programme verändern und sie dadurch kaputtmachen.
Früher gab es schon einmal einen Artikel darüber, dass
setenvschrecklich ist: https://www.evanjones.ca/setenv-is-not-thread-safe.htmlEs gab auch eine Diskussion dazu, und schon der erste Kommentar sagte, dass es in Rust Probleme verursacht: https://news.ycombinator.com/item?id=38342642
Die meisten der übrigen Probleme hier scheinen eher die Entwicklungsumgebung zu betreffen. Es wurde per Docker auf einer Remote-Maschine in einem Amazon-Rechenzentrum getestet, und diese Maschine konnte Prozessabstürze nicht melden.
Außerdem gab es im Container nicht genug Debug-Symbolinformationen, um einen Backtrace zu erhalten. Hätte man beim ersten Fehlschlag einen sauberen Backtrace bekommen, wäre es sofort klar gewesen.
Fraglich ist ohnehin, warum überhaupt
setenvverwendet wird.Dabei musste ich an die 12-factor app-Bewegung denken, an die einige frühere Kollegen sehr fest geglaubt haben. Einer dieser „Factors“ besagte, dass Anwendungskonfiguration über Umgebungsvariablen erfolgen sollte.
Ich fand das immer etwas unsinnig, weil diese Art der Konfiguration im Grunde Werte vom Typ String in einem flachen Namensraum wie in einen Korb wirft.
Die Risiken von
getenv()/setenv()/environsehe ich ebenfalls als starkes Argument dafür, Umgebungsvariablen nicht für Konfiguration zu verwenden.Natürlich gibt es nicht immer eine hervorragende und gut unterstützte Alternative. Ich bevorzuge Konfigurationsdateien, und man kann auch Template-Konfigurationen verwenden, in die nur noch die Werte für Entwicklung, Staging und Produktion eingetragen werden. Meist nutze ich YAML, trotz seiner Nachteile und Fallstricke; auch wenn es bessere Formate für Konfigurationsdateien geben mag, halte ich YAML für deutlich besser als Umgebungsvariablen.
Unter NT können Umgebungsvariablen typisiert und templatisiert werden, und es gibt mit der Registry auch eine Konfigurationsdatenbank mit Namensräumen. Umständlich und seltsam ist sie allerdings schon.
Außerdem bietet MSVC für fast alle Standardbibliotheksfunktionen Thread-sichere Varianten an.
Ich höre oft, wie neue C/C++-Entwickler die mangelnde POSIX-Kompatibilität von MSVC beklagen, aber sie scheinen nicht wirklich darüber nachzudenken, was das tatsächlich bedeutet. Es wirkt eher so, als wollten sie einfach Kreuzkompatibilität mit C-Programmen, die in den 1990ern geschrieben wurden.
Das behindert die Fähigkeit, das Verhalten allein aus der Signatur einer Funktion abzuleiten, und macht viele Funktionen unrein, die rein hätten sein können.
Wenn es ein Sprachfeature gäbe, mit dem eine App markieren kann, dass Umgebungsvariablen in keinem Prozess beliebig nutzbar sind und nur einmal, nicht pro Variable, sondern einmal gesammelt gelesen werden dürfen, würde ich es überall einsetzen.
getenv()selbst ist völlig in Ordnung; das Problem istsetenv().Theoretisch wird die Umgebung gesetzt, bevor diese mysteriöse App startet, sodass man es gar nicht brauchen sollte.
Aber ein flacher Namensraum, String-Werte und ein frei zugänglicher globaler Bereich, den alle teilen, ohne zu wissen, welche Libraries und Module noch dazukommen, sind auch ohne die Sicherheitsprobleme von
setenv()keine gute Idee.Die meisten Probleme, über die hier gesprochen wird, scheinen daher zu kommen, dass Leute die Umgebung als Key-Value-Store für veränderlichen globalen Zustand missbrauchen wollen. Ich weiß nicht, warum man das überhaupt tun sollte.
Die JVM behandelt die Umgebung praktisch als unveränderlich, und es ist gut möglich, dass Scala- und Java-Firmen wie SoundCloud die 12-factor-app-Bewegung beeinflusst haben. Ich habe nie erlebt, dass sich die Umgebung geändert oder Threading-Probleme verursacht hätte.
Selbst wenn sich die Umgebung ändert, bleibt die beim Start der JVM erstellte unveränderliche Kopie gleich, und Code, der über die normalen Java-APIs mit der Umgebung interagiert, sieht diese Änderung nicht.
Das Problem bei Konfigurationsdateien ist, dass das Parsen pro Prozess erfolgt. Deshalb ist Linux/Unix so ein Durcheinander. Jedes Tool hat andere Konventions- und Mechanismen für Konfiguration, und es gibt keinen Standard.
Im Docker-Ökosystem läuft es darauf hinaus, dass man im Container zwar tun kann, was man will, die Schnittstelle nach außen aber entweder darin besteht, ein Volume zu mounten und die komplexe Konfigurationsweise jeder einzelnen App zu befolgen, oder einfach Umgebungsvariablen zu verwenden.
Die meiste moderne Software, die heute mit Docker läuft, ist so Docker-freundlich, dass ihr Verhalten vollständig über die Umgebung gesteuert werden kann, und in vielen Fällen reicht das aus.
Mit Docker Compose oder Kubernetes hat man dann eine Liste von Umgebungsvariablen, die definiert, wie der Prozess gestartet wird, in einer YAML-Datei; damit bekommt man bis zu einem gewissen Grad die gewünschte Struktur. Ich mag YAML nicht, aber es funktioniert gut genug, und auch wenn Syntaxprobleme einem den Tag ruinieren können, sind die Alternativen ebenfalls nicht frei von Problemen.
Ich verwende ebenfalls den 12-factor-app-Stil, aber sobald die Variablen in der App angekommen sind, validiere und speichere ich die Daten. Danach gibt es keinerlei Problem.
Ein großartiger Artikel, der einem schwer erkennbaren Bug nachgeht.
Es war alles dabei: sporadische Bugs, Architekturspezifika, in Abhängigkeiten versteckt, Rust, Python GIL und sogar gettext.
Solche detaillierten Fehleranalyse-Berichte kommen dem eigenen Erleben am nächsten. In Situationen, in denen eine Abhängigkeit das intern nutzt und man gar nicht wissen konnte, ist es schwer, einfach zu sagen: „Dann benutzt halt X nicht.“
Es heißt einerseits: „Die nächtliche CI-Maschine läuft auf Amazon AWS und hat den Vorteil, dass man einen echten root-Benutzer statt eines Containers verwenden kann“, und zugleich: „Außerhalb des Containers liegen keine benötigten Dateien, und der Container ist sehr minimal, sodass sich
gdbnicht leicht installieren lässt.“Haben die Leute inzwischen die Fähigkeit verloren, lokal zu bauen und zu debuggen, ohne Cloud und Container?
Für die allerkleinsten Dinge braucht man inzwischen allerlei Cloud-Komplexität und Deployment-Schichten. Damit wurde die PC-Revolution zu 100 % zurückgedreht, zurück in die klobige und teure Ära des Mainframe-Computing.
Der Grund ist, dass in der Cloud Geld steckt, und dass die Cloud DRM ist. Wenn man Software dorthin bringt, kann man Abogebühren verlangen, sie lassen sich nicht umgehen, und man kann perfekte Abhängigkeit für immer aufrechterhalten. Oft können Nutzer nicht einmal ihre eigenen Daten herausbekommen.
Praktisch ist es auch für Echtzeitanalysen zur Produktoptimierung.
Computing-Architektur ist dem Geschäftsmodell nachgelagert. Mainframes starben ursprünglich, weil es kein Internet gab und PCs günstiger waren, aber auch, weil Anbieter viel Macht zur Bindung der Kunden verloren.
Jetzt gibt es einen Weg, dieses deutlich profitablere Modell wiederzubeleben. Die lästige Freiheit der Nutzer ist verschwunden, und ehrlich gesagt: Wenn Nutzer solche Freiheit bekommen, zahlen sie oft nicht, wodurch es schwer wird, ein Geschäft mit hochwertiger Software aufzubauen.
Lokal konnten sie den Crash sehr wahrscheinlich nicht reproduzieren. Die Entwicklermaschinen waren vermutlich überwiegend x86, und dort trat der Crash wohl nicht auf.
Das Crash-Handling hätte besser sein müssen, aber sie scheinen sich dessen bewusst zu sein, und es ist nicht der Kern dessen, was hier behandelt wurde.
Veränderlicher globaler Zustand ist böse. Freunde lassen Freunde keinen veränderlichen globalen Zustand verwenden.
Ich hasse Umgebungsvariablen. Das ist der „Linux-Weg“, aber ich meide ihn wie die Pest. Kann ich nur dringend empfehlen.
libc ist furchtbar, und die Welt sollte endlich darüber hinwegkommen.
Das Problem ist weder Linux noch veränderlicher globaler Zustand oder Ressourcen, noch libc.
Das Problem ist, dass man am Arbeitsplatz nicht die Zeit bekommt, Dinge richtig zu machen. Wenn man etwa ein Problem mit GDB finden will, bevor es knallt, muss der Chef einem die Zeit geben, den Code und alles, was dieser Code berührt, beharrlich zu debuggen und zurückzuverfolgen.
In halbgaren Code fließt zu viel Geld. Traurig, aber wahr.
Wegen zu vieler solcher Probleme habe ich schließlich
getenv/setenv/putenvperLD_PRELOADgepatcht.