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

Python 增强提案

PEP 522 – 允许在安全敏感 API 中阻塞 BlockingIOError

作者:
Alyssa Coghlan <ncoghlan at gmail.com>, Nathaniel J. Smith <njs at pobox.com>
状态:
已拒绝
类型:
标准跟踪
要求:
506
创建日期:
2016 年 6 月 16 日
Python 版本:
3.6
决议:
Security-SIG 消息

目录

摘要

标准库中的一些 API,它们返回的随机值名义上适合用于安全敏感操作,目前有一个晦涩的、依赖于操作系统的故障模式,允许它们返回实际上不适合此类操作的值。

这是因为一些操作系统内核(最典型的是 Linux 内核)允许在系统随机数生成器完全初始化之前读取 /dev/urandom,而大多数其他操作系统会隐式阻塞此类读取,直到随机数生成器准备就绪。

对于更底层的 os.urandomrandom.SystemRandom API,此 PEP 提议在 Python 3.6 中将此类故障从当前静默、难以检测和难以调试的错误更改为易于检测和调试的错误,通过引发具有适当错误消息的 BlockingIOError,使开发人员有机会明确指定他们处理这种情况的首选方法。

对于新的高级 secrets API,它建议在需要时隐式阻塞,只要该模块生成随机数,以及公开新的 secrets.wait_for_system_rng() 函数,以允许其他使用低级 API 的代码显式等待系统随机数生成器可用。

此更改将影响提供 getrandom() 系统调用的任何操作系统,无论 /dev/urandom 设备在系统随机数生成器未准备好时返回可能可预测结果的默认行为(例如 Linux、NetBSD)还是阻塞(例如 FreeBSD、Solaris、Illumos)。阻止用户空间代码在系统随机数生成器初始化之前执行的操作系统,或者不提供 getrandom() 系统调用的操作系统,将完全不受拟议更改的影响(例如 Windows、Mac OS X、OpenBSD)。

新的异常或 secrets 模块中的阻塞行为可能在以下情况下遇到:

  • Python 代码在 Linux 系统初始化期间调用这些 API
  • Python 代码在未正确初始化的 Linux 系统上运行(例如,没有足够熵源来播种系统随机数生成器的嵌入式硬件,或未配置为从 VM 主机接受熵的 Linux VM)

与其他 PEP 的关系

此 PEP 依赖于已接受的 PEP 506,该 PEP 添加了 secrets 模块。

此 PEP 与 Victor Stinner 的 PEP 524 竞争,该 PEP 提议使 os.urandom 本身在系统 RNG 未就绪时隐式阻塞。

PEP 驳回

对于参考实现,Guido 否决了此 PEP,转而支持 PEP 524 中的无条件隐式阻塞提案(这使 CPython 在 Linux 上的行为与其在其他操作系统上的行为一致)。

这意味着任何关于 Linux 发行版中 os.urandom() 在系统 Python 安装中的适当默认行为的进一步讨论,应在各自的发行版邮件列表中进行,而不是在 CPython 邮件列表中。

与此 PEP 无关的更改

CPython 解释器初始化和 random 模块初始化已更新,以便在系统随机数生成器未准备好时,优雅地回退到替代的播种选项。

此 PEP 与 PEP 524 中关于为提供 getrandom 系统调用的平台添加 os.getrandom() API 的提案不冲突。在 os 模块作为可能依赖于平台的操作系统的潜在平台相关功能的薄包装器的角色中,有足够的动机添加该 API,无论这些系统上 os.urandom() 的默认行为如何。

提案

更改 os.urandom() 在具有 getrandom() 系统调用的平台上

此 PEP 提议,在 Python 3.6+ 中,os.urandom() 将更新为在可用时以非阻塞模式调用 getrandom() 系统调用,如果内核报告调用会阻塞,则引发 BlockingIOError: system random number generator is not ready; see secrets.token_bytes()

此行为将通过现有 random.SystemRandom 传播,它提供了围绕 os.urandom() 的相对薄的包装器,该包装器与 random.Random() API 匹配。

