Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 446 – 使新创建的文件描述符不可继承

作者:
Victor Stinner <vstinner at python.org>
状态:
最终版
类型:
标准跟踪
创建:
2013年8月5日
Python 版本:
3.4
替换:
433

目录

摘要

子进程中泄漏的文件描述符会导致各种恼人的问题,并且是已知的重大安全漏洞。在某些情况下,无法使用 subprocess 模块并将 close_fds 参数设置为 True

本 PEP 提出默认情况下使 Python 创建的所有文件描述符不可继承,以降低这些问题带来的风险。本 PEP 还修复了在支持原子标志以创建不可继承文件描述符的操作系统上的多线程应用程序中的竞争条件。

我们知道这可能会导致代码中断,但为了人类的福祉,我们还是这么做了。(详细信息请参阅下面的“向后兼容性”部分。)

基本原理

文件描述符的继承

每个操作系统处理文件描述符的继承方式都不同。Windows 默认创建不可继承的句柄,而 UNIX 和 Windows 上的 POSIX API 默认创建可继承的文件描述符。Python 更偏向于 POSIX API 而不是原生 Windows API,以获得单个代码库并使用相同类型的文件描述符,因此它创建可继承的文件描述符。

有一个例外:os.pipe() 在 Windows 上创建不可继承的管道,而在 UNIX 上创建可继承的管道。原因是实现上的产物:os.pipe() 在 Windows 上调用 CreatePipe()(原生 API),而在 UNIX 上调用 pipe()(POSIX API)。对 CreatePipe() 的调用是在 1994 年添加到 Python 中的,早于 Windows 98 中 POSIX API 中引入 pipe()问题 #4708 提出更改 Windows 上的 os.pipe() 以创建可继承的管道。

Windows 上的文件描述符继承

在 Windows 上,文件对象的原生类型是句柄(C 类型 HANDLE)。这些句柄有一个 HANDLE_FLAG_INHERIT 标志,该标志定义句柄是否可以在子进程中继承。对于 POSIX API,C 运行时 (CRT) 也提供文件描述符(C 类型 int)。可以使用函数 _get_osfhandle(fd) 检索文件描述符的句柄。可以使用函数 _open_osfhandle(handle) 从句柄创建文件描述符。

使用 CreateProcess(),只有当其可继承标志 (HANDLE_FLAG_INHERIT) 设置并且 CreateProcess()bInheritHandles 参数为 TRUE 时,才会继承句柄;子进程中会关闭所有文件描述符(标准流 (0、1、2) 除外),即使 bInheritHandlesTRUE。使用 spawnv() 函数,所有可继承的句柄和所有可继承的文件描述符都会在子进程中继承。此函数使用 STARTUPINFO 结构的未公开字段 cbReserved2lpReserved2 传递文件描述符数组。

要使用 CreateProcess() 替换标准流(stdin、stdout、stderr),必须在 STARTUPINFO 结构的 dwFlags 字段中设置 STARTF_USESTDHANDLES 标志,并且 CreateProcess()bInheritHandles 参数必须设置为 TRUE。因此,当至少替换了一个标准流时,所有可继承的句柄都会被子进程继承。

如果 stdinstdoutstderr 参数为 None,则 subprocess 进程的 close_fds 参数的默认值为 True (bInheritHandles=FALSE),否则为 False (bInheritHandles=TRUE)。

另请参阅

仅在 Windows 上继承某些句柄

从 Windows Vista 开始,CreateProcess() 支持 STARTUPINFOEX 结构 的扩展:STARTUPINFO 结构。使用此新结构,可以指定要继承的句柄列表:PROC_THREAD_ATTRIBUTE_HANDLE_LIST。阅读 以编程方式控制 Win32 中新进程继承的句柄(Raymond Chen,2011 年 12 月)以获取更多信息。

