LWN:再次尝试vDSO中实现getrandom()!

关注了就能看到更多这么棒的文章哦~

Another try for getrandom() in the vDSO

By Jonathan Corbet
July 4, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/980447/

看起来,随机数永远不够随机,而且生成速度也不够快。内核的 getrandom() 系统调用经过多年的讨论后,可能被大多数用户认为足够安全,但它仍然是一个系统调用。Linux 系统调用相对较快,但它们必然比直接调用函数慢。为了加快将安全随机数据提供给用户空间的速度,Jason Donenfeld 已将 getrandom() 的实现 放入 虚拟动态共享对象 (vDSO) 区域。

随机数据被用在无数的应用中,包括对自然现象的建模、生成像 UUID 这样的标识符以及游戏玩法;这正是 NetHack 能够在一个黑暗房间的一个角落里召唤出三个炎魔的原因。安全相关的操作,例如生成 nonce 和密钥,也大量使用随机数据——而且严重依赖这些数据实际上是随机的。一些应用需要大量的随机数据,以至于它们在 getrandom() 系统调用中花费了大量时间。

解决这个问题的一个可能方案是在用户空间生成随机数,也许用来自内核的随机数据来播种;多年来,许多开发人员都采用了这种方法。但是,正如 Donenfeld 在他的补丁系列的附信中解释的那样,这种方法并不理想。内核最清楚地了解系统中熵的数量以及生成真正随机数据所需的熵。它还知道可能损害随机数生成器并导致重新播种的事件,例如虚拟机分叉。他总结道:

你可以说,在某个时间点 T1 扩展 getrandom() 种子的用户空间 RNG 几乎总是 更差,因为这样或那样,它们都不如每次需要随机数时都调用 getrandom()。

始终调用 getrandom() 可以确保最好的随机数据,但随之而来的性能问题仍然存在。将该函数移到 vDSO 可以帮助解决这个问题。

vDSO 中的 getrandom()

vDSO 是一种特殊的机制,用于加速需要一些内核参与但可以在用户空间同样好地执行的任务。它包含代码和数据,这些代码和数据由内核直接提供给用户空间,位于映射到每个线程地址空间的内存区域中。经典的 vDSO 函数是 gettimeofday(),它返回内核保存的当前系统时间。这个函数可以实现为一个系统调用,但这会减慢频繁查询时间的应用程序的速度,而这类应用程序很多。因此,Linux vDSO 包含 gettimeofday() 的实现;该实现可以简单地读取与内核共享的内存中的时间变量并将其返回给调用者,从而避免进行系统调用。

getrandom() 是一种类似的函数;它从内核读取数据并将其返回给用户空间。因此,=getrandom()= 的 vDSO 实现可能是有意义的。但是,这种实现必须小心进行;它应该返回与直接调用内核一样随机的数据,并且必须能够抵抗可能损害线程随机数生成状态的事件类型(例如,分叉)。

在 Donenfeld 的实现中,用户空间程序将继续像往常一样调用 getrandom(),无需任何更改。然而,在幕后,C 库(为系统调用提供 getrandom() 包装器)内部需要一些重大的更改。

状态区域分配

随机数生成器在存储在内存中的某些状态数据上运行。当需要随机数据时,伪随机数生成器会根据该状态生成随机数据,在此过程中修改状态。每个线程都必须拥有自己的状态,并且必须注意在进程分叉、核心转储、虚拟机分叉或检查点等事件期间避免暴露该状态。该状态应该定期用随机数据重新播种,特别是在其内容可能已被破坏的任何时候。

getrandom() 的 vDSO 实现要求由内核分配用于此状态的内存。因此,C 库首先要做的是为其认为可能运行的线程数分配此状态存储空间。这通过一个新的系统调用来完成:

struct vgetrandom_alloc_args {
    u64 flags;
    u64 num;
    u64 size_per_each;
    u64 bytes_allocated;
};

void *vgetrandom_alloc(struct vgetrandom_alloc_args *args, size_t args_len);

args 指向的结构描述分配请求,而 args_len 为 sizeof(*args)=;这使得结构可以在将来需要时以兼容的方式进行扩展。在该结构中,=flags 目前必须为零,而 num 是内核被请求分配的线程状态区域数量。成功返回时,=num= 将被设置为实际分配的区域数量,=size_per_each= 描述状态区域的大小,而 bytes_allocated 是分配的总内存量。返回值将指向分配区域的基地址。

