PEP 433 – 更轻松地抑制文件描述符继承
- 作者:
- Victor Stinner <vstinner at python.org>
- 状态:
- 已取代
- 类型:
- 标准跟踪
- 创建:
- 2013年1月10日
- Python 版本:
- 3.4
- 取代者:
- 446
摘要
在创建文件描述符的函数上添加一个新的可选 *cloexec* 参数,添加更改此参数默认值的不同方法,并添加四个新函数
os.get_cloexec(fd)
os.set_cloexec(fd, cloexec=True)
sys.getdefaultcloexec()
sys.setdefaultcloexec(cloexec)
基本原理
文件描述符具有一个 close-on-exec 标志,指示文件描述符是否会被继承。
在 UNIX 上,如果设置了 close-on-exec 标志,文件描述符将不会被继承:它将在子进程执行时被关闭;否则,文件描述符将被子进程继承。
在 Windows 上,如果设置了 close-on-exec 标志,文件描述符将不会被继承;如果清除了 close-on-exec 标志,并且如果 CreateProcess()
被调用时 *bInheritHandles* 参数设置为 TRUE
(例如,当 subprocess.Popen
被创建时 close_fds=False
),则文件描述符会被子进程继承。Windows 没有“close-on-exec”标志,而是一个继承标志,它的值恰好相反。例如,设置 close-on-exec 标志意味着清除句柄的 HANDLE_FLAG_INHERIT
标志。
Python 3.3 中的状态
在 UNIX 上,subprocess 模块从 Python 3.2 开始默认关闭大于 2 的文件描述符 [1]。父进程创建的所有文件描述符在子进程中都会自动关闭。
xmlrpc.server.SimpleXMLRPCServer
设置监听套接字的 close-on-exec 标志,父类 socketserver.TCPServer
不会设置此标志。
还有其他情况会创建子进程或执行新程序,其中文件描述符不会被关闭:os.spawn*()
和 os.exec*()
系列的函数,以及调用 exec()
或 fork()
+ exec()
的第三方模块。在这种情况下,文件描述符会在父进程和子进程之间共享,这通常是意料之外的,会导致各种问题。
本 PEP 提议继续在 Python 3.2 中对 subprocess 进行更改的基础上继续工作,以解决任何代码中的问题,而不仅仅是使用 subprocess 的代码。
继承的文件描述符问题
在父进程中关闭文件描述符不会关闭相关的资源(文件、套接字等),因为它仍然在子进程中打开。
TCPServer 的监听套接字在 exec()
上不会关闭:子进程能够从新客户端获取连接;如果父进程关闭监听套接字并在同一个地址上创建一个新的监听套接字,它会收到一个“地址已被使用”错误。
不关闭文件描述符会导致资源耗尽:即使父进程关闭了所有文件,创建新的文件描述符也可能因“文件过多”而失败,因为文件仍在子进程中打开。
另请参阅以下问题
安全
泄漏文件描述符是一个主要的安全性漏洞。不受信任的子进程可以通过泄漏的文件描述符读取敏感数据(如密码)并控制父进程。例如,这是一个从 chroot 逃逸的已知漏洞。
另请参阅 CERT 建议:FIO42-C. 确保在不再需要文件时正确关闭它们。
漏洞示例
- OpenSSH 安全公告:portable-keysign-rand-helper.adv(2011 年 4 月)
- CWE-403: 文件描述符暴露给意外控制范围 (2008)
- 通过 mod_php 劫持 Apache https(2003 年 12 月)
- Apache: 如果未设置 APR_FOPEN_NOCLEANUP,Apr 应该设置 FD_CLOEXEC(在 2009 年修复)
- PHP: system()(以及类似函数)没有清理 Apache 的打开句柄(在 2013 年 1 月未修复)
原子性
在多线程应用程序中使用 fcntl()
设置 close-on-exec 标志并不安全。如果线程在创建文件描述符和调用 fcntl(fd, F_SETFD, new_flags)
之间调用 fork()
和 exec()
:文件描述符将被子进程继承。现代操作系统提供了在创建文件描述符期间设置标志的函数,从而避免了竞争条件。
可移植性
Python 3.2 添加了 socket.SOCK_CLOEXEC
标志,Python 3.3 添加了 os.O_CLOEXEC
标志和 os.pipe2()
函数。在打开文件和创建管道或套接字时,已经可以在 Python 3.3 中原子地设置 close-on-exec 标志。
问题是这些标志和函数不可移植:只有最近的操作系统版本支持它们。O_CLOEXEC
和 SOCK_CLOEXEC
标志会被旧版本的 Linux 忽略,因此必须使用 fcntl(fd, F_GETFD)
检查 FD_CLOEXEC
标志。如果内核忽略 O_CLOEXEC
或 SOCK_CLOEXEC
标志,则需要调用 fcntl(fd, F_SETFD, flags)
来设置 close-on-exec 标志。
注意
OpenBSD 5.2 之前的版本,如果在 exec()
之前使用 fork()
,不会关闭带有 close-on-exec 标志设置的文件描述符,但在没有 fork()
的情况下调用 exec()
则可以正常工作。尝试 openbsd_bug.py.
范围
应用程序仍然必须在 fork()
之后显式关闭文件描述符。close-on-exec 标志仅在 exec()
之后关闭文件描述符,因此在 fork()
+ exec()
之后关闭。
本 PEP 仅更改 Python 标准库或使用标准库的模块创建的文件描述符的 close-on-exec 标志。不使用标准库的第三方模块应修改为符合本 PEP。例如,可以使用新的 os.set_cloexec()
函数。
注意
对于没有 exec()
的 fork()
,请参阅 在 fork 之后关闭文件描述符,以了解可能的解决方案。
提议
在创建文件描述符的函数上添加一个新的可选 *cloexec* 参数,以及更改此参数默认值的不同方法。
添加新函数
os.get_cloexec(fd:int) -> bool
: 获取文件描述符的 close-on-exec 标志。并非所有平台都提供。os.set_cloexec(fd:int, cloexec:bool=True)
: 在文件描述符上设置或清除 close-on-exec 标志。并非所有平台都提供。sys.getdefaultcloexec() -> bool
: 获取 *cloexec* 参数的当前默认值sys.setdefaultcloexec(cloexec: bool)
: 设置 *cloexec* 参数的默认值
在以下函数中添加一个新的可选 *cloexec* 参数
asyncore.dispatcher.create_socket()
io.FileIO
io.open()
open()
os.dup()
os.dup2()
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()
*cloexec* 参数的默认值为 sys.getdefaultcloexec()
。
添加一个新的命令行选项 -e
和一个环境变量 PYTHONCLOEXEC
,以默认设置 close-on-exec 标志。
subprocess
清除 pass_fds
参数的文件描述符的 close-on-exec 标志。
标准库中所有创建文件描述符的函数都必须尊重 *cloexec* 参数的默认值:sys.getdefaultcloexec()
。
预期文件描述符 0(stdin)、1(stdout)和 2(stderr)会被继承,但 Python 对它们的处理方式不同。当使用 os.dup2()
替换标准流时,必须显式指定 cloexec=False
。
该提议的缺点
- 现在无法通过仅阅读源代码来了解新创建的文件描述符的 close-on-exec 标志是否会被设置。
- 如果文件描述符的继承很重要,现在必须显式指定 *cloexec* 参数,否则库或应用程序将根据 *cloexec* 参数的默认值无法正常工作。
替代方案
默认启用继承,默认不可配置
在创建文件描述符的函数上添加一个新的可选参数cloexec。cloexec参数的默认值为False
,并且此默认值不可更改。默认情况下启用的文件描述符继承也是 POSIX 和 Windows 上的默认设置。此选项是最保守的选择。
此选项不能解决“基本原理”部分中列出的问题,它只能提供一个帮助程序来解决这些问题。为了解决所有这些问题,所有创建文件描述符的函数都必须在应用程序使用的每个模块中修改为设置cloexec=True。
默认启用继承,默认值只能设置为 True
此选项基于该提案:唯一的区别是sys.setdefaultcloexec()
不接受任何参数,它只能用于将cloexec参数的默认值设置为True
。
默认情况下禁用继承
此选项基于该提案:唯一的区别是cloexec参数的默认值为True
(而不是False
)。
如果文件必须被子进程继承,可以使用cloexec=False
参数。
将 close-on-exec 标志设置为默认值的优势
- 受 FD 继承影响的程序远多于依赖它的程序(请参阅“继承文件描述符问题”和“安全性”),而不是依赖它的程序(请参阅“使用文件描述符继承的应用程序”)。
将 close-on-exec 标志设置为默认值的缺点
- 这违反了最少惊奇原则。使用 os 模块的开发人员可能期望 Python 遵守 POSIX 标准,因此默认情况下不会设置 close-on-exec 标志。
- os 模块被编写为对系统调用的薄包装器(对 C 标准库的函数)。如果不支持设置 close-on-exec 标志的原子标志(请参阅“附录:操作系统支持”),单个 Python 函数调用可能会调用 2 或 3 个系统调用(请参阅“性能”部分)。
- 如果有额外的系统调用,可能会减慢 Python 的速度:请参阅“性能”。
向后兼容性:只有少数程序依赖于文件描述符的继承,而且它们只传递几个文件描述符,通常只有一个。这些程序会立即失败,并出现EBADF
错误,并且修复起来很简单:添加cloexec=False
参数或使用os.set_cloexec(fd, False)
。
无论如何,subprocess
模块将被更改为清除pass_fds
参数中列出的文件描述符上的 close-on-exec 标志Popen 构造函数。因此,如果这些程序使用subprocess
模块,它们可能不需要任何修复。
在 fork 之后关闭文件描述符
此 PEP 不会解决在没有exec()
的情况下使用fork()
的应用程序的问题。Python 需要一个通用进程来注册回调,这些回调将在 fork 后被调用,请参阅#16500:添加 atfork 模块。此类注册表可用于在fork()
后立即关闭文件描述符。
缺点
- 它不能解决 Windows 上的问题:Windows 上不存在
fork()
- 此选项不能解决在没有
fork()
的情况下使用exec()
的程序的问题。 - 第三方模块可能会直接调用 C 函数
fork()
,它不会调用“atfork”回调。 - 所有创建文件描述符的函数都必须更改为注册回调,然后在关闭文件时注销其回调。或者,必须维护所有打开文件描述符的列表。
- 操作系统是自动关闭文件描述符比 Python 更好的地方。例如,在关闭文件和注销关闭文件的回调之间避免竞争条件并不容易。
open(): 在 mode 中添加“e”标志
一种新的“e”模式将设置 close-on-exec 标志(尽力而为)。
此选项只解决了open()
的问题。例如,socket.socket() 和 os.pipe() 没有mode
参数。
从 2.7 版开始,GNU libc 支持"e"
标志用于fopen()
。它使用O_CLOEXEC
(如果可用),或使用fcntl(fd, F_SETFD, FD_CLOEXEC)
。使用 Visual Studio,fopen() 接受一个“N”标志,该标志使用O_NOINHERIT
。
关于新参数名称的讨论
inherit
,inherited
:更接近 Windows 定义敏感
sterile
:“不产生后代。”
使用文件描述符继承的应用程序
大多数开发人员不知道文件描述符默认情况下是继承的。大多数程序不依赖于文件描述符的继承。例如,在 Python 3.2 中,subprocess.Popen
已更改为默认情况下在子进程中关闭所有大于 2 的文件描述符。还没有用户抱怨过此行为更改。
使用 fork 的网络服务器可能希望将客户端套接字传递给子进程。例如,在 UNIX 上,CGI 服务器使用dup2()
通过文件描述符 0(stdin)和 1(stdout)传递客户端套接字。
为了访问受限资源(例如,创建监听低于 1024 的 TCP 端口的套接字或读取包含敏感数据(如密码)的文件),一种常见的做法是:以 root 用户身份启动,创建文件描述符,创建子进程,降低权限(例如:更改当前用户),将文件描述符传递给子进程并退出父进程。
在这种情况下,安全性非常重要:泄露另一个文件描述符将是一个严重的安全漏洞(请参阅“安全性”)。root 进程可能不会退出,而是监视子进程,并在上一个子进程崩溃时重新启动一个新的子进程并将相同的描述符传递给它。
使用命令行选项从父进程获取文件描述符的程序示例
- gpg:
--status-fd <fd>
,--logger-fd <fd>
等。 - openssl:
-pass fd:<fd>
- qemu:
-add-fd <fd>
- valgrind:
--log-fd=<fd>
,--input-fd=<fd>
等。 - xterm:
-S <fd>
在 Linux 上,可以使用"/dev/fd/<fd>"
文件名将文件描述符传递给期望文件名的程序。
性能
设置 close-on-exec 标志可能需要为每个新文件描述符的创建添加额外的系统调用。额外的系统调用数量取决于用于设置标志的方法
O_NOINHERIT
:没有额外的系统调用O_CLOEXEC
:一个额外的系统调用,但只在创建第一个文件描述符时,用于检查标志是否受支持。如果标志不受支持,Python 必须回退到下一种方法。ioctl(fd, FIOCLEX)
:每个文件描述符一个额外的系统调用fcntl(fd, F_SETFD, flags)
:每个文件描述符两个额外的系统调用,一个用于获取旧标志,另一个用于设置新标志
在 Linux 上,设置 close-on-flag 对性能的影响很小。bench_cloexec.py 在 Linux 3.6 上的结果
- 未设置 close-on-flag:7.8 us
O_CLOEXEC
:慢 1%(7.9 us)ioctl()
:慢 3%(8.0 us)fcntl()
:慢 3%(8.0 us)
实现
os.get_cloexec(fd)
获取文件描述符的 close-on-exec 标志。
伪代码
if os.name == 'nt':
def get_cloexec(fd):
handle = _winapi._get_osfhandle(fd);
flags = _winapi.GetHandleInformation(handle)
return not(flags & _winapi.HANDLE_FLAG_INHERIT)
else:
try:
import fcntl
except ImportError:
pass
else:
def get_cloexec(fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
return bool(flags & fcntl.FD_CLOEXEC)
os.set_cloexec(fd, cloexec=True)
设置或清除文件描述符上的 close-on-exec 标志。标志是在创建文件描述符后设置的,因此它不是原子的。
伪代码
if os.name == 'nt':
def set_cloexec(fd, cloexec=True):
handle = _winapi._get_osfhandle(fd);
mask = _winapi.HANDLE_FLAG_INHERIT
if cloexec:
flags = 0
else:
flags = mask
_winapi.SetHandleInformation(handle, mask, flags)
else:
fnctl = None
ioctl = None
try:
import ioctl
except ImportError:
try:
import fcntl
except ImportError:
pass
if ioctl is not None and hasattr('FIOCLEX', ioctl):
def set_cloexec(fd, cloexec=True):
if cloexec:
ioctl.ioctl(fd, ioctl.FIOCLEX)
else:
ioctl.ioctl(fd, ioctl.FIONCLEX)
elif fnctl is not None:
def set_cloexec(fd, cloexec=True):
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
if cloexec:
flags |= FD_CLOEXEC
else:
flags &= ~FD_CLOEXEC
fcntl.fcntl(fd, fcntl.F_SETFD, flags)
ioctl 比 fcntl 更可取,因为它只需要一个系统调用,而不是 fcntl 的两个系统调用。
注意
fcntl(fd, F_SETFD, flags)
仅支持一个标志(FD_CLOEXEC
),因此可以避免fcntl(fd, F_GETFD)
。但是,它可能会在将来删除其他标志,因此保持两个函数调用更安全。
注意
GNU libc 的fopen()
函数会忽略fcntl(fd, F_SETFD, flags)
失败的错误。
open()
- Windows:带有
O_NOINHERIT
标志的open()
[原子] - 带有
O_CLOEXEC flag
的open()
[原子] open()
+os.set_cloexec(fd, True)
[尽力而为]
os.dup()
- Windows:
DuplicateHandle()
[原子] fcntl(fd, F_DUPFD_CLOEXEC)
[原子]dup()
+os.set_cloexec(fd, True)
[尽力而为]
os.dup2()
fcntl(fd, F_DUP2FD_CLOEXEC, fd2)
[原子]- 带有
O_CLOEXEC
标志的dup3()
[原子] dup2()
+os.set_cloexec(fd, True)
[尽力而为]
os.pipe()
- Windows:
CreatePipe()
带有SECURITY_ATTRIBUTES.bInheritHandle=TRUE
,或_pipe()
带有O_NOINHERIT
标志 [原子操作] pipe2()
带有O_CLOEXEC
标志 [原子操作]pipe()
+os.set_cloexec(fd, True)
[尽力而为]
socket.socket()
- Windows:
WSASocket()
带有WSA_FLAG_NO_HANDLE_INHERIT
标志 [原子操作] socket()
带有SOCK_CLOEXEC
标志 [原子操作]socket()
+os.set_cloexec(fd, True)
[尽力而为]
socket.socketpair()
socketpair()
带有SOCK_CLOEXEC
标志 [原子操作]socketpair()
+os.set_cloexec(fd, True)
[尽力而为]
socket.socket.accept()
accept4()
带有SOCK_CLOEXEC
标志 [原子操作]accept()
+os.set_cloexec(fd, True)
[尽力而为]
向后兼容性
没有向后不兼容的更改。默认行为保持不变:默认情况下不设置 close-on-exec 标志。
附录: 操作系统支持
Windows
Windows 有一个 O_NOINHERIT
标志:“子进程中不继承”。
例如,它受 open()
和 _pipe()
支持。
可以使用 SetHandleInformation(fd, HANDLE_FLAG_INHERIT, 0)
清除该标志。
CreateProcess()
有一个 bInheritHandles
参数:如果它是 FALSE
,则不会继承句柄。如果它是 TRUE
,则会继承设置了 HANDLE_FLAG_INHERIT
标志的句柄。 subprocess.Popen
使用 close_fds
选项来定义 bInheritHandles
。
ioctl
函数
ioctl(fd, FIOCLEX, 0)
:设置 close-on-exec 标志ioctl(fd, FIONCLEX, 0)
:清除 close-on-exec 标志
可用性:Linux、Mac OS X、QNX、NetBSD、OpenBSD、FreeBSD。
fcntl
函数
flags = fcntl(fd, F_GETFD); fcntl(fd, F_SETFD, flags | FD_CLOEXEC)
:设置 close-on-exec 标志flags = fcntl(fd, F_GETFD); fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC)
:清除 close-on-exec 标志
可用性:AIX、Digital UNIX、FreeBSD、HP-UX、IRIX、Linux、Mac OS X、OpenBSD、Solaris、SunOS、Unicos。
原子标志
新标志
O_CLOEXEC
:在 Linux (2.6.23)、FreeBSD (8.3)、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 上可用。WSA_FLAG_NO_HANDLE_INHERIT
标志用于WSASocket()
:支持 Windows 7 with SP1、Windows Server 2008 R2 with SP1 及更高版本。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()
来检查该标志是否受支持。如果它不起作用,我们必须使用 ioctl()
或 fcntl()
设置该标志。
在早于 2.6.27 的 Linux 上,如果在套接字类型中设置了 SOCK_CLOEXEC
标志,socket()
或 socketpair()
会失败,并且 errno
被设置为 EINVAL
。
在 Windows XPS3 上,WSASocket()
带有 WSAEPROTOTYPE
当使用 WSA_FLAG_NO_HANDLE_INHERIT
标志时。
新函数
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()
,accept4()
会返回 -1
(失败)并且 errno
被设置为 ENOSYS
。
链接
链接
- 安全文件描述符处理 (Ulrich Drepper,2008)
- Tornado 项目的 win32_support.py:使用
SetHandleInformation(fd, HANDLE_FLAG_INHERIT, 1)
模拟 fcntl(fd, F_SETFD, FD_CLOEXEC) - LKML:[PATCH] nextfd(2)
Python 问题
- #10115:支持 accept4() 以在套接字创建时原子设置标志
- #12105:open() 无法设置标志,例如 O_CLOEXEC
- #12107:创建的 TCP 监听套接字没有 FD_CLOEXEC 标志
- #16500:添加 atfork 模块
- #16850:在 open() 中添加“e”模式:close-and-exec (O_CLOEXEC) / O_NOINHERIT
- #16860:在 tempfile 模块中使用 O_CLOEXEC
- #17036:PEP 433 的实现
- #16946:subprocess:_close_open_fd_range_safe() 在 Linux < 2.6.23 上未设置 close-on-exec 标志,如果定义了 O_CLOEXEC
- #17070:PEP 433:使用新的 cloexec 来提高安全性并避免错误
其他语言
- Perl 在新建文件描述符上设置 close-on-exec 标志,如果它们的数量大于
$SYSTEM_FD_MAX
($^F
)。参见 $SYSTEM_FD_MAX 文档。Perl 自创建以来就一直在执行此操作(它在 Perl 1 中就已经存在)。 - Ruby:为所有 fd (除了 0, 1, 2) 设置 FD_CLOEXEC
- Ruby:Kernel::open 缺少 O_CLOEXEC 标志:提交后来被撤回
- OCaml:PR#5256:使用 Unix.open_process* 打开的进程继承所有打开的文件描述符(包括套接字)。OCaml 有一个
Unix.set_close_on_exec
函数。
脚注
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0433.rst