在 Windows Vista 之前,可以使句柄可继承并使用 bInheritHandles=TRUE 调用 CreateProcess()。如果所有其他句柄都不可继承,则此选项有效。存在竞争条件:如果另一个线程使用 bInheritHandles=TRUE 调用 CreateProcess(),则句柄也会在第二个进程中继承。

Microsoft 建议使用锁来避免竞争条件:阅读 Q315939:PRB:在 CreateProcess 调用期间子进程继承了意外的句柄(上次审查:2006 年 11 月)。 Python 问题 #16500“添加一个 atfork 模块” 提出添加此类锁,它可用于在没有竞争条件的情况下使句柄不可继承。此类锁仅防止 Python 线程之间的竞争条件;C 线程不受保护。

另一种选择是复制必须继承的句柄,将复制的句柄的值传递给子进程,以便子进程可以使用 DuplicateHandle()DUPLICATE_CLOSE_SOURCE 窃取复制的句柄。由于句柄被复制(两次),因此父进程和子进程之间的句柄值会发生变化;父进程和/或子进程必须适应此更改。如果无法修改子程序,则可以使用中间程序在生成最终子程序之前从父进程窃取句柄。中间程序必须将句柄从子进程传递到父进程。如果所有句柄均未被窃取,例如中间进程失败,则父进程可能必须关闭复制的句柄。如果使用命令行传递句柄值,则在复制句柄时必须修改命令行,因为其值已修改。

本 PEP 不包含此问题的解决方案,因为没有适用于所有 Windows 版本的完美解决方案。此点将推迟到依赖于 Windows 上的句柄或文件描述符继承的用例为人所知时,以便我们可以选择最佳解决方案并仔细测试其实现。

UNIX 上的文件描述符继承

POSIX 在文件描述符上提供了一个 close-on-exec 标志,以便在调用 C 函数 execv() 时自动关闭文件描述符。清除 close-on-exec 标志的文件描述符会在子进程中继承,设置了该标志的文件描述符会在子进程中关闭。

可以使用 fcntl() 在两个系统调用中设置该标志(一个用于获取当前标志,另一个用于设置新标志)

int flags, res;
flags = fcntl(fd, F_GETFD);
if (flags == -1) { /* handle the error */ }
flags |= FD_CLOEXEC;
/* or "flags &= ~FD_CLOEXEC;" to clear the flag */
res = fcntl(fd, F_SETFD, flags);
if (res == -1) { /* handle the error */ }

FreeBSD、Linux、Mac OS X、NetBSD、OpenBSD 和 QNX 也支持使用 ioctl() 在单个系统调用中设置该标志

int res;
res = ioctl(fd, FIOCLEX, 0);
if (!res) { /* handle the error */ }

注意:close-on-exec 标志对 fork() 无效:所有文件描述符都会被子进程继承。 Python 问题 #16500“添加一个 atfork 模块” 提出添加一个新的 atfork 模块以在分叉时执行代码,这可用于自动关闭文件描述符。

可继承文件描述符的问题

大多数情况下,泄漏到子进程的可继承文件描述符不会被注意到,因为它们不会导致重大错误。但这并不意味着这些错误一定不能修复。

可继承文件描述符的两个常见问题

  • 在 Windows 上,在关闭目录中打开的所有文件句柄之前,无法删除该目录。文件也存在相同的问题,除非该文件是使用 FILE_SHARE_DELETE 标志 (O_TEMPORARY 模式用于 open()) 创建的。
  • 如果侦听套接字泄漏到子进程,则在父进程和子进程终止之前无法重用套接字地址。例如,如果 Web 服务器生成一个新程序来处理进程,并且服务器在程序未完成时重新启动,则服务器无法启动,因为 TCP 端口仍在使用中。