然而,由 PEP 506 引入的新 secrets 模块将更新为捕获新异常,并在遇到异常时隐式等待系统随机数生成器。

在所有情况下,一旦对这些安全敏感 API 之一的调用成功,该进程中对此类 API 的所有后续调用都将成功而不阻塞(一旦操作系统随机数生成器在系统启动后准备就绪,它将保持就绪状态)。

在 Linux 和 NetBSD 上,这将取代之前从 /dev/urandom 读取可能可预测结果的行为。

在 FreeBSD、Solaris 和 Illumos 上,这将取代之前直到系统随机数生成器准备好时隐式阻塞的行为。然而,尚不清楚这些操作系统是否真的允许用户空间代码(因此是 Python)在系统随机数生成器准备好之前运行。

请注意,在所有情况下,如果底层 getrandom() API 的调用报告 ENOSYS 而不是返回成功响应或报告 EAGAIN,CPython 将继续回退到直接从 /dev/urandom 读取。

添加 secrets.wait_for_system_rng()

不应在没有关于如何解决遇到的新异常的直接建议的情况下添加新异常(尽管遇到新异常的罕见程度预计会很低)。对于确实需要使用系统随机数生成器低级接口(而不是新的 secrets 模块)的安全敏感代码,并且确实收到了实时错误报告表明这是该特定应用程序用户群体的实际问题而不是理论问题,此 PEP 的建议将是(直接或间接)将以下代码段添加到 __main__ 模块中:

import secrets
secrets.wait_for_system_rng()

或者,如果需要与 Python 3.6 之前的版本兼容

try:
    import secrets
except ImportError:
    pass
else:
    secrets.wait_for_system_rng()

secrets 模块本身中,这将用于 token_bytes(),以便在遇到新异常时隐式阻塞

def token_bytes(nbytes=None):
    if nbytes is None:
        nbytes = DEFAULT_ENTROPY
    try:
        result = os.urandom(nbytes)
    except BlockingIOError:
        wait_for_system_rng()
        result = os.urandom(nbytes)
    return result

模块的其他部分将更新为使用 token_bytes() 作为其基本随机数生成构建块,而不是直接调用 os.urandom()

涵盖了几乎肯定需要访问系统随机数生成器用例的应用程序框架(例如 Web 框架)可能会选择将 secrets.wait_for_system_rng() 调用隐式包含到启动应用程序的命令中,以便现有的 os.urandom() 调用保证永远不会在这些框架下引发新异常。

对于无法直接修改的应用程序遇到的错误情况,可以使用以下命令在系统随机数生成器初始化之前等待其准备就绪,然后再启动该应用程序:

python3 -c "import secrets; secrets.wait_for_system_rng()"

例如,可以将此代码段添加到 shell 脚本或 systemd ExecStartPre 挂钩中(并且可能有助于可靠地等待系统随机数生成器准备就绪,即使后续命令本身不是 Python 3.6 下运行的应用程序):

考虑到上述对 os.urandom() 的更改,以及添加 os.getrandom() API(在支持它的系统上),此函数的建议实现将是:

if hasattr(os, "getrandom"):
    # os.getrandom() always blocks waiting for the system RNG by default
    def wait_for_system_rng():
        """Block waiting for system random number generator to be ready"""
        os.getrandom(1)
        return
else:
   # As far as we know, other platforms will never get BlockingIOError
   # below but the implementation makes pessimistic assumptions
    def wait_for_system_rng():
        """Block waiting for system random number generator to be ready"""
        # If the system RNG is already seeded, don't wait at all
        try:
            os.urandom(1)
            return
        except BlockingIOError:
            pass
        # Avoid the below busy loop if possible
        try:
            block_on_system_rng = open("/dev/random", "rb")
        except FileNotFoundError:
            pass
        else:
            with block_on_system_rng:
                block_on_system_rng.read(1)
        # Busy loop until the system RNG is ready
        while True:
            try:
                os.urandom(1)
                break
            except BlockingIOError:
                # Only check once per millisecond
                time.sleep(0.001)

