C stdlib isn't threadsafe and even safe Rust didn't save us
Mutating environment variables in POSIX programs turns out to be fundamentally unsafe in multi‑threaded contexts, and this bites not just C but higher‑level languages like Rust that wrap libc’s `getenv`/`setenv` APIs. Commenters unpack how the shared, mutable “environ” model makes true thread safety essentially impossible without leaks or breaking changes, why Rust is moving `std::env::set_var` to `unsafe`, and why glibc is adopting partial fixes that trade correctness for memory growth. Many argue the only robust approach is to treat process environments as immutable after startup and to stop using them as a global, mutable configuration store.
Non-thread-safe environment APIs in C/POSIX
- Core issue:
setenv/unsetenv(andputenv,environ) are not required to be reentrant or thread‑safe by POSIX; many implementations aren’t. getenvis also tricky: the returned pointer may be invalidated by later env calls; POSIX/ISO C give weak guarantees.- Exposing
extern char **environmakes it impossible to fully “fix” things without breaking ABI: code can mutate the environment behind libc’s back.
Impact on Rust and other languages
- Rust’s
std::env::set_varoriginally wrappedsetenvas a safe function; it’s now documented as unsafe on most non‑Windows platforms and will requireunsafein Rust 2024. - Rust can’t synchronize with C libraries that call
getenv/setenvdirectly, so a Rust-only mutex cannot fix the problem. - Even a Rust‑native libc or alternative runtime doesn’t help if C/FFI code still uses the platform libc and its environment.
Can libc or POSIX fix this?
- One camp: libc should make env operations safe by default (e.g., mutexes, copy‑on‑write, per‑thread views, never freeing old strings, Solaris/illumos-style fixes, glibc’s recent “leak instead of crash” change).
- Counterarguments:
- The API shape (shared pointers,
environ) fundamentally prevents full MT safety. - Retrofitting safety risks breaking existing programs or causing unbounded leaks.
- Standards explicitly say these calls need not be thread‑safe; changing that is hard.
- The API shape (shared pointers,
Environment variables as mutable global state
- Strong sentiment that env vars should be read-only after startup, like argv.
- Many argue
setenv“shouldn’t have existed” or should be reserved for building child-process environments, not intra-process configuration. - Others note legitimate cases (e.g., dlopen’ing libraries configured via env, allocators/sanitizers, shells) but agree mutation after threads start is dangerous.
Workarounds and safer patterns
- Common recommendations:
- Read env exactly once at startup, copy into your own data structure, then treat as immutable.
- Avoid libraries that mutate env at runtime; prefer APIs that accept explicit config instead of reading env.
- In extreme cases, override
getenv/setenvviaLD_PRELOADor use alternative libcs that leak instead of corrupting.
- Some suggest new, explicitly thread-safe env APIs (buffer‑based or copy‑returning), but migration and coexistence with
environremain unsolved.
Broader reflections
- Debate over “safe by default” vs performance and historical constraints.
- Comparisons to Windows’ and JVM’s environment handling, which are effectively thread-safe or immutable, seen as vindicating stricter designs.