Описание на open(2)
странице руководства дает некоторые подсказки для начала:
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.
Иногда мы не хотим открывать файл или каталог. Вместо этого нам просто нужна ссылка на этот объект файловой системы для выполнения определенных операций (например, на fchdir()
каталог, на который ссылается дескриптор файла, который мы открыли с помощью O_PATH
). Итак, тривиальный момент: если это наша цель, то открытие с помощью O_PATH
должно быть немного дешевле, так как сам файл фактически не открывается.
И менее тривиальный момент: до существования O_PATH
способа получения такой ссылки на объект файловой системы было открывать объект с помощью O_RDONLY
. Но использование O_RDONLY
требует, чтобы у нас было разрешение на чтение объекта. Однако существуют различные случаи использования, когда нам фактически не нужно читать объект: например, выполнение двоичного файла или доступ к каталогу ( fchdir()
) или обращение к каталогу для прикосновения к объекту внутри каталога.
Использование с системными вызовами "* at ()"
Распространенное, но не только, использование O_PATH
, чтобы открыть каталог, чтобы иметь ссылку на этот каталог для использования с «* в» системных вызовов, таких как openat()
, fstatat()
, fchownat()
, и так далее. Это семейство системных вызовов, которые мы можем примерно думать как современные продолжатели старых системных вызовов с похожими названиями ( open()
, fstat()
, fchown()
и так далее), которые служат несколько целей, первые из которых вы касаетесь, когда вы спрашиваете " почему я хочу использовать файловый дескриптор вместо пути к каталогу? " Если мы посмотрим дальше на open(2)
странице руководства , мы найдем этот текст (в подзаголовке с обоснованием системных вызовов "* 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().
Чтобы сделать это более конкретным ... Предположим, у нас есть программа, которая хочет выполнять несколько операций в каталоге, отличном от текущего рабочего каталога, что означает, что мы должны указать некоторый префикс каталога как часть имен файлов, которые мы используем. Предположим, например, что путь есть, /dir1/dir2/file
и мы хотим выполнить две операции:
- Выполните некоторую проверку
/dir1/dir2/file
(например, кто владеет файлом или в какое время он был последний раз изменен).
- Если мы удовлетворены результатом этой проверки, возможно, мы захотим затем выполнить какую-то другую операцию файловой системы в том же каталоге, например, создать файл с именем
/dir1/dir2/file.new
.
Теперь сначала предположим, что мы сделали все, используя традиционные системные вызовы на основе имени пути:
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 */
}
Теперь, кроме того, предположим, что в префиксе каталога /dir1/dir2
один из компонентов (скажем dir2
) был фактически символической ссылкой (которая относится к каталогу), и что между вызовомstat()
open()
злоумышленника и вызовом злоумышленника смогла изменить цель символическая ссылка, dir2
указывающая на другой каталог. Это классическое состояние гонки на время проверки. Наша программа проверила файл в одном каталоге, но затем его обманули, чтобы создать файл в другом каталоге, возможно, в защищенном каталоге. Ключевым моментом здесь является то, что путь /dir/dir2
выглядел одинаково, но то, что он ссылается, изменилось полностью.
Мы можем избежать подобных проблем, используя вызовы "* at". Прежде всего, мы получаем дескриптор, ссылающийся на каталог, где мы будем выполнять нашу работу:
dirfd = open("/dir/dir2", O_PATH);
Критическим моментом здесь является то, что dirfd
это стабильная ссылка на каталог, на который ссылался путь /dir1/dir2
во время open()
вызова. Если цель символической ссылки dir2
впоследствии изменится, это не повлияет на то, что dirfd
относится к. Теперь мы можем сделать наш чек + операцию с использованием «* на» вызовы , которые эквивалентны stat()
и open()
вызовы выше:
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 */
}
Во время этих шагов любая манипуляция символическими ссылками в пути /dir/dir2
не окажет никакого влияния: check ( fstatat()
) и operation ( openat()
) гарантированно будут происходить в одном и том же каталоге.
Существует еще одна цель использования вызовов «* at ()», которая связана с идеей «текущих рабочих каталогов на поток» в многопоточных программах (и снова мы могли бы открывать каталоги, используя O_PATH
), но я думаю, что это использование, вероятно, менее актуален для вашего вопроса, и я оставляю вас читать open(2)
справочную страницу, если вы хотите узнать больше.
Использование с файловыми дескрипторами для обычных файлов
Одно из применений O_PATH
с обычными файлами - открыть двоичный файл, для которого у нас есть разрешение на выполнение (но не обязательно разрешение на чтение, чтобы мы не могли открыть файл с помощью O_RDONLY
). Этот дескриптор файла затем может быть передан fexecve(3)
для выполнения программы. Все, что fexecve(fd, argv, envp)
делает с его fd
аргументом, по сути:
snprintf(buf, "/proc/self/fd/%d", fd);
execve(buf, argv, envp);
(Хотя, начиная с glibc 2.27, реализация будет использовать execveat(2)
системный вызов в ядрах, которые предоставляют этот системный вызов.)