- As funções
setenv() e unsetenv() da linguagem C não podem ser usadas com segurança em programas que utilizam threads
- Essas funções modificam estado global e podem causar conflitos quando outra thread chama
getenv()
- Colisões também ocorrem em outras linguagens que usam funções da biblioteca padrão de C, como
os.Setenv do Go e std::env::set_var() do Rust
- Foram necessários 2 dias para rastrear o problema relacionado e reportar o bug em um programa Go
- Isso acontece porque o resolver de DNS interno do Go usa
getaddrinfo(), que por sua vez chama getenv()
- Mas esse problema é muito antigo. Já havia um texto sobre isso em 2017, e no final estava escrito “nos vemos em 5 anos, em 2022!”, mas ele reapareceu novamente em 2023
- Isso é uma falha do padrão POSIX, que estendeu o padrão C para permitir a modificação de variáveis de ambiente
- A parte mais irritante é que muitas pessoas que podem influenciar padrões ou manter bibliotecas C não consideram isso um problema
- O motivo é que a especificação deixa claro que
setenv() não pode ser usado junto com threads
- Portanto, se alguém fizer isso e ocorrer um crash, a culpa seria dessa pessoa
- Então, deveríamos “ler cuidadosamente a especificação de todas as funções, não usar software escrito por outras pessoas e não usar threads”
- Mas isso é uma suposição irrealista no software moderno
- Em vez disso, deveríamos tentar criar APIs que sejam difíceis de estragar e que evoluam junto com as mudanças do ecossistema
- Como a linguagem C e sua biblioteca padrão continuam desempenhando um papel importante na base da maior parte do software, precisamos encontrar uma forma de melhorá-las — ou uma forma de abandoná-las
Por que setenv() não é thread-safe
getenv() retorna char*, e a aplicação não precisa liberar isso depois
- Enquanto uma thread usa esse ponteiro, outra thread pode alterar a mesma variável de ambiente com
setenv() ou unsetenv()
- O padrão C inclui apenas
getenv(), mas a maioria das implementações segue o padrão POSIX e inclui funções para modificar o ambiente
putenv() adiciona um char* ao conjunto de variáveis de ambiente, e se a aplicação modificar essa memória após o retorno de putenv(), a variável de ambiente também será modificada
environ é um array de ponteiros terminado em NULL (char**) que a aplicação pode ler e atribuir, e o acesso a esse array não é thread-safe
Como as variáveis de ambiente são implementadas
- Quando a aplicação sobrescreve uma variável existente, a implementação precisa decidir como lidar com isso
- glibc e Solaris/Illumos nunca liberam variáveis de ambiente, e os valores retornados por
getenv() são imutáveis e podem ser usados com segurança entre threads
- musl e FreeBSD/Apple liberam variáveis de ambiente, então pode ocorrer um crash se outra thread usar o ponteiro retornado por
getenv() depois que setenv() for chamado
- Garantir que o conjunto de variáveis de ambiente seja atualizado de forma thread-safe é um segundo problema, e é isso que causa crashes no glibc
Por que programas usam variáveis de ambiente
- Variáveis de ambiente são úteis para configurar bibliotecas compartilhadas ou runtimes de linguagem incluídos em outros programas
- Usuários podem alterar a configuração sem que o autor do programa precise passar explicitamente esses parâmetros
- Muitas bibliotecas chamam
getenv(), e os programas precisam alterar essas variáveis para configurar as bibliotecas que utilizam
Esse problema precisa ser resolvido, e há formas de fazer isso
- Na minha opinião, é absurdo que isso seja um problema conhecido há tanto tempo
- Milhares de horas foram desperdiçadas depurando o problema ou discutindo formas de contorná-lo
- Formas de resolver o problema
- Criar uma implementação thread-safe como a de Illumos/Solaris
- Isso tem algumas limitações. Há vazamento de memória em
setenv(), e ainda não é seguro se o programa usar putenv() ou environ
- Mesmo assim, isso é uma melhoria em relação às implementações atuais de Linux e Apple
- A segunda opção é adicionar uma nova API para obter todas as variáveis de ambiente que seja thread-safe por projeto, como
getenv_s() da Microsoft
- A solução que eu prefiro é usar as duas abordagens
- Isso reduz a chance de problemas em programas e bibliotecas existentes, além de oferecer um caminho para evitar completamente o problema em código novo ou em linguagens como Go e Rust
- Adicionar uma função semelhante a
getenv_s() que copie uma variável de ambiente para um buffer fornecido pelo usuário
- Adicionar uma API thread-safe para iterar por todas as variáveis de ambiente ou copiar todas elas
- Marcar
getenv() como obsoleto e recomendar, em vez dele, uma nova função getenv() thread-safe
- Marcar
putenv() como obsoleto e recomendar setenv() no lugar
- Marcar
environ como obsoleto e recomendar o uso das funções de variáveis de ambiente
- Atualizar a implementação de variáveis de ambiente para que seja thread-safe
3 comentários
"Porque a especificação deixa claro que não se pode usar
setenv()junto com threads" ==> ao usar uma API ou SDK, o básico do básico é sempre verificar a specification com muito cuidado. Sinceramente, só parece uso forçado.O problema é usar uma funcionalidade que já foi mal projetada desde o início.
setenvnão é thread-safe, e C não quer corrigir isso