Setenv Is Not Thread Safe and C Doesn't Want to Fix It
Unix’s `setenv`/`getenv` API for environment variables is fundamentally not thread-safe, and POSIX explicitly permits this, leading to rare but nasty crashes when modern, multi-threaded programs (or languages like Go and Rust) rely on it. Commenters debate whether the real bug is the mutable, process‑global environment itself, with many arguing that env vars should be treated as immutable after startup and that libraries should offer explicit configuration APIs instead. Others point to existing thread-safe implementations (like Solaris/Illumos and Windows-style copy-out calls) and call for either new, safer C library functions or even a “post‑C” standard library that drops legacy footguns while preserving compatibility.
Core issue: setenv/getenv and thread safety
- POSIX explicitly says
setenv/unsetenvare not required to be thread‑safe;getenvreturns a pointer that may be invalidated by later calls. - In multithreaded programs,
setenvracing withgetenv(or any env‑using libc call) can cause crashes or bogus data. - Some argue this makes the API “broken and unfixable” because it exposes global mutable state without a safe variant.
Why it’s hard to fix
- Programs can mutate the environment through multiple channels:
setenv/putenv, writing throughenviron, and modifying the strings themselves. - Locking just
setenv/getenvis insufficient if code is allowed to touchenvirondirectly. getenv’s API returns internal pointers; you can’t safely rearrange or free backing storage without potentially breaking existing code.- Backwards compatibility is a major objection: lots of legacy code depends on the current semantics.
How systems and languages differ
- Some libcs (Solaris/Illumos, some BSDs, Apple) add locking and often “leak” old env storage to avoid use‑after‑free; others (musl, some BSDs) don’t lock at all.
- glibc historically had unsafe races; recent versions added a lock around
setenv, butgetenvremains racy when env is resized. - Windows APIs (
GetEnvironmentVariable,GetEnvironmentStrings) copy into caller buffers and are effectively thread‑safe. - Rust’s stdlib wraps env access in a lock and returns owned copies, but FFI into C can still break invariants; this caused real CVEs around time zone handling.
- Go similarly ran into issues when its DNS/time code called into libc, which reads env vars.
Use cases and whether env should mutate
- Many argue the process environment should be treated as immutable: set at startup or between
forkandexec, not used as a runtime config bus. - Others note real cases: shells, test harnesses, debuggers, process launchers, and libraries that only expose config via env vars.
Proposed fixes and workarounds
- New APIs: reentrant/thread‑safe
getenv_r/getenv_s/tgetenvthat copy into caller buffers; possibly deprecate old APIs over time. - Implementation tricks: leak old env blocks (Solaris/Illumos, Eyra) to avoid UAF; add internal mutexes; or crash/panic on
setenvin multithreaded code to surface bugs. - Higher‑level advice: don’t modify env after threads start; avoid libraries that do; use explicit configuration APIs or pass
envptoexecve/posix_spawn.
Meta‑discussion: responsibility and standards
- One side: C/POSIX are “correct” as documented; programmers must read the manpages and avoid using non‑reentrant APIs in threaded code.
- Other side: documenting a sharp edge doesn’t justify keeping it; standards should evolve to reduce footguns, even at the cost of new APIs and complexity.