在可能等待系统 RNG 准备就绪的系统上,此函数将在 os.getrandom() 已定义、os.urandom() 本身隐式阻塞,或 /dev/random 设备可用时,不会进行忙等待。如果系统随机数生成器已准备就绪,则保证此调用永远不会阻塞,即使系统的 /dev/random 设备采用的设计允许它在正常系统操作期间间歇性地阻塞。

范围限制

未对 Windows 或 Mac OS X 系统提出任何更改,因为这两个平台均未提供任何机制允许在操作系统随机数生成器初始化之前运行 Python 代码。Mac OS X 甚至会在无法正确初始化随机数生成器时内核恐慌并中止启动过程(尽管 Apple 对支持的硬件平台施加的限制使其在实践中极不可能发生)。

同样,未对不提供 getrandom() 系统调用的其他 *nix 系统提出任何更改。在这些系统上,os.urandom() 将继续阻塞,等待系统随机数生成器初始化。

虽然提供非阻塞 API(getrandom() 除外)以请求可用于安全敏感应用程序的随机数的其他 *nix 系统可能收到与此 PEP 中为 getrandom() 提出的类似更新,但此类更改超出了此特定提案的范围。

受影响平台的老版本(不提供新的 getrandom() 系统调用)上的 Python 行为也将保持不变。

基本原理

确保 secrets 模块在需要时隐式阻塞

这样做是为了帮助传播一种观念,即对于那些想要获得生成安全敏感随机数的最简单方法的开发者来说,应该“在可用时使用 secrets 模块,否则您的应用程序可能会意外崩溃”,而不是更冗长的“可用时始终调用 secrets.wait_for_system_rng(),否则您的应用程序可能会意外崩溃”。

这也是因为 BDFL 对可能意外阻塞的 API 的容忍度高于对可能引发意外异常的 API 的容忍度 [11]

在 Linux 上的 os.urandom() 中引发 BlockingIOError

多年来,安全社区的指导一直是,在 Python 中实现安全敏感操作时,使用 os.urandom()(或 random.SystemRandom() 包装器)。

为了提高 API 可发现性,并更清楚地表明保密性和模拟不是同一个问题(即使它们都涉及随机数),PEP 506 将基于更低级 os.urandom() API 的几个单行代码片段收集到一个新的 secrets 模块中。

然而,这个指导也附带了一个长期的注意事项:编写安全敏感软件的开发者,至少是针对 Linux,可能还需要等待操作系统随机数生成器准备就绪,然后才能依赖它进行安全敏感操作。这通常只发生在 os.urandom() 在系统初始化过程非常早期被读取,或者在熵源有限的系统上(例如某些类型的虚拟化或嵌入式系统),但遗憾的是,触发这种情况的确切条件很难预测,发生这种情况时,用户空间没有直接方法可以知道它发生了,除非查询操作系统特定的接口。

在 *BSD 系统上(如果特定 *BSD 变体允许问题发生),以及可能还有 Solaris 和 Illumos,遇到这种情况意味着 os.urandom() 要么阻塞等待系统随机数生成器准备就绪(相关症状是受影响的脚本在第一次调用 os.urandom() 时意外暂停),要么表现与 Linux 上的情况相同。

在 Linux 上,对于 Python 3.4 及更早版本,以及 Python 3.5.2 之后的 Python 3.5 维护版本,当在 Linux 启动过程的早期运行,或在没有良好熵源来播种操作系统随机数生成器的硬件上运行时,没有明确的指标告诉开发者他们的软件可能无法按预期工作:由于底层 /dev/urandom 设备的行为,Linux 上的 os.urandom() 无论如何都会返回一个结果,并且需要大量的统计分析才能显示存在安全漏洞。

相比之下,如果 BlockingIOError 在这些情况下被引发,那么使用 Python 3.6+ 的开发者可以轻松选择他们期望的行为:

  1. 在应用程序启动时或之前等待系统 RNG(安全敏感)
  2. 切换到使用 random 模块(非安全敏感)

公开 secrets.wait_for_system_rng()

此 PEP 的早期版本提出了一些用于包装 os.urandom() 的代码片段,使其适用于安全敏感用例。

