RFC-0137:舍弃 FIDL 中的未知数据

RFC-0137:舍弃 FIDL 中的未知数据
状态已接受
区域
  • FIDL
说明

使 FIDL 绑定舍弃未知数据,而不是保留并代理这些数据。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-08-25
审核日期(年-月-日)2021-10-13

摘要

大多数 FIDL 绑定会保留未知的表字段和联合变体,从而允许用户代码检查和重新编码原始字节和句柄。此行为会带来安全和隐私风险,显著增加 FIDL 的复杂性,使线格式迁移变得困难,并且无法在所有绑定中实现。我们建议让绑定舍弃未知数据,从而实现表 1 中所示的行为。

表 1:具有未知数据的灵活类型的变更

类型 能否访问未知内容? 可以重新编码? 代理未知?
枚举
桌子 是 → 是 →
并集 是 → 仅限序数 是 → 是 →

背景

灵活类型是 FIDL 中用于编写可演变 API 的一项重要功能。它们在 RFC-0033:处理未知字段和严格性中引入,自 2020 年末以来已在所有绑定中提供。使用灵活类型时,即使存在未知成员,解码也会成功。FIDL 表格始终是灵活的,而位、枚举和联合可以标记为严格或灵活。对于灵活的位和枚举,未知值只是一个整数。不过,对于未知表字段或联合变体,该值由原始字节和句柄组成,我们将其称为未知数据

目前,大多数绑定都会在网域对象保留未知数据。LLCPP 是一个例外,其设计限制使得难以支持此功能。对于其他数据,保留未知数据可实现以下行为。假设进程 A、B 和 C 通过 FIDL 进行通信。如果 A 和 C 知道某个新表字段,但 B 不知道,并且该字段从 A 发送到 B 再发送到 C,那么尽管 B 不知道该字段,C 仍会收到并理解该字段。换句话说,B 会代理未知数据。应用还可以根据对架构的假设(例如,前四个字节始终是标识符)来解读未知数据。不过,此类情况是人为设计的,最好直接使用 FIDL 类型进行建模。代理是保留未知数据的唯一实际用例。

设计初衷

在设计 FIDL 时,我们力求使用尽可能少的必要功能来解决实际问题保留未知数据的功能未能达到这些原则的要求。自实现以来,它在 Fuchsia 中几乎没有使用过,并且在其他 FIDL 工作中多次成为复杂因素

这让我们开始质疑代理未知数据的优点。这个想法本身是否可行?我们认为,至少不应将其作为默认行为。对于真正打算作为代理的 FIDL 服务器,这可能是一个有用的选择启用功能。不过,这些情况最好通过 FIDL 中的专用代理支持来处理,这样可以解决问题的各个方面,而不仅仅是未知数据。

即使我们假设默认代理是可取的,它也仅在直接重新编码 FIDL 域对象时有效。不过,通常(也建议)在进一步处理之前将这些对象转换为更丰富的特定于应用的类型。这种做法与代理方法背道而驰,在代理方法中,最好是原封不动地传递编码消息,或者直接重新编码解码消息,并尽可能减少处理。例如,Rust crate fidl_table_validation 在将 FIDL 网域对象转换为应用网域对象时提供验证。因此,如果任何参与者使用此模式,则在复杂系统中跨多个跃点发送表的对等方无法依赖所有字段到达最终目的地。

无论是否需要代理,保留未知数据都有一些缺点。这会增加线格式迁移的难度。在迁移过程中,当所有对等互联方都可以读取旧格式和新格式时,就可以安全地开始写入新格式了。由于此更改无法在所有位置同时发生,因此在一段时间内,对等方必然会同时收到旧格式和新格式的消息。假设它接收到两个表,每个表都包含未知表字段,然后尝试将这两个表编码到一条消息中。在这种情况下,保留未知数据的唯一方法是在每个信封中包含线格式元数据,但这会增加不可接受的复杂性和开销。

另一个缺点是 FIDL 绑定之间的功能对等性。在支持就地解码的绑定(例如 LLCPP)中,很难选择既能拥有句柄又能表示未知句柄的网域对象表示形式。对于已知数据,解码器会通过覆盖其存在指示器在网域对象中插入句柄。对于未知数据,解码器仅知道要在句柄表中跳过的句柄数量,而不知道其存在指示器的位置。因此,除非同时返回网域对象和重新打包的句柄表,否则无法返回所有权网域对象。相反,这些绑定只是不支持保留未知数据。这可能会让在其他绑定中依赖它的用户感到意外,并且会增加我们的测试负担,要求针对所有涉及未知数据的情况进行两次 GIDL 测试。

