Для чего я должен использовать `O_PATH` и как?

8

Я использую дистрибутив на основе Linux 4.x, и недавно я заметил, что open()системный вызов ядра поддерживает O_PATHфлаг открытия.

Хотя на этой manстранице есть список системных вызовов, с которыми она может теоретически использоваться, я не совсем понимаю, в чем идея. Я open(O_PATH)только каталоги, а не файлы? И если я это сделаю, почему я хочу использовать файловый дескриптор вместо пути к каталогу? Кроме того, большинство системных вызовов, перечисленных там, не относятся к каталогам; Итак, я также открываю обычные файлы, O_PATHчтобы как-то получить их каталог в качестве файлового дескриптора? Или получить файловый дескриптор для них, но с ограниченной функциональностью?

Может ли кто-нибудь дать убедительное объяснение того, что O_PATHи как и для чего, мы должны использовать?

Ноты:

  • Не нужно описывать историю того, как это развивалось (в соответствующих справочных страницах упоминаются изменения в Linux 2.6.x, 3.5 и 3.6), если это не нужно - мне просто важно, как обстоят дела сейчас.
  • Пожалуйста, не говорите мне просто использовать libc или другие высокоуровневые средства, я это знаю.
einpoklum
источник
@sebasth: Это действительно связано, но: 1. Сейчас оно немного старое, и все могло измениться. 2. Честно говоря, я не совсем понимаю суть ответа.
einpoklum
1
Вы можете оставить комментарий в этом вопросе, спрашивая, изменилось ли что-нибудь.
Бармар

Ответы:

8

Описание на 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и мы хотим выполнить две операции:

  1. Выполните некоторую проверку /dir1/dir2/file(например, кто владеет файлом или в какое время он был последний раз изменен).
  2. Если мы удовлетворены результатом этой проверки, возможно, мы захотим затем выполнить какую-то другую операцию файловой системы в том же каталоге, например, создать файл с именем /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)системный вызов в ядрах, которые предоставляют этот системный вызов.)

холодный морской тропический воздух
источник
The problem is that between the existence check and the file creation step, path or to ... could be modified - не могу разобрать это предложение. Но я понимаю суть этого, я думаю. Так что это служит своего рода механизмом блокировки каталога. Но зачем использовать open()результат, а не фактическую блокировку?
einpoklum
@einpoklum проблема в том, что у 'path' и 'to' нет форматирования, показанного на исходной странице руководства. Это компоненты гипотетического пути "/ path / to / xxx". И это не похоже на блокировку: это стабильная ссылка на объект файловой системы; несколько программ могут иметь такую ​​ссылку на один и тот же объект.
МТК