¿Para qué se supone que debo usar `O_PATH` y cómo?

8

Utilizo una distribución basada en Linux 4.x, y recientemente me di cuenta de que la open()llamada al sistema del núcleo admite una O_PATHbandera abierta.

Si bien la manpágina tiene una lista de llamadas al sistema con las que teóricamente podría usarse, no entiendo muy bien cuál es la idea. ¿ open(O_PATH)Solo hago directorios, en lugar de archivos? Y si lo hago, ¿por qué quiero usar un descriptor de archivo en lugar de la ruta del directorio? Además, la mayoría de las llamadas al sistema enumeradas allí no parecen ser particulares de los directorios; entonces, ¿también abro archivos normales con O_PATHpara obtener de alguna manera su directorio como descriptor de archivo? ¿O para obtener un descriptor de archivo para ellos pero con una funcionalidad limitada?

¿Alguien puede dar una explicación convincente de qué O_PATHse trata, cómo y para qué se supone que debemos usarlo?

Notas:

  • No es necesario describir la historia de cómo evolucionó esto (las páginas de manual relevantes mencionan los cambios en Linux 2.6.x, 3.5 y 3.6) a menos que sea necesario, solo me importa cómo están las cosas ahora.
  • Por favor, no me digas que solo use libc u otras instalaciones de nivel superior, lo sé.
einpoklum
fuente
@sebasth: De hecho, está relacionado, pero: 1. Es un poco viejo por ahora y las cosas pueden haber cambiado. 2. Francamente, no entiendo la esencia de la respuesta.
einpoklum
1
Puede publicar comentarios en esa pregunta preguntando si algo ha cambiado.
Barmar

Respuestas:

8

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_PATHdeberí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_RDONLYrequiere 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_PATHes 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/filey queremos realizar dos operaciones:

  1. Realice alguna verificación /dir1/dir2/file(por ejemplo, a quién pertenece el archivo o a qué hora se modificó por última vez).
  2. 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/dir2uno 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 dir2para 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/dir2veí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 dirfdes una referencia estable al directorio al que hacía referencia la ruta /dir1/dir2en el momento de la open()llamada. Si el objetivo del enlace simbólico dir2se modifica posteriormente, esto no afectará a lo que se dirfdrefiere. 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/dir2no 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_PATHcon 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 fdargumento 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).

mtk
fuente
The problem is that between the existence check and the file creation step, path or to ... could be modified - No puedo analizar esta oración. Pero entiendo lo esencial, creo. Por lo tanto, sirve como una especie de mecanismo de bloqueo en un directorio. Pero, ¿por qué usar el open()resultado en lugar de un bloqueo real?
einpoklum
@einpoklum el problema es que 'ruta' y 'a' no tienen el formato que se muestra en la página de manual original. Estos son componentes del nombre de ruta hipotético "/ ruta / a / xxx". Y no es como un bloqueo: es una referencia estable a un objeto de sistema de archivos; varios programas pueden tener tal referencia al mismo objeto.
mtk