开源项目中问题的示例

  • Mozilla (Firefox):从 2002 年 5 月开始
  • dbus 库:在 2008 年 5 月修复 (dbus 提交),在子进程中关闭文件描述符
  • autofs:在 2009 年 2 月修复,设置 CLOEXEC 标志
  • qemu:在 2009 年 12 月修复 (qemu 提交),设置 CLOEXEC 标志

  • Tor:已于 2010 年 12 月修复,设置了 CLOEXEC 标志。
  • OCaml:自 2011 年 4 月起开放,“PR#5256:使用 Unix.open_process* 打开的进程继承所有打开的文件描述符(包括套接字)”。
  • ØMQ:自 2012 年 8 月起开放。
  • Squid:自 2012 年 7 月起开放。

另见:对不起,儿子,但你的代码泄漏了!!!(Dan Walsh,2012 年 3 月)了解有关 SELinux 文件描述符泄漏问题的详细信息。

安全漏洞

泄漏敏感的文件句柄和文件描述符可能导致安全漏洞。不受信任的子进程可能通过泄漏的文件描述符读取敏感数据(如密码)或控制父进程。如果监听套接字泄漏,子进程可以接受新的连接以读取敏感数据。

漏洞示例

另请参阅 CERT 安全编码标准:FIO42-C. 确保在不再需要时正确关闭文件

subprocess 模块中修复的问题

继承的文件描述符导致 subprocess 模块出现 4 个问题

这些问题已在 Python 3.2 中通过 subprocess 模块中的 4 个不同的更改得到修复。

  • 管道现在不可继承;
  • close_fds 参数的默认值现在为 True,但在 Windows 上有一个例外:如果至少替换了一个标准流,则默认值为 False
  • 添加了一个新的 pass_fds 参数;
  • 创建了一个用 C 实现的 _posixsubprocess 模块。

不可继承文件描述符的原子创建

在多线程应用程序中,可能在生成新程序之前(在将文件描述符设为不可继承之前)创建可继承的文件描述符。在这种情况下,文件描述符会泄漏到子进程。如果直接创建不可继承的文件描述符,则可以避免这种竞争条件。

FreeBSD、Linux、Mac OS X、Windows 和许多其他操作系统支持在创建文件描述符时原子地清除可继承标志,从而创建不可继承的文件描述符。

在 Windows 7 SP1 和 Windows Server 2008 R2 SP1 中添加了一个新的 WSA_FLAG_NO_HANDLE_INHERIT 标志用于 WSASocket(),以创建不可继承的套接字。如果在较旧的 Windows 版本(例如 Windows XP SP3)上使用此标志,则 WSASocket() 将失败,并出现 WSAEPROTOTYPE 错误。

在 UNIX 上,为文件和套接字添加了新的标志。

  • O_CLOEXEC:在 Linux(2.6.23)、FreeBSD(8.3)、Mac OS 10.8、OpenBSD 5.0、Solaris 11、QNX、BeOS、下一个 NetBSD 版本(6.1?)中可用。此标志是 POSIX.1-2008 的一部分。
  • SOCK_CLOEXEC 标志用于 socket()socketpair(),在 Linux 2.6.27、OpenBSD 5.2、NetBSD 6.0 中可用。
  • fcntl()F_DUPFD_CLOEXEC 标志,在 Linux 2.6.24、OpenBSD 5.0、FreeBSD 9.1、NetBSD 6.0、Solaris 11 中可用。此标志是 POSIX.1-2008 的一部分。
  • fcntl()F_DUP2FD_CLOEXEC 标志,在 FreeBSD 9.1 和 Solaris 11 中可用。
  • recvmsg()MSG_CMSG_CLOEXEC,在 Linux 2.6.23、NetBSD 6.0 中可用。

在低于 2.6.23 的 Linux 版本上,O_CLOEXEC 标志将被简单地忽略。因此,必须调用 fcntl() 来检查文件描述符是否不可继承:如果缺少 FD_CLOEXEC 标志,则不支持 O_CLOEXEC。在低于 2.6.27 的 Linux 版本上,如果在套接字类型中设置了 SOCK_CLOEXEC 标志,则 socket()socketpair() 将失败,并将 errno 设置为 EINVAL

