Setenv Não É Thread-Safe e o C Não Quer Corrigi-lo

A API Unix `setenv`/`getenv` para variáveis de ambiente é fundamentalmente não thread-safe, e o POSIX explicitamente permite isso, o que leva a travamentos raros, mas graves, quando programas modernos e multithread — ou linguagens como Go e Rust — dependem dela. Comentadores debatem se o verdadeiro problema é o próprio ambiente mutável e global do processo, com muitos argumentando que variáveis de ambiente deveriam ser tratadas como imutáveis após o startup e que bibliotecas deveriam oferecer APIs explícitas de configuração. Outros apontam implementações já thread-safe (como Solaris/Illumos e chamadas no estilo Windows que copiam para fora) e pedem novas funções mais seguras na biblioteca C, ou até mesmo um padrão de biblioteca “pós-C” que abandone armadilhas legadas sem perder compatibilidade.

Problema central: setenv/getenv e segurança em threads

  • POSIX diz explicitamente que setenv/unsetenv não precisam ser thread-safe; getenv retorna um ponteiro que pode ser invalidado por chamadas posteriores.
  • Em programas multithread, uma corrida entre setenv e getenv (ou qualquer chamada da libc que use variáveis de ambiente) pode causar falhas ou dados incorretos.
  • Alguns argumentam que isso torna a API “quebrada e impossível de corrigir”, porque expõe estado global mutável sem uma variante segura.

Por que é difícil corrigir

  • Programas podem modificar o ambiente por vários canais: setenv/putenv, escrita via environ e modificação das próprias strings.
  • Colocar locks apenas em setenv/getenv é insuficiente se o código puder tocar environ diretamente.
  • A API de getenv retorna ponteiros internos; não é possível reorganizar ou liberar com segurança o armazenamento de base sem potencialmente quebrar código existente.
  • Compatibilidade retroativa é uma objeção importante: muito código legado depende da semântica atual.

Como sistemas e linguagens diferem

  • Algumas libcs (Solaris/Illumos, alguns BSDs, Apple) adicionam locking e muitas vezes “vazam” o armazenamento antigo do ambiente para evitar use-after-free; outras (musl, alguns BSDs) não usam lock algum.
  • Historicamente, a glibc tinha corridas inseguras; versões recentes adicionaram um lock em torno de setenv, mas getenv continua sujeito a corrida quando o ambiente é redimensionado.
  • As APIs do Windows (GetEnvironmentVariable, GetEnvironmentStrings) copiam para buffers do chamador e são efetivamente thread-safe.
  • A stdlib do Rust encapsula o acesso ao ambiente com um lock e retorna cópias proprietárias, mas FFI para C ainda pode quebrar invariantes; isso levou a CVEs reais em torno do tratamento de fuso horário.
  • O Go também enfrentou problemas quando seu código de DNS/tempo chamava libc, que lê variáveis de ambiente.

Casos de uso e se o ambiente deve mutar

  • Muitos defendem que o ambiente do processo deve ser tratado como imutável: definido no startup ou entre fork e exec, e não usado como barramento de configuração em runtime.
  • Outros apontam casos reais: shells, harnesses de teste, depuradores, lançadores de processos e bibliotecas que expõem configuração apenas via variáveis de ambiente.

Correções e contornos propostos

  • Novas APIs: getenv_r/getenv_s/tgetenv reentrantes/ thread-safe que copiem para buffers do chamador; talvez descontinuar as APIs antigas ao longo do tempo.
  • Truques de implementação: vazar blocos antigos de ambiente (Solaris/Illumos, Eyra) para evitar UAF; adicionar mutexes internos; ou falhar/panicar em setenv em código multithread para tornar bugs visíveis.
  • Orientação de nível mais alto: não modificar o ambiente depois que as threads começarem; evitar bibliotecas que façam isso; usar APIs explícitas de configuração ou passar envp para execve/posix_spawn.

Meta-discussão: responsabilidade e padrões

  • Um lado: C/POSIX estão “corretos” como documentados; programadores devem ler os manpages e evitar APIs não reentrantes em código com threads.
  • O outro lado: documentar uma armadilha não justifica mantê-la; os padrões devem evoluir para reduzir armadilhas, mesmo ao custo de novas APIs e complexidade.