为什么在调用fork()之后和调用exec ...()之前关闭所有文件描述符?我该怎么办?

Mec*_*cki 4 c posix fork file-descriptor exec

我已经看到了很多C代码,它们试图在调用fork()和调用之间关闭所有文件描述符exec...()。为什么我通常会这样做,以及在我自己的代码中执行此操作的最佳方法是什么,因为我已经看到了许多不同的实现?

Mec*_*cki 9

调用时fork(),您的操作系统仅通过克隆现有进程即可创建一个新进程。新进程将与从其克隆的进程几乎完全相同,除了它的进程ID和记录为由fork()调用替换或重置的所有属性外。

当调用任何形式的时exec...(),调用过程的过程映像将被新的过程映像替换,但会保留过程状态。结果是,调用之前进程文件描述符表中的打开文件描述符在调用后exec...()仍存在于该表中,因此新的进程代码继承了对其的访问。我想这可能是为了使子进程自动继承STDIN,STDOUT和STDERR。

但是,请记住,在POSIX C中,文件描述符不仅用于访问实际文件,而且还用于所有类型的系统和网络套接字,管道,共享内存标识符等。如果您没有在调用之前关闭它们exec...(),则新的子进程将可以访问所有这些子进程,甚至可以访问那些无法获得访问权限的资源,因为它甚至没有所需的访问权限。考虑一个根进程创建一个非根子进程,但是该子进程将有权访问根父进程的所有打开文件描述符,包括只能由根或端口1024以下的受保护服务器套接字写入的打开文件。

因此,除非您明确希望子进程继承对当前打开的文件描述符的访问权限,例如捕获进程的STDOUT或通过STDIN向该进程提供数据,否则您需要在调用之前将其关闭exec...()。不仅是由于安全性(有时可能根本不起作用),还因为子进程将具有较少的可用自由文件描述符(并考虑一长串进程,每个进程打开文件,然后生成子进程)。 ..可用的免费文件描述符将越来越少)。

一种方法是始终使用flag打开文件,该标志O_CLOEXEC可确保在exec...()调用该文件描述符时自动将其关闭。该解决方案的一个问题是,您无法控制外部库如何打开文件,因此您不能依靠所有代码始终设置该标志。

另一个问题是,该解决方案仅适用于使用创建的文件描述符open()。创建插座,管道等,这是一个已知的问题,某些系统附近工作时,你无法通过该标记,通过提供非标准acccept4()pipe2()dup3(),和SOCK_CLOEXEC对于插座的标志,然而这些还不是POSIX标准,它的未知如果它们将成为标准(这是计划中的,但是直到我们无法确定发布新标准之前,还需要很多年才能被所有系统采用。)

您可以做的是稍后在文件描述符上FD_CLOEXEC使用来设置标志fcntl(),但是请注意,这在多线程环境中并不安全。只需考虑以下代码:

int so = socket(...);
fcntl(so, F_SETFD, FD_CLOEXEC);
Run Code Online (Sandbox Code Playgroud)

如果fork()有可能在第一行和第二行之间调用另一个线程,则该标记尚未设置,因此该文件描述符将不会关闭。

因此,真正安全的唯一方法是显式关闭它们,这并不像看上去那样简单!

我看过很多代码,这些代码可以做如下愚蠢的事情:

for (int i = STDERR_FILENO + 1; i < 256; i++) close(i);
Run Code Online (Sandbox Code Playgroud)

但是,仅仅因为某些POSIX系统的默认限制为256并不意味着就不能提高此限制。同样在某些系统上,默认限制始终始终较高。

使用FD_SETSIZE而不是256同样是错误的,因为select()在大多数系统上默认情况下,API都有一个硬限制,并不意味着一个进程的打开文件描述符不能超过此限制(毕竟,您不必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);
Run Code Online (Sandbox Code Playgroud)

