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,那么 C 将会收到并理解该字段,即使 B 不知道该字段也是如此。换句话说,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

咨询对象:bryanhenry@google.com

共享:此 RFC 的草稿已发送给 FIDL 团队以供评论。

设计

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

解码表和灵活联合时:

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

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

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

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

  • 在使用未知变体编码灵活联合时,绑定必须失败并返回错误。

关于表和灵活联合的域对象:

  • 绑定不应提供任何机制来区分没有未知字段的表与已舍弃未知字段的表。如果绑定提供深层等式函数,则应将它们视为等效。

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

在 Rust 中,后一点意味着从可变联合体和传递包含 Eq 的类型中移除 Eq trait,就像对浮点数所做的那样。

实现

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

目前,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{}; };

首先,我们会失去载荷轴的一致性。目前,从 Bits 改为 Table 或从 Enum 改为 Union 会增强功能,允许每个成员携带载荷。采用此提案后,该功能的代价是不再保留未知数。

其次,代数类型轴的一致性会丢失。目前,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 评分标准建议使用部分更新模式,而不是“读取-修改-写入”模式。

节俭

Apache Thrift 会舍弃未知字段