新函数

  • dup3():在 Linux 2.6.27(以及 glibc 2.9)中可用。
  • pipe2():在 Linux 2.6.27(以及 glibc 2.9)中可用。
  • accept4():在 Linux 2.6.28(以及 glibc 2.10)中可用。

在低于 2.6.28 的 Linux 版本上,accept4() 将失败,并将 errno 设置为 ENOSYS

总结

操作系统 原子文件 原子套接字
FreeBSD 8.3 (2012) X
Linux 2.6.23 (2007) 2.6.27 (2008)
Mac OS X 10.8 (2012) X
NetBSD 6.1 (?) 6.0 (2012)
OpenBSD 5.0 (2011) 5.2 (2012)
Solaris 11 (2011) X
Windows XP(2001) Seven SP1(2011)、2008 R2 SP1(2011)

图例

  • “原子文件”:支持使用 open() 原子地创建不可继承的文件描述符的操作系统的第一个版本。
  • “原子套接字”:支持原子地创建不可继承的套接字的操作系统的第一个版本。
  • “X”:尚不支持。

另请参阅

Python 3.3 的状态

Python 3.3 在所有平台上创建可继承的文件描述符,除了 os.pipe(),它在 Windows 上创建不可继承的文件描述符。

在 Python 3.3 中添加了与不可继承文件描述符的原子创建相关的新的常量和函数:os.O_CLOEXECos.pipe2()socket.SOCK_CLOEXEC

在 UNIX 上,subprocess 模块默认情况下关闭子进程中的所有文件描述符,除了标准流(0、1、2)和 pass_fds 参数的文件描述符。如果 close_fds 参数设置为 False,则所有可继承的文件描述符都将继承到子进程中。

在 Windows 上,subprocess 默认情况下关闭子进程中的所有句柄和文件描述符。如果至少替换了一个标准流(stdin、stdout 或 stderr)(例如:重定向到管道),则所有可继承的句柄和文件描述符 0、1 和 2 将继承到子进程中。

使用 os.execv*()os.spawn*() 系列的函数,所有可继承的句柄和所有可继承的文件描述符都将由子进程继承。

在 UNIX 上,multiprocessing 模块使用 os.fork(),因此所有文件描述符都将由子进程继承。

在 Windows 上,使用 multiprocessing 模块,所有可继承的句柄和文件描述符 0、1 和 2 将由子进程继承,所有文件描述符(除了标准流)都将关闭。

总结

模块 UNIX 上的 FD Windows 上的句柄 Windows 上的 FD
subprocess,默认 STD,pass_fds STD
subprocess,替换 stdout STD,pass_fds 全部 STD
subprocess,close_fds=False 全部 全部 STD
multiprocessing 不适用 全部 STD
os.execv(),os.spawn() 全部 全部 全部

图例

  • “全部”:所有可继承的文件描述符或句柄都将继承到子进程中。
  • “无”:子进程中所有句柄都将关闭。
  • “STD”:只有文件描述符 0(stdin)、1(stdout)和 2(stderr)将继承到子进程中。
  • “pass_fds”:subprocess 的 pass_fds 参数的文件描述符将被继承。
  • “不适用”:在 UNIX 上,multiprocessing 使用 fork(),因此此情况不受此 PEP 的影响。

关闭所有打开的文件描述符

在 UNIX 上,subprocess 模块关闭子进程中的几乎所有文件描述符。此操作需要 MAXFD 个系统调用,其中 MAXFD 是文件描述符的最大数量,即使只有少数打开的文件描述符也是如此。可以使用以下命令读取此最大值:os.sysconf("SC_OPEN_MAX")

如果 MAXFD 很大,则此操作可能很慢。例如,在具有 MAXFD=655,000 的 FreeBSD buildbot 上,此操作花费了 300 毫秒:请参阅问题 #11284:关闭文件描述符速度慢

