Linux 应用通常从内核的密码学安全生成器取得随机字节,不是每输出一位,就必须重新采集一位物理噪声。不过,初始化和状态复用仍然重要。机器繁忙不自动意味着安全,机器安静也不自动意味着故障。判断之前,应确认所用接口、内核版本,以及启动或克隆流程,不能只凭一个计数器就宣布密钥已经不安全。

区分采集与输出

Linux 6.12 的实现把输入混入基于 BLAKE2s 的池,并用 ChaCha20 生成输出。这是该版本的实现细节,不是所有未来版本永远不变的接口契约。硬件来源、中断时序和引导程序输入,按具体平台的信任规则参与。加入字节与把这些字节计入初始化熵估算,是两件不同的事;内核不能假设用户任意提供的一串字节都包含新鲜、不可预测的信息。

在这个实现中,poolsize 与初始化完成后的熵计数都达到二百五十六位,读取不会像消耗燃料一样扣减计数。因此,旧手册里四千零九十六位的描述,不应直接变成所有机器的告警阈值。应记录运行内核并检查对应行为,不能声称所有 Linux 版本的计数都一样,也不能仅因数字较小就判定随机输出薄弱。

选择会等待初始化的接口

默认 getrandom 会等到初始化完成。带 GRND_NONBLOCK 时,尚未初始化会报告 EAGAIN,使应用能够说明状态而不无限等待。初始化以后,默认源的一次请求若不超过二百五十六字节,具有文档规定的完整读取与信号保证;调用方仍应检查错误和返回长度。更大请求要正常处理部分返回和中断,不能把小请求的保证扩大到所有缓冲区。

random 设备从 Linux 5.6 起改变了行为:初始化后不再因为所谓熵耗尽而阻塞。在核对的 Linux 6.12 源码中,GRND_RANDOM 也不选择一个独立、会被耗尽的输出池。有些手册仍保留旧行为描述,不应把历史限制与当前内核结论混合。直接读取 urandom 可能发生在初始化之前;早期启动生成密钥,应使用支持初始化等待的接口。

坏掉的样子

早期启动仍可能缺少足够的可信计入输入。容器中缺少设备节点则是另一类接口问题:getrandom 不需要设备路径,普通容器共享宿主内核。操作等待时,应有明确的启动失败或等待策略,而不是改用时间戳、可预测种子或不安全的备用输出。让服务尽快启动,不是降低密钥生成要求的理由。

保存的种子可以帮助下一次启动,但把文件写回 urandom 并不会自动增加熵计数。一个三十二字节文件,或者某种硬件设备的存在,都不能单独保证立即就绪。应使用发行版提供的种子管理机制,了解它何时、依据什么信任条件计入熵。保护种子文件,避免在克隆镜像里分发相同的保存种子,并让引导程序专用的种子处理与操作系统流程一致。

复制磁盘镜像和恢复运行中的虚拟机,是两种不同风险。前者可能复制种子文件或已经生成的主机密钥,后者可能复制正在使用的生成器内存状态。新输入及虚拟机生成标识处理,可以让后续状态分离。未来输出完全相同是需要调查的风险,不是每次克隆必然出现的结果;两台机器使用同一份种子,也不足以证明它们全部生成器状态完全一致。

按版本限定的输入混合与初始化计入连接生成器,非阻塞请求按就绪状态分支。
输入计入、初始化与输出各有边界。仅标明的 Linux 6.18.44 结果属于保留历史观察;本轮没有请求随机字节。

快速验证

在 Linux 上运行下面的 Python 3 脚本。它读取计数器,并请求一次非阻塞的三十二字节输出;不会打印随机内容,不读取私钥,不更改种子文件,也不需要管理员权限。这样可以观察接口是否就绪,而不把诊断过程变成密钥材料收集或系统配置变更。

from pathlib import Path
import json
import os
import platform

def counters():
    result = {}
    for name in ("entropy_avail", "poolsize"):
        try:
            result[name] = int(Path("/proc/sys/kernel/random", name).read_text())
        except OSError as error:
            result[name] = type(error).__name__
    return result

before = counters()
try:
    sample = os.getrandom(32, os.GRND_NONBLOCK)
except BlockingIOError:
    print(json.dumps({"kernel": platform.release(), "ready": False,
                      "counters": before}))
else:
    assert len(sample) == 32
    print(json.dumps({"kernel": platform.release(), "ready": True,
                      "bytes_returned": len(sample), "before": before,
                      "after": counters()}))

保留的历史审计输出记录了 Linux 6.18.44、Python 3.12.14 上的结果: ready=true、bytes_returned=32,前后两个计数器都为二百五十六。这只是已初始化主机上的结果,不是早期启动、硬件质量或恢复虚拟机的测试。出现 EAGAIN 时程序报告 ready=false,这个分支没有被复现。本次仅审阅源码,没有请求随机字节。其他系统调用错误会向外传播,不能直接解释成尚未初始化,也不能把未执行分支标记成实测通过。

成功调用说明当前接口在这一时刻提供了要求的字节,不是随机性统计检验,更不是密码学不可预测性的证明;比较两次样本也无法给出这种证明。如果 proc 访问受限,计数器可能显示错误类型,而系统调用仍然成功。应分别解释这些情况,不能把诊断信息不可读直接当作熵故障,也不能把成功返回当作所有启动和克隆风险均已排除。

用直接证据调查身份复用

密钥文件的修改时间不能证明生成时使用了哪个随机源。文件会跨重启保存,复制可以保留时间戳,系统时钟也可能调整;看不到初始化日志同样不能证明初始化失败。应检查镜像准备流程、密钥生成代码、启动策略和重复的公钥指纹。确认身份重复或泄露时再制定轮换措施,不应仅凭时间戳对比就宣布一批密钥已经受损。

来源

  • Linux 6.12 随机数实现,初始化计数、getrandom、sysctl 与虚拟机分叉重播种,已核验 2026-10-15。
  • Linux getrandom(2),初始化等待、小请求与错误;历史 GRND_RANDOM 描述需结合版本,已核验 2026-10-15。
  • Linux random(4),写入不计熵及 Linux 5.6 阻塞变化;历史池大小不代表当前内核,已核验 2026-10-15。
  • systemd 256 随机种子服务,种子生命周期、熵信任策略与镜像准备,已核验 2026-10-15。