在 security-sig 邮件列表中对该提案的讨论促使人们意识到 [9],此 PEP 中 API 设计的核心假设是,在让异常导致应用程序失败、阻塞等待系统 RNG 准备就绪以及切换到使用 random 模块而不是 os.urandom 之间进行选择,是一个应用程序和用例特定的决定,应考虑到应用程序和用例的特定细节。

解释器运行时或支持库无法确定特定用例是否安全敏感,而应用程序开发者很容易决定如何处理特定 API 引发的异常,但他们无法轻易规避 API 阻塞(当他们期望它不阻塞时)。

因此,PEP 被更新为添加 secrets.wait_for_system_rng() 作为应用程序、脚本和框架使用的 API,以指示它们希望在继续之前确保系统 RNG 可用,而库开发人员可以继续调用 os.urandom(),而不必担心它可能会意外开始阻塞等待系统 RNG 可用。

向后兼容性影响评估

PEP 476 类似,这是一个将之前静默的安全故障转化为吵闹异常的提案,需要应用程序开发者明确决定他们期望的行为。

由于未对不提供 getrandom() 系统调用的操作系统进行更改,os.urandom() 保留其作为名义上阻塞 API 的现有行为,但在实践中由于很难在操作系统随机数生成器准备就绪之前调度 Python 代码运行,因此它实际上是非阻塞的。我们认为在至少某些 *BSD 变体上可能遇到类似于此 PEP 中描述的问题,但没有人明确证明这一点。在 Mac OS X 和 Windows 上,似乎完全不可能在启动过程的早期尝试运行 Python 解释器。

在 Linux 和其他具有类似 /dev/urandom 行为的平台上,os.urandom() 保留其作为保证非阻塞 API 的状态。然而,实现该状态的方式在操作系统随机数生成器尚未准备好用于安全敏感操作这一特定情况下发生了变化:历史上它会返回可能可预测的随机数据,根据此 PEP,它将改为引发 BlockingIOError

受影响的应用程序开发者将需要根据他们开发的应用程序类型,对以下一项或多项进行更改,以获得与 Python 3.6 的前向兼容性:

不受影响的应用程序

以下类型的应用程序将完全不受更改的影响,无论它们是否执行安全敏感操作:

  • 不支持 Linux 的应用程序
  • 仅在桌面或传统服务器上运行的应用程序
  • 仅在系统 RNG 准备就绪后运行的应用程序(包括应用程序框架代表其调用 secrets.wait_for_system_rng() 的应用程序)

此类别中的应用程序将根本不会遇到新异常,因此开发人员可以合理地等待并观察是否会收到与新运行时行为相关的 Python 3.6 兼容性错误,而不是试图预先确定他们是否受影响。

受影响的安全敏感应用程序

安全敏感应用程序需要更改其系统配置,以便应用程序仅在操作系统随机数生成器准备好进行安全敏感操作后才启动,更改应用程序启动代码以调用 secrets.wait_for_system_rng(),或者切换到使用新的 secrets.token_bytes() API。

例如,对于通过 systemd 单元文件启动的组件,以下代码段将延迟激活,直到系统 RNG 准备就绪:

ExecStartPre=python3 -c “import secrets; secrets.wait_for_system_rng()”

或者,以下代码段将在可用时使用 secrets.token_bytes(),否则回退到 os.urandom()

try
import secrets.token_bytes as _get_random_bytes
except ImportError
import os.urandom as _get_random_bytes

受影响的非安全敏感应用程序

非安全敏感应用程序应更新为使用 random 模块而不是 os.urandom

def pseudorandom_bytes(num_bytes):
    return random.getrandbits(num_bytes*8).to_bytes(num_bytes, "little")

根据应用程序的详细信息,random 模块可能提供其他可以直接使用的 API,而无需模拟 os.urandom() API 生成的原始字节序列。

其他背景信息

为什么现在提出这个?

