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

Python 增强提案

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. 确保在不再需要文件时正确关闭它们

漏洞示例

原子性

在多线程应用程序中使用 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_CLOEXECSOCK_CLOEXEC 标志会被旧版本的 Linux 忽略,因此必须使用 fcntl(fd, F_GETFD) 检查 FD_CLOEXEC 标志。如果内核忽略 O_CLOEXECSOCK_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* 参数的默认值无法正常工作。

替代方案

默认启用继承,默认不可配置

在创建文件描述符的函数上添加一个新的可选参数cloexeccloexec参数的默认值为False,并且此默认值不可更改。默认情况下启用的文件描述符继承也是 POSIX 和 Windows 上的默认设置。此选项是最保守的选择。

此选项不能解决“基本原理”部分中列出的问题,它只能提供一个帮助程序来解决这些问题。为了解决所有这些问题,所有创建文件描述符的函数都必须在应用程序使用的每个模块中修改为设置cloexec=True

默认启用继承,默认值只能设置为 True

此选项基于该提案:唯一的区别是sys.setdefaultcloexec()不接受任何参数,它只能用于将cloexec参数的默认值设置为True

默认情况下禁用继承

此选项基于该提案:唯一的区别是cloexec参数的默认值为True(而不是False)。

如果文件必须被子进程继承,可以使用cloexec=False参数。

将 close-on-exec 标志设置为默认值的优势

将 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

关于新参数名称的讨论

  • inheritinherited:更接近 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 flagopen() [原子]
  • 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

脚注


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

最后修改:2023-09-09 17:39:29 GMT