Condiciones de carrera

Otro error muy conocido en el mundo de los sistemas operativos son las condiciones de carrera, situaciones en las que dos o más procesos leen o escriben en un área compartida y el resultado final depende de los instantes de ejecución de cada uno ([Tan91]). Cuando una situación de este tipo se produce y acciones que deberían ser atómicas no lo son, existe un intervalo de tiempo durante el que un atacante puede obtener privilegios, leer y escribir ficheros protegidos, y en definitiva violar las políticas de seguridad del sistema ([Bis95]).

Por ejemplo, imaginemos un programa setuidado perteneciente a root que almacene información en un fichero propiedad del usuario que está ejecutando el programa; seguramente el código contendrá unas líneas similares a las siguientes (no se ha incluido la comprobación básica de errores por motivos de claridad):
if(access(fichero, W_OK)==0){
        open();
        write();
}
En una ejecución normal, si el usuario no tiene privilegios suficientes para escribir en el fichero, la llamada a access() devolverá -1 y no se permitirá la escritura. Si esta llamada no falla open() tampoco lo hará, ya que el UID efectivo con que se está ejecutando el programa es el del root; así nos estamos asegurando que el programa escriba en el fichero si y sólo si el usuario que lo ejecuta puede hacerlo - sin privilegios adicionales por el setuid -. Pero, >qué sucede si el fichero cambia entre la llamada a access() y las siguientes? El programa estará escribiendo en un archivo sobre el que no se han realizado las comprobaciones necesarias para garantizar la seguridad. Por ejemplo, imaginemos que tras la llamada a access(), y justo antes de que se ejecute open(), el usuario borra el fichero referenciado y enlaza /etc/passwd con el mismo nombre: el programa estará escribiendo información en el fichero de contraseñas.

Este tipo de situación, en la que un programa comprueba una propiedad de un objeto y luego ejecuta determinada acción asumiendo que la propiedad se mantiene, cuando realmente no es así, se denomina TOCTTOU (Time of check to time of use). >Qué se puede hacer para evitarla? El propio sistema operativo nos da las diferentes soluciones al problema ([BD96]). Por ejemplo, podemos utilizar descriptores de fichero en lugar de nombres: en nuestro caso, deberíamos utilizar una variante de la llamada access() que trabaje con descriptores en lugar de nombres de archivo (no es algo que exista realmente, sería necesario modificar el núcleo del operativo para conseguirlo); con esto conseguimos que aunque se modifique el nombre del fichero, el objeto al que accedemos sea el mismo durante todo el tiempo. Además, es conveniente invertir el orden de las llamadas (invocar primero a open() y después a nuestra variante de access()); de esta forma, el código anterior quedaría como sigue:
if((fd=open(fichero, O_WRONLY))==NULL){
        if (access2(fileno(fp),W_OK)==0){
            write();
        }
}
No obstante, existen llamadas que utilizan nombres de fichero y no tienen un equivalente que utilice descriptores; para no tener que reprogramar todo el núcleo de Unix, existe una segunda solución que cubre también a estas llamadas: asociar un descriptor y un nombre de fichero sin restringir el modo de acceso. Para esto se utilizaría un modo especial de apertura, O_ACCESS - que sería necesario implementar -, en lugar de los clásicos O_RDONLY, O_WRONLY o O_RDWR; este nuevo modo garantizaría que si el objeto existe se haría sobre él un open() habitual pero sin derecho de escritura o lectura (sería necesario efectuar una segunda llamada a la función, con los parámetros adecuados), y si no existe se reserva un nombre y un inodo de tipo `reservado', un tipo de transición que posteriormente sería necesario convertir en un tipo de fichero habitual en Unix (directorio, socket, enlace...) con las llamadas correspondientes.
© 2002 Antonio Villalón Huerta