RFC-0080:检测 VMO 不可变性

RFC-0080:检测 VMO 不可变性
状态已接受
区域
  • 内核
说明

此 RFC 提出了一个新标志“ZX_INFO_VMO_IMMUTABLE”,可用于确定 VMO 是否不可变以及自创建以来是否一直不可变。

作者
审核人
提交日期(年-月-日)2021-02-18
审核日期(年-月-日)2021-03-29

摘要

此 RFC 提出了一个新标志 ZX_INFO_VMO_IMMUTABLE,可用于确定 VMO 是否不可变以及自创建以来是否一直不可变。

设计初衷

添加用于检查不可变性的机制有以下三个主要原因。

提高使用共享内存的安全性

编写使用共享内存的安全且正确的程序很难。某些程序不需要完全可变的 VMO,它们可能会向 VMO 写入一次,然后只从中读取。

如果能够对 VMO 进行不可变性等限制的测试,则更容易缩小安全漏洞面并验证正确性。

FIDL 中的大型消息支持

我们明确选择将通道消息限制为 64k,并让需要更大消息大小的应用自行寻找解决方法。

未来,FIDL 可能会支持大型消息。处理此问题的一种可能方法是将消息封装为离线 VMO。如果 FIDL 定义包含 Box<Type>,则 FIDL 会自动将该消息部分复制到 VMO。

另一种处理方式可能是让所有大于 64k 的 FIDL 消息自动发送在 VMO 中。已撤消的 RFC 采用的就是这种方法。

在这两种情况下,消息的收件人都无法验证消息在他们阅读时不会发生变异。如果有办法确认 VMO 不会被修改,则更容易验证 VMO 的使用是否安全,并且可能还能发现优化机会。

Overnet

Overnet 是通过网络传输消息的渠道级代理。

Overnet 示意图

Overnet 是一个透明代理。Overnet 客户端发送通道消息的方式与发送到本地进程的方式相同。然后,这些消息会由 Overnet 序列化,通过网络发送,并在另一端重建。接收进程接收消息的方式与接收任何其他渠道消息的方式相同。

使用 Overnet 的应用会看到以下内容:

从应用角度查看

Overnet 目前不支持发送包含 VMO 的消息。这对于需要发送大于现有 64k 通道限制的消息的多个 Overnet 客户端来说是一个难题,否则它们将在 VMO 中发送大量数据(封装在 fuchsia.mem.Buffer 或 fuchsia.mem.Data FIDL 类型中)。

在单个机器中,无论 VMO 是可变的还是不可变的,都会通过引用传递,也就是说,在通道中发送 VMO 不会导致 Zircon 隐式复制它。这种方法简单且高效。 但是,在网络上通过引用在多台机器之间传递数据会效率低下且不切实际。相反,传值方式为“按副本传递”适合这些场景。

但问题在于,按副本传递并不总是安全的。由于 Overnet 是一个透明代理,因此远程进程希望对同一 VMO 的引用保持同步。通过复制传递会更改 VMO 的语义,并且可能会破坏应用。例如,假设有一个媒体播放器进程向远程音箱进程发送 VMO,然后将歌曲写入 VMO,那么音箱将永远不会收到该歌曲,也无法播放该歌曲,因为在歌曲首次发送时,它并不存在于 VMO 中。

幸运的是,在某些情况下,通过复制传递是安全的。其中一种情况是,到达 Overnet 代理的 VMO 不可更改。应用已放弃对数据进行更改的功能,因此可以保证对数据创建的任何副本都将保持一致。

如果 Overnet 能够检测不可变的 VMO,则在遇到可变的 VMO 时可以快速失败。这样可以避免导致应用静默崩溃的细微 bug。

设计

zx_info_vmo_tflags 字段添加了一个新的 ZX_INFO_VMO_IMMUTABLE 标志,用于指示 VMO 是否以可保证和验证的方式不可变。

目前,如果 VMO 是使用 zx_vmo_create_child 系统调用创建的,并且 ZX_VMO_CHILD_SNAPSHOTZX_VMO_CHILD_NO_WRITE 标志已设置,而 ZX_VMO_RESIZABLEZX_VMO_DISCARDABLE 标志未设置,则 VMO 是不可变的。不过,如果我们推出了创建不可变 VMO 的新方法,则用于确定 VMO 是否不可变的公式未来可能会发生变化。

确定何时将 VMO 设为不可变由应用决定。

实现

实现相对简单。计算 ZX_INFO_VMO_IMMUTABLE 值所需的大多数状态已存储在内核中。

