La descripción en la open(2)
página del manual ofrece algunas pistas para comenzar:
O_PATH (since Linux 2.6.39)
Obtain a file descriptor that can be used for two purposes:
to indicate a location in the filesystem tree and to per‐
form operations that act purely at the file descriptor
level. The file itself is not opened, and other file oper‐
ations (e.g., read(2), write(2), fchmod(2), fchown(2),
fgetxattr(2), ioctl(2), mmap(2)) fail with the error EBADF.
A veces, no queremos abrir un archivo o un directorio. En cambio, solo queremos una referencia a ese objeto del sistema de archivos para realizar ciertas operaciones (por ejemplo, fchdir()
a un directorio al que hace referencia un descriptor de archivo que abrimos usando O_PATH
). Entonces, un punto trivial: si este es nuestro propósito, entonces abrir con O_PATH
debería ser un poco más barato, ya que el archivo en sí no está realmente abierto.
Y un punto menos trivial: antes de la existencia de O_PATH
, la forma de obtener dicha referencia a un objeto del sistema de archivos era abrir el objeto con O_RDONLY
. Pero el uso de O_RDONLY
requiere que tengamos permiso de lectura sobre el objeto. Sin embargo, hay varios casos de uso en los que no necesitamos leer realmente el objeto: por ejemplo, ejecutar un binario o acceder a un directorio ( fchdir()
) o alcanzar un directorio para tocar un objeto dentro del directorio.
Uso con llamadas al sistema "* at ()"
El común, pero no el único, de uso O_PATH
es abrir un directorio, con el fin de tener una referencia a ese directorio para su uso con el "*" en las llamadas al sistema, tales como openat()
, fstatat()
, fchownat()
, y así sucesivamente. Esta familia de las llamadas al sistema, que se nos ocurren más o menos de que los sucesores modernos a las llamadas al sistema de mayor edad con nombres similares ( open()
, fstat()
, fchown()
, etc.), sirven un par de propósitos, el primero de los cuales se toca en cuando se pregunta " ¿por qué quiero usar un descriptor de archivo en lugar de la ruta del directorio? ". Si miramos más abajo en la open(2)
página del manual, encontramos este texto (bajo un subtítulo con la justificación de las llamadas al sistema "* at"):
First, openat() allows an application to avoid race conditions
that could occur when using open() to open files in directories
other than the current working directory. These race conditions
result from the fact that some component of the directory prefix
given to open() could be changed in parallel with the call to
open(). Suppose, for example, that we wish to create the file
path/to/xxx.dep if the file path/to/xxx exists. The problem is
that between the existence check and the file creation step, path
or to (which might be symbolic links) could be modified to point
to a different location. Such races can be avoided by opening a
file descriptor for the target directory, and then specifying that
file descriptor as the dirfd argument of (say) fstatat(2) and ope‐
nat().
Para hacer esto más concreto ... Supongamos que tenemos un programa que desea realizar múltiples operaciones en un directorio que no sea su directorio de trabajo actual, lo que significa que debemos especificar algún prefijo de directorio como parte de los nombres de archivo que usamos. Supongamos, por ejemplo, que la ruta es /dir1/dir2/file
y queremos realizar dos operaciones:
- Realice alguna verificación
/dir1/dir2/file
(por ejemplo, a quién pertenece el archivo o a qué hora se modificó por última vez).
- Si estamos satisfechos con el resultado de esa verificación, tal vez entonces queramos realizar alguna otra operación del sistema de archivos en el mismo directorio, por ejemplo, creando un archivo llamado
/dir1/dir2/file.new
.
Ahora, primero supongamos que hicimos todo usando llamadas de sistema tradicionales basadas en el nombre de ruta:
struct stat stabuf;
stat("/dir1/dir2/file", &statbuf);
if ( /* Info returned in statbuf is to our liking */ ) {
fd = open("/dir1/dir2/file.new", O_CREAT | O_RDWR, 0600);
/* And then populate file referred to by fd */
}
Ahora, además, suponga que en el prefijo de directorio /dir1/dir2
uno de los componentes (por ejemplo dir2
) era en realidad un enlace simbólico (que se refiere a un directorio), y que entre la llamada stat()
y la llamada aopen()
una persona malintencionada pudo cambiar el objetivo del enlace simbólico dir2
para apuntar a un directorio diferente. Esta es una condición clásica de carrera de tiempo de verificación y tiempo de uso. Nuestro programa verificó un archivo en un directorio pero luego fue engañado para crear un archivo en un directorio diferente, quizás un directorio sensible a la seguridad. El punto clave aquí es que el nombre de ruta se /dir/dir2
veía igual, pero lo que se refiere cambió por completo.
Podemos evitar este tipo de problemas usando las llamadas "* at". En primer lugar, obtenemos un identificador que hace referencia al directorio donde haremos nuestro trabajo:
dirfd = open("/dir/dir2", O_PATH);
El punto crítico aquí es que dirfd
es una referencia estable al directorio al que hacía referencia la ruta /dir1/dir2
en el momento de la open()
llamada. Si el objetivo del enlace simbólico dir2
se modifica posteriormente, esto no afectará a lo que se dirfd
refiere. Ahora, podemos hacer nuestra operación de verificación + usando las llamadas "* at" que son equivalentes a las llamadas stat()
y open()
anteriores:
fstatat(dirfd, ""file", &statbuf)
struct stat stabuf;
fstatat(dirfd, "file", &statbuf);
if ( /* Info returned in statbuf is to our liking */ ) {
fd = openat(dirfd, "file.new", O_CREAT | O_RDWR, 0600);
/* And then populate file referred to by fd */
}
Durante estos pasos, cualquier manipulación de enlaces simbólicos en el nombre de ruta /dir/dir2
no tendrá ningún impacto: se garantiza que la verificación ( fstatat()
) y la operación ( openat()
) se realicen en el mismo directorio.
Hay otro propósito para usar las llamadas "* at ()", que se relaciona con la idea de "directorios de trabajo actuales por subproceso" en programas multiproceso (y nuevamente podríamos abrir los directorios usando O_PATH
), pero creo que este uso es probablemente menos relevante para su pregunta, y le dejo que lea la open(2)
página del manual si desea obtener más información.
Uso con descriptores de archivos para archivos normales
Un uso de O_PATH
con archivos normales es abrir un archivo binario para el que tenemos permiso de ejecución (pero no necesariamente permiso de lectura, para que no podamos abrir el archivo con O_RDONLY
). Ese descriptor de archivo se puede pasar a fexecve(3)
para ejecutar el programa. Todo lo que fexecve(fd, argv, envp)
está haciendo con su fd
argumento es esencialmente:
snprintf(buf, "/proc/self/fd/%d", fd);
execve(buf, argv, envp);
(Aunque, comenzando con glibc 2.27, la implementación utilizará la execveat(2)
llamada del sistema, en los núcleos que proporcionan esa llamada del sistema).