15 pontos por xguru 2023-11-23 | 3 comentários | Compartilhar no WhatsApp
  • 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

 
ahwjdekf 2023-11-24

"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.

 
carnoxen 2025-01-24

O problema é usar uma funcionalidade que já foi mal projetada desde o início.

 
cosine20 2023-11-27

setenv não é thread-safe, e C não quer corrigir isso