15 points par xguru 2023-11-23 | 3 commentaires | Partager sur WhatsApp
  • Les fonctions setenv() et unsetenv() du langage C ne peuvent pas être utilisées en toute sécurité dans les programmes qui utilisent des threads
  • Ces fonctions modifient un état global et peuvent entrer en conflit lorsqu’un autre thread appelle getenv()
  • Des conflits surviennent aussi dans d’autres langages qui s’appuient sur les fonctions de la bibliothèque standard C, comme os.Setenv en Go et std::env::set_var() en Rust
  • Il a fallu 2 jours pour suivre le problème concerné et signaler le bug dans un programme Go
    • Cela vient du fait que le résolveur DNS interne de Go utilise getaddrinfo(), qui appelle getenv()
  • Mais ce problème est très ancien. Un article en parlait déjà en 2017, et il se terminait par « rendez-vous dans 5 ans, en 2022 ! », sauf qu’on l’a revu en 2023
  • Il s’agit d’un défaut de la norme POSIX, qui a étendu la norme C pour permettre la modification des variables d’environnement
    • La partie la plus frustrante est que beaucoup de personnes capables d’influencer les normes ou de maintenir les bibliothèques C ne considèrent pas cela comme un problème
    • La raison est que la spécification indique explicitement qu’on ne peut pas utiliser setenv() avec des threads
    • Donc si quelqu’un le fait et que ça plante, c’est considéré comme sa faute
  • Il faudrait donc « lire attentivement la spécification de chaque fonction, ne pas utiliser de logiciel écrit par d’autres, et ne pas utiliser de threads »
    • Mais c’est une hypothèse irréaliste dans les logiciels modernes
    • Il vaudrait mieux chercher à concevoir des API plus difficiles à casser et capables d’évoluer avec les changements de l’écosystème
  • Le langage C et sa bibliothèque standard continuent de jouer un rôle essentiel à la base de la plupart des logiciels, il faut donc trouver un moyen de les améliorer, ou bien un moyen de les abandonner

Pourquoi setenv() n’est pas thread-safe

  • getenv() renvoie un char*, que l’application n’a pas besoin de libérer ensuite
  • Pendant qu’un thread utilise ce pointeur, un autre thread peut modifier la même variable d’environnement via setenv() ou unsetenv()
  • La norme C ne comprend que getenv(), mais la plupart des implémentations suivent POSIX et incluent des fonctions permettant de modifier l’environnement
  • putenv() ajoute un char* à l’ensemble des variables d’environnement, et si l’application modifie cette mémoire après le retour de putenv(), la variable d’environnement est elle aussi modifiée
  • environ est un tableau de pointeurs terminé par NULL (char**) que l’application peut lire et assigner, et l’accès à ce tableau n’est pas thread-safe

Comment les variables d’environnement sont implémentées

  • Lorsqu’une application écrase une variable existante, l’implémentation doit décider comment la traiter
  • glibc et Solaris/Illumos ne libèrent jamais les variables d’environnement, et les valeurs renvoyées par getenv() sont immuables et peuvent être utilisées en toute sécurité entre threads
  • musl et FreeBSD/Apple libèrent les variables d’environnement, ce qui peut provoquer un plantage si un autre thread utilise un pointeur renvoyé par getenv() après un appel à setenv()
  • Garantir qu’un ensemble de variables d’environnement soit mis à jour de manière thread-safe constitue un second problème, qui provoque des plantages dans glibc

Pourquoi les programmes utilisent des variables d’environnement

  • Les variables d’environnement sont utiles pour configurer des bibliothèques partagées ou des runtimes de langage inclus dans d’autres programmes
  • Elles permettent aux utilisateurs de modifier la configuration sans que l’auteur du programme doive transmettre explicitement cette configuration
  • De nombreuses bibliothèques appellent getenv(), et les programmes doivent modifier ces variables pour configurer les bibliothèques qu’ils utilisent

Il faut résoudre ce problème, et voici comment

  • À mon avis, il est absurde que ce problème soit connu depuis si longtemps
  • Des milliers d’heures ont été gaspillées à déboguer le problème ou à discuter de solutions de contournement
  • Façons de résoudre le problème
    • Créer une implémentation thread-safe, comme sur Illumos/Solaris
      • Cela a toutefois certaines limites. setenv() provoque alors des fuites mémoire, et le programme reste non sûr s’il utilise putenv() ou environ
      • Mais c’est malgré tout une amélioration par rapport aux implémentations actuelles de Linux et d’Apple
    • Deuxièmement, ajouter une nouvelle API conçue pour être thread-safe afin de récupérer toutes les variables d’environnement, comme getenv_s() de Microsoft
  • La solution que je préfère est de combiner les deux
    • Cela réduit le risque de problèmes dans les programmes et bibliothèques existants, tout en offrant un chemin pour éviter complètement ces problèmes dans du nouveau code ou dans des langages comme Go et Rust
    • Ajouter, à l’image de getenv_s(), une fonction qui copie une variable d’environnement dans un tampon fourni par l’utilisateur
    • Ajouter une API thread-safe permettant soit d’itérer sur toutes les variables d’environnement, soit de toutes les copier
    • Marquer getenv() comme obsolète et recommander à la place une nouvelle fonction getenv() thread-safe
    • Marquer putenv() comme obsolète et recommander setenv() à la place
    • Marquer environ comme obsolète et recommander à la place les fonctions liées aux variables d’environnement
    • Mettre à jour l’implémentation des variables d’environnement pour la rendre thread-safe

3 commentaires

 
ahwjdekf 2023-11-24

« parce que la spécification indique clairement que setenv() ne peut pas être utilisé avec des threads » ==> lorsqu’on utilise une API ou un SDK, il est indispensable, à la base, de vérifier attentivement la spécification. Franchement, ça donne juste l’impression d’un usage forcé.

 
carnoxen 2025-01-24

Le problème, c’est surtout d’utiliser une fonctionnalité mal conçue dès le départ.

 
cosine20 2023-11-27

....