唯一的例外是,有一个位表示 VMO 在创建时不可写。此值可存储在 VmObjectDispatcher 中。

性能

添加该标志不会对性能产生直接影响。

不过,创建不可变的 VMO 目前需要支付不小的费用,因此仅应在绝对必要时使用。对于 Overnet,不可变克隆的相对开销可能不太重要,因为 Overnet 已经在执行发送消息的大量工作,包括网络通信。

我们运行并比较了两个基准测试,以进一步了解不可变克隆 (CL) 的开销:

  • 创建 VMO、写入、读取、关闭
  • 创建 VMO、写入、不可变克隆、读取克隆的 VMO、关闭

需要强调的是,此基准反映的是当前的性能,但仍有优化克隆的空间,尤其是对于不可变克隆的情况。

此基准测试是在 Intel NUC NUC7i5DNHE 上记录的。

快照克隆的影响

在 64 KiB 之前,执行不可变克隆的开销为 2 微秒,之后开销会开始增加。超过 2 MiB 后,费用会显著增加。

不过,值得注意的是,与整个“创建-写入-克隆-读取-销毁”流程相比,克隆的相对开销会随着大小的增加而降低:

快照克隆的相对影响

对于 64k VMO,不可变克隆会额外增加 20% 的时间,这是一个相当大的开销,但随着大小的增加,开销会逐渐减小到零。

安全注意事项

请务必验证声称通过此技术实现不可变性的 VMO 是否确实不可变。如果 VMO 被错误地识别为不可变,则会导致安全 bug。

隐私注意事项

无影响。

测试

标志实现将通过单元测试进行验证。

文档

目前,ZX_INFO_VMO_* 标志缺少文档,读者需要参阅代码库。在此变更过程中,我们将编写文档来介绍 ZX_INFO_VMO_* 选项。

缺点、替代方案和未知情况

针对确定不可变性的不同属性的多个标志

此 RFC 建议使用单个 ZX_INFO_VMO_IMMUTABLE 标志来确定不可变性。而是可以在用户端使用多个单独的标志来确定这一点:

  • ZX_INFO_VMO_IS_CHILD_SNAPSHOT
  • ZX_INFO_VMO_IS_INITIALLY_WRITABLE
  • ZX_INFO_VMO_IS_RESIZABLE
  • ZX_INFO_VMO_IS_DISCARDABLE

基于底层状态使用多个标志的好处在于,无需专用标志即可实现不可变性,并且只会公开可用于不同组合的底层属性。

不过,通过这些标志确定不可变性相对较为复杂,而不可变性本身可以被视为一种基本属性。通过为不可变性提供专用标志,可以更轻松地避免安全 bug。此外,只需一个标志,就可以更轻松地引入创建不可变 VMO 的新方法。

检查最后剩余的引用

您可能很想使用 ZX_INFO_HANDLE_COUNT 检查进程是否持有 VMO 的最后一个句柄,并假定如果是,则任何其他进程都无法更改 VMO。不过,这种假设并不正确,因为句柄只是 Zircon 中一种引用。例如,VMO 的内存映射不会计入 ZX_INFO_HANDLE_COUNT 值,因此进程无法使用此值来确定它是否对 VMO 具有独占访问权限。

通过 FIDL 向 Overnet 传达安全信息

可以对 FIDL 语言进行更改,以便标记 VMO 是不可变的还是“可跨网络传输”的。如果 Overnet 能够确定给定 VMO 是否具有此属性,则可以决定是发送 VMO 还是拒绝 VMO。

您可以通过多种方式将此类信息传达给 Overnet:

  • 在句柄上的未使用的位中发送它
  • 添加了一种机制,用于在 VMO 上存储 Overnet 可以读取的位
  • 在 FIDL 消息标头中预留 64 位,以将 VMO 句柄标记为不可变
  • 让 Overnet 通过协议与客户端通信

遗憾的是,这些方法都存在缺点,使其复杂且不如所提解决方案。

在先技术和参考文档

VMO 密封 已废弃的 RFC 提出了一种机制,用于限制可对 VMO 执行的操作。您可以使用此方法来创建和测试不可变的 VMO,而无需克隆。不过,与简单的不可变性检查相比,VMO 密封对 VMO 实现的侵入性更高。

近期,我们一直在研究 zx_vmar_map 的安全功能,例如 ZX_VM_ALLOW_FAULTS。未来,我们可能需要向 zx_vmar_map 添加与不可变性相关的功能。