openat(2), к сожалению, в основном бесполезен
Это перевод "рацпредложения" от Theo de Raadt (deraadt@) в списке рассылки OpenBSD:
Семейство системных вызовов openat(2) [2] на практике в основном бесполезно и поэтому редко используются. А когда они применяются, то это часто не эффективно или даже приводит к дополнительным накладным расходам в плане производительности.
int openat(int fd, const char *path, int flags, ...);
Это относится ко всему семейству системных вызовов:
sys_fstatat sys_utimensat sys_chflagsat sys_pathconfat sys_faccessat sys_fchmodat sys_fchownat sys_linkat sys_mkdirat sys_mkfifoat sys_mknodat sys_readlinkat sys_renameat sys_symlinkat sys_unlinkat
Идея, объединяющая эти системные вызовы, заключается в том, что вы можете предварительно открыть директорию как fd (file descriptor, файловый дескриптор), обычно используя флаг O_DIRECTORY, а затем использовать этот дескриптор для доступа к дочерним элементам на файловой системе. По задумке это должно сократить накладные расходы при поиске дочерних элементов ФС, включая соответствующие блокировки в ядре ОС. В спецификации POSIX говорится:
Функция openat() должна быть эквивалентна функции open(), за исключением случая, когда передан относительный путь.
На практике это выливается в две проблемы:
- А что будет, если передан не относительный путь? В примере ниже аргумент herefd будет проигнорирован и откроется файл /etc/passwd по абсолютному пути:
openat(herefd, "/etc/passwd", O_RDONLY)
- А что будет, если переданный относительный путь выходит за пределы указанной директории ("../../../../чтото")? Системный вызов "выйдет" из открытой директории и откроет указанный относительный путь.
Проще говоря, этот системный вызов был спроектирован без поддержки какой-либо модели безопасности.
И FreeBSD, и Linux разработали свои варианты, которые решают эти проблемы. Поскольку все функции *at(2) имеют параметр flags, их подход заключается в добавлении дополнительного флага, который не позволял бы переходить вверх относительно уже открытой директории. Я думаю, что это не то, что нужно. И у меня есть другое предложение.
Давайте создавать файловый дескриптор директории, который не позволяет выходить за её пределы на ФС. Пометим сам объект, вместо того чтобы требовать от программиста указывать флаг при каждом последующем системном вызове с этим объектом. Добавляются два новых флага: O_BELOW и F_BELOW.
Создание файлового дескриптора "заблокированной" директории выглядит следующим образом:
dirfd = open("path", O_DIRECTORY | O_BELOW);
или можно "заблокировать" уже существующий файловый дескриптор dirfd:
fcntl(dirfd, F_BELOW);
У такого dirfd есть две особенности. Обращения по абсолютному пути всегда завершаются неудачей с кодом ошибки ENOENT. Обращения по относительному пути, которые пытаются перейти выше уровня открытой директории, также возвращают ошибку ENOENT. Вы можете вызвать openat(dirfd, "."), но не можете вызвать openat(dirfd, "..").
Код, использующий readdir() и аналогичные системные вызовы, теперь должен быть рассчитан на то, что может возникнуть ошибка, если будет указан путь "..".
Есть интересный вариант использования, который немного похож на системный вызов chroot(2) [3], но при этом доступен для пользователей, не имеющих прав суперпользователя (root). Достаточно сделать так:
dirfd = open("path", O_DIRECTORY | O_BELOW);
fchdir(dirfd);
Теперь ваш процесс находится внутри "заблокированной" директории. В таком подходе нет классических рисков, которые препятствовали предоставлению chroot() обычным процессам (имеется в виду, что открытие абсолютных путей внутри chroot могло запутать библиотечные функции, поскольку теперь они обращаются к файлам, созданным пользователем, а последствия этого слишком серьезные). Доступ по абсолютным путям с помощью вызова open() начинается с текущей директории процесса и теперь просто не работает. Я еще не до конца исследовал все особенности такого подхода, предоставляющего возможности схожие с chroot, но для обычных пользователей. Возможно, потребуются некоторые семантические изменения. Но есть вероятность, что это станет тем, что мы будем использовать во многих демонах вместо chroot().
Пока этого всего лишь черновик. Основная идея возникла в результате разбора одной программы, которая неожиданным образом использует openat(), и размышлений о том, можно ли сделать ограничения использования путей в ядре ОС. Такой подход может хорошо работать вместе с unveil() [4], но он требует меньше накладных расходов, так как ядру не нужно хранить ссылки на vnod'ы, как это происходит для unveil().