RFC-0167:早期用户空间引导中的软件包 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 向 BootFS 引入了软件包和启动解析。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2022-05-10 |
审核日期(年-月-日) | 2022-06-13 |
摘要
此 RFC 提议向 bootfs 引入软件包。这将为早期启动带来后期用户模式所享的软件包隔离和命名空间优势,并消除第三方驱动程序开发的阻碍因素。
设计初衷
在组件框架拥有稳固的封装架构之前,就已经有了早期启动可执行文件汇编和沙盒化功能,而到目前为止,我们还没有投入资源将目前可用于后期用户空间的工具引入到早期启动阶段。因此,用户空间引导会遇到打包引入来解决的几个问题,例如进程沙盒化、可验证内容和可变库版本。
这些早期启动问题会导致在运行时和构建时与 bootfs 映像的互动出现意外的复杂性和低效率,但通过在早期启动时采用打包方式,可以大大减少这些问题。除了解决现有问题之外,对早期启动进行封装化还可以改进系统的运行状况。通过在整个用户空间中为可执行文件和库的内容标识符进行标准化,我们可以重复使用之前分离存储空间(例如 bootfs 和 blobfs)中等效数据的副本。
组件框架有一个“软件包”概念,可用作 fuchsia-pkg 的抽象;虽然严格来说,组件框架和封装系统是分开的,但它们之间紧密交织,因此分解世界和封装世界的目标是协同的。
近期的工作(例如,组件管理器作为用户启动后的第一个进程执行)正在推动越来越多的“组件贯穿始终”架构。即使是文件系统和设备驱动程序等早期启动可执行文件,也以 Fuchsia 组件的形式启动,或者目前正在向该模型迁移。现在,我们需要将系统的这种组件化与“从上到下全部打包”的系统汇编进行匹配,并将促成打包的所有价值带到用户空间引导中。
更具体地说,由于无法生成有效的 Fuchsia 映像(其中 bootfs 中编码的可执行文件在共享库依赖项中存在版本偏差),bootfs 中缺少软件包命名空间将成为树外驱动程序开发的阻碍。此外,目前的 bootfs 映像会泄露 ABI。驱动程序可以根据其他驱动程序的存在与否来更改其启动时的行为,例如选择使用较新的库,而无需明确定义对其他驱动程序的依赖项。同样,由于驱动程序最终都会位于 bootfs 中的同一文件夹中,因此驱动程序的名称本身就会创建一个 ABI。这类无意间使用的 ABI 在可用时间越长,就越难弃用,而且这种行为早有先例(Windows 驱动程序和视频游戏 DRM 驱动程序会公开此类无意间使用的 ABI)。
利益相关方
主持人:hjfreyer@google.com
审核者:geb@google.com、mcgrathr@google.com、surajmalhotra@google.com、aaronwood@google.com、galbanum@google.com、wittrock@google.com、jfsulliv@google.com
咨询了:
共享:我们通过设计文档与利益相关方探讨了该主题,然后在 RFC 之前向 tq-eng 开放了该主题以供一般讨论。
设计
此更改涉及对 bootfs 和 /boot 目录、BootResolver、BootUrl 和产品组件的更改。
bootfs 变更
图片更改
Fuchsia bootfs 软件包将由 bootfs 中的 meta.far 文件表示,bootfs 目录条目将命名为 blob/<merkle root of meta.far>
。软件包清单中的每个 blob 都会在 bootfs 中收到一个新的目录条目 blob/<merkle root of the dependency>
。
- 我们已经在运行时计算了 Merkle 树以验证内容,并且速度足够快,因此在 zbi 创建(同时会压缩图片)等过程中执行此操作并不重要。
- 此提案所要求的新图片构建流程已在实际图片构建步骤之前确定内容 ID,以便填充 meta.far 文件。这意味着,在生成用于驱动映像构建步骤的清单的过程中,基于内容的重复数据删除操作大多已经完成。这应该很简单,只需确保我们在此阶段已完成所有可能的重复删除即可。
当我们讨论向 /blob 下的 bootfs 添加新条目时,仅指添加引用同一底层文件的新 bootfs 目录条目(在概念上是硬链接)。
bootfs 中名为“pkg_map”的新文件将维护一个映射,用于将人类可读的软件包名称映射到用于编码软件包的 meta.far 的 Merkle 根。
由于新增了 meta.far 和新的 pkg_map 文件,bootfs 的大小会增大。 所有其他内容都只是现有 bootfs 文件的新目录条目。在 x64 架构上,bootfs 的压缩大小保守估计会增加约 70 KiB。
/boot 变更
/boot 中将引入一个名为 /blob 的新子目录。bootfs 映像中所有名称以 /blob 开头的文件都将放置在该目录中。将 bootfs 中的所有组件迁移到软件包后,顶级目录将仅包含内核 vmos、shell 脚本以及使 /boot 成为组件管理器的“命名空间”所需的文件。
组件管理器命名空间最初将是 /boot 的子目录,其“布局”方式与其所有依赖项的预期一致。最终,我们希望将组件管理器也合并到依赖于 SWD 堆栈进行解析的 meta.far 编码中。
BootResolver 变更
软件包命名空间化 bootfs 组件的核心工作与 package-resolver 执行的工作高度重叠。可读取的软件包名称必须映射到内容 ID 的 meta.far,必须对 meta.far 进行解码,并且必须使用其内容文件构建命名空间。因此,该设计旨在尽可能重复使用现有的软件包解析器逻辑。
如需重复使用 package-resolver 的逻辑,最简单的入口点是 package-directory::serve。此入口点接受 BlobFS 和用于标识 meta.far 的内容 ID,打开 meta.far,解析其 meta/contents 文件,并构建并提供文件中编码的命名空间。只要我们能够将 blob 的 bootfs 后备目录作为 BlobFS 客户端提供给 API,就可以原封不动地重复使用此库。
let (proxy, server) =
fidl::endpoints::create_proxy().map_err(ResolverError::CreateEndpoints)?;
let () = package_directory::serve(
package_directory::ExecutionScope::new(),
<some blobfs::Client-like view on top of the bootfs blobs>,
<meta.far hash>,
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
server,
)
.await
.map_err(ResolverError::ServePackageDirectory)?;
blobfs::Client 只是封装了 fio::DirectoryProxy,这是从组件管理器公开 bootfs 的方式。我们可以简单地使用 Blobfs 客户端封装此 bootfs,并将其传递给 package_directory::Serve 调用。需要进行一些小更改,以确保某些 blobfs::Client API 在位于 bootfs 上时能够正常失败(即需要可变性的 API;例如 open_blob_for_write、delete_blob),但在 package_directory::serve 执行期间不需要可变性。
BootUrl 变更
BootUrl 目前不使用其网址的主机或路径部分,因为没有要编码的代码库或打包。这意味着,我们可以使用现有的 fuchsia-boot 架构在现有的 BootResolver 中引入新的组件加载,而不是引入新的网址架构和解析器。网址中是否存在 package-path 将成为指示应使用“BootResolver 变更”中所述的新解析路径的指标。
示例:
fuchsia-boot:///#my_component.cm
bootresolver 将此网址解读为已在 /boot 目录中正确设置命名空间的未打包组件。
fuchsia-boot:///my_package#my_component.cm
bootresolver 将此网址解读为打包的组件,对于此组件,存在从“my_package”到 my_package 的 meta.far 的 Merkle 根的映射,并且应用于构建特定于“my_package”命名空间的命名空间。
产品/图片组装变更
通过构建系统和映像汇编系统完成的两阶段工作来构建 bootfs 映像。
首先,我们将引入一个名为 bootfs_packages
的新 gn 变量。此列表将在 product.gni 中声明,并分配给一个名为 bootfs_package_labels
的调用方变量,该变量会传递给将 build/input:bootfs
分配给 bootfs_labels
变量的任何 assemble_system
调用。
将 bootfs 组件从无软件包编码迁移到 bootfs 软件包时,我们会将其从将其包含在 bootfs_labels
依赖项集中的组依赖项中移除,并将其添加到 bootfs_packages
集中。
接下来,在生成映像汇编配置时,我们将使用现有的 list_package_manifests
模板从调用方的 bootfs_package_labels
变量中定义的软件包收集软件包清单。
接下来,映像汇编会从构建遍历中获取清单,并使用该清单调用 zbi
等工具,将构建目录中的文件打包到映像中。软件包清单格式包含一个“blob”对象列表,该列表在创建映像时将用于将名为 blob/<merkle_root>
的文件添加到 bootfs 映像中。
在迭代这些 blob 对象时,我们会检查哪个 blob 是软件包 meta.far 的标识符,并将从软件包名称到 meta.far 的 Merkle 根的映射添加到映射中。在创建 bootfs 映像结束时,系统会将映射以 JSON 格式写入上文 bootfs 更改部分中所述的“pkg_map”文件。
我们之所以选择在 Image Assembly 级别实现此转换,有以下三个原因:
ProductAssembly 只会合并其各种软件包中的软件包列表。
ImageAssemblyConfig 验证仍然很简单。
在商品组装中,“文件包”的验证仍会对“文件包”进行操作。
实现
功能实现
您可以同时进行映像汇编更改、bootfs 更改、BootResolver 更改和 BootUrl 更改。
为了确保在实现过程中不会向 bootfs_package_labels 引入软件包,我们将从映像汇编更改开始。我们将在 bootfs 软件包清单聚合点实现此功能,并在构建时检查该集是否为空。
BootUrl 的语义将在 3 个 CL 中发生变化。首先,在将第一个 bootfs 组件迁移到软件包命名空间之前,如果 BootUrl 包含软件包路径或代码库,我们会将其更改为无效(以确保我们保留软件包路径的可用性,作为解析策略的指标)。其次,随着第一个 bootfs 组件迁移到 fuchsia_package,我们将允许在 fuchsia-boot 网址中使用软件包路径。第三,随着最后一个 bootfs 组件迁移到 fuchsia_package,我们将禁止使用不包含软件包的 fuchsia-boot 网址。
Migration
在这些功能全面实现后,我们将逐步迁移 bootfs 组件。给定组件的迁移如下所示:
- 我们将找到将组件的未打包依赖项(.cml 文件、二进制文件等)添加到 bootfs_labels 依赖项的位置。
- 我们将将该未打包的依赖项集合转换为 fuchsia_package gn 目标。
- 我们将从现有的 bootfs_labels 组中移除该软件包,并将其添加到 bootfs_package_labels 组。
- 我们将更新 bootstrap.cml 文件中组件的网址,以添加软件包名称。
性能
系统大小
在 x64 架构上,bootfs 映像的压缩大小保守估计会增加约 70 KiB。这是因为,所有组件都迁移到软件包后,bootfs 大小将因新添加的 meta.far 和新的 pkg_map 文件而增加。所有其他操作都只是对打包之前的 bootfs 文件的目录条目进行重命名。
运行时影响
目前,整个 bootfs 会在 component_manager 启动时被提前解析到一个目录中。迁移后,我们最终会推迟等同于在 /boot 中设置组件命名空间的解析工作,直到该组件启动。
除了之前仅解析 bootfs 头文件之外,我们还需要完成一些额外的工作;我们必须解析 meta.far。
ZBI 已签名,因此目前我们不会验证 ZBI 中的 bootfs 文件的内容,只会验证 ZBI 本身。如果我们不对 bootfs 中的 blob 进行运行时验证,那么安全性将与现在完全一样。
引导文件系统今天可能会遇到的一个潜在错误状态是,汇编可能会错误地将源文件放置在错误的引导文件系统链接下。由于 bootfs 中存在 blob,因此我们可以根据需要对 bootfs blob 进行运行时验证,以防范此类情况。我们最初不打算这样做。
构建时影响
zbi 构建的构建时性能保持不变;迁移后,我们会遍历软件包清单来编码 blob,而不是遍历 bootfs 中包含的每个依赖项的目标条目清单。实际上,我们会遍历完全相同数量的工件。
向后兼容性
此提案中涉及的许多更改都独立于 bootfs 中。在更新 bootfs 中的执行单元时,一次更新整个 bootfs 的粒度是最小的。因此,系统不会因 bootfs 封装实现不完整而提供已封装的 bootfs 组件。
向后兼容性问题的一个潜在来源是,如果映像汇编器以某种方式发现自己在同一 zbi 中包含旧版 bootfs 映像和新版组件管理器。如果发生这种情况,在迁移完成且不允许在 bootfs 中使用未打包的组件后,可能会引入错误。不过,鉴于产品组装构建 bootfs 的方式,目前无法出现这种情况。
同样,功能实现中所述的对 BootUrl 语义的更改不会与功能实现或迁移分道扬镳,因为它也包含在 bootfs 中。
安全注意事项
通过减少进程有权访问的工件,软件包命名空间/进程沙盒可以严格提高运行时安全性。
bootfs 映像已签名且为只读,因此主要安全问题是映像汇编器是否可信,这与映像的格式无关。如果我们决定应逐步更新 bootfs 映像,那么在 bootfs 中提供可执行文件的 Merkle 根身份有助于进一步提高安全性。
隐私注意事项
无。
测试
组件解析器和产品组装中的测试实践已得到充分记录,并将扩展以涵盖新功能。
在语义更改的 3 个阶段中,每个阶段都会添加 BootUrl 解析器测试,以表明系统正在强制执行网址的预期行为。
文档
需要更新用户空间引导的 API 文档以及有关 bootfs 映像汇编的文档。
缺点、替代方案和未知情况
重复使用 bootfs_labels 与引入新标签。
目前,如果意外将软件包包含在 bootfs_labels 列表中的依赖项列表中,则不会产生任何影响。通过这项更改,软件包的存在将具有语义意义。因此,我们需要从 bootfs_labels 命名空间中“清除”无意中包含的软件包。
这里唯一的复杂性涉及 fuchsia_driver_package。fuchsia_driver_package 是一种独特的 build 模板;产品组装与 fuchsia_driver_package 的互动方式因驱动程序是放置在 bootfs 还是 blobfs 中而异。放置在 bootfs_labels 中时,产品组装会遍历驱动程序软件包,将驱动程序视为一组依赖项;放置在 blobfs 中时,驱动程序会通过 meta.far 正确打包。这样做是为了让单个驱动程序目标能够根据产品在 bootfs 和 blobfs 之间切换。它过去之所以有效,是因为 bootfs 产品组装不会对其依赖项图中存在的软件包分配任何重要性。不过,使用 bootfs 软件包命名空间时,我们会将 fuchsia_package(及其关联的 package_manifest)的存在解读为将用于将软件包命名空间编码为 meta.far 的声明放入 bootfs。
如果我们今天就发布 bootfs 命名空间功能,大约 20 个驱动程序最终会作为未打包的依赖项放入 bootfs,同时还会通过关联的 meta.far 和关联的 blob 在 bootfs 中进行编码;在对 driver_manager 的运行程序进行更改之前,驱动程序的 meta.far 编码将不会被使用。我们希望从 bootfs 中清除所有 fuchsia_packages,而不是用未使用的封装膨胀 bootfs,然后逐步将目标迁移到适当的封装。我们选择通过引入新的 gn 元数据屏障来清除 fuchsia_driver_packages 的 package_manifests,该屏障可防止 list_package_manifests 遍历到 fuchsia 驱动程序。这样做,而不是将使用 fuchsia_driver_package 的每个驱动程序拆分为 bootfs 目标和 blobfs 目标,因为这样可以显著降低驱动程序产品组装逻辑的复杂性。此外,build 中的单个软件包定义在最终结果中可以发挥这两种作用,这样可以避免分解每项内容并移除所有“仅限旧 bootfs”伪软件包的繁琐工作。
遗憾的是,这会带来其他复杂性,例如引入对真实 fuchsia_package 目标的阴影,从而向用户隐藏其名称。这会导致与其他现有和正在进行的工作不兼容,例如金标准测试(用于验证设备上存在的所有软件包清单是否符合预期名称),或产品组装工作(要求用于生成软件包索引的标签仅直接依赖于软件包目标本身)。
因此,更不显眼的方法是只添加一个新标签,并在将早期启动阶段的组件迁移到软件包时,将它们从旧标签移至新标签。最终,我们需要合并这些列表,如果驱动程序尚未迁移到适当的组件,我们需要重新审视 fuchsia-driver-package 模板的复杂性。
在产品或图片组装操作中实现
- 应在产品组装级别还是映像组装级别解析 bootfs 组件的软件包清单?换句话说,我们是否应推迟清单的解析,直到我们即将调用 zbi 工具来实际构建 bootfs 时?
- ProductAssembly(ffx 组合产品)
- 优点:
- ImageAssembly 始终非常专注于生成映像文件本身(zbi、blobfs 等)
- ImageAssemblyConfig 包含所有 bootfs 文件的更简单的列表。
- 缺点:
- 需要进行更复杂的验证,确保用于生成旧版汇编输入软件包的 ImageAssemblyConfig 与 ProductAssembly 创建的 ImageAssemblyConfig 相匹配(或者需要舍弃/弱化验证)。
- 在产品组装结束时进行的验证需要知道在 bootfs 中哪里可以找到组件的“软件包”(例如,为结构化配置执行的值文件存在性验证)。
- ImageAssembly(ffx assembly create-system)
- 优点:
- ProductAssembly 只会合并其各种软件包中的软件包列表
- ImageAssemblyConfig 验证保持简单
- 在产品组装中,“文件包”的验证仍会对“文件包”进行操作。
- 缺点:
- ImageAssemblyConfig 不再包含 bootfs 文件的完整内容。
- 在创建 zbi 之前,Image Assembly 需要执行软件包到条目映射。
在 bootfs 中编码 meta.far,还是在构建 bootfs 映像时进行命名空间设置?
另一种方法是,通过构建映像,只需在 bootfs 中编码“命名空间”,以便 /boot 下的每个组件都有自己的子目录,组件管理器将其用作该组件的命名空间根目录。
在 bootfs 中编码 meta.far 时,我们通过在 boot-resolver 中引入对 SWD 软件包解析库的依赖项来增加了早期启动的依赖项。SWD 软件包解析库用于解析 meta.far 格式,并配备了人员和维护人员,以便在 meta.far 格式发生变化时继续执行解析操作。
在映像构建期间执行命名空间设置时,我们会通过教导它如何解读软件包清单/meta.far 格式,并编写代码将 meta.far 格式转换为 bootfs 映像中的软件包命名空间,从而增加产品组装的复杂性。这意味着需要编写新代码,并且第二个团队需要负责解读 meta.far 并确保其产品与之保持同步。SWD 团队对让新团队承担 meta.far 格式的依赖项表示担忧,但通过采用单一软件包编码方式并重复使用 SWD 维护的工具与该格式进行交互,可以完全避免这种情况。
这两种策略对资源的影响几乎相同(以 KB 为单位)。
最后,软件包不仅仅是它们编码的命名空间。meta.far 中的元数据(例如软件包版本)对于平台版本控制等运行时功能至关重要。如果不使用 meta.far,我们需要引入一些新的方法来基于基于 bootfs 的命名空间对这些“额外信息”进行编码,并向组件管理器等服务介绍这种额外的软件包元数据编码。