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) 除外),即使 bInheritHandles
为 TRUE
。使用 spawnv()
函数,所有可继承的句柄和所有可继承的文件描述符都会在子进程中继承。此函数使用 STARTUPINFO 结构的未公开字段 cbReserved2 和 lpReserved2 传递文件描述符数组。
要使用 CreateProcess()
替换标准流(stdin、stdout、stderr),必须在 STARTUPINFO
结构的 dwFlags 字段中设置 STARTF_USESTDHANDLES
标志,并且 CreateProcess()
的 bInheritHandles 参数必须设置为 TRUE
。因此,当至少替换了一个标准流时,所有可继承的句柄都会被子进程继承。
如果 stdin、stdout 和 stderr 参数为 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 文件描述符泄漏问题的详细信息。
安全漏洞
泄漏敏感的文件句柄和文件描述符可能导致安全漏洞。不受信任的子进程可能通过泄漏的文件描述符读取敏感数据(如密码)或控制父进程。如果监听套接字泄漏,子进程可以接受新的连接以读取敏感数据。
漏洞示例
- 通过 mod_php 劫持 Apache https (2003)
- Apache:如果未设置 APR_FOPEN_NOCLEANUP,则 Apr 应设置 FD_CLOEXEC:已于 2009 年修复。
- PHP:system()(以及类似函数)不会清理 Apache 打开的句柄:自 2006 年起开放。
- CWE-403:文件描述符暴露于意外控制范围 (2008)
- OpenSSH 安全公告:portable-keysign-rand-helper.adv (2011)
另请参阅 CERT 安全编码标准:FIO42-C. 确保在不再需要时正确关闭文件。
subprocess 模块中修复的问题
继承的文件描述符导致 subprocess
模块出现 4 个问题
- 问题 #2320:使用 stdin 的 subprocess 中的竞争条件(于 2008 年开放)。
- 问题 #3006:subprocess.Popen 导致套接字在关闭后保持打开状态(于 2008 年开放)。
- 问题 #7213:subprocess 在 Popen 实例之间泄漏打开的文件描述符,导致挂起(于 2009 年开放)。
- 问题 #12786:当 stdin 关闭时,subprocess wait() 挂起(于 2011 年开放)。
这些问题已在 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”:尚不支持。
另请参阅
- 安全的文件描述符处理(Ulrich Drepper,2008)
- Unix 过去的幽灵,第二部分:混淆的设计(Neil Brown,2010)解释了
O_CLOEXEC
和O_NONBLOCK
标志的历史。 - 2.6.27 中的文件描述符处理更改
- FreeBSD:在执行时原子关闭。
Python 3.3 的状态
Python 3.3 在所有平台上创建可继承的文件描述符,除了 os.pipe()
,它在 Windows 上创建不可继承的文件描述符。
在 Python 3.3 中添加了与不可继承文件描述符的原子创建相关的新的常量和函数:os.O_CLOEXEC
、os.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 默认情况下被创建为可继承的,但如果 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
。
PEP 433
PEP 433,“更容易抑制文件描述符继承”,是之前尝试提出的各种其他替代方案,但没有达成共识。
Python 问题
- #10115:支持 accept4() 以在套接字创建时原子地设置标志
- #12105:open() 无法设置标志,例如 O_CLOEXEC
- #12107:创建的 TCP 监听套接字没有 FD_CLOEXEC 标志
- #16850:向 open() 添加“e”模式:关闭并执行 (O_CLOEXEC) / O_NOINHERIT
- #16860:在 tempfile 模块中使用 O_CLOEXEC
- #16946:subprocess:_close_open_fd_range_safe() 如果定义了 O_CLOEXEC,则在 Linux < 2.6.23 上不设置关闭执行标志
- #17070:使用新的 cloexec 提高安全性并避免错误
- #18571:PEP 446 的实现:不可继承的文件描述符
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0446.rst
上次修改时间:2023-09-09 17:39:29 GMT