Разбор имён файлов в VFS

Файловые системы (ФС) в Unix доступны через промежуточный слой абстракции - виртуальную файловую систему (VFS). VFS организована в виде дерева, в котором узлы ветвления это всегда каталоги, а листья - любые допустимые в VFS объекты (файлы, каталоги, символические ссылки, сокеты, и т.д.). Чтобы подключить в VFS новый носитель данных, выполняется операция монтирования, которая сопоставляет одному из каталогов, уже присутствующему в дереве, ссылку на корневой каталог в ФС на носителе. Доступ к ранее существовавшему содержимому каталога до отмены операции монтирования прекращается. Говорят, что в результате монтирования носитель становится смонтированным в каталог. Корневой каталог всей VFS также должен быть смонтирован на корневой каталог какой-либо конкретной ФС.

Имена объектов в VFS - это байтовые строки с завершающим нулевым байтом. Интерпретацией кодировки символов VFS не занимается. При записи путей символ "слэш" "/" используется как разделитель каталогов и потому не может быть использован в именах объектов VFS. Кроме слэша внутри имён объектов не может присутствовать нулевой байт '\0'. Длина имени не должна превышать NAME_MAX (определена в файле linux/limits.h как 255 байт). В остальном в VFS ограничений на имена нет, но такие ограничения могут быть обусловлены реализацией конкретной файловой системы.

В каждом каталоге существует два зарезервированных имени "." и "..", обозначающие текущий и вышележащий каталоги соответственно. Если ФС на носителе не поддерживает такие имена, то они должны быть сэмулированы драйвером. При выполнении монтирования значение "." берётся из корневого каталога на носителе, а ".." из того каталога, в который было произведено монтирование. В корневом каталоге (см. ниже) ".." ссылается на сам каталог как и ".".

Разбор пути

С каждым процессом в Unix связано два каталога:

  • текущий рабочий (current work directory, cwd)
  • корневой (root directory, rtd)

Эти каталоги используются при разборе путей в VFS теми системными вызовами, которые получают пути в качестве аргументов (open("path", ...), unlink("path") и т.п.). Если путь начинается с символа "слэш" "/" то он называется абсолютным и отсчитывается от корневого каталога процесса, а если начинается с любого другого символа, то называется относительным и отсчитывается от текущего рабочего каталога.

Разбор пути идет по следующим правилам (man path_resolution):

  1. Если длина пути превышает PATH_MAX (4096 байт вместе с завершающим нулевым байтом - определено в linux/limits.h)то возвращается ошибка ENAMETOOLONG. Такая же ошибка может появиться в дальнейшем при рекурсивном разборе символических ссылок.
  2. В зависимости от первого символа разбор пути начинается с корневого или с текущего рабочего каталога;
  3. В текущем разбираемом каталоге ищется имя следующего компонента пути. Если на чтение каталога не хватает прав, то возвращается ошибка EACCES; если имя не найдено, то возвращается ошибка ENOENT
  4. Если найденное имя является символической ссылкой, то её содержимое разбирается в рекурсивном вызове. Внутри рекурсии увеличивается счётчик вложенности. Если счётчик вложенности при разборе символических ссылок превышает максимальное допустимое число, то разбор останавливается и возвращается ошибка ELOOP. Если разбор символической ссылки вернул каталог, то он делается текущим для разбора. Разбор продолжается с п.2.
  5. Если найденное имя является каталогом, то проверяется, не используется ли этот каталог как точка монтирования. Если нет, то он устанавливается текущим для разбора. Если каталог используется для монтирования, то текущим для разбора устанавливается корневой каталог смонтированной ФС. Разбор продолжается с п.2. Если найденное имя это каталог ".." то, по правилам формирования этого имени, в корневом каталоге он сошлётся снова на корневой каталог, а в каталоге, используемым для монтирования, прозрачно позволит перейти из одной ФС в другую
  6. Если найденное имя последнее в пути, то соответствующий объект обрабатывается системным вызовом по своим правилам. Если найденное имя не последнее в пути и, при этом, указывает не на каталог и не на символическую ссылку, то выдаётся ошибка ENOTDIR

Текущий и корневой каталоги процесса

Смена текущего каталога выполняется вызовом chdir(const char *dir) или fchdir(int fd). Второй способ требует предварительно получить файловый дескриптор, ссылающийся на каталог.

Смена корневого каталога процесса выполняется вызовом chroot(const char *dir). В новом корневом каталоге имя ".." указывает на него самого, что не позволяет с помощью абсолютных путей подняться по дереву выше корневого каталога процесса. После выполнения этого вызова вся файловая система "выше" указанного корневого каталога становится невидимой для последующих системных вызовов, но открытые файлы за пределами видимости остаются доступными. Более того, текущий каталог может оказаться за пределами корневого, так что при практическом применении, например для ограничения доступа процесса к ФС, надо сочетать chroot() и chdir().

Chroot в shell

Чтобы запустить программу с переопределённым корневым каталогом используется программа

chroot NEWROOT [COMMAND [ARG]...]

Если команда COMMAND не указана, то выполняется shell, указанный в файле passwd для текущего пользователя.

Внутри chroot выполняются следующая последовательность вызовов:

chroot("NEWROOT");
chdir("/");
execve("COMMAND", ...);

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

Если мы хотим запереть пользователя внутри chroot нам необходимо скопировать вовнутрь /bin/bash, /lib/libc.so, /etc/passwd, /dev/tty и ещё ряд важных файлов, полный состав которых зависит от версии ОС и набора решаемых в "тюрьме" задач.