Манипуляции с файловой системой

В этой главе мы обсуждаем общие вопросы манипуляций с файловой системой, с акцентом на доступ к файлам и каталогам, в которые другой, потенциально недоверенный пользователь имеет доступ на запись.

Временные файлы рассматриваются в отдельной главе, Временные файлы.

Работа с файлами и каталогами, принадлежащими другим пользователям

Иногда возникает необходимость выполнять операции над файлами и каталогами, принадлежащими другим (потенциально недоверенным) пользователям. Например, системный администратор может удалить домашний каталог пользователя, или менеджер пакетов может обновить файл в каталоге, который принадлежит пользователю, специфичному для приложения. Это отличается от доступа к файловой системе от имени конкретного пользователя; см. Доступ к файловой системе от имени другого пользователя.

Доступ к файлам через границы доверия сопряжён с несколькими проблемами, особенно если обходится всё дерево каталогов:

  1. Другой пользователь может в любой момент добавить имена файлов в доступный для записи каталог. Это может помешать созданию файлов и порядку имён, возвращаемых readdir.

  2. Простое открытие и закрытие файла может иметь побочные эффекты. Например, может сработать автомонтировщик или перемотаться ленточное устройство. Открытие файла в локальной файловой системе может блокироваться бесконечно из-за обязательной блокировки файлов, если не указан флаг O_NONBLOCK.

  3. Жёсткие и символические ссылки могут перенаправлять результат операций с файловой системой неожиданным образом. Варианты системных вызовов O_NOFOLLOW и AT_SYMLINK_NOFOLLOW влияют только на последний компонент пути.

  4. Структура дерева каталогов может меняться. Например, родительский каталог того, что раньше было подкаталогом внутри обрабатываемого дерева каталогов, может внезапно указывать за пределы этого дерева каталогов.

Файлы всегда следует создавать с флагами O_CREAT и O_EXCL, чтобы создание файла завершалось ошибкой, если он уже существует. Это защищает от неожиданного появления имён файлов, либо из-за создания нового файла, либо из-за создания жёсткой ссылки на существующий файл. В многопоточных программах вместо манипулирования umask создавайте файлы с режимом 000, если это возможно, и корректируйте его позже с помощью fchmod.

Чтобы избежать проблем, связанных с символическими ссылками и реструктуризацией дерева каталогов, необходимо использовать варианты системных вызовов с «at» (то есть функции, такие как openat, fchownat, fchmodat и unlinkat, вместе с O_NOFOLLOW или AT_SYMLINK_NOFOLLOW). Имена путей, передаваемые этим функциям, должны содержать только один компонент (то есть без косой черты). При спуске по дереву дескрипторы родительских каталогов должны оставаться открытыми. Отсутствующую функцию opendirat можно эмулировать с помощью openat (с флагом O_DIRECTORY, чтобы избежать открытия специальных файлов с побочными эффектами), после чего следует использовать fdopendir.

Если функции с «at» недоступны, их можно эмулировать, изменяя текущий каталог. (Очевидно, это работает только в том случае, если процесс не является многопоточным.) Для изменения текущего каталога необходимо использовать fchdir, а дескрипторы родительских каталогов должны оставаться открытыми, как и в подходе с использованием «at». chdir("…​") небезопасен, поскольку может выйти за пределы предполагаемого дерева каталогов.

Эта эмуляция функций с «at» в настоящее время требуется при работе с расширенными атрибутами. В этом случае можно использовать функцию lsetxattr с относительным путём, состоящим из одного компонента. Это также относится к контекстам SELinux и функции lsetfilecon.

В настоящее время невозможно избежать открытия специальных файлов и изменений файлов с жёсткими ссылками, если каталог, содержащий их, принадлежит недоверенному пользователю. (Узлы устройств могут иметь жёсткие ссылки, как и обычные файлы.) fchmodat и fchownat влияют на файлы, счётчик ссылок которых больше единицы. Но открытие файлов, проверка с помощью fstat того, что счётчик ссылок равен единице, и использование fchmod и fchown на дескрипторе файла могут иметь нежелательные побочные эффекты из-за пункта 2 выше. Поэтому при создании каталогов важно изменять владельца и права доступа только после того, как каталог полностью создан. До этого момента имена файлов стабильны, и невозможно добавить файлы с неожиданными жёсткими ссылками.

Аналогично, при простом чтении каталога, принадлежащего недоверенному пользователю, в настоящее время невозможно надёжно избежать открытия специальных файлов.

Не существует обходного пути для нестабильности списка файлов, возвращаемого readdir. Одновременная модификация каталога может привести к тому, что будет возвращён список файлов, которых на самом деле никогда не существовало на диске.

Жёсткие и символические ссылки можно безопасно удалять с помощью unlinkat без дополнительных проверок, поскольку удаление затрагивает только имя в обрабатываемом дереве каталогов.

Доступ к файловой системе от имени другого пользователя

Этот раздел посвящён доступу к файловой системе от имени конкретного пользователя. Это отличается от доступа к файлам и каталогам, принадлежащим другому, потенциально недоверенному пользователю; см. Доступ к файловой системе от имени другого пользователя.

Один из подходов — создать дочерний процесс, который работает под целевыми идентификаторами пользователя и группы (как эффективными, так и реальными). Обратите внимание, что этот дочерний процесс может блокироваться бесконечно, даже при обработке только обычных файлов. Например, специальная файловая система FUSE может вызвать зависание процесса в непрерываемом сне внутри системного вызова stat.

