RFC-0033:未知字段处理和严格处理

RFC-0033:未知字段的处理和严格性
状态已接受
区域
  • FIDL
说明

此 FTP 修正并阐明了 FIDL 解码器在遇到包含类型未知的字段的表、可扩展的联合、枚举和位(可扩展的消息)时的行为。

作者
提交日期(年-月-日)2019-02-07
审核日期(年-月-日)2019-03-07

摘要

此 FTP 修正并阐明了 FIDL 解码器在遇到类型未知的表、可扩展联合、枚举和位(即可扩展消息[^1])时,对其中包含的字段的处理方式。

具体而言,我们建议:

  • 为可扩展消息定义严格和灵活的行为,指定遇到未知字段(包括句柄)的解码器的行为方式;
  • 可作为可扩展消息声明前缀的 strict 关键字。这可确保在验证期间拒绝包含未知字段的消息,从而保证收到的消息不包含未知字段。
  • 默认可扩展消息具有灵活性,即允许未知值并通过绑定公开;
  • 定义并推荐绑定为客户端提供的用于检查具有未知字段的消息的 API

与其他 RFC 的关系

此 RFC 已由以下 RFC 修正:

设计初衷

可扩展消息是一种有价值的机制,可使数据交换格式在发展的同时保持与线格式(二进制)的兼容性。不过,更改架构会给 FIDL 解码器带来设计决策,因为如何验证、解析这些字段并将其公开给最终用户会产生疑问。

虽然每种语言都有不同的数据结构访问机制和规范,但通过强制执行验证行为来指定解码器及其 API 的行为,可以提高安全性,并通过提高类型和语言之间的一致性来改善总体人体工程学。

我们还希望为受限环境启用绑定,在这些环境中,解析未知字段可能对正确运行不是必需的,并且会增加不必要的性能负担。这也适用于已成熟且预计不会进一步发展的消息。

设计

