RFC-0138:处理未知交互

RFC-0138:处理未知互动
状态已接受
区域
  • FIDL
说明

我们扩展了 FIDL 语义,以允许对等方处理未知互动。

Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-05-25
审核日期(年-月-日)2021-10-27

摘要

我们扩展了 FIDL 语义,以允许对等方处理未知互动,即接收未知事件或接收未知方法调用。为此,请执行以下操作:

  • 我们在 FIDL 语言中引入了灵活的互动严格的互动。即使是未知的灵活互动,也可以由对等方妥善处理。严格的互动会导致突然终止。

  • 我们为协议引入了三种运行模式。封闭协议是指绝不允许未知互动的协议。相反,开放协议允许任何类型的未知互动。最后,半开协议是指仅支持一种未知交互方式的协议。

从宏观角度了解 FIDL 对演进的支持

在深入探讨此提案的具体细节之前,最好先了解 FIDL 如何解决演变问题。

该问题有两个方面:源代码兼容性 (API) 和二进制兼容性 (ABI)。

API 兼容性旨在保证,在发生变更之前根据生成的代码编写的用户代码在发生变更之后仍可根据生成的代码进行编译。例如,可以合理地预期,向 FIDL 库添加新声明(例如定义新的 type MyNewTable = table {};)不会导致使用该库的现有代码无法编译。

解决源代码兼容性问题的方法有三种:

  1. 尽可能使更多更改与来源兼容(例如 RFC-0057:默认无句柄);
  2. 提供明确的保证(例如 RFC-0024:强制性源代码兼容性);
  3. 提供版本控制(例如 RFC-0083:FIDL 版本控制)。

另一方面,ABI 兼容性旨在提供针对不同版本的库构建的程序的互操作性。例如,两个程序对表的架构可能有不同的理解,但仍能成功通信。

实现 ABI 兼容性可分为三部分:

  1. 静态兼容性是指在数据层面实现互操作性,即具有相同表但架构不同的两个对等方何时可以互操作?
  2. 动态兼容性假设所有数据类型都兼容,并侧重于在对等方具有不同版本的协议(例如不同的方法)时实现互操作性;
  3. 最后,在某些情况下,使用不同的协议是不可行的,而解决方案是了解每个对等方的功能(协商),然后根据这些功能调整通信(使用哪种协议)。

如果需要“局部灵活性”(例如对基本保持不变的运营模式进行小幅添加),动态兼容性就特别适合。在其他情况下,例如 fuchsia.io1 相对于 fuchsia.io2,则需要进行网域模型转换。需要“全局灵活性”,并且寻求的解决方案属于协议协商类别。

我们在本 RFC 中专门讨论的机制(严格和灵活的互动)改进了动态兼容性 (2) 的现状。

术语

关于协议的组合模型的提醒。

两个对等实体之间的通信称为互动。交互以请求开始,并且可能需要响应(可选)

请求和响应都是事务性消息,表示为一个标头(“事务性标头”),后面可以选择性地跟一个载荷1

互动是有方向的,我们将两个对等方分别命名为客户端服务器客户端与服务器之间的互动始于客户端向服务器发送的请求,如果存在响应,则以相反的方向发送。同样,我们也会谈到服务器到客户端的互动

我们通常使用“fire and forget”(发射后不管)或“one way”(单向)来表示由客户端发起的无响应互动,使用“call”(调用)或“two way”(双向)来表示需要响应的互动(在当前模型中始终由客户端发起)。当服务器是无响应交互的发起对等方时,通常称为事件2

协议是一组互动。我们将会话定义为客户端与服务器之间使用协议进行通信的特定实例,即客户端与服务器之间的一系列交互。

应用错误是指遵循错误语法的错误。传输错误是指因内核错误(例如写入已关闭的通道)而发生的错误,或 FIDL 中发生的错误。

设计初衷

Fuchsia 的核心原则是可更新:软件包设计为可彼此独立更新。即使是驱动程序,也应保持二进制稳定性,以便设备在保持现有驱动程序的同时,能够无缝更新到较新版本的 Fuchsia。FIDL 在实现这种可更新性方面发挥着核心作用,并且从一开始就旨在定义应用二进制接口 (ABI),从而为向前兼容性和向后兼容性奠定坚实的基础。

具体而言,我们希望允许对彼此之间的通信协议理解略有不同的两个对等方安全地进行互操作。更好的是,我们希望获得强静态保证,确保两个对等方是“兼容的”。

我们投入了大量精力,为 FIDL 类型的编码和解码提供灵活性和保证,我们称之为静态兼容性。我们推出了 table 布局union 布局,选择了显式 union 序号,推出了 strictflexible 布局修饰符,推出了 protocol 序号哈希降低了 protocol 序号哈希的碰撞概率,并改进了事务性消息头格式,以确保其在未来能够正常运行。