主要原因是 Python 3.5.0 发布版已切换到使用新的 Linux getrandom() 系统调用(如果可用),以避免消耗文件描述符 [1],这带来了副作用,使得以下操作阻塞等待系统随机数生成器准备就绪:

  • os.urandom(及其依赖的 API)
  • 导入 random 模块
  • 初始化某些内置类型使用的随机化哈希算法

虽然前两种行为可以说是可取的(并且与 os.urandom 在其他操作系统上的现有行为一致),但后两种行为是不必要和不受欢迎的,并且后者已知会导致系统级别死锁,当尝试在 Linux init 过程中使用 Python 3.5.0 或 3.5.1 运行 Python 脚本时 [2],而前者在使用没有配置鲁棒熵源的虚拟机时可能导致问题 [3]

由于在 CPython 中分离这些行为将涉及比维护版本更适合功能版本的实现更改,因此 Python 3.5.2 中相对简单的解决方案是将其全部三项恢复到与先前 Python 版本类似的行为:如果新的 Linux 系统调用指示它将阻塞,那么 Python 3.5.2 将隐式回退到直接读取 /dev/urandom [4]

然而,这个错误报告*也*导致了一系列提案,用于添加*新的* API,如 os.getrandom() [5]os.urandom_block() [6]os.pseudorandom()os.cryptorandom() [7],或者向 os.urandom() 本身添加新的可选参数 [8],然后尝试教育用户何时应该调用这些 API 而不是仅仅使用普通的 os.urandom() 调用。

这些提案可能被认为是反应过度,因为在 Linux 上可靠地获取适合安全敏感工作用途的随机数的问题,是一个相对晦涩的问题,主要引起操作系统开发人员和嵌入式系统程序员的兴趣,可能不值得将 Python 标准库的跨平台 API 扩展到新的 Linux 特定问题。特别是 secrets 模块已经作为“使用这个,不用担心低级细节”的选项被添加给编写安全敏感软件但出于某种原因无法依赖甚至更高级别的领域特定 API(如 Web 框架)并且也不需要担心 Python 3.6 之前版本的情况。

也就是说,低成本 ARM 设备也越来越多,其中许多运行 Linux,并且有很多人编写在这些设备上运行的 Python 应用程序。这提供了一个机会,将一个晦涩的安全问题——目前需要大量的 Linux 启动过程和可证明不可预测的随机数生成知识才能诊断和解决——变成一个相对平凡且易于在互联网搜索中找到的运行时异常。

跨平台行为 os.urandom()

在 Linux 和 NetBSD 以外的操作系统上,os.urandom() 可能已经阻塞等待操作系统随机数生成器准备就绪。这最多在进程生命周期中发生一次,并且后续调用保证是非阻塞的。

Linux 和 NetBSD 是特例,因为即使操作系统随机数生成器不认为自己已准备好用于安全敏感操作,读取 /dev/urandom 设备也会返回基于其可用熵的随机值。

这种行为可能存在问题,因此 Linux 3.17 添加了一个新的 getrandom() 系统调用,它(除其他优点外)允许调用者阻塞等待随机数生成器准备就绪,或者在随机数生成器未准备好时请求错误返回。值得注意的是,新 API 不支持返回不适合安全敏感用例的数据的旧行为。

Python 3.4 及更早版本直接访问 Linux /dev/urandom 设备。

Python 3.5.0 和 3.5.1(在提供新系统调用的系统上构建时)以阻塞模式调用 getrandom(),以避免使用文件描述符访问 /dev/urandom。虽然没有关于 os.urandom() 在用户代码中阻塞的特定问题报告,但确实存在由于 CPython 在解释器启动期间和导入 random 模块时隐式调用阻塞行为而导致的问题。

而不是尝试将 SipHash 初始化与 os.urandom() 实现分离,Python 3.5.2 切换到以非阻塞模式调用 getrandom(),并在系统调用指示 /dev/urandom 池尚未完全初始化时回退到读取 /dev/urandom

因此,在所有 Python 版本(包括 Python 3.5)中,os.urandom() 将底层 /dev/urandom 设备的行为传播到 Python 代码。

Linux 上 /dev/urandom 行为的问题

