- Las funciones
setenv() y unsetenv() de C no se pueden usar de forma segura en programas que usan hilos
- Estas funciones modifican estado global y pueden provocar conflictos cuando otro hilo llama a
getenv()
- También ocurren fallos en otros lenguajes que usan funciones de la biblioteca estándar de C, como
os.Setenv de Go y std::env::set_var() de Rust
- Se necesitaron 2 días para rastrear el problema relacionado y reportar el bug en un programa de Go
- Esto se debe a que el resolvedor DNS interno de Go usa
getaddrinfo(), que a su vez llama a getenv()
- Pero este problema es muy antiguo. Ya había un artículo relacionado en 2017, y al final decía “¡nos vemos en 5 años, en 2022!”, pero volvieron a encontrarse con él en 2023
- Esto es un defecto del estándar POSIX, que extendió el estándar de C para permitir modificar variables de entorno
- La parte más frustrante es que muchas personas con capacidad de influir en el estándar o mantener bibliotecas de C no consideran que esto sea un problema
- La razón es que la especificación deja claro que
setenv() no puede usarse junto con hilos
- Por lo tanto, si alguien hace eso, el fallo es culpa suya
- Así que supuestamente “debemos leer con cuidado la especificación de todas las funciones, no usar software escrito por otros y no usar hilos”
- Pero en el software moderno, esa es una suposición poco realista
- En lugar de eso, habría que esforzarse por crear APIs que sean difíciles de romper y evolucionen junto con los cambios del ecosistema
- Como C y su biblioteca estándar siguen desempeñando un papel importante en la base de la mayoría del software, hay que encontrar una forma de mejorarlos o una forma de dejarlos atrás
Por qué setenv() no es thread-safe
getenv() devuelve un char*, y la aplicación no necesita liberar ese valor más tarde
- Mientras un hilo usa ese puntero, otro hilo puede cambiar la misma variable de entorno con
setenv() o unsetenv()
- El estándar de C solo incluye
getenv(), pero la mayoría de las implementaciones siguen POSIX e incluyen funciones para modificar el entorno
putenv() agrega un char* al conjunto de variables de entorno, y si la aplicación modifica esa memoria después de que putenv() retorna, la variable de entorno también cambia
environ es un arreglo de punteros terminado en NULL (char**) que la aplicación puede leer y asignar, y el acceso a ese arreglo no es thread-safe
Cómo se implementan las variables de entorno
- Cuando una aplicación sobrescribe una variable existente, la implementación debe decidir cómo manejarlo
- glibc y Solaris/Illumos nunca liberan las variables de entorno, por lo que el valor devuelto por
getenv() es inmutable y puede usarse de forma segura entre hilos
- musl y FreeBSD/Apple sí liberan variables de entorno, por lo que puede haber fallos si otro hilo llama a
setenv() y luego se usa el puntero devuelto por getenv()
- Garantizar que el conjunto de variables de entorno se actualice de forma thread-safe es un segundo problema, y eso es lo que provoca fallos en glibc
Por qué los programas usan variables de entorno
- Las variables de entorno son útiles para configurar bibliotecas compartidas o runtimes de lenguaje incluidos dentro de otros programas
- Permiten que los usuarios cambien la configuración sin que el autor del programa tenga que pasarla explícitamente
- Muchas bibliotecas llaman a
getenv(), y los programas necesitan cambiar estas variables para configurar las bibliotecas que usan
Este problema debe resolverse, y se puede hacer así
- Me parece absurdo que esto haya sido un problema conocido durante tanto tiempo
- Se han desperdiciado miles de horas depurando el problema o discutiendo soluciones alternativas
- Formas de resolverlo
- Crear una implementación thread-safe como en Illumos/Solaris
- Esto tiene algunas limitaciones.
setenv() filtra memoria y sigue sin ser seguro si el programa usa putenv() o manipula directamente las variables de entorno
- Aun así, es una mejora frente a las implementaciones actuales de Linux y Apple
- Una segunda opción es agregar una API nueva que, por diseño, obtenga variables de entorno de forma thread-safe, como
getenv_s() de Microsoft
- La solución que prefiero es usar ambas
- Reduce la probabilidad de problemas en programas y bibliotecas existentes, y ofrece un camino para evitar por completo el problema en código o lenguajes nuevos como Go y Rust
- Agregar una función similar a
getenv_s() que copie una variable de entorno a un búfer proporcionado por el usuario
- Agregar una API thread-safe para iterar sobre todas las variables de entorno o copiar todas las variables
- Marcar
getenv() como obsoleta y recomendar en su lugar una nueva versión thread-safe de getenv()
- Marcar
putenv() como obsoleta y recomendar setenv() en su lugar
- Marcar
environ como obsoleto y recomendar funciones de variables de entorno en su lugar
- Actualizar la implementación de las variables de entorno para que sea thread-safe
3 comentarios
"Porque la especificación deja claramente establecido que no se puede usar
setenv()junto con hilos" ==> al usar una API o un SDK, lo más básico entre lo básico es revisar con mucho cuidado la specification. Solo parece un uso forzado a la mala.El problema es usar una función que estuvo mal diseñada desde el principio
....