一般来说,保留未知数据的需求会显著增加 FIDL 的复杂性。这种复杂性不仅限于实现,还会因与其他功能的互动而影响用户。例如,值类型和资源类型之间的区别旨在仅影响 API 兼容性,而不影响 ABI。不过,后来发现,在为灵活值类型接收到未知句柄的情况下,它会产生不可避免的 ABI 影响。此特殊情况的存在仅仅是因为需要保留网域对象中的未知数据。

利益相关方

哪些人会受到此 RFC 是否被接受的影响?(此部分为可选,但建议填写。)

教员:pascallouis@google.com

审核者:abarth@google.com、yifeit@google.com、ianloic@google.com

Consulted: bryanhenry@google.com

社会化:此 RFC 的草稿已发送给 FIDL 团队征求意见。

设计

对于灵活位和枚举,未知值的处理方式保持不变。

解码表和灵活的并集时:

  • 除非绑定专门用于代理,否则不得在网域对象中存储未知字节和句柄。

  • 绑定必须关闭所有未知句柄。

重新编码之前已解码的表和灵活的联合时:

  • 绑定必须成功重新编码表的已知字段,并且不得包含未知字段(这会暗示存储这些字段)。

  • 在对具有未知变体的灵活联合进行编码时,绑定必须失败并返回错误。

对于表和灵活的并集,网域对象如下:

  • 绑定不应提供任何机制来区分没有未知字段的表和丢弃了未知字段的表。如果绑定提供深层相等性函数,则应将它们视为相等。

  • 绑定必须提供一种机制来确定灵活的联合是否具有未知变体,并且应该提供对未知序号的访问权限(即,网域对象的未知变体应该只存储序号)。如果绑定提供深度相等性函数,则未知变体应表现得像 NaN 一样,即使序号相同,也会比较为不相等。

在 Rust 中,后一点意味着从灵活的联合和传递性包含灵活联合的类型中移除 Eq 特征,就像已经对浮点数所做的那样。

实现

实现过程主要是删除负责在所有绑定中保留未知项的代码。我们认为,未知数据访问器没有任何正式版用途。如果有,我们必须了解相应用例,并尝试找到解决方案。

目前,LLCPP 无法重新编码包含未知字段的表。这需要根据设计进行更改,才能成功仅对已知字段进行编码。

安全注意事项

此提案可提高安全性,因为它可以减少隐式传递的信息和功能。当未知数据被保留时,很容易通过毫无戒心的组件传递任意字节和句柄。舍弃后,数据边界会通过 FIDL 架构进行准确编码,从而使系统更易于审核。

隐私注意事项

此提案可限制未知数据的传输,从而提高隐私保护水平,因为未知数据可能包含敏感信息。

测试

测试主要在 GIDL 中进行。涉及未知数据的 success 测试将分为两部分:decode_successencode_success(仅对已知表字段进行编码)或 encode_failure(联合无法编码)。具有未知数据的值的表示形式也会发生变化。GIDL 不应再解析未知字节和句柄,而应使用语法 123: unknown 来指示序号为 123 的未知信封。

可以移除拆分 LLCPP 和非 LLCPP 的许可名单和拒绝名单。所有绑定在处理未知数据时都将具有相同的编码/解码行为。此外,可以移除在 fxrev.dev/428410 中添加的 LLCPP 特有的单元测试,转而使用 GIDL 测试。

测试应保留,以涵盖严格/灵活和值/资源的所有组合,不过,使用句柄解码灵活值类型的未知数据将不再失败。

文档

以下文档需要更新:

缺点、替代方案和未知因素

替代方案:选择性保留未知内容

我们不会完全移除对保留未知数据的支持,而是继续支持,只是不再默认支持。例如,它可以是类型上的属性,或许仅限于值类型,以缓解对代理未知句柄的担忧。不过,这种方法会进一步增加很少使用的功能的复杂性,并且无法解决线格式迁移问题。

缺点:灵活类型不一致

此提案的缺点是,它会使灵活类型的行为不太一致,或许也不太直观。为了解释这一点,我们可以沿两个轴对位、枚举、表和联合进行分类,如表 2 所示:代数类型(积或和)和载荷(有或没有载荷)。

表 2:灵活类型的分类

产品类型 总和类型
无载荷 枚举
含载荷 桌子 并集

目前,所有灵活类型都会代理未知信息。此提案打破了这两个轴上的对称性。例如,请考虑以下 FIDL 类型:

// Product types (multiple fields set)
type Bits  = bits  {    A = 1;         B = 2; };
type Table = table { 1: a struct{}; 2: b struct{}; };

// Sum types (one variant selected)
type Enum  = enum  {    A = 1;         B = 2; };
type Union = union { 1: a struct{}; 2: b struct{}; };

首先,我们在载荷轴上失去了一致性。目前,从 BitsTable 或从 EnumUnion 会增加功能,允许每个成员携带载荷。根据此提案,该功能会带来不再保留未知信息的代价。