sysconf(_SC_OPEN_MAX)如果RLIMIT_NOFILE已使用来提高打开文件的限制(),则记录为可以正确更新setrlimit()。资源限制(rlimits)是正在运行的进程的有效限制,并且对于文件,它们始终必须介于两者之间_POSIX_OPEN_MAX(记录为始终允许打开的文件描述符的最小数量,必须至少为20)和OPEN_MAX(必须为至少_POSIX_OPEN_MAX并设置上限)。

虽然在循环中关闭所有可能的描述符在技术上是正确的,并且可以按预期工作,但它可能会尝试关闭数千个文件描述符,但大多数情况下通常不存在。即使close()对不存在的文件描述符的调用很快(任何标准都不能保证),但在较弱的系统上(可能是嵌入式设备,请考虑小型单板计算机),可能会花费一些时间,这可能是问题。

因此,一些系统已经开发出更有效的方法来解决此问题。著名的例子是BSD和Solaris系统closefrom()以及fdwalk()它们所支持的例子。不幸的是公开组投票反对加入closefrom()到标准(报价):“ 这是不可能进行标准化收盘高于一定值任意文件描述符,同时仍然保证符合标准的环境的接口。 ”(来源)这当然是胡说八道,因为它们自己制定规则,并且如果它们定义某些文件描述符可以在环境或系统要求或代码本身要求的情况下始终静默关闭,则这不会破坏现有的实现功能,并仍然为我们其他人提供所需的功能。没有这些功能,人们将使用循环并完全按照The Open Group试图避免的方式进行,因此不添加循环只会使情况变得更糟。

在某些平台上,您基本上不走运,例如macOS,它完全符合POSIX。如果您不想在macOS上循环关闭所有文件描述符,则唯一的选择是不使用fork()/ exec...()而是posix_spawn()posix_spawn()是不支持流程派生的平台的较新API,它可以在支持派生并且可以使用平台提供的其他一些API来启动子流程的那些平台之上fork()/ 纯在用户空间中实现exec...()。在macOS上,存在一个非标准标志POSIX_SPAWN_CLOEXEC_DEFAULT,该标志会踩踏所有文件描述符,就像CLOEXEC已在其上设置了标志之外,除非您明确指定了文件操作。

在Linux上,你可以通过查看路径得到的文件描述符的列表/proc/{PID}/fd/{PID}为你的进程(进程ID getpid()),也就是说,如果proc文件系统已经安装在所有的,它已经被安装到/proc(但很多Linux工具依赖于此,否则这样做也会破坏很多其他东西)。基本上,您可以限制自己关闭此路径下列出的所有描述符。


Ste*_*mit 6

真实故事: 曾几何时,我写了一个简单的小 C 程序来打开一个文件,我注意到返回的文件描述符open是 4。“这很有趣,”我想。“标准的输入、输出和错误总是文件描述符 0、1 和 2,所以你打开的第一个文件描述符通常是 3。”

所以我写了另一个从文件描述符 3 开始读取的小 C 程序(没有打开它,也就是说,假设 3 是一个预先打开的 fd,就像 0、1 和 2 一样)。很明显,在我使用的 Unix 系统上,文件描述符 3 是在系统密码文件上预先打开的。这显然是登录程序中的一个错误,它在密码文件上仍然打开 fd 3 的情况下执行我的登录 shell,而杂散的 fd 反过来又被我从 shell 运行的程序继承。

自然而然,我接下来尝试的是一个简单的小 C 程序来写入预先打开的文件描述符 3,看看我是否可以修改密码文件并授予自己 root 访问权限。然而,这没有用。以只读模式在密码文件上打开了stray fd 3。

但无论如何,这有助于解释为什么在执行子进程时不应该让文件描述符保持打开状态。

[脚注:我说的是“真实故事”,而且大部分是这样,但为了叙述的缘故,我确实更改了一个细节。事实上,/bin/login 的错误版本让 fd 3 在组文件上打开/etc/group,而不是密码文件。]

  • 嗯,它是只读的,但是可写的组文件处理程序已经足够危险了。它将允许您将用户添加到组中,突然间我成为“sudo”组的成员,并且可以获得 root shell。 (2认同)