未知字段是指读者不知道其序号(表)、标记(可扩展的联合)、值(枚举)或特定位(位)的字段。为简洁起见,下文将使用“标记”来指代未知的序号/标记/值/特定位。

  • 包含未知标记的消息必须经过验证并成功解析。
    • 不过,对于严格消息,也有例外情况,详见下文。
  • 解码器必须处理消息中的未知句柄。
    • 默认处理行为必须是关闭所有句柄。
    • 绑定可以提供一种机制,让客户端能够以特殊方式处理未知句柄。
  • 绑定必须提供一种机制来检测是否收到了作为消息一部分的未知标记。
  • 绑定应提供一种机制来检测具有给定标记的字段是否存在于收到的消息中。
  • 绑定可提供一种机制来读取未知字段中的标记、原始数据和(无类型)句柄。
  • 如果目标语言提供在编译时详尽检查标记的机制(例如,C/C++ 中的 switch()、Rust 中的 match):

    • 该语言绑定应提供一个特殊的“未知”标记,该标记可作为详尽检查的一部分包含在内,以便提供一个兜底情况(例如,default(在 C/C++ 中)和 _(在 Rust 中)可以省略。
    • 此建议的目的是防止在正常编译时需要使用 catch-all 情况,因为如果需要使用,将来添加的标记将不会引发编译器警告。
    • 此 FTP 未定义应如何实现此功能,因为不同语言的实现策略可能有所不同。
    • 示例:

      // Bindings SHOULD NOT offer this API:
      switch(union.Which()) {
        case Tag1: ...
        case Tag2: ...
        case Tag3: ...
        default: ...
        // no unknown tag in bindings forces handling using default case
      }
      
      // Bindings SHOULD offer this API:
      switch(union.Which()) {
        case Tag1: ...
        case Tag2: ...
        case Tag3: ...
        case Tag_Unknown: ...
        // no default case: new tags cause a non-exhaustiveness warning
      }
      

严格处理消息

  • 我们引入了一个 strict 关键字,可用于为可扩展的消息声明添加前缀,例如:strict table T { ... }strict enum T { ... }
  • 包含未知字段的严格消息必须视为无效。
  • 如果绑定为灵活消息提供此类机制,则不得为严格消息的详尽标记检查提供特殊的“未知”标记。
  • 从严格消息到灵活消息的过渡,以及从灵活消息到严格消息的过渡,必须作为非破坏性源代码级 (API) 更改来支持,可能需要使用 [Transitional] 属性来实现软过渡。
    • 此类过渡不得更改线格式 (ABI)。
  • 严格消息具有传递性。 如果某条消息被标记为严格,则只有该消息是严格的。 相应消息中包含的子消息不严格。

  • 语法示例:

    // One simply doesn't walk into Mordor and add a new file mode, so this is
    // reasonable to be strict.
    strict bits UnixFilePermission : uint16 {
        ...
    };
    
    // It's too dangerous for clients to ignore data in this table if we
    // extend it later, but we wish to keep the wire format compatible if we
    // do change it, so it's not a struct.
    strict table SecurityPolicy {
        ...
    };
    

实施策略

  1. 更新了 FIDL 兼容性测试,以验证现有语言绑定是否符合此规范。
    1. 添加测试用例,以测试以下消息:(1) 仅包含已知字段;(2) 仅包含未知字段;(3) 至少包含一个已知字段和一个未知字段。
  2. 确保 FIDL 兼容性测试包含所有相应类型的空消息的测试用例。
  3. fidlc 中添加了对严格消息的支持。
  4. 更新了语言绑定,以支持严格的消息。
  5. 向 FIDL 兼容性测试添加了针对严格消息的测试用例。

展望未来:使用网站调节系数

在设计阶段,我们还考虑了除了提议的声明位置之外,是否允许将严格关键字放在声明的使用位置。

语法示例:

protocol Important {
    SomeMethod(...) -> (strict other.library.Message response);
}

在此示例中,other.library.Message 可能未定义 strict,但我们希望使用它,同时要求进行严格的验证。

这会增加绑定作者的设计复杂性,因为在严格模式和灵活模式下可能都需要 other.library.Message

在编码/验证/解码方面,根据上下文为同一消息同时公开严格模式和灵活模式,这与处理字符串或向量的方式并无不同。它们具有相同的布局,但边界可能会因使用位置而异。 它还类似于在可为 null 或不可为 null 的上下文中使用可扩展联合的方式。 一般来说,绑定会选择一种类型架构,并以某种方式来指明边界、可为 null 性或(如本文所探讨的)严格模式。

同时公开严格模式和灵活模式的第二个问题是,在用户代码中处理消息的组装和查询。

例如,假设有一个包含三个成员的枚举,分别为 ABC。为了公开灵活模式,我们需要一个特殊的枚举成员“unknown”。因此,现在可以组装一个未通过严格验证的枚举,这样在需要此枚举的其他严格上下文中,编码期间会失败。 同样,字符串和向量的并行性非常重要:如果没有高度专业化的 API,绑定会允许创建过长的字符串和向量,然后无法进行编码。

在需要同时支持严格模式和灵活模式时,应遵循的策略是为灵活模式生成所有额外的部分,并确保在编码、解码和验证期间,在需要时应用严格的验证。

工效学设计

此 FTP 通过以下几种方式改进了人体工程学设计:

  • 我们更好地设定了用户对跨语言 FIDL 行为的预期。
  • 严格消息可让用户避免编写不必要的代码来处理未知字段。

文档和示例

  • 需要更新严格字段的语法和语言规范。
  • 应更新 FIDL 样式指南,以提供有关何时将消息声明为严格消息的指导。

向后兼容性

  • 此变更不会影响 ABI 兼容性。
  • 如果需要更改解码器或绑定以符合此 FTP,这些更改可能会导致源代码级 (API) 损坏,应根据具体情况解决。

性能

  • 强制解码器和绑定符合此 FTP 可能会导致(可能并不明显的)性能损失,因为这会强制它们处理所有未知字段并关闭所有句柄。
  • 绑定可能需要额外的间接级别(因此会使用额外的内存/二进制大小)来提供“未知”标记,以进行详尽的标记检查。

安全

这种 FTP 可提高安全性。

  • 我们为包含未知内容的消息指定了验证行为。
  • 严格消息可让解码器在客户端检查未知内容之前对其进行验证并将其舍弃,从而降低出现 bug 的可能性。

测试

请参阅实现策略部分(我们计划使用 FIDL 兼容性测试)。此外,每种语言绑定都应有自己的测试来断言正确的行为。

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

此 FTP 大大明确了行为,并具有相关的实现成本,以确保语言绑定符合其建议。

替代方案:默认采用严格模式或混合模式

严格性应被视为与向量或字符串的大小界限类似;它是一种独立于消息布局的约束,可以在不破坏 ABI 的情况下进行更改。

我们希望 FIDL 作者明确选择限制(约束)其消息。

此外,我们不希望采用混合模式,即某些消息(例如枚举)默认情况下是严格的,而其他消息(例如表)则不是。

替代方案:使用 [严格] 属性,而不是新关键字

这个概念非常重要,值得拥有自己的关键字。 其他语言中类似功能的先例足以让其顺利转换为 FIDL。

替代方案:其他关键字

在设计阶段,我们提出了几种不同的替代方案。最有可能的竞争者是 final:它表示“主题的最终结论”,在 C++、Java、C#(以及其他语言)中具有优先权。

不过,由于我们可能希望在协议中使用关键字“final”来表示不能在组合中使用该协议(即“final”的传统用法),因此我们选择使用另一个关键字来表示严格验证。

这样一来,我们就可以引入以下语法:

final strict protocol Important {
    MyMethod(SomeTable arg);
};

这表示协议 Important 无法组合,并且所有验证都必须严格。

我们还探索了其他关键字,包括:sealedrigidfixedclosedknownstandardized

替代方案:仅限严格

我们可以将所有可扩展消息定义为始终严格。目前,枚举和位仅为严格模式,因此此替代方案会将此模式扩展到表和可扩展的联合。

在这种情况下,对可扩展结构(例如,添加新字段)的更改需要先更新读取器,然后再更新写入器。这严重限制了这些可扩展数据结构的使用,对于更高级别的应用场景来说,限制过于严格。

此外,如果这是设计选择,我们就不需要为表和可扩展的联合使用信封(即,不需要字节数和句柄数)。 实际上,在严格的“仅”解释下,未知字段会被拒绝,否则架构会以类似于其余消息 FIDL 进程的方式确定要消耗的字节数和句柄数。

替代方案:仅限灵活型

我们可以将所有可扩展的消息定义为始终灵活。

对于枚举(和位)来说,这会非常令人惊讶,并且与预期相反。 这导致了两种糟糕的次优方案:

  • 针对枚举(和位)设置例外情况,使其变得严格 - 如上所述,这会造成混淆,并使语言规则更难理解。
  • 保持这些消息的灵活性会与预期相反,为 bug(例如读取无效值)打开大门,并且肯定会导致大量普通的验证代码需要手动编写,而不是由绑定提供。

继续探索其他可扩展的消息(表和可扩展的联合),我们发现有必要实施严格的检查。

例如,假设安全日志记录协议 LogEntry 定义为一个表。此协议的实现可能需要保证客户端不会发送服务器无法理解的字段,以免这些客户端对这些新字段如何控制日志条目的处理抱有不切实际的期望。 例如,新版本可能会添加一个字段“pii ranges”,用于提供包含 PII 的日志条目的范围,这些条目必须专门记录(例如,替换为唯一 ID,原始数据归档在该唯一 ID 下)。 为了保护旧服务器免于接受此类载荷,并防止其可能错误处理这些日志条目,作者会为其 LogEntry 选择严格模式,从而保护自己免受日后潜在的滥用行为的侵害。

在先技术和参考资料

部分理由参考了 go/proto3-unknown-fields,其中介绍了 proto3 为何放弃对保留未知字段的支持,然后又为何撤销了该决定。

  • FTP-037:事务性消息标头 v3(尚未发布)

Footnote1

枚举和位包含在可扩展的消息中,因为在定义消息后可以添加或移除新成员。