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

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

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

问题
  • 85383
Gerrit 更改
  • 574045
作者
审核人
提交日期(年-月-日)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 时,我们尽可能使用最少的特征解决实际问题。保留未知数据的功能不符合这些原则。自实现以来,它在紫红色领域几乎没被用过,甚至根本没有用过,并且它已多次成为其他 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 特征,就像浮点数所做的那样。

实现

实现主要是指删除负责保留所有绑定中的未知项的代码。我们认为未知数据访问器没有任何生产用途。如果有,我们就必须了解用例并尝试找到前进的方向。

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

安全注意事项

此方案提高了安全性,因为它减少了隐式传递的信息和功能。如果保留未知数据,很容易通过不知情的组件传递任意字节和句柄。数据边界被舍弃后,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。

致谢:此建议由 yifeit@google.com 提出。

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

此方案消除了 RFC-0057 对 ABI 的影响,并认为可以通过舍弃未知数据进行改进。不过,可以认为对 ABI 的影响是可取的,应该保留下来。

减少 ABI 影响的优势(此方案):

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

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

  • 它可以更准确地模拟接口的意图。如果您指明不需要句柄(通过使用值类型),并且在运行时收到句柄,则表明存在一个缺口,相应失败是适当的。
  • 如果我们改变主意,以后可以消除对 ABI 的影响。切换到其他方向更可能会导致中断。

早期技术和参考资料

协议缓冲区

协议缓冲区的设计在这一点上已来回切换。在 proto2 中,系统会保留和代理未知字段(就像现在的 FIDL)。在 proto3 中,该行为在解码期间更改为舍弃未知字段(如此方案所示)。不过,这一决定后来又撤消了,因此在 3.5 版及更高版本的 proto3 中,proto3 再次保留了未知字段。

这会引发一个问题:如果我们接受此提案,FIDL 会遵循相同的路径吗?我们认为答案是否定的,因为 FIDL 和 Protobuf 占用的设计空间不同。由于中间服务器和读取-修改-写入模式这两个用例,协议缓冲区必须还原为旧的保留行为。这两种情况在 Fuchsia 中都不常见。Fuchsia 的安全和隐私权原则鼓励直接通信,而不是使用中间代理服务器。FIDL API 评分准则建议使用部分更新模式,而不是读取-修改-写入模式。

二手

Apache Thrift 会舍弃未知字段