Существующий процесс может изменить свой идентификатор пользователя и группы, используя setfsuid и setfsgid. (Эти функции предпочтительнее seteuid и setegid, поскольку они не позволяют пользователю, от имени которого выполняется процесс, отправлять сигналы процессу.) Эти функции не являются потокобезопасными. В многопоточных процессах эти операции должны выполняться в однопоточном дочернем процессе. Также может возникнуть неожиданная блокировка.

Не рекомендуется пытаться перереализовывать проверки прав доступа ядра в пользовательском пространстве, поскольку требуемые проверки сложны. Также очень трудно избежать состояний гонки при разрешении имён путей.

Ограничения файловых систем

По историческим причинам существуют константы препроцессора, такие как PATH_MAX, NAME_MAX. Однако в большинстве систем длина канонических имён путей (абсолютных имён путей со всеми разрешёнными символическими ссылками, возвращаемых realpath или canonicalize_file_name) может превышать PATH_MAX байт, а отдельные компоненты имён файлов могут быть длиннее NAME_MAX. Это также справедливо для значений _PC_PATH_MAX и _PC_NAME_MAX, возвращаемых pathconf, и члена f_namemax структуры struct statvfs. Поэтому эти константы не следует использовать. Это также причина, по которой никогда не следует использовать readdir_r (вместо этого используйте readdir).

Не следует писать код, который предполагает наличие верхнего предела для количества подкаталогов в каталоге, количества обычных файлов в каталоге или счётчика ссылок инода.

Особенности файловых систем

Не все файловые системы поддерживают все возможности. Это сильно усложняет написание универсальных инструментов для копирования файлов. Например, операция копирования, предназначенная для сохранения прав доступа к файлам, обычно завершается ошибкой при копировании в файловую систему FAT.

  • Некоторые файловые системы не чувствительны к регистру символов. Однако большинство из них должны сохранять регистр.

  • Ограничения на длину имён сильно варьируются — от восьми до тысяч байт. Ограничения на длину путей также различаются. Большинство систем налагают верхнюю границу на имена путей, передаваемые ядру, но, используя относительные имена путей, можно создавать и открывать файлы, абсолютное имя пути которых по существу имеет неограниченную длину.

  • Некоторые файловые системы не хранят имена как относительно неограниченные последовательности байтов, как это традиционно принято в системах GNU. Это означает, что некоторые последовательности байтов (вне безопасного набора символов POSIX) не являются допустимыми именами. И наоборот, имена существующих файлов могут быть непредставимы в виде последовательностей байтов, и поэтому файлы недоступны в системах GNU. Некоторые файловые системы выполняют канонизацию Unicode для имён файлов. Эти файловые системы сохраняют регистр, но чтение имени только что созданного файла с помощью readdir всё равно может привести к другой последовательности байтов.

  • Права доступа и владельцы поддерживаются не везде (и биты SUID/SGID могут быть недоступны). Например, файловые системы FAT назначают владельца на основе опции монтирования и обычно помечают все файлы как исполняемые. Любая попытка изменить права доступа приведёт к ошибке.

  • Нерегулярные файлы (узлы устройств, FIFO) обычно недоступны.

  • Только в некоторых файловых системах файлы могут иметь разрежения, то есть не всё их содержимое хранится на диске.

  • Поддержка ioctl (даже таких довольно общих функций, как FIEMAP для обнаружения физического расположения файлов и разрежений) зависит от конкретной файловой системы.

  • Не все файловые системы поддерживают расширенные атрибуты, списки контроля доступа (ACL) и метаданные SELinux. Ограничения на размер и имена расширенных атрибутов различаются.

  • Жёсткие ссылки могут вообще не поддерживаться (FAT) или только в пределах одного каталога (AFS). Символические ссылки также могут быть недоступны. Reflinks (жёсткие ссылки с семантикой копирования при записи) всё ещё очень редки. Современные системы ограничивают создание жёстких ссылок пользователями, которые владеют целевым файлом или имеют доступ на чтение/запись к нему, но старые системы этого не делают.

  • Переименование (или перемещение) файлов с помощью rename может завершиться ошибкой (даже если stat показывает, что исходный и целевой каталоги находятся в одной файловой системе). Однако этот системный вызов должен работать, если старый и новый пути находятся в одном каталоге.

  • Семантика блокировки различается в разных файловых системах. Это влияет на рекомендательные и обязательные блокировки. Например, некоторые сетевые файловые системы не позволяют удалять файлы, открытые каким-либо процессом.

  • Разрешение временных меток варьируется от двух секунд до наносекунд. Не все временные метки доступны во всех файловых системах. Время создания файла (время рождения) не предоставляется через интерфейс stat/fstat, даже если оно хранится файловой системой.

Проверка свободного места

Функции statvfs и fstatvfs позволяют программам проверять количество доступных блоков и инодов через члены f_bfree, f_bavail, f_ffree и f_favail структуры struct statvfs. Некоторые файловые системы возвращают вымышленные значения в полях f_ffree и f_favail, поэтому единственный надёжный способ узнать, есть ли в файловой системе место для файла — попытаться создать его. Поле f_bfree должно быть достаточно точным.