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结构的未文档化字段_cbReserved2_和_lpReserved2_来传递文件描述符数组。

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

如果_stdin_、_stdout_和_stderr_参数为None,则subprocess进程的_close_fds_参数的默认值为TruebInheritHandles=FALSE),否则为FalsebInheritHandles=TRUE)。

另请参阅

Windows上只继承部分句柄

自Windows Vista以来,CreateProcess()支持STARTUPINFO结构的扩展:STARTUPINFOEX结构。使用此新结构,可以指定要继承的句柄列表:PROC_THREAD_ATTRIBUTE_HANDLE_LIST。有关更多信息,请阅读通过编程控制新进程在Win32中继承哪些句柄(Raymond Chen,2011年12月)。

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

微软建议使用锁来避免竞态条件:请阅读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模块,用于在fork时执行代码,这可以用于自动关闭文件描述符。

可继承文件描述符的问题

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

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

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

开源项目中的问题示例

  • Mozilla (Firefox):自2002-05年以来一直开放
  • dbus 库:于2008-05年修复(dbus 提交),在子进程中关闭文件描述符
  • autofs:2009-02年修复,设置CLOEXEC标志
  • qemu:2009-12年修复(qemu 提交),设置CLOEXEC标志
  • Tor:2010-12年修复,设置CLOEXEC标志
  • OCaml:自2011-04年以来一直开放,“PR#5256:使用Unix.open_process*打开的进程继承所有打开的文件描述符(包括套接字)”
  • ØMQ:自2012-08年以来一直开放
  • Squid:自2012-07年以来一直开放

另请参阅:对不起,儿子,你的代码正在泄漏!!!(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的一部分。
  • 用于socket()socketpair()SOCK_CLOEXEC标志,在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上可用。

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

新函数

  • dup3():在Linux 2.6.27(和glibc 2.9)上可用
  • pipe2():在Linux 2.6.27(和glibc 2.9)上可用
  • accept4():在Linux 2.6.28(和glibc 2.10)上可用

在Linux 2.6.28之前的版本上,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上的文件描述符 Windows上的句柄 Windows上的文件描述符
subprocess,默认 STD, pass_fds STD
subprocess,替换stdout STD, pass_fds 所有 STD
subprocess,close_fds=False 所有 所有 STD
多进程 不适用 所有 STD
os.execv(), os.spawn() 所有 所有 所有

图例

  • “所有”:所有_可继承_的文件描述符或句柄都将在子进程中被继承
  • “无”:所有句柄在子进程中都被关闭
  • “STD”:只有文件描述符0(stdin)、1(stdout)和2(stderr)在子进程中被继承
  • “pass_fds”:subprocess的_pass_fds_参数中的文件描述符被继承
  • “不适用”:在UNIX上,多进程使用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 Bug #837033:Squid应在打开的FD上设置CLOEXEC。“每个子进程中32k+的close()调用在Xen PV guest中需要很长时间([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_默认创建为可继承的,但如果_inheritable_为False,则创建为不可继承的。

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] 关于新函数“open_noinherit”的提案,以避免子进程问题和安全风险

PEP 433

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

Python 问题


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

最后修改日期: 2025-02-01 08:59:27 GMT