Почему я должен закрывать все файловые дескрипторы после вызова fork () и до вызова exec ... ()? И как бы я это сделал? - PullRequest
4 голосов
/ 18 июня 2019

Я видел много кода на C, который пытается закрыть все файловые дескрипторы между вызовом fork() и вызовом exec...(). Почему это обычно делается и как лучше всего это сделать в моем собственном коде, поскольку я уже видел так много разных реализаций?

Ответы [ 2 ]

8 голосов
/ 18 июня 2019

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

При вызове любой формы exec...() образ процесса вызывающего процесса заменяется новым образом процесса, но кроме этого состояние процесса сохраняется.Одним из следствий этого является то, что дескрипторы открытого файла в таблице дескрипторов файла процесса до вызова exec...() все еще присутствуют в этой таблице после вызова, поэтому новый код процесса наследует доступ к ним.Я предполагаю, что это, вероятно, было сделано для того, чтобы STDIN, STDOUT и STDERR автоматически наследовались дочерними процессами.

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

Таким образом, если вы не хотите, чтобы дочерний процесс наследовал доступ к открытым в данный момент дескрипторам файлов, что может быть явно необходимо, например, для захвата STDOUT процесса или передачи данных через STDIN этому процессу, вы должны закрыть их перед вызовомexec...().Не только из-за безопасности (которая иногда может вообще не играть никакой роли), но также потому, что в противном случае у дочернего процесса будет меньше доступных файловых дескрипторов (и представьте себе длинную цепочку процессов, каждый из которых открывает файлы, а затем порождает подпроцесс... будет доступно все меньше и больше свободных файловых дескрипторов).

Один из способов сделать это - всегда открывать файлы, используя флаг O_CLOEXEC, который гарантирует, что этот файловый дескриптор будет автоматически закрыт при exec...()когда-либо называется.Одна из проблем этого решения заключается в том, что вы не можете контролировать, как внешние библиотеки могут открывать файлы, поэтому вы не можете полагать, что весь код всегда будет устанавливать этот флаг.

Другая проблема заключается в том, что это решение работает только для файловых дескрипторов, созданных с помощью open().Этот флаг нельзя передать при создании сокетов, каналов и т. Д. Это известная проблема, и некоторые системы обходят ее, предлагая нестандартные acccept4(), pipe2(), dup3() и флаг SOCK_CLOEXEC длясокеты, однако это еще не стандарт POSIX, и неизвестно, станут ли они стандартом (это запланировано, но пока не будет выпущен новый стандарт, мы не можем знать наверняка, также потребуются годы, пока все системы не примут их).

В дальнейшем вы можете установить флаг FD_CLOEXEC, используя fcntl() в дескрипторе файла, однако обратите внимание, что это небезопасно в многопоточной среде.Просто рассмотрите следующий код:

int so = socket(...);
fcntl(so, F_SETFD, FD_CLOEXEC);

Если другой поток вызывает fork() между первой и второй строкой, что, конечно, возможно, флаг еще не установлен и, таким образом, этот дескриптор файлане закроется.

Таким образом, единственный надежный способ - это явно закрыть их, а это не так просто, как может показаться!

Я видел много кодаэто делает такие глупые вещи, как это:

for (int i = STDERR_FILENO + 1; i < 256; i++) close(i);

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

Использование FD_SETSIZE вместо 256 в равной степени неверно, поскольку только то, что API select() имеет жесткое ограничение по умолчанию в большинстве систем, не означает, что процесс не может иметь больше дескрипторов открытых файлов, чем этот предел (после все, что вам не нужно использовать select() с ними, вы можете использовать poll() API в качестве замены, а poll() не имеет верхнего предела номеров файловых дескрипторов).

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

Чтобы избежать необходимости закрывать слишком много несуществующих файловых дескрипторов, вы можете использовать вместо этого код:

int fdlimit = (int)sysconf(_SC_OPEN_MAX);
for (int i = STDERR_FILENO + 1; i < fdlimit; i++) close(i);