接下来,我们来了解动态灵活性和保证,我们称之为“动态兼容性”。假设两个对等方处于静态兼容状态,即它们用于交互的所有类型都处于静态兼容状态,那么动态兼容性是指这两个对等方能够成功互操作,并且任何一方都不会因意外交互而中止通信。

利益相关方

  • 主持人:jamesr@google.com。
  • 审核者
    • abarth@google.com (FEC)
    • bprosnitz@google.com (FIDL)
    • ianloic@google.com (FIDL)
    • yifeit@google.com (FIDL)
  • 咨询
    • jamesr@google.com
    • jeremymanson@google.com
    • jsankey@google.com
    • tombergan@google.com
  • 社会化:RFC 草稿已与 FIDL 团队共享,并与 Fuchsia 团队的各种成员进行了讨论。该文档已在 Eng Council Discuss 邮件列表 (eng-council-discuss@fuchsia.dev) 中广泛分享。

设计

我们引入了灵活互动严格互动的概念。简而言之,即使是未知的灵活互动,也可以由对等方妥善处理。相反,如果接收方对某个交互一无所知,那么该交互会导致接收方突然终止会话,因此属于严格交互。我们将互动的严格程度定义为互动是灵活互动还是严格互动。请参阅灵活互动和严格互动的语义

如果没有保护措施,灵活的互动可能会被无意中用于危及隐私的方式:

  • 例如,考虑一个旨在不断发展的渲染引擎。新版本添加了 flexible SetAlphaBlending(...); 单向互动,目的是让以旧版渲染器为目标的新版客户端的设置被忽略(但大多数渲染仍会正常运行)。现在,如果该新方法与特殊的 PII 呈现模式 StartPIIRendering(); 有关,那么旧版渲染器必须停止处理,而不是忽略此方法,因此使用 strict 互动是合适的。
  • 另一个示例是,恶意对等方尝试通过发送各种消息来反射性地发现公开的界面,以查看哪些消息可以被理解。通常,反射功能会带来额外的性能开销,并可能导致隐私问题(您可能会暴露超出您预期的信息)。从原则上讲,FIDL 选择禁止反射,或要求明确选择启用。

因此,我们还推出了三种协议运行模式:

  • 封闭协议是指不允许或不预期进行灵活互动,收到灵活互动属于异常情况。
  • 开放协议是指允许任何灵活的互动(无论是单向还是双向)的协议。此类协议可提供最大的灵活性。
  • 半开协议是指允许灵活的单向互动(即发后不管的调用和事件),但不允许灵活的双向互动(如果对等方不知道某个方法,则无法进行方法调用)。

如需了解详情,请参阅协议的语义

严格互动和灵活互动的语义

严格交互的语义非常简单:当接收到未知请求(即序号接收方不知道的请求)时,对等方会突然终止会话(通过关闭渠道)。

灵活互动旨在让接收者能够妥善处理未知互动。这会带来一些影响,从而指导设计。

灵活互动的发送者必须知道,接收者可能会忽略其请求(因为接收者不理解该请求)。

接收者必须能够判断出此请求是灵活的(而非严格的),并采取相应行动。

由于双向互动需要接收者回复发送者,因此未知请求的接收者必须能够在没有任何其他详细信息的情况下构建回复。接收者必须向发送者传达未理解请求的信息。为了满足此要求,灵活的双向互动的响应是结果并集(请参阅详细信息)。

根据语义,在单向互动的情况下,发送者无法知道接收者是否知道其请求。使用灵活的单向交互时,FIDL 作者应仔细考虑其整体协议的语义。

值得注意的是,单向互动在某种程度上是“尽力而为”,因为发送者无法知道对等方是否收到了互动。不过,渠道可提供排序保证,确保互动顺序是确定且已知的。严格的单向互动可确保某些互动仅在理解了前面的互动时发生。例如,日志记录协议可能具有 StartPii()StopPii() 严格的互动,以确保任何对等方都不会忽略这些互动。

如需进一步了解在选择严格互动和灵活互动时需要考虑的权衡因素,另请参阅:

打开、关闭和半开协议的语义

closed 协议的语义是限制性的,只有严格的互动,没有灵活的互动。如果 closed 协议有任何 flexible 交互,则会发生编译时错误。

ajar 协议的语义允许严格的交互和单向灵活的交互。如果 ajar 协议有任何 flexible 双向互动,则会发生编译时错误。

open 协议没有严格和灵活的限制,允许单向和双向互动。

如需进一步了解在选择封闭、半开放或开放协议时需要考虑的权衡因素,另请参阅:

语言方面的变化

我们引入了修饰符 strictflexible,用于将互动标记为严格或灵活:


protocol Example {
    strict Shutdown();
    flexible Update(value int32) -> () error UpdateError;
    flexible -> OnShutdown(...);
};

默认情况下,互动是灵活的。

从样式指南的角度来看,建议始终明确指明互动的严格程度,即应为每次互动设置严格程度。3

我们引入了修饰符 closedajaropen,用于将协议标记为关闭、半开或打开:


closed protocol OnlyStrictInteractions { ...
ajar protocol StrictAndOneWayFlexibleInteractions { ...
open protocol AnyInteractions { ...

在封闭式协议中,不能定义任何灵活的互动。封闭协议只能组合其他封闭协议。

在非开放式协议中,不能定义双向灵活互动。半开协议只能由关闭或半开协议组成。

(开放协议不受任何限制。)

默认情况下,协议处于开放状态。

此提案的先前版本将“半开”指定为默认状态。不过,这会导致冲突,即在声明了双向方法但未指定明确的修饰符时,开放程度修饰符的默认值“ajar”与严格程度修饰符的默认值“flexible”发生冲突。这意味着,如果协议或方法中至少有一个没有修饰符,则包含双向方法的协议无法编译。请看下文:开放程度的默认值以粗体显示,严格程度的默认值以斜体显示。

可视化效果:网格,显示了打开/半开/关闭与严格/灵活的哪些组合可以编译。

为了解决此问题,我们将开放性的默认值从“半开”更改为“打开”,这样一来,协议就可以编译双向方法,而无需在协议或方法上使用修饰符。

从样式指南的角度来看,建议始终明确指明协议的模式,即应为每个协议设置模式。[^default-debate]

对有线格式的更改:事务性消息标头标志

我们将事务性消息标头修改为:

  • 交易 ID (uint32)
  • 静态标志(array<uint8>:2,即 2 个字节)
  • 动态标志 (uint8)
  • 魔数 (uint8)
  • 序数 (uint64)

也就是说,标志字节分为两部分,静态标志为两个字节,动态标志为一个字节。

动态标志字节的结构如下:

  • 第 7 位,第一个最高有效位“严格程度位”:严格方法为 0,灵活方法为 1。
  • 位 6 至 0,未使用,设置为 0。

以下是有关使用“动态标志”的一些更多详细信息:

  1. 我们在事务性消息标头的第三个版本中添加了标志。这些标志旨在“临时用于软迁移”。例如,在从严格联合迁移到可扩展联合期间,使用了一个位。不过,目前还没有需要同时使用这么多标志的方案,因此我们可以将这些标志的用途从仅临时使用更改为用作线格式的一部分。

  2. 严格性位是必需的,以便发送者向接收者指示接收者不知道的 strict 互动。在这种情况下,预期的语义是通信突然终止。如果没有此严格性位,发送者和接收者之间的这种偏差可能不会被注意到。例如,考虑一个半开(或打开)协议,其中新添加了 strict StopSomethingImportant(); 单向互动。如果没有严格性位,接收方将不得不猜测未知交互是严格的还是灵活的,并选择灵活的交互,因为此 RFC 旨在改进可演化性。因此,FIDL 作者在扩展协议时,将不得不依赖双向严格互动。

另请参阅在事务标识符中放置严格性位,了解替代表示形式;以及交互模式位,了解未来可能需要的替代表示形式。

线上传输格式的更改:结果联合

结果联合目前有两个变体(成功响应的序号为 1,错误响应的序号为 2),现在扩展为具有第三个变体(序号为 3),该变体将携带一个新的枚举 fidl.TransportError,用于指示“传输级”错误。

例如,以下互动:

open protocol AreYouHere {
    flexible Ping() -> (struct { pong Pong; }) error uint32;
};

具有响应载荷:

type result = union {
    1: response struct { pong Pong; };
    2: err uint32;
    3: transport_err fidl.TransportError;
};

具体来说,如果灵活方法使用 error 语法,则会相应地设置成功类型和错误类型(分别为序号 1 和 2)。否则,如果灵活方法未使用 error 语法,则结果联合的错误变体(序号 2)标记为 reserved4

一些精确度:5

  • 我们选择名称 transport_err,因为从应用的角度来看,该错误来自何处应无法区分。有应用错误,还有“传输错误”,后者是由于 FIDL 编码/解码、FIDL 协议错误、内核错误等而导致的各种错误的混合体。从本质上讲,“传输错误”是框架(包括许多软件层)中可能发生的所有类型错误的集合。

  • 我们将类型 fidl.TransportErr 定义为具有单个变体 UNKNOWN_METHOD 的严格 int32 枚举。相应变体的价值与 ZX_ERR_NOT_SUPPORTED 相同,即 -2:

    type TransportErr = strict enum : int32 {
      UNKNOWN_METHOD = -2;
    };
    

    向客户端显示传输错误时,如果绑定提供了一种获取未知互动 transport_errzx.status 的方式,则绑定必须使用 ZX_ERR_NOT_SUPPORTED。不过,如果绑定不符合向客户端显示错误的方式,则无需将未知互动 transport_err 映射到 zx.status

    另一种方法是仅使用 zx.status,并始终使用 ZX_ERR_NOT_SUPPORTED 作为表示未知方法的值,但这种方法存在两个明显的缺点:

    • 它需要依赖于库 zx,而许多库可能不会直接使用该库。这使得在 IR 中定义结果联合变得困难,因为我们需要自动插入对 zx 的依赖,或者在 IR 中将类型降级为 int32,但让生成的绑定将其视为 zx.status

    • 它并未定义绑定应如何处理不是 ZX_ERR_NOT_SUPPORTEDtransport_err 值。通过指定类型为严格枚举,我们明确定义了接收到无法识别的 transport_err 值的绑定的语义;然后,该值会被视为解码错误。

  • 为简单起见,我们使用单数“结果联合”,但实际上我们描述的是一类共享共同结构的联合类型,即三个序数,第一个变体不受约束(成功类型可以是任何类型),第二个变体必须是 int32uint32 或其枚举,第三个变体必须是 fidl.transport_err

对 JSON IR 的更改

我们在 JSON IR 中公开了交互的严格程度。在实践中,我们更新了 #/definitions/interface-method 类型,并添加了一个 strict 布尔值作为 ordinalnameis_composed 等的同级。

我们在 JSON IR 中公开了协议的模式。在实践中,我们更新了 #/definitions/interface 类型,并添加了一个 mode 枚举,其成员 closedajaropencomposed_protocolsmethods 等同级。

对绑定的更改

我们希望绑定成为自动处理请求的可见体现。例如,虽然绑定可能能够自动构建指示请求未知的请求,但重要的是要同时提出收到了未知请求(可能包含有关该请求的一些元数据),以及选择以“请求未知”进行响应还是突然终止通信。

静态数据相关问题。

  • 对于灵活的互动,绑定应通过与呈现其他传输级错误(例如来自 zx_channel_write 的错误或解码期间的错误)相同的机制,向客户端呈现结果联合的 transport_err 变体。结果联合的 errresponse 变体应以与绑定在方法声明为严格时呈现这些类型的方式相同的方式呈现给客户端。

    • 例如,在 Rust 绑定中,Result<T, fidl::Error> 用于呈现来自调用的其他传输级错误,因此 transport_err 应折叠到 fidl::Error 中。同样,在低级 C++ 绑定中,fit::result<fidl::Error> 用于传达传输级错误,因此 transport_err 应合并到 fidl::Error 中。responseerr 变体的传达方式与严格方法相同。在 Rust 中,对于具有错误语法的函数,这意味着 Result<Result<T, ApplicationError>, fidl::Error>,对于没有错误语法的函数,这意味着 Result<T, fidl::Error>,其中 response 值为 Terr 值为 ApplicationError

    • 对于将错误折叠到 zx.status 中的绑定,transport_errUNKNOWN_METHOD 必须转换为 ZX_ERR_NOT_SUPPORTED

动态问题。

  • 使用 zx_channel_writezx_channel_call 或其同类方法发送请求时,必须按如下方式设置动态标志:
    • 对于严格互动,严格性位(第 7 位)必须设置为 0;对于灵活互动,必须设置为 1。
    • 接下来的 6 位必须设置为 0。
  • 收到已知互动时:
    • 与当前绑定机制相比没有任何变化。
    • 具体来说,绑定不应验证严格性,以简化从严格互动到灵活互动(或反之)的迁移。
  • 当收到未知互动(即未知序号)时:
    • 如果互动严格(如收到的严格程度标志所示):
    • 绑定必须关闭通信(即关闭渠道)。
    • 如果互动是灵活的(如收到的严格程度标志所示):
    • 对于封闭协议,绑定必须关闭通道。
    • 如果互动是单向的(交易 ID 为零):
      • 绑定必须向应用引发此未知互动(详见下文)。
    • 如果互动是双向的(交易 ID 不为零):
      • 对于半开协议,绑定必须关闭通道。
      • 对于开放协议,绑定必须向应用引发此未知互动(详见下文)。
    • 有关提出未知互动的详细信息:
      • 如果互动是双向的,绑定必须通过发送选择第三个变体的结果联合和 fidl.TransportErrUNKNOWN_METHOD 的结果来响应请求。此操作必须在将未知互动提升到用户代码之前进行。
      • 绑定应将未知互动引发到应用,可能通过调用之前注册的处理程序(或类似的处理程序)来实现。
      • 建议绑定需要注册未知互动处理程序,以避免构建可能被误解的“默认行为”。绑定可以提供“无操作处理程序”或类似功能,但建议明确使用。
      • 绑定可以选择在处理未知互动时向应用提供关闭渠道的选项。

当未知消息包含句柄时,服务器必须关闭传入消息中的句柄。服务器必须在以下情况之前关闭传入消息中的所有句柄:

  • 关闭渠道(如果是严格方法、已关闭协议中的灵活方法,或半开协议中的灵活双向方法)
  • 在开放协议上使用灵活的双向方法时回复消息
  • 在开放或半开放协议上出现灵活的单向方法时,通知用户代码未知的方法调用。

同样,当客户端收到包含句柄的未知事件时,必须关闭传入消息中的句柄。客户端必须先关闭入站消息中的所有句柄,然后才能执行以下操作:

  • 关闭渠道(如果是严格事件或封闭协议上的灵活事件)。
  • 在开放或半开放协议的情况下,通知用户代码未知事件。

一般来说,处理未知互动时,操作顺序如下。

  1. 关闭传入消息中的句柄。
  2. 如果适用,请关闭渠道或发送 UNKNOWN_METHOD 回复。
  3. 将未知互动提升到未知互动处理程序或报告错误。

在异步环境中,多个线程可能会同时尝试在渠道上发送/接收消息,因此可能无法保证在报告未知方法错误之前关闭渠道,或者这样做不切实际。因此,当该互动是致命的,在报告未知方法或事件的错误之前,不需要关闭渠道。不过,对于此 RFC 中指定的可恢复的未知互动,必须在调度未知互动处理程序之前关闭句柄并回复(如果适用)。

此 RFC 的先前版本未指定以下操作之间的顺序:关闭传入消息中的句柄、响应未知的双向方法,以及向用户引发未知的互动。

兼容性影响

ABI 兼容性

将互动从 strict 更改为 flexible,或从 flexible 更改为 strict 不符合 ABI 兼容性要求。

更改协议模式(例如从 closed 更改为 ajar)不兼容 ABI。虽然从限制性更强的模式更改为限制性更弱的模式似乎可以实现 ABI 兼容,但实际上并非如此,因为协议同时定义了发送方和接收方(即发后不管和事件)。

所有更改都可以软过渡。如有需要,可以对修饰符进行版本控制

来源兼容性

将互动从 strict 更改为 flexible 或从 flexible 更改为 strict 可能在来源上兼容。建议绑定通过折叠现有传输错误 API 来提供相同的 API,无论交互的严格程度如何。

更改协议模式(例如从 closed 更改为 ajar)不兼容源代码。建议绑定根据协议模式专门化其提供的 API。例如,封闭协议不需要提供“未知方法”处理程序,并且建议不要提供此类不会使用的处理程序。

与平台版本控制的关系

RFC-0002 的演变部分中所述,每当平台对 Fuchsia 系统接口的语义进行不向后兼容的更改时,我们都会更改 ABI 修订版本。

衡量我们实现可更新目标效果的一个指标是,我们创建新 ABI 修订版本的速度。由于添加或移除灵活的互动可以向后兼容,因此该功能有助于提高 Fuchsia 的可更新性。

实现

  • 我们可以想象一个绑定只实现规范的严格部分的世界,在这种情况下,通信会提前停止,就像对等方遇到了其他错误或 bug 一样,因此是安全的。
  • 鉴于可演化性对 FIDL 的重要性(这是首要目标),这种未来并不理想,因此我们要求绑定遵循此规范。
  • 为了符合绑定规范,绑定必须实现严格且灵活的交互语义,以及协议的三种模式。
  • 鉴于此,我们详细介绍了绑定规范的变更。这会破坏 ABI,并且是线格式的一项重大演变(涵盖“静态”和“动态”问题)。

此 RFC 的先前版本要求使用新的 magic number 来控制未知互动的发布。不过,如上所述,未知互动与现有协议向后兼容,因为用于指示严格性的标头位之前未使用/预留,并且只有灵活的双向方法的有线格式会发生变化,而这些方法只能存在于开放协议中。我们不会更改魔数,而是使用两阶段发布,先启用未知互动支持,但将默认修饰符设置为 closedstrict,然后将这些修饰符明确添加到现有 FIDL 文件,最后将默认值更改为 openflexible

性能考虑因素

closed 协议没有影响。如对绑定的更改部分中所述,对于封闭协议,无需检查严格性位。

ajaropen 协议的影响较小:

  • 处理未知互动与处理已知互动类似,系统会调用预注册的处理程序并运行应用代码。
  • 此外,如果发生双向未知互动(仅限 open 协议),绑定将构建并发送响应。

我们预计性能考虑因素很少会发挥作用,协议模式的选择主要取决于安全性考虑因素

工效学设计

这使得 FIDL 更难理解,但解决了可演变性方面的一个非常重要的需求,而这方面一直以来都是一个尖锐的问题。

向后兼容性

此功能不向后兼容,需要对所有 FIDL 客户端和服务器进行软迁移。

安全注意事项

如果允许向同级发送未知请求(即在灵活互动的情况下),则会带来安全问题。

对于特别敏感的协议,可能需要通过非常严格的交互来抢占式地解决演变问题,因此倾向于使用 closed 协议。预计 Fuchsia 的大部分内部结构都依赖于 closed 协议(例如 fuchsia.ldsvc)。

在考虑 ajaropen 协议时,FIDL 作者需要考虑以下两个问题:

  • 恶意对等互联方发送具有大型载荷的未知请求。(这与使用 flexible 类型时存在的疑虑类似,因为 flexible 类型也可能携带大量未知载荷。)如大小会影响 ABI 中所述,我们需要更多功能来为 FIDL 作者提供控制权,这将在未来的工作中解决。
  • 打开了协议嗅探的大门,对等方尝试在没有先验知识的情况下发现实现了哪些方法,然后努力精心设计一条消息来利用发现的方法。如果实现公开的方法比预期多,这可能会造成问题。例如,打算公开父协议,但却绑定了构成父协议的子协议。请注意,灵活互动不会改变攻击向量,但由于对等方能够连续尝试多个序号,而无需重新连接(在某些情况下,这可能会非常耗费资源),因此攻击向量可能更容易被利用。
  • 在选择 ajar 协议与 open 协议之间进行权衡时,请考虑以下情况:对等方无法判断单向互动是否已处理或被忽略,而对于双向未知互动(如 open 协议允许的),处理对等方会披露其无法理解互动,这样做可能会向恶意对等方泄露有价值的信息。

隐私注意事项

打开协议嗅探的大门可能会导致隐私问题。如安全性注意事项部分中所述,此威胁模型不会因本 RFC 而发生变化,但可能会更容易被利用。

测试

开发此 RFC 中描述的新功能集,关键在于确保所有绑定都遵循相同的规范,并且行为类似。为此,我们需要能够在测试中表达规范,例如“发送此请求,使用正确的交易 ID 但错误的序号进行响应,预期发送方渠道会关闭”。根据我们的经验,如果更加注重流畅地表达规范,就会增加测试,从而提高所有绑定到规范的合规性,并增强回归保护。

我们将采用与编码和解码相同的做法,最终开发出 GIDL:首先手动编写测试,尽可能多地练习绑定,然后逐步将可以声明的部分概括为基于声明的测试方法。虽然我们希望能够构建一个与 GIDL 类似的工具来解决动态问题,并且会为此努力,但我们不会将此作为最终结果,而是可能会更倾向于使用手动编写的流畅表达的测试。

文档

此功能将有详尽的文档。在规范方面:

FIDL API 评分标准中将添加更多条目,涵盖协议演变。

对于此功能在给定目标语言中的具体使用,我们希望每个绑定都能更新其文档,并提供可正常运行的示例。

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

缺点:消息的最大大小会影响 ABI

处理未知内容(无论是 flexible 类型中可能遇到的未知载荷,还是此处介绍的未知互动)时存在一个问题,即对等方预期读取的消息的最大大小会影响 ABI,但此限制从未明确描述,也未进行静态验证。

目前,无法对渠道进行矢量化读取,也无法进行部分读取。因此,即使向满足所有要求的对等方(例如,在对等方预期的时间进行灵活互动)发送消息,也可能会导致通信失败,从而破坏 ABI。如果相关消息过大,对等方无法读取(因为对等方期望的消息大小小于 1KiB),那么超出该限制的新消息将永远无法读取,而是会关闭通道,并中止两个对等方之间的通信。

灵活交互的引入增加了此类问题的可能发生率,而此类问题已因 flexible 类型而存在。

以下是一些未来方向的构想:

  • 一种矢量化通道读取,使接收者能够仅读取消息的标头,然后决定是读取其余载荷还是舍弃该消息(这也需要新的系统调用)。
  • 将消息的最大大小设为协议的显式属性,可能具有预定义的大小类别,例如 smallmediumlargeunbounded

替代方案:与命令模式的比较

命令模式有助于让客户端批量处理许多要由服务器处理的请求。还可以使用命令模式来实现此 RFC 中描述的那种可演化性。

例如,请考虑以下情况:

open protocol AnOpenProtocol {
    flexible FirstMethod(FirstMethodRequest) -> (FirstMethodResponse);
    flexible SecondMethod(SecondMethodRequest) -> (SecondMethodResponse);
};

这可以通过以下封闭协议来近似实现,也就是说,如果使用当前的 FIDL 功能集,就必须采用这种方式才能实现相同的可演化性:

closed protocol SimulateAnOpenProtocol {
    strict Call(Request) -> (Response);
};

type Request = flexible union {
    1: first FirstMethodRequest;
    2: second SecondMethodRequest;
    ...
};

type Response = flexible union {
    1: first FirstMethodResponse;
    2: second SecondMethodResponse;
    ...
    n: transport_err zx.status;
};

不出所料,命令模式方法并不令人满意。

由于我们必须将并集中的每个请求与一个响应相匹配,因此我们失去了“匹配对”的语法强制执行,这反过来也会导致语法局部性丢失。

由于不守规矩的服务器可能会使用 SecondMethodResponse 来响应 FirstMethodRequest,因此我们也会失去类型安全性。有人可能会说,智能绑定可以注意到这种模式,也许是在 @command 属性的帮助下,并提供与我们目前为方法提供的相同的人体工程学设计。

在有线级别,命令模式强制使用“两种方法判别器”。我们在事务性消息标头中包含序号(用于标识 Call 是互动),并且包含联合序号(用于标识选择了联合的哪个变体,即 1 表示 FirstMethodRequest,2 表示 SecondMethodRequest)。

同样,有人可能会认为,如果所有方法都遵循命令模式,即所有方法的请求和响应都是联合,那么我们就无需在事务性消息标头中使用序号。从本质上讲,上述灵活的协议会使用命令模式“编译为”封闭协议。联合的线格式需要计算变体的字节数和句柄数,并要求合规的解码器验证这些计数。这在两个方面存在问题:

  • 事务性消息标头允许的刚性(没有载荷说明,如果可以则解码)是联合线格式(实际上是按设计)无法比拟的。这种刚性和简单性特别适合低级别用途,而 FIDL 过度倾向于此。

  • 组合模型没有任何“协议分组”的概念。这非常强大,因为我们可以(并且确实)在同一渠道上多路复用多个协议。我们会尽可能使用结构化组合(即 compose stanza),也会采用动态组合(例如服务发现)。如果我们采用“所有编译都归结为联合”的观点,就会施加严格的分组。

最后,某些 FIDL 作者希望实现“自动批量处理请求”。例如,fuchsia.ui.scenic 库因在 fuchsia.ui.scenic/Session.Enqueue 方法中使用命令模式而闻名。不过,提供“自动批处理请求”是一项危险的功能,因为在一个单元中处理多个命令的语义往往因应用而异。我们应该如何处理未知命令?我们应该如何处理失败的命令?是应忽略命令、停止执行,还是导致中止和回滚?即使是围绕“批量工作单元”(事务)概念设计的 RDBMs 系统,也往往会提供多种批处理模式([隔离级别](https://en.wikipedia.org/wiki/Isolation_(database_systems)))。可以说,FIDL 并没有计划支持“自动批量处理请求”。

总而言之,虽然从表面上看,严格互动和灵活互动的语义可能与命令模式相同,但它们之间的差异足以证明需要使用特殊的语义。

替代方案:协议协商

什么是协议协商

协议协商是一个广义术语,用于描述对等方相互交互以逐步建立彼此上下文的一组技术,从而使它们能够进行正确、快速、高效的通信。

例如,假设您随机拨打了一个电话号码。对方可能会以“某某,是吗?”开头。您从完全不了解同伴到能够识别同伴。我们可以继续说:“哦,某某某。我说得对吗?”鉴于营销电话的普及,您现在很可能会遇到“此来电是关于什么的?”你是谁?”。依此类推。双方逐渐了解对方的身份和能力。

  • 哪些数据元素可被理解?例如,向对等方指明所需的表字段,但要谨慎,避免对等方生成大量复杂数据,结果却在收到后被忽略。
  • 对等互连支持哪些方法?在渲染引擎中,您可以想象一下,询问 alpha 混合是否可用作一项功能,如果不可用,则调整与渲染器的互动(可能通过发送不同的内容)。
  • 应使用哪些性能特征?通常需要协商缓冲区的大小或允许的调用频率(配额)。

每种类型往往需要略有不同的解决方案,不过本质上都是将交互模型的抽象描述(例如“对等方理解的方法集”)转换为可以交换的数据。

为了妥善解决协议协商问题,第一步是提供一种描述这些概念(“协议”“方法 foo 的响应类型”)的方式。由于对等方从低上下文世界开始,即它们彼此不了解,并且必须假设它们对世界的定义不同,因此概念的描述往往依赖于结构属性。例如,“响应类型为 MyCoolType”这句话毫无意义,需要解读,但“响应类型为 struct { bool; }”这句话本身就具有意义,无需解读。

协议协商与严格互动和灵活互动有何关系

此 RFC 中提出的严格和灵活的交互在协议发展方面提供了一些回旋余地。现在,可以添加或移除方法。甚至可能更多。但是,滥用进化能力会导致协议变得无定形,并且很难从其形状中了解其网域。这类似于随着时间的推移会包含大量字段的表,因为它们现在代表一种“汇总结构”,其中包含随时间变化的多组要求。

在合同协议协商中,如果使用得当,可以隔离版本控制负担,并在进行一些动态选择(协商)后,确定一个更简洁、更严格的协议(可能是 closed 协议)。

这两种进化技巧都有其用武之地,并且都是进化工具箱中必不可少的工具。

替代方案:将严格性位放在事务标识符中

使用事务标识符来传达严格和灵活互动所需的位存在一个重要缺点。某些事务标识符由内核生成,即 zx_channel_call 将消息的前四个字节视为 zx_txid_t 类型的事务标识符。将更多信息打包到事务标识符中会强制内核与 FIDL 之间实现更强的耦合,这是不可取的。通过改用事务性标头标志,使用 zx_channel_call 的 FIDL 代码可以继续在标头中构建所有内容,标识符除外。

替代方案:互动模式位

此 RFC 的早期版本要求添加一个“互动模式”位来区分单向互动和双向互动,并期望扩展到更复杂的互动(例如终端互动)。

主要缺点是,交互模式位与交易标识符中提供的信息冗余:单向交互的交易标识符为零,双向交互的交易标识符不为零。由于信息冗余,这为使用冗余位的不同子集来决定如何处理消息的不同实现(例如绑定)打开了大门。这反过来又为恶意制作消息打开了大门,系统不同部分对该消息的解读各不相同。

虽然我们希望为所有互动分配交易标识符,并扩展互动模式(这两种更改都需要额外的位,如互动模式中所述),但我们更希望在设计这些功能时再讨论此设计。

替代方案:命名

随着此 RFC 的迭代,我们对如何正确命名引入的新概念进行了大量讨论。下面总结了其中的一些讨论。

为了区分哪些互动可以“未知”,哪些互动需要“已知”:

  • openclosed 所选的原始名称。
  • (none)required,因为对等方必须实现该方法,否则协议会终止。
  • 最终版本flexiblestrict 借鉴了 RFC-0033:处理未知字段和严格性

为了区分以下协议:永远无法接收未知互动的协议、可以接收单向未知互动的协议、可以接收单向和双向互动的协议:

  • staticstandarddynamic 原名称已选择。“静态”和“动态”的缺点在于,我们一直使用“静态”和“动态”这两个术语来指代 FIDL 的线格式和消息传递方面。例如,此 RFC 的一部分提到了“动态问题”,其中“动态”的含义与“动态协议”中的“动态”不同。
  • strict(none)flexible 再次借鉴了 RFC-0033
  • 使用 sealed 而不是 static 来强调该协议无法轻松扩展。
  • 使用 hybridmixed,而不是 standard
  • 入围者closedajaropen。由于打开和关闭不用于互动,因此我们可以将它们用于协议修饰符。“半开”的字面意思是“部分打开”,这正是我们想要描述的概念。是的,所有相关人员都觉得它有点诡异。

在先技术和参考资料

(如正文中所述)。


  1. 令人困惑的是,消息(与事务性消息相对)是指 FIDL 值的编码形式。 

  2. 对于 fidlc 和 JSON IR 爱好者,请注意,编译器的内部结构将事件表示为 maybe_request_payload,其中 nullptr 等于 maybe_response_payload,而 present。不过,从模型角度来看,我们将此载荷称为请求,但方向是从服务器到客户端。我们应与组合模型保持一致,更改 fidlc 和 JSON IR。这不在本 RFC 的范围内,但为了完整性,在此记录。 

  3. 我们倾向于使用宽松的语法,并通过 linting 强制执行样式指南。做出此设计选择的原因是,我们希望既能让新手更容易上手,同时又能为 Fuchsia 平台制定非常明确(进而非常详细)的标准。 

  4. 值得注意的是,向 flexible 互动添加 error 可以作为软 ABI 兼容性更改。 

  5. 后来,我们将 transport_errTransportErr 分别重命名为 framework_errFrameworkErr。如需了解详情,请参阅 https://fxbug.dev/42061151。