Python os 模块在很大程度上与 Linux API 同步发展,因此在 Linux 上运行时 os 模块函数密切遵循其 Linux 操作系统级别对应物的行为,通常被认为是一项理想的功能。

然而,/dev/urandom 代表了一个当前行为被认为是有问题的案例,但单方面在内核层面修复它已被证明会阻止某些 Linux 发行版的启动(至少部分原因是像 Python 这样的组件在系统初始化早期将其用于非安全敏感目的)。

作为类比,考虑以下两个函数:

def generate_example_password():
    """Generates passwords solely for use in code examples"""
    return generate_unpredictable_password()

def generate_actual_password():
    """Generates actual passwords for use in real applications"""
    return generate_unpredictable_password()

如果你将操作系统随机数生成器视为一种生成不可预测、秘密密码的方法,那么你可以将 Linux 的 /dev/urandom 实现为一个类似:

# Oversimplified artist's conception of the kernel code
# implementing /dev/urandom
def generate_unpredictable_password():
    if system_rng_is_ready:
        return use_system_rng_to_generate_password()
    else:
        # we can't make an unpredictable password; silently return a
        # potentially predictable one instead:
        return "p4ssw0rd"

在此场景下,generate_example_password 的作者是没问题的——即使 "p4ssw0rd" 出现的频率比他们预期的稍高,它也只用于示例。然而,generate_actual_password 的作者遇到了一个问题——他们如何证明他们对 generate_unpredictable_password 的调用从未遵循返回可预测答案的路径?

在现实生活中,这比这要复杂一点,因为可能存在一定程度的系统熵——所以回退可能更像是 return random.choice(["p4ssword", "passw0rd", "p4ssw0rd"]) 或者更具可变性,因此只有在比 generate_actual_password 作者预期的更好的几率下才能进行统计预测。但这并没有真正使事情更具可证明的安全性;它主要意味着,如果你尝试以显而易见的方式捕获问题——if returned_password == "p4ssw0rd": raise UhOh——那么它将不起作用,因为 returned_password 可能而是 p4ssword 甚至 pa55word,或者只是从少于 2**64 种可能性中选择的一个任意 64 位序列。因此,这个粗略的草图确实给出了“比预期更可预测”的回退行为的后果的正确总体想法,即使它完全不公平地对待 Linux 内核团队在不诉诸破坏向后兼容性的情况下缓解此问题实际后果的努力。

这种设计普遍被认为是一个坏主意。据我们所知,没有任何用例是你真正想要的这种行为。它导致在真实系统上使用不安全的 ssh 密钥,并且许多类 Unix 系统(包括至少 Mac OS X、OpenBSD 和 FreeBSD)已修改其 /dev/urandom 实现,使其在此情况下从不返回可预测的输出,方法是让读取阻塞,或者干脆拒绝运行任何用户空间程序,直到系统 RNG 已初始化。不幸的是,Linux 到目前为止未能效仿,因为经验表明启用阻塞行为会导致一些现有的发行版无法启动。

相反,引入了新的 getrandom() 系统调用,使得用户空间应用程序可以安全地访问系统随机数生成器,而不会在现有 Linux 发行版的系统初始化过程中引入难以调试的死锁问题。

getrandom() 可用性对 Python 的影响

在引入 getrandom() 系统调用之前,根本无法以可证明安全的方式访问 Linux 系统随机数生成器,因此我们被迫采用从 /dev/urandom 读取作为可用的最佳选项。然而,由于 getrandom() 坚持引发错误或阻塞而不是返回可预测数据,并且还有其他优点,因此它现在是 Linux 上访问内核 RNG 的推荐方法,直接读取 /dev/urandom 被降级为“遗留”状态。这使 Linux 进入了与 Windows 等其他操作系统相同的类别,Windows 根本不提供 /dev/urandom 设备:实现 os.urandom() 的最佳可用选项不再仅仅是从 /dev/urandom 设备读取字节。

这意味着以前别人的问题(Linux 内核开发团队的问题)现在是 Python 的问题——给定一种检测系统 RNG 未初始化的方法,每次我们尝试使用系统 RNG 时,都必须选择如何处理这种情况。

