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

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

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

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

摘要

此 FTP 修订并阐明了当 FIDL 解码器遇到表、可扩展联合体、枚举和位(可扩展消息[^1])时,如果这些消息包含类型未知的字段,其行为。

具体而言,我们建议:

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

与其他 RFC 的关系

此 RFC 已通过以下修订:

设计初衷

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

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

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

设计

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

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

    • 该语言绑定应提供一个特殊的“未知”标记,可作为详尽检查的一部分包含在内,以便提供一个万能情况(例如default(在 C/C++ 中)或 _(在 Rust 中)可以省略。
    • 此建议旨在防止在正确编译时需要使用万能字符串,因为如果需要,日后添加的标记将不会引发编译器警告。
    • 此 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

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