在 Linux 上,Python 3.3 从 /proc/<PID>/fd/ 获取所有打开的文件描述符的列表,因此性能取决于打开的文件描述符的数量,而不是 MAXFD。

另请参阅

  • Python 问题 #1663329:如果 SC_OPEN_MAX 较高,则 subprocess close_fds 性能较差。
  • Squid 错误 #837033:Squid 应在打开的 FD 上设置 CLOEXEC。“每个子进程中的 32k+ close() 调用在 Xen PV 访客中花费很长时间([12-56] 秒)。”

提案

不可继承的文件描述符

以下函数已修改,以使新创建的文件描述符默认情况下不可继承。

  • asyncore.dispatcher.create_socket()
  • io.FileIO
  • io.open()
  • open()
  • os.dup()
  • os.fdopen()
  • os.open()
  • os.openpty()
  • os.pipe()
  • select.devpoll()
  • select.epoll()
  • select.kqueue()
  • socket.socket()
  • socket.socket.accept()
  • socket.socket.dup()
  • socket.socket.fromfd()
  • socket.socketpair()

os.dup2() 默认情况下仍然创建可继承的文件描述符,请参见下文。

在可用时,使用原子标志将文件描述符设置为不可继承。由于在原子标志不可用时需要回退,因此无法保证原子性。

新的函数和方法

所有平台上都可用的新函数

  • os.get_inheritable(fd: int):如果文件描述符可以被子进程继承,则返回 True,否则返回 False
  • os.set_inheritable(fd: int, inheritable: bool):设置指定文件描述符的可继承标志。

仅在 Windows 上可用的新函数

  • os.get_handle_inheritable(handle: int):如果句柄可以被子进程继承,则返回 True,否则返回 False
  • os.set_handle_inheritable(handle: int, inheritable: bool):设置指定句柄的可继承标志。

新方法

  • socket.socket.get_inheritable():如果套接字可以被子进程继承,则返回 True,否则返回 False
  • socket.socket.set_inheritable(inheritable: bool):设置指定套接字的可继承标志。

其他更改

在 UNIX 上,subprocess 使 pass_fds 参数的文件描述符可继承。文件描述符在 fork() 之后和 execv() 之前在子进程中被设置为可继承,因此文件描述符的可继承标志在父进程中保持不变。

os.dup2() 有一个新的可选 inheritable 参数:os.dup2(fd, fd2, inheritable=True)fd2 默认情况下被创建为可继承的,但如果 inheritableFalse,则不可继承。

os.dup2() 的行为与 os.dup() 不同,因为 os.dup2() 最常见的用例是替换标准流的文件描述符:stdin (0)、stdout (1) 和 stderr (2)。预期标准流会被子进程继承。

向后兼容性

此 PEP 会破坏依赖于文件描述符继承的应用程序。鼓励开发人员重用高级 Python 模块 subprocess,该模块以可移植的方式处理文件描述符的继承。

使用 subprocess 模块和 pass_fds 参数或仅使用 os.dup2() 重定向标准流的应用程序应该不会受到影响。

Python 不再符合 POSIX,因为文件描述符现在默认设置为不可继承。Python 的设计目标并非符合 POSIX,而是设计用于开发可移植的应用程序。

被拒绝的替代方案

添加一个新的 open_noinherit() 函数

2007 年 6 月,Henning von Bargen 在 python-dev 邮件列表中提议添加一个新的 open_noinherit() 函数来修复子进程中继承的文件描述符问题。当时,subprocess 模块的 close_fds 参数的默认值为 False

阅读邮件线程:[Python-Dev] Proposal for a new function “open_noinherit” to avoid problems with subprocesses and security risks

PEP 433

PEP 433,“更容易抑制文件描述符继承”,是之前尝试提出的各种其他替代方案,但没有达成共识。

Python 问题


来源:https://github.com/python/peps/blob/main/peps/pep-0446.rst

上次修改时间:2023-09-09 17:39:29 GMT