分配的区域是普通的匿名内存,只是它将在内核中使用多个虚拟内存区域标志进行特殊标记。=VM_WIPEONFORK= 标志会导致进程分叉时其内容被清零(以便两个进程不会生成相同的随机数流),=VM_DONTDUMP= 会阻止其内容被写入核心转储,而 VM_NORESERVE 会导致其不被计入进程的锁定内存限制。Donenfeld 还添加了一个新的标志供此区域使用:=VM_DROPPABLE= 允许内存管理子系统在需要时简单地回收内存;因为这是匿名内存,所以访问已回收的内存会导致分配一个新的、用零填充的页面。结果是内存应该保持私有,但内核可以随时对其进行清零(或回收,这具有相同的效果)。

生成随机数据

内核还与包含此结构的 vDSO 共享一些内存:

struct vdso_rng_data {
u64 generation;
u8  is_ready;
};

这个结构被 getrandom() 的 vDSO 版本使用,该版本具有以下原型:

ssize_t vgetrandom(void *buffer, size_t len, unsigned int flags,
                   void *opaque_state, size_t opaque_len);

前三个参数反映了 getrandom(),描述了所需的随机数据量以及调用是否应该阻塞等待内核的随机数生成器准备就绪。最后两个参数描述了由 vgetrandom_alloc() 分配的状态区域之一。这个函数的任务是提供与 getrandom() 相同的行为。

它首先查看共享结构中的 is_ready 字段;如果内核的随机数生成器尚未准备好,=vgetrandom()= 将只调用 getrandom() 来处理请求。但是,一旦随机数生成器初始化完毕,这种回退就不再需要。因此,接下来要做的就是将 generation 计数(跟踪内核的随机数生成器已被重新播种的次数)与存储在状态区域中的生成计数进行比较。如果这两个计数不匹配,那么必须使用从内核获取的随机数据来重新播种状态区域。

当状态区域首次分配时,它会被清零,因此在其中找到的生成编号将为零,这永远不会与内核的生成编号匹配;这会导致状态区域在第一次调用 vgetrandom() 时被播种。如果该区域已被内核清除,例如由于分叉 (VM_WIPEONFORK) 或内存被回收 (VM_DROPPABLE) 的结果,也会发生同样的事情。因此,内核能够随时清除该内存,因为知道 vgetrandom() 会做正确的事情。

一旦知道状态区域处于良好状态,=vgetrandom()= 就会使用它来生成请求的随机数据,使用与内核本身中使用的算法相同的算法。安全地执行此计算有点棘手;如果进程在进行中分叉或进行核心转储,则存储在堆栈上的任何数据都可能被暴露。因此,=vgetrandom()= 必须使用 ChaCha20 流密码 的实现,该实现完全不使用堆栈。补丁系列只包括该密码的 x86 实现;其他架构似乎肯定会紧随其后。

作为在将生成的数据返回给调用者之前的最后一步,=vgetrandom()= 再次检查生成编号。例如,如果状态区域在调用执行期间被内核清除,则生成编号检查将失败。在这种情况下,=vgetrandom()= 会丢弃其工作并重新开始。

Donenfeld 将这项工作的最终结果描述为“相当出色(对于 uint32_t 生成,大约快了 15 倍)”,并高兴地指出“它似乎在工作”。

前景

LWN 最近在 2023 年初 关注了这项工作。当时,有一些反对意见,其中很多都集中在对内存管理子系统的 VM_DROPPABLE 更改上,这些更改包括一些棘手的、x86 特定的技巧。当补丁系列的 第 15 版 在几个月后发布时,=VM_DROPPABLE= 保持不变,但逻辑已大大简化,以期解决这些问题,显然已经成功了。现在似乎没有人反对包含这个系列。

截至目前版本(20),这项工作已被添加到 linux-next 中进行更广泛的测试;如果一切顺利,它可能会在本月晚些时候的 6.11 合并窗口中尽快进入上游。当然,“如果一切顺利”包括通过 Linus Torvalds 的审核,这次他没有发表评论;不过,他之前 对之前的版本并不满意。如果 mainline 合并发生,那么将 C 库集成到所需的更改的工作就可以开始了。最终结果将是一个重大的内部更改,但用户应该注意的唯一事情是他们的程序运行速度更快。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

1ca2455eb89bae733f2f37fa1d496e5a.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值