RFC-0225:Fxblob:在 Fxfs 中存储 blob | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 从 Fxfs 提供 blob,从系统中移除 Blobfs 和 FVM。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2023-07-25 |
审核日期(年-月-日) | 2023-09-05 |
摘要
一项提案,旨在将 Fuchsia 的卷管理器 (FVM) 和 blob 存储文件系统 (Blobfs) 替换为 Fuchsia 的下一代文件系统 Fxfs。系统 blob(包括库、二进制文件和静态配置)将存储在与 Blobfs 具有相同属性(内容寻址、经过验证且不可变)的 Fxfs 卷中。
设计初衷
Fxfs 是 Fuchsia 的下一代文件系统,旨在提供出色的性能、可更新性和丰富的功能集。事实证明,它是存储团队可依赖的坚实基础,越来越多的工程工作都集中在增强 Fxfs 上。Blobfs 和 FVM 是在 Fuchsia 的早期创建的,并且在构建时考虑了特定用途:
- Blobfs 旨在提供经过验证且不可变的内容寻址存储空间。
- FVM 旨在让 Blobfs 和 Minfs(通用可变文件系统)共享单个块设备,并随着分区增长而从设备中动态分配。
在 Fuchsia 不断演进的过程中,这些系统发挥了重要作用,但它们已成为存储团队实现以下目标的限制因素:性能、可更新性和可维护性。
性能改进
我们已经完成了许多可以轻松提升 Fuchsia 存储堆栈性能的改进,现在需要对文件系统进行更复杂的更改才能进一步提升性能。
例如,由于 Blobfs 是单线程实现,因此其写入性能受到限制。向文件系统添加多线程支持并非易事,需要进行重大更改。
由于写入放大率较高,Blobfs 格式不利于我们提升性能。必须更新存储在各种位置的多个元数据结构,才能写入文件,这会导致写入放大。此外,Blobfs 的日志是基于块的(与 Fxfs 等逻辑日志相比),在许多情况下效率较低。
这些工作必须在我们维护的每个文件系统中重复,这会大幅增加使用各种文件系统的成本。能够专注于单个文件系统 (Fxfs) 是一件好事,这也是提出此提案的主要动机。
我们的 Fxblob 原型已经证明了 Fxfs 的性能优势。 在 Intel NUC 上,Fxblob 是:
- 在 blob 中分页的速度提高了 55-80%,
- 写入单个 Blob 的速度提高了 17%,
- 与 Blobfs 相比,并发写入多个 blob 的速度提高了 130%。
我们还在不断改进性能。
可更新性
Fxfs 的关键设计原则之一是可更新性:我们设计 Fxfs 时考虑到其存储格式会随时间推移而发生变化。如需了解详情,请参阅 RFC-0136:Fxfs。
FVM 和 Blobfs 在设计时并未考虑到这一点,在实践中,目前无法在不分叉格式和实现的情况下对 FVM 或 Blobfs 进行任何重大更改。
如果需要添加功能,或者需要对我们在磁盘上存储数据的方式进行性能优化,则 FVM 和 Blobfs 将非常难以使用。已经有多个这样的示例:
- RFC-0005:Blobfs 快照,该 RFC 提出了对 FVM 的格式更改,以提高 Fuchsia 对软件更新的弹性。由于更改 FVM 格式被认为风险太高且技术上太复杂,因此此 RFC 已被撤消。
- Blobfs 中的紧凑 Merkle 树。这项功能于 2021 年实现,旨在节省空间,但由于该功能的发布复杂,因此从未完全发布。C++ Blobfs 仍然支持紧凑布局和旧版布局。
与之相反,Fxfs 的磁盘格式会定期更改,并且几乎不会造成任何麻烦。在撰写本 RFC 时(2023 年 7 月),Fxfs 的版本为 31,自上次重大更改(版本 21,创建时间为 2022 年 6 月 14 日)以来,已经历 10 次格式更改。(请注意,即使这项破坏性更改本可以以向后兼容的方式进行,但出于实用原因,我们选择不这样做。)
可维护性
Fxfs 已被证明是一个经过充分测试且高效的工程师工作环境。它采用 Rust 实现,具有广泛的单元测试、集成测试和模糊测试覆盖率,并很好地封装了更细微或更难解决的问题(层文件合并、日志记录、事务和锁等)。此外,由于 Fxfs 的层文件在创建后不可变,并且元数据更新会附加到内存层文件(和日志)中,而不是分块写入磁盘,因此处理并发问题变得更加简单。
相比之下,FVM 存在大量的技术债务、缺乏可靠的测试覆盖率,并且其格式封装不当(Fuchsia 中的大量软件会解读 FVM 格式),所有这些都使得该系统非常难以更改。
如性能改进中所述,大多数改进都必须在我们维护的每个文件系统实现中复制,这会成倍增加改进的成本。这也适用于维护费用。需要维护的软件越少,所需的工程工作量就越少。
请注意,由于现有产品依赖于 FVM 和 Blobfs,因此我们在未来一段时间内无法完全弃用它们。不过,我们预计 FVM 和 Blobfs 在现有产品上的稳定性足够高,维护费用可以忽略不计。
利益相关方
教员:
Reviewers:
- 安全团队 (joshlf@google.com)joshlf@google.com
- 存储空间团队 (csuter@google.com)
- 软件交付团队 (galbanum@google.com)
- 内核团队 (cpu@google.com)
咨询了:
- 软件交付团队 (galbanum@google.com)
- 安全团队(joshlf@google.com、mvanotti@google.com、markdittmer@google.com)joshlf@google.commvanotti@google.commarkdittmer@google.com
社交:
我们首先向相关利益相关方介绍了该设计(软件交付团队在早期就参与了,安全/内核团队在初始原型更详尽地完善后才参与)。
要求
- Fxblob 必须提供与 Blobfs 的语义完全匹配的文件系统 API。即:
- Fxblob 必须允许创建静态文件(写入一次,读取多次)。
- Fxblob 必须具有内容可寻址的文件名(即文件的名称必须是其内容的 Merkle 根)。
- 为了实现可移植性和便于迁移,应使用 Blobfs 中的相同 Merkle 树算法(即 Blobfs 中的文件名应与 Fxblob 中的文件名匹配)。
- Fxblob 必须先确保文件内容与其 Merkle 根哈希相符,然后才能允许客户端读取这些文件。
- Fxblob 必须向 pkg-cache 提供一个包含系统上每个 blob 的扁平目录结构。
- Fxblob 必须支持可执行文件和分页。
- Fxblob 必须支持 RFC-0207 中指定的“传送 blob”线格格式。
- Fxblob 应在所有方面都具有与 Blobfs 相当或更好的性能。
- 在延迟时间和吞吐量方面,Fxblob 的读写性能应明显优于 Blobfs。
- Fxblob 的磁盘用量和内存用量应与 Blobfs 相当。
- Fxblob 必须具有精确的核算机制,以便根据系统中已知存在的其他 blob,预先确定存储预算并对可用空间建立合理的狭窄边界。
- Fxblob 应尽可能减少可信代码库 (TCB) 的扩展,即所有代码的集合,这些代码必须正确无误,才能确保 Fuchsia 的经过验证的执行的安全属性。Fxfs 不应需要信任,并且应将可信操作委托给其他位置。
设计
从架构的角度来看,目标是从图 1 转变为图 2:
图 1:Fuchsia 设备的当前存储架构。
图 2:适用于 Fuchsia 设备的 Fxblob 存储架构。
Fxfs 中的“blob”卷将是一个特殊卷,可模拟 blobfs 的属性(内容寻址性、不可变性等)。对于系统的上层,此更改是透明的,但系统的许多底层部分都需要进行更改:
- 系统汇编和构建工具
- 设备引导加载程序软件和工具(铺设和刷写),
- 恢复软件
- Fshost
- Fxfs,用于支持新的 blob 分区的特殊属性,
- Pkg-cache 和 pkg-resolver,以使用一组新 API 读取和写入 blob。
Fxfs 的变更
Fxfs 已经做好了适应这种架构的准备,它对卷提供了原生支持,并且采用了灵活且可随时间推移而更改的格式。
我们已完成原型实现(请参阅 //src/storage/fxfs/platform/src/fuchsia/fxblob),该实现实现了满足 Blob 存储属性的特殊卷。该卷只有一个根目录,无法使用 fuchsia.io 协议在其中创建文件或目录。相反,该卷提供特殊的 BlobWriter 协议,pkg-cache 将使用该协议将新的 blob 安装到该卷。
此卷中的文件使用与 blobfs 使用的相同的 Merkle 树算法进行内容寻址(文件名是根哈希)。Merkle 树是在安装 blob 时生成的,Fxfs 会确保 Merkle 根与文件名相匹配,然后再将文件持久化。每当从磁盘读取文件的某个部分时,Fxfs 都会根据文件的 Merkle 树验证内容,以确保完整性。
对 Fshost 的更改
Fshost 是一个低级组件,用于检测块设备并挂载其包含的文件系统,从而为系统的其余部分提供与 /blob 和 /data 的连接。
当 Fshost 配置为(通过静态配置)预期使用采用 Fxblob 格式的系统时,它会将 /blob 目录连接到 Fxfs 实例中的 blob 卷,并将 /data 目录连接到 Fxfs 实例中的数据卷。Fshost 还将继续支持 FVM+Blobfs 的旧版配置。
系统汇编和构建工具的变更
系统汇编是 Fuchsia build 的最后一步,其中工件会汇编到一组映像和清单中,这些映像和清单构成了 Fuchsia 系统映像。
系统汇编的输出之一是 FVM+Blobfs 映像,其中包含系统中的“基础”blob 集。我们将调整系统汇编,使其能够根据构建时配置发出包含这些 blob 的 Fxfs 格式映像。
我们还将以“稀疏”格式发出 Fxfs 映像,这种格式更适合传输到设备。使用的稀疏格式是 Android 稀疏格式,fastboot 已支持该格式,因此这是自然的选择。
设备引导加载程序(铺设和刷写)的变更
虽然铺路已正式弃用(RFC-0075:弃用基于 Zedboot 的铺路),但由于其仍被广泛使用,因此我们选择在铺路工作流中添加对 Fxfs 映像的支持。
刷写是依赖于 fastboot 协议的新型设备引导流程。与铺砌(用于解释收到的 FVM 映像)相比,此协议相对简单,因此这是一个简单的更改。(Fastboot 不会解析收到的原始映像,因此刷写 FVM 与刷写 Fxfs 映像没有区别;相反,铺路会解析 FVM 映像,因此必须向铺路工具添加自定义逻辑来处理 Fxfs 映像。)
上述稀疏映像格式既用于铺砌,也用于刷写,以减少传输到设备的字节数并减小内存占用空间。
软件包安装和 OTA 路径的变更
与 blobfs 不同,Fxblob 不支持 fuchsia.io 写入路径(该路径存在严重的性能问题)。相反,fxblob 支持新的写入 API,该 API 会为其数据平面(而非 FIDL)使用共享 VMO,从而减少写入 blob 所需的 FIDL 往返次数。
新 API 分为两个协议:fuchsia.fxfs.BlobCreator 和 fuchsia.fxfs.BlobWriter。BlobCreator 是一种可检测的协议,pkg_cache 使用它来创建新的 blob(如果尚不存在),而 BlobWriter 是一种私有协议,用于简化将 blob 数据流式传输到 Fxfs 的机制。
共享 VMO 的组织方式为环形缓冲区,这让我们可以对写入请求进行流水线处理。Pkg_cache 会填满环形缓冲区的一个部分,然后使用 BlobWriter.BytesReady
FIDL 方法通知 Fxfs 某些字节可用。Fxfs 会等待这些通知,从环形缓冲区加载一些数据,计算相应数据的 Merkle 哈希,并将数据流式传输到磁盘。
当最终的 BytesReady 请求发送到 Fxfs 时,Fxfs 会阻塞该请求,直到 Merkle 树完全计算并通过收到的内容进行验证;如果哈希和内容不匹配,Fxfs 会将此请求失败并返回 ZX_ERR_IO_DATA_INTEGRITY。在最终调用完成后,Fxfs 将确保文件可见(即对 fuchsia.io.Directory.ReadDirents)。
创建了一个客户端库来封装环形缓冲区管理。
流程如下所示:
对 Merkle 树验证、解压缩和 VMEX 处理进行了更改
Blobfs 目前拥有 VMEX capability,负责在需要时铸造可执行的 VMO,以及在读取时解压缩 blob 内容。由于 VMEX 允许执行任意代码,因此 blobfs 还依赖于沙盒化解压缩程序在读取时解压缩 blob 内容。(Blob 内容由外部提供,因此可以被视为不可信数据。)与解压缩相关的额外跳转和上下文切换会导致显著的性能开销。
通过 Fxblob,我们旨在优化分页路径的性能,这需要(除其他外)将解压缩移至 Fxfs 进程本身。不过,将更多功能集成到高度可信的组件中可能会带来安全问题。我们认为,通过移除对 Fxfs 的信任,可以解决这些问题。
为了实现此目标,需要将两个受信任的操作移出 Fxfs:
- 通过 Merkle 树验证文件内容,
- 使用 VMEX 资源铸造可执行页面。
为避免 Fxfs 需要保留 VMEX 功能,pkg-cache 将接管此责任。鉴于 pkg-cache 必须已被信任才能从 blob 名称映射到哈希,因此这不会实质性地影响分配给该组件的信任范围,并且无需将 Fxfs 视为高信任组件。
我们还需要向 /bootstrap/base_resolver
提供 VMEX 资源;此组件会提供基本集中的软件包。它与 pkg-cache 类似(事实上,其大部分业务逻辑都使用相同的库)。
为了使 Fxfs 完全不可信,我们还需要将 Merkle 验证移至其他位置(尽管 Fxfs 仍会存储和提供 Merkle 树内容)。您可以选择以下几种方式:
- 创建一个单独的高可信组件,该组件仅用于验证数据。
- 将此功能移至已获得高度信任的内核。
请注意,在打开 blob 时,pkg-cache 需要直接与可信验证程序通信,或者需要能够直接查询此可信验证程序,以便将验证程序正在使用的根哈希与给定 blob 的预期哈希进行比较。这样,即使遭到入侵的 Fxfs 完全更改了数据和 Merkle 树(因此验证程序会将被篡改的数据视为正确),pkg-cache 也能够检测到篡改并拒绝文件内容。
选择在何处进行 Merkle 验证归根结底是安全性和性能之间的权衡,值得深入探讨。因此,我们将在单独的 RFC 中解决此问题,目前验证将在 Fxfs 进程中进行。
请注意,Fuchsia 未来还需要支持另外两个功能,它们也依赖于 Merkle 树:Fs-verity 和 dm-verity,一些 Linux 应用分别使用它们来确保文件和映像完整性。Fuchsia 打算在 RFC-0082:在 Fuchsia 上运行未修改的 Linux 程序中支持这两者。在设计外部进程 Merkle 验证时,还需要考虑这些用例。
实现
我们采用了以下方法:实现功能原型,并在测试和开发环境中启用这些原型以进行“浸泡”,这种方法到目前为止效果良好。我们打算继续在未来的工作和未完成的功能中采用这种做法(如需了解与 Merkle 验证相关的工作,请参阅“安全”部分)。
功能将受 build 时启用的功能标志控制。例如,pkg-cache 会检测是否存在新的 Fxblob 读写 API,并在可用时优先使用这些 API,否则会回退到常规的 fuchsia.io 机制。
性能
对原型进行基准测试的初步结果表明,读写性能有所提升。在 Intel NUC 上,Fxblob 是:
- 在 blob 中分页的速度提高了 55-80%,
- 写入单个 Blob 的速度提高了 17%,
- 与 Blobfs 相比,并发写入多个 blob 的速度提高了 130%。
性能的其他重要方面包括内存消耗和存储用量。我们正在分析这些方面,并将解决 Blobfs 和 Fxblob 之间的任何重大差距。一般来说,Rust 组件往往需要更多内存,但 Blobfs 中的大部分内存用量是由于缓存的页面造成的,而这对 Fxblob 来说也是相同的开销。
Fxfs 的存储用量比 Blobfs 更复杂,这是因为 Fxfs 会将元数据更新存储为一组层文件中的一系列更改,而不是为 inode、extent 等分配固定的空间。不过,由于移除了 FVM(它需要为自己的元数据分配一些磁盘空间,并会在分配的 slice 中引入内部碎片),我们也有望重新获得一些存储空间。此外,Fxfs 日后更改其格式的灵活性更高,从而可以提升存储性能(例如优化层文件格式)。在撰写此 RFC 时,我们的原型实现使用与 Blobfs 相当的磁盘可用空间来存储等量的 blob。
工效学设计
这些更改对大多数 Fuchsia 组件来说是透明的。目前,只有 pkg-cache 会直接与 Blobfs 交互。
向后兼容性
我们将继续支持采用旧版配置 (FVM + Blobfs) 的系统,这些系统将支持已发布的产品。在相关产品停用之前,我们将继续为旧版配置(以及适当的测试基础架构)提供支持。
所有未来的 Fuchsia 产品都将使用此新配置,包括大多数开发者。
没有计划迁移路径;设备将使用 Fxblob 或使用旧版配置。
安全注意事项
Blobfs 是 Fuchsia 中高度可信的组件,在 Fuchsia 的经过验证的执行中发挥着关键作用,因为它会验证系统上几乎所有其他组件的软件完整性。(存储在 bootfs 中的低级组件通过其他方式进行验证)。此外,blobfs 能够创建可执行页面,并且由于其他系统组件信任 blobfs 为其提供的内容,因此 blobfs 可以将任意代码插入任何组件的地址空间,从而使 blobfs 能够冒充依赖于 Blobfs 的可执行代码的任何组件的功能。
我们采取的一种策略是在 Blobfs 中对某些操作进行沙盒化,以缓解这些风险。例如,由于解压缩存在解析不可信输入的风险,因此已移至单独的进程。如果沙盒化解压缩程序遭到入侵,最糟糕的情况是发生拒绝服务攻击,因为 Blobfs 会直接拒绝从解压缩程序返回的任何无效数据。我们认为,由于本地攻击的风险较低,因此 Fxfs 的读取路径不需要采用相同的策略,但我们会探索在写入路径上实现类似的沙盒策略,以防范远程攻击。
虽然从安全角度来看,Fxfs 比 Blobfs 有一些优势(例如,它是用 Rust 这种内存安全型语言实现的),但它仍然比 C++ Blobfs 复杂得多,而复杂性会带来风险,因此需要采取更严格的缓解措施。
我们建议的缓解措施在上文的“设计”部分中有所介绍。
安全团队指出,基于共享内存的 IPC 机制(即用于写入 blob 的环形缓冲区)需要格外小心,以确保读取器和写入器能够对抗不合作的对等方,尤其是在共享内存已映射的情况下。我们会仔细设计这些协议,并根据需要咨询安全团队。
破坏 Fxfs 的结果
由于此提案会扩大 Fxfs 的攻击面,因此值得记录能够破坏 Fxfs 的攻击者能够执行的操作(以及无法执行的操作)。
在此提案中,Fxfs 不会具有 Vmex 资源,虽然 Fxblob 会为系统中的大多数软件包提供二进制文件和库的(可执行)内容,但由于这些内容将由另一个组件(内核或某个用户空间组件)独立验证,因此 Fxfs 无法伪造可执行页面,也无法让其他进程运行任意可执行数据。
因此,威胁模型涵盖了 Fxfs 直接有权访问的内容:
- Fxfs 可以任意读取和写入数据文件内容。这可能会导致用户数据的敏感信息泄露,或者可能利用未验证其存储数据的其他组件中的 bug。
- Fxfs 可以删除 blob 内容,这会导致其他组件遭到拒绝服务攻击。
- Fxfs 可以通过拒绝 blob 写入来防止系统更新。
- Fxfs 可以与块设备驱动程序互动,并且可能会利用其中的漏洞来获取 Fxfs 不具备的驱动程序功能。
隐私注意事项
不适用。
测试
我们已实现此提案的原型,该原型可以启动 Fuchsia 并执行所有常规系统功能。此原型在 CQ 中运行常规系统测试套件的构建器上启用,确保我们对原型的使用和覆盖率符合实际情况。
最终,我们会将其扩展到更多情境中(例如,在特定产品上启用,这将促使开发者自然而然地使用该功能,并在 CQ/CI 中获得更广泛的使用)。我们预计需要经过长时间的浸泡测试,才能在任何用户设备上使用此架构。
此外,我们还为相关功能添加了单元测试、集成测试和端到端测试的常规测试金字塔。我们还实现了自动化基准测试,以跟踪从 Fxblob 读取和写入的性能,并将其与 blobfs 的性能进行比较。
文档
在此提案中,需要更新一些存储文档。例如,应更新文件系统架构文档。
缺点、替代方案和未知情况
缺点:设备变砖的风险
在旧版架构中,minfs 中的 bug 只会影响 minfs,因为系统的重要部分(二进制文件、库、配置等)存储在 blobfs 中,因此即使重置数据分区,系统仍会保持正常运行状态。
将 blob 和数据合并到单个文件系统意味着 Fxfs 中的 bug 可能会导致设备变砖,因此这里不存在相同的安全保障。
我们认为,Fxfs 足够强大,可以作为 Fuchsia 基础架构的重要组成部分,自 2021 年以来已过广泛测试和使用。文件系统损坏的概率较低。
如果确实发生了损坏,最佳做法与其他操作系统类似:提供可用于将设备恢复为正常状态的恢复路径。从恢复模式执行 OTA (fxrev.dev/563864) 是解决此问题的一种方法,作者支持这种方法(因为它提供了修复更多问题的设备的机会)。
请注意,这并不是 Fuchsia 独有的风险,大多数其他操作系统都只有一个全局文件系统,用于存储所有数据(无论是否必需)。
替代方案:依赖 zx::streams 以加快 blob 写入速度
为 Fxblob 创建的自定义写入 API 与 zx::streams 有某些相似之处,后者使用 VMO 作为共享数据缓冲区,并使用页面故障作为控制机制。不过,流旨在适应更广泛的用例(常规文件 I/O),并且附带更多开销:内容大小跟踪、页面故障开销、I/O 批处理次数减少(因为故障是页面级别的)。专用 API 可以更简单、性能更高。
由于 Blob 写入性能非常重要(它是系统 OTA 和安装大型软件包的关键瓶颈),并且与常规文件 I/O 相比具有足够的具体约束条件,因此我们决定开发自定义接口。我们取得的出色成效证明了这一点。
在先技术和参考文档
可以说,此提案使 Fuchsia 更接近大多数操作系统,这些操作系统使用通用文件系统来实现更广泛的用途,并使用其他机制提供内容可寻址性和验证等专用功能。虽然我们会为此提案向 Fxfs 添加一些特殊功能,但大部分工作都发生在 Fxfs 的外层;核心文件系统保持不变。
例如,在 Linux 上,通用文件系统可以与 dm-verity 或 fs-verity 结合使用,以提供内容验证(分别针对映像和特定文件)。