其次,我们会失去代数类型轴上的一致性。目前,TableUnion 都允许在解码包含未知数据的对象后重新编码。在此提案中,Table 可以重新编码,但 Union 不可以。

我们认为,这种以实用性换取一致性的做法是值得的,可以避免动机中描述的复杂情况。不过,下文介绍了一些可保持更高一致性的替代设计。

替代方案:舍弃所有未知信息

为了提高一致性,我们可以舍弃所有未知信息,即使是易于存储的未知整数。这意味着,对于位和枚举,只有一个未知状态;对于联合,除了有效负载之外,还会舍弃未知序号。表 3 显示了最终行为。

表 3:对表 1 的调整:舍弃所有未知信息

类型 能否访问未知内容? 可以重新编码? 代理未知?
是 → 是 →
枚举 是 → 是 → 是 →
桌子 是 → 是 →
并集 是 → 是 → 是 →

替代方案:可选的灵活联合

为了提高一致性,我们可以要求灵活的联合始终是可选的,然后将未知变体解码为缺少的联合。这样就可以重新编码联合,使其与表保持一致。表 4 显示了最终行为。

表 4:对表 1 的调整:可选的灵活联合

类型 能否访问未知内容? 可以重新编码? 代理未知?
枚举
桌子 是 → 是 →
并集 是 → 是 →

替代方案:记住是否舍弃了未知字段

在提议的设计中,绑定用户无法判断在解码表时是否舍弃了未知字段。另一种方法是将布尔值或一组未知序号存储在表格网域对象中。然后,用户可以通过 has_unknown_fields() 等函数查询此信息。例如,存储服务可能希望在这种情况下发生故障,以避免数据丢失。

此替代方案的缺点是,它会向表格网域对象添加额外的隐藏状态。它们不再是简单的值类型,而是其字段的总和。 例如,它提出了一个问题,即 == 运算符是否应考虑此类布尔值标志。

替代方案:重要字段

前所述,检查 had_unknown_fields() 的唯一实际用例是,如果它返回 true,则失败。我们可以在绑定中提供该访问器,也可以接受表和灵活的联合成员上的属性来选择启用该行为:

type Data = table {
  1: foo string;
  @important
  2: bar string;
};

此属性的效果是在信封标头中为相应字段设置一个新预留的位。当解码器遇到设置了重要位且未知的字段时,必须失败。换句话说,@important 属性会选择不使用向前兼容性,其功能类似于我们允许在位、枚举和联合上使用的静态 strict 修饰符的动态版本。

此替代方案可能需要在本 RFC 的基础上再制定一个 RFC。

致谢:此想法源自 yifeit@google.com。

替代方案:保留值/资源的 ABI 影响

此提案消除了 RFC-0057 的 ABI 影响,并认为这是通过舍弃未知数据实现的改进。 不过,有人认为 ABI 影响是可取的,应该保留。

降低 ABI 影响的优势(此提案):

  • 这使得严格/灵活和价值/资源成为更独立的特征。它们的交集不再是特殊情况。鉴于我们在编写各自的 RFC 很久之后才注意到这种情况,因此用户可能也会感到意外。
  • 这样可以更轻松地将类型从值转换为资源,因为这只会破坏 API,而不会破坏 ABI。如果没有任何代码中断(对于无法在某些绑定中直接引用的请求和响应类型,这种情况是合理的),此过渡不再会默默地更改行为。

保持 ABI 影响(此替代方案)的优势:

  • 它能更准确地模拟接口的意图。如果您指示不希望接收句柄(通过使用值类型),但在运行时接收到句柄,则表明存在差距,因此失败是合适的。
  • 如果我们改变了主意,可以稍后放弃 ABI 影响。反向切换更有可能导致中断。

在先技术和参考资料

Protobuf

协议缓冲区的设计曾就此问题反复讨论。在 proto2 中,未知字段会被保留并代理(与今天的 FIDL 类似)。在 proto3 中,行为已更改为在解码期间舍弃未知字段(如本提案中所述)。不过,该决定后来被撤销,因此在 3.5 及更高版本中,proto3 再次保留了未知字段。

这引发了一个问题:如果我们接受此提案,FIDL 是否会走上同样的道路?我们认为答案是否定的,因为 FIDL 和 Protobuf 占据不同的设计空间。由于存在两种使用情形(中间服务器和读取-修改-写入模式),Protobuf 不得不恢复到旧的保留行为。这两种情况在 Fuchsia 中都不常见。Fuchsia 的安全和隐私原则鼓励直接通信,而不是使用中间代理服务器。FIDL API 规范建议使用部分更新模式,而不是“读取-修改-写入”模式。

Thrift

Apache Thrift 会舍弃未知字段