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 目前不支持发送包含 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_t
的 flags
字段添加了一个新的 ZX_INFO_VMO_IMMUTABLE
标志,用于指示 VMO 是否以可保证和验证的方式不可变。
目前,如果 VMO 是使用 zx_vmo_create_child
系统调用创建的,并且 ZX_VMO_CHILD_SNAPSHOT
和 ZX_VMO_CHILD_NO_WRITE
标志已设置,而 ZX_VMO_RESIZABLE
和 ZX_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
添加与不可变性相关的功能。