sysconf(_SC_OPEN_MAX) задокументировано для корректного обновления, если лимит открытого файла (RLIMIT_NOFILE) был увеличен с помощью setrlimit(). Ограничения ресурсов (rlimits) являются эффективными пределами для запущенного процесса, и для файлов они всегда должны быть между _POSIX_OPEN_MAX (задокументировано как минимальное количество файловых дескрипторов, которое процессу всегда разрешено открывать, должно быть не менее 20) и OPEN_MAX (должно быть не менее _POSIX_OPEN_MAX и устанавливает верхний предел).

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

Таким образом, несколько систем разработали более эффективные способы решения этой проблемы. Известными примерами являются closefrom() и fdwalk(), которые поддерживают системы BSD и Solaris. К сожалению, Open Group проголосовала против добавления closefrom() к стандарту (цитата): « невозможно стандартизировать интерфейс, который закрывает произвольные файловые дескрипторы выше определенного значения, в то же время гарантируя соответствующую среду. » ( Источник ) Это, конечно, бессмыслица, поскольку они сами создают правила и если они определяют, что определенные файловые дескрипторы всегда можно молча пропускать при закрытии, если этого требует среда или система, или сам код запрашивает это, тогда это не сломал бы существующую реализацию этой функции и все еще предложил бы желаемую функциональность для всех нас. Без этих функций люди будут использовать цикл и делать именно то, что здесь пытается избежать Open Group, поэтому, если не добавить его, ситуация только ухудшится.

На некоторых платформах вам в основном не повезло, например, macOS, который полностью соответствует POSIX. Если вы не хотите закрывать все файловые дескрипторы в цикле в macOS, вы можете не использовать fork() / exec...(), а вместо этого posix_spawn(). posix_spawn() - это более новый API для платформ, которые не поддерживают разветвление процессов, его можно реализовать исключительно в пользовательском пространстве поверх fork() / exec...() для тех платформ, которые поддерживают разветвление и в противном случае могут использовать какой-то другой API Платформа предлагает для запуска дочерних процессов. В macOS существует нестандартный флаг POSIX_SPAWN_CLOEXEC_DEFAULT, который будет использовать все файловые дескрипторы, как если бы для них был установлен флаг CLOEXEC, за исключением тех, для которых вы явно указали действия с файлами.

В Linux вы можете получить список файловых дескрипторов, посмотрев путь /proc/{PID}/fd/, где {PID} - это идентификатор процесса (getpid()), то есть если файловая система proc была смонтирована в все, и он был смонтирован на /proc (но многие инструменты Linux полагаются на это, в противном случае это сломало бы и многие другие вещи). По сути, вы можете ограничить себя, чтобы закрыть все дескрипторы, перечисленные по этому пути.

4 голосов
/ 18 июня 2019

Правдивая история: Однажды я написал простую небольшую программу на C, которая открыла файл, и заметил, что дескриптор файла, возвращаемый open, равен 4. «Это забавно», - подумал я. «Стандартный ввод, вывод и ошибка всегда являются файловыми дескрипторами 0, 1 и 2, поэтому первый открываемый файловый дескриптор обычно равен 3.»

Итак, я написал еще одну маленькую C-программу, которая начала чтение из файлового дескриптора 3 (то есть, не открывая его, а скорее предполагая, что 3 был предварительно открытым fd, как 0, 1 и 2). Быстро стало очевидно, что в системе Unix, которую я использовал, дескриптор файла 3 был предварительно открыт в файле системных паролей . Очевидно, это была ошибка в программе входа в систему, которая выполняла мою оболочку входа в систему с fd 3, все еще открытой в файле паролей, а заблудший fd в свою очередь наследовался программами, которые я запускал из своей оболочки.

Естественно, следующее, что я попробовал, была простая маленькая C-программа, которая записывала в предварительно открытый дескриптор файла 3, чтобы посмотреть, смогу ли я изменить файл пароля и дать себе root-доступ. Это, однако, не сработало; блуждающий fd 3 был открыт в файле паролей в режиме только для чтения.

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

[Сноска. Я сказал «правдивая история», и это в основном так, но ради повествования я изменил одну деталь. Фактически, в версии / bin / login с ошибками оставалось открытое fd 3 для файла групп, /etc/group, а не для файла пароля.]

...