它可以简单地阻塞,正如 3.5.0 中有些意外地实现的,以及 Victor Stinner 的竞争 PEP 中提议的

# artist's impression of the CPython 3.5.0-3.5.1 behavior
def generate_unpredictable_bytes_or_block(num_bytes):
    while not system_rng_is_ready:
        wait
    return unpredictable_bytes(num_bytes)

或者它可以引发错误,正如此 PEP 所提出的(在*某些*情况下)

# artist's impression of the behavior proposed in this PEP
def generate_unpredictable_bytes_or_raise(num_bytes):
    if system_rng_is_ready:
        return unpredictable_bytes(num_bytes)
    else:
        raise BlockingIOError

或者它可以显式模拟 /dev/urandom 的回退行为,正如在 3.5.2rc1 中实现的,并且预计在 3.5.x 周期其余部分保持不变

# artist's impression of the CPython 3.5.2rc1+ behavior
def generate_unpredictable_bytes_or_maybe_not(num_bytes):
    if system_rng_is_ready:
        return unpredictable_bytes(num_bytes)
    else:
        return (b"p4ssw0rd" * (num_bytes // 8 + 1))[:num_bytes]

(并且与上面 /dev/urandomgenerate_unpredictable_password 草图相同的注意事项适用于此草图。)

CPython 和标准库有五个地方尝试使用操作系统随机数生成器,因此这五个地方都需要做出这个决定:

  • 初始化 SipHash,用于保护 str.__hash__ 及其相关功能免受 DoS 攻击(在启动时无条件调用)
  • 初始化 random 模块(在导入 random 时调用)
  • 服务用户对 os.urandom 公共 API 的调用
  • 更高级别的 random.SystemRandom 公共 API
  • PEP 506 添加的新 secrets 模块公共 API

以前,这五个地方都使用相同的底层代码,因此以相同的方式做出此决定。

这个问题最初之所以被注意到,是因为 3.5.0 将该底层代码切换到了 generate_unpredictable_bytes_or_block 行为,结果发现一些罕见的案例,Linux 启动脚本尝试在系统初始化期间运行 Python 程序,Python 启动序列在尝试初始化 SipHash 时阻塞,然后这触发了死锁,因为系统停止执行任何操作——包括收集新的熵——直到 Python 脚本被外部计时器强制终止。这一点尤其令人遗憾,因为相关脚本从不处理不受信任的输入,因此 SipHash 根本不需要使用可证明不可预测的随机数据进行初始化。这促使 3.5.2rc1 更改为在所有情况下模拟旧的 /dev/urandom 行为(通过以非阻塞模式调用 getrandom(),然后在系统调用指示 /dev/urandom 池尚未完全初始化时回退到读取 /dev/urandom)。

我们不知道 Fedora/RHEL/CentOS 生态系统中是否也存在此类问题,因为这些发行版的构建系统使用 chroot 在运行不支持 getrandom() 系统调用的旧操作系统内核的服务器上,这意味着 CPython 当前的构建配置会编译掉对该系统调用的运行时检查 [10]

由于 random 模块作为导入的副作用调用 os.urandom 来播种默认的全局 random.Random() 实例,也发现了类似问题。

我们尚未收到关于直接调用 os.urandom()random.SystemRandom() 在 3.5.0 或 3.5.1 中阻塞的特定投诉——只有关于在解释器启动和导入 random 模块的副作用期间隐式阻塞的问题报告。

独立于此 PEP,前两个情况已更新为永不阻塞,无论 os.urandom() 的行为如何。

其中 PEP 524 提议使后面三个情况(secrets 模块)全部隐式阻塞,而此 PEP 提议仅对最后一个情况(secrets)采用该方法,os.urandom()random.SystemRandom() 则在检测到底层操作系统调用会阻塞时引发异常。

参考资料

除了本文档和 Victor 的竞争 PEP 中涵盖的附加背景信息外,还可以参考 Victor 之前收集的相关信息和链接:https://haypo-notes.readthedocs.io/summary_python_random_issue.html


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

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