RFC-0120:單獨使用 FIDL 傳輸格式

RFC-0120:獨立使用 FIDL 線路格式
狀態已接受
區域
  • FIDL
說明

本 RFC 正式規定使用 (即編碼和解碼) FIDL 傳輸格式時,沒有傳輸作業的要求。此外,也指定了繫結應如何公開這項功能的評量標準。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-07-02
審查日期 (年-月-日)2021-08-04

摘要

本 RFC 正式規定使用 (即編碼和解碼) FIDL 線路格式的要求,但不得使用傳輸作業。此外,也指定了繫結應如何公開這項功能的評量標準。我們將介紹線路格式中繼資料的概念,說明修訂版本和線路格式的功能,並要求在編碼和解碼 API 中使用該資料,因此:

  • 繫結正式支援使用 FIDL 線路格式,而不需傳輸。
  • 使用者必須傳輸線路格式中繼資料和編碼訊息。
  • 繫結可能會支援持續性慣例,也就是訊息會以中繼資料為前置字元。

提振精神

Fuchsia 的核心原則是可更新。在 IPC 環境中使用 FIDL 時,我們投入了大量資源,確保 ABI 相容性,例如兩個對等互連裝置透過 Zircon 管道使用 FIDL 通訊協定。另一方面,FIDL 線路格式的獨立用途相對罕見,因此相容性較少受到關注。舉例來說,有時會錯誤地假設單獨傳遞 FIDL 訊息的編碼位元組,會產生可演進的 ABI。

驅動程式中繼資料 RFCRFC-0109:快速資料封包插座都要求透過位元導向介面傳送 FIDL。現在是將 FIDL 傳輸格式的獨立用途正式化的好時機,可為這些用途提供演進和互通性保證。

設計

繫結「必須」支援編碼和解碼 FIDL 線路格式,且不含傳輸作業,相關 API 需求詳述如下。請注意,許多繫結已具備某種形式的公開編碼/解碼 API (例如高階 C++ 繫結中的 fidl::Encode)。請按照這項 RFC 調整。因此,RFC 的這個部分可視為核心功能的正式化,釐清 FIDL 的分層。

FIDL 線路格式

FIDL 傳輸格式的重點在於二進位檔相容性:一組有關結構定義演進的保證,可支援讀取使用不同版本結構定義寫入的資料。舉例來說,版面配置為 struct{uint8;uint8;} 的型別可能會演變為版面配置 struct{uint16;}。雖然 FIDL 提供可擴充的資料結構,但這些結構不支援線路格式本身的演進,例如將 FIDL 資料表切換為更有效率的表示法。透過通訊協定和傳輸方式使用 FIDL 線路格式時,交易標頭中的兩項資訊有助於實現二進位檔相容性:

  • 魔術數字:識別線路格式的修訂版本。如果接收器不支援這個修訂版本,可以確定拒絕解碼,而不是將錯誤的解讀結果指派給不相符的線路格式。
  • 旗標:指出這則訊息中啟用的任何軟轉換。舉例來說,在從聯集遷移至 xunion 的過程中,其中一個位元是用於指出聯集是使用可擴充的表示法編碼。

如果單獨使用 FIDL 線路格式,編碼結果會缺少這項資訊。我們建議將部分資訊納入編碼和解碼程序。具體情形如下:

  • 編碼會將繫結/語言專屬網域物件轉換為 FIDL 編碼形式,以及說明所用線路格式修訂版本和特徵的不透明 線路格式中繼資料 Blob。
  • 解碼會以編碼形式和相應的連線格式中繼資料取用 FIDL 訊息,產生繫結/語言專屬網域物件。

以虛擬程式碼表示,這些函式會有下列函式簽章:

function Encode<T>(object: T) -> (EncodedMessage, WireFormatMetadata);
function Decode<T>(message: EncodedMessage, metadata: WireFormatMetadata) -> T;

EncodedMessage 包含編碼的位元組,以及訊息中的任何控制代碼。大多數繫結都會定義用途類似或相同的型別。

線路格式中繼資料本身會具有與 64 位元整數相容的 ABI。其版面配置如下 (以偽 C 標記表示):

struct fidl_wire_format_metadata_t {
    uint8_t disambiguator;
    uint8_t magic_number;
    uint8_t at_rest_flags[2];
    uint8_t reserved[4];
};

RFC-0138:處理不明互動建議將交易標頭中的旗標細分為 dynamic_flags (與通訊協定的要求/回應互動模型相關) 和 at_rest_flags (與線路格式相關)。本 RFC 假設採用此設計,但可輕鬆進行相應調整,且不會失去其主要屬性。

線路格式中繼資料應為 8 位元組對齊,方便訊息就地解碼。繫結必須在外部將中繼資料表示為不透明結構,長度為 8 個位元組,且對齊 8 個位元組 (例如具有單一 uint64 欄位的結構體)。這樣可避免使用者依賴中繼資料內的特定欄位,讓中繼資料本身不斷演進。

繫結必須檢查 reserved 位元組是否為零。繫結「不得」依附於 at_rest_flags 位元組,以取得任何特定值。繫結「必須」驗證 magic_number 代表支援的線路格式修訂版本。

繫結必須檢查 disambiguator 位元組是否為零。中繼資料開頭的零位元可防止程式在訊息以檔案形式保存時,將 FIDL 訊息誤認為文字 (請參閱「資料保存慣例」)。

請注意,FIDL 交易式標頭中包含的資訊是線路格式中繼資料的超集。交易標頭和線路格式中繼資料的 at_rest_flags 欄位和 magic_number 欄位語意相同。

每則訊息只能搭配對應的中繼資料使用。換句話說,您不得共用中繼資料 (例如使用中繼資料 A 解碼訊息 A 和訊息 B),也不得交換中繼資料 (例如使用中繼資料 A 解碼訊息 B,並使用中繼資料 B 解碼訊息 A)。這樣一來,訊息線路格式修訂內容就能在執行階段變更,例如在線路格式軟體遷移期間。

繫結「必須」支援獨立使用,且具有下列頂層型別:

  • 結構
  • 表格
  • Union

如果提供任何其他資料類型,編碼和解碼函式「必須」失敗。如果可以,失敗「應」發生在編譯階段。

FIDL 語言並未規定如何傳輸線路格式中繼資料,或如何將中繼資料與編碼訊息建立關聯。舉例來說,在生產環境 IPC 情境中使用 FIDL 時,中繼資料可能衍生自交易訊息標頭。

資料保留慣例

為進一步支援相關應用情境,我們希望指定將中繼資料附加至已編碼訊息的慣例,也就是在訊息的位元組內容前加上中繼資料。繫結「應」支援這個前置字元風味的獨立線路格式用法,稱為「持續性」

以下是適用範圍內的持續性用途:

  • 將單一 FIDL 物件寫入網路、磁碟或其他不支援傳輸 Zircon 控制代碼的位元/封包導向介面,且不選擇要求/回應範例。也就是說,資料處於「靜態」狀態。
  • 支援超過 64 KiB 的訊息。64 KiB 的訊息大小上限是 Zircon 管道傳輸的屬性。將訊息保存到位元組向量時,則不受這類限制。現有的 Rust 持續性 API 支援大型訊息,且已用於解決管道訊息大小限制問題,方法是將大型值手動保存到 VMO 中,等待內建 FIDL 支援大型訊息。

下列用途不在範圍內:

  • 內建支援編碼相同類型的訊息序列。應用程式可以定義更適合特定用途的自訂串流方式。

使用這個前置字元 API 變體,可從多方面提升人體工學和安全性:

  • 使用者不必手動追蹤資料和中繼資料之間的關聯。資料會緊接在中繼資料之後,並可做為一個單元傳送。相較之下,透過頻外傳遞中繼資料會增加版本不符的風險。如果接收器需要處理多個多路複用至相同持續性媒體的線路格式版本,就會增加複雜度:
    • 如果串流 API 中的傳送者變更身分,新傳送者使用的線路格式修訂版本可能與原始傳送者不同。
    • 假設有個 Proxy 會接收多個元件的持續性訊息,這些訊息使用不同的連線格式修訂版本,並將訊息儲存在資料庫中。為了保留不同的連線格式修訂版本,Proxy 必須將頻外風味轉換回加上前置字元的風味。
  • 緩衝區管理作業會簡化,效能也可能提升,如果熱路徑中使用獨立線路格式,這項優點就相當實用。舉例來說,繫結可以分配一個緩衝區,同時保存中繼資料和酬載,也可以描述單一向量化寫入作業,其中第一個元素指向中繼資料。
  • 使用者不必在多種語言和用戶端程式庫中重新實作傳遞中繼資料的相同邏輯,因為 FIDL 已提供實作項目。

持續性 API 必須支援下列頂層型別:

  • 非資源結構體
  • 非資源資料表
  • 非資源聯集

如果資料類型不是上述兩種,則必須無法保存。如果可以,失敗「應」發生在編譯時期。

以虛擬程式碼來說,持續性 API 會具有下列函式簽章:

function Persist<T>(object: T) -> vector<uint8>;
function Unpersist<T>(bytes: vector<uint8>) -> T;

只要從資料流的角度來看,繫結遵循 API 的形狀,就可使用最適合目標語言的替代命名/方法簽章。

繫結「可能」支援向量化 Persist 變體,可支援向量化輸出,例如產生連結至多個緩衝區的 zx_channel_iovec_tzx_iovec_t,或與目標語言的慣用寫入器介面整合。如果繫結已在 IPC 程式碼路徑中使用向量化變體,則應提供該變體。

繫結 MAY 支援向量化 Unpersist 變數,該變數會採用向量化輸入內容,例如取用連結至多個緩衝區的 zx_iovec_t,或與目標語言的慣用讀取器介面整合。

請注意,持續性會產生位元組,而非可能產生控制代碼的獨立編碼/解碼。

繫結必須支援保存大型值,導致編碼訊息大小超過 64 KiB。

FIDL 樣式指南和 API 評分標準應更新,納入持續性考量:

  • 清楚指出二進位大型物件是否使用持續性慣例,或使用自訂/頻外機制傳遞中繼資料。

FIDL 來源語言

這項 RFC 不會變更 FIDL 來源語言。

實作

繫結應調整獨立的編碼/解碼 API,以配合涉及中繼資料的建議設計。只要函式在資料依附元件方面與提案一致,就不必完全遵循函式簽章。舉例來說,解碼器的行為必須可透過中繼資料以某種方式設定。

應使用相同的獨立編碼/解碼 API 實作訊息傳遞,例如透過 Zircon 管道傳送交易訊息。

您可以獨立新增持續性 API 變種版本的繫結支援。

Rust 繫結中已實作持續性 API,但資料格式和 API 與本 RFC 中的設計不符。Rust 實作項目將會調整,以符合已接受的設計。

Rust 變更

目前 Rust 繫結提供下列函式:

fn create_persistent_header() -> PersistentHeader;
fn encode_persistent_header(header: &mut PersistentHeader) -> Result<Vec<u8>>;
fn encode_persistent<T: Persistable>(body: &mut T) -> Result<Vec<u8>>;
fn encode_persistent_body<T: Persistable>(body: &mut T, header: &PersistentHeader) -> Result<Vec<u8>>;
fn decode_persistent<T: Persistable>(bytes: &[u8]) -> Result<T>;
fn decode_persistent_header(bytes: &[u8]) -> Result<PersistentHeader>;
fn decode_persistent_body<T: Persistable>(bytes: &[u8], header: &PersistentHeader) -> Result<T>;

請將這些項目替換為下列項目 (由於借用和生命週期細微差異,確切簽章可能有所不同):

fn persist<T: Persistable, W: std::io::Write>(body: &mut T, writer: W) -> Result<()>;
fn unpersist<T: Persistable, R: std::io::Read>(reader: R) -> Result<T>;

fn standalone_encode<T: TopLevel, W: std::io::Write, H: core::iter::Extend<HandleDisposition>>(body: &mut T, writer: W, out_handles: &mut H) -> Result<WireMetadata>;
fn standalone_decode<T: TopLevel, R: std::io::Read>(reader: R, handles: &mut [HandleInfo], metadata: &WireMetadata) -> Result<T>;

struct WireMetadata { /* private fields */ }

TopLevel 特徵會針對結構體、聯集和資料表實作。

具體來說,使用者無法再從無到有建立持續性標頭,並重複使用同一標頭編碼多則訊息。

此外,繫結「應」提供將 WireMetadata 序列化/還原序列化為位元組的方法,以支援傳遞頻外中繼資料。

效能

獨立編碼和解碼是 FIDL 交易式用法的一部分,而持續性 API 應共用大部分的程式碼路徑。因此,我們可以沿用相同的標準和效能基準。

人體工學

繫結人體工學設計應鼓勵採用持續性慣例。舉例來說,繫結可以使用較短且更符合慣例的函式名稱來代表持續性風味 (例如 fidl::persist),並使用較長且更明確的函式名稱來代表公開獨立的編碼/解碼 API (例如 fidl::standalone::encode)。

回溯相容性

這項變更本身是回溯相容,因為除了 Rust FIDL 持續性實作項目外,其餘部分都是純粹的加法。據我們所知,Rust FIDL 持久性目前的所有讀取器、寫入器和儲存資料,一律會同步演進。

新增線路格式中繼資料可提升日後的向後相容性,為即將推出的 FIDL 線路格式遷移作業做好準備。

線路格式中繼資料包含 5 個保留位元組。這些位元組日後可能會重新用於其他用途。舉例來說,我們可能會使用一個位元組來描述與持續性相關的問題。

安全性考量

FIDL 線路格式的驗證規定也適用於此,且具有相同的安全性屬性。

請注意,FIDL 並非自述式格式。使用某種訊息類型成功還原序列化持續性訊息,並不保證資料最初是使用該訊息類型序列化:

  • 程式可能會混淆 FIDL 訊息是否包含前置中繼資料標頭,或中繼資料是否以頻外方式傳遞,導致輸入內容剖析錯誤。我們相信這類錯誤通常會在測試階段早期發現。加上清楚的文件,這種混淆造成的安全風險應該很小。

  • 惡意行為人可能會利用路徑處理中的安全漏洞,欺騙程式以其他類型的訊息 Bar 覆寫持續性 FIDL 訊息 (類型為 Foo),藉此控制該訊息。這會讓惡意行為人間接影響 Foo 訊息的內容。

替代方案部分會介紹更複雜的格式,可透過擴充中繼資料標頭並加入訊息類型資訊,降低這項風險。

隱私權注意事項

FIDL 線路格式中的填補位元組必須為零,有助於避免洩漏敏感資訊。

相較於 IPC 中迅速耗用的暫時性資料,永久性資料通常會帶來較大的隱私權疑慮,但我們也會將 IPC 資料傳送至會保留資料或透過網路傳送資料的元件。因此,IPC 和持續性 API 的隱私權疑慮相似。

請注意,即使我們未提供 API,開發人員仍可透過其他方式 (例如 JSON 或 XML) 手動保存 FIDL 資料。如果未來的設計提到會保留使用者或其他私密資料,無論方法是否為透過 FIDL 持續性,都應適用一般隱私權審查。

FIDL API 元素上的隱私權註解可簡化隱私權審查,並啟用更完善的下游工具 (例如自動修訂)。這些註解不在本 RFC 的範圍內,因為本 RFC 著重於傳輸 FIDL 訊息的特定方法。

測試

我們將擴充 FIDL 一致性測試套件 GIDL,測試持續性格式的編碼和解碼。

說明文件

  • 擴增繫結規格,納入這項 RFC 新增的需求 (例如 LLCPP)。

  • 建立 FIDL 參考頁面,說明獨立編碼/解碼和持續性,以及這兩個 API 之間的關係。

    • RustLLCPP 已有相關說明文件。現有文件將會更新。
  • 在所有語言中,將獨立編碼/解碼和持續性示範新增至 //examples/fidl/,並新增相應的教學課程。

缺點、替代方案和未知事項

替代方案 1:僅支援持續性 API

我們可以進一步規定,所有中繼資料都必須位於訊息酬載之前,並將此規定視為標準。雖然目前觀察到的用途已足夠,但這個方向未來可能會變得過於僵化。我們提供不帶偏見的獨立編碼/解碼 API,以及帶有偏見的頂層持續性 API,使用者可選擇最符合設計需求的 API。

替代方案 2:允許共用線路格式中繼資料

我們可以允許在工作階段中,為所有訊息共用相同的中繼資料。 這樣一來,中繼資料就能在開頭傳送一次,之後兩部對等互連裝置之間的通訊就不會再傳送中繼資料。這可用於透過單一持續性媒體例項,串流多個 FIDL 物件。舉例來說,快速資料包通訊端 RFC 可先透過通訊端傳送中繼資料,再以編碼形式傳送多個物件,避免在每個 UDP 資料包中新增 8 個位元組。

因此,我們採用一項限制,也就是中繼資料不得與編碼的特定訊息相關,只能與編譯至訊息產生器的 FIDL 執行階段版本相關。這也表示,如果 FIDL 編碼器支援多種傳輸格式表示法,就不得在執行階段任意切換傳輸格式表示法。

中繼資料造成的 8 位元組額外稅金似乎不會過於昂貴。一律在訊息中加入這項資訊,可大幅減輕使用者端額外中繼資料追蹤的複雜度。

如果使用案例確實需要不含中繼資料的原始效能,我們可以考慮在 FIDL 中加入串流功能。舉例來說,您可以想像定義透過插座傳輸的資料,該資料會串流單一型別的值。繫結可實作,盡可能略過傳送中繼資料,並禁止變更傳送者/接收者身分 (舉例來說,每當引入新對等互連時,都必須重新建立傳輸)。

@stream
protocol UdpSocketPayload over zx.socket {
    Send(SendMsgPayload);
    -> Recv(RecvMsgPayload);
};

如果中繼資料一律專屬於訊息,就能針對每則訊息使用旗標。舉例來說,我們可能會使用一個旗標表示主體已壓縮。我們也可能設計更緊密的線路表示法,減少朝向高效能記憶體內 IPC 限制的旋轉 (例如,不需要為指標或對齊保留空間)。替代格式可由中繼資料保留區域中的另一個位元表示。

替代方法 3:使用交易訊息標頭

持續性期間的線路格式中繼資料可與交易訊息標頭相容。

我們會將持續性視為傳輸,並使用單一方法寫入物件:

@persistence
type Metadata = table {
    1: foo int32;
    2: bar vector<int64>;
};

// desugars to
protocol MetadataSink over persistence {
    // Ordinal is the hash of `mylib/MetadataSink.Metadata`.
    Metadata(Metadata);
};

這種做法有以下優點:

  • 重複使用現有的 FIDL 功能。除了將位元組寫入其他類型的接收器 (VMO、檔案、通訊端) 之外,持續性與透過管道傳訊相同。我們也可以在稍後新增串流或多重訊息支援,方法是加入控制封包 (控制序數),告知訊息大小等資訊。
  • 重複使用序數雜湊檢查,減少串擾並提升安全性:攻擊者無法在資料平面 (酬載) 中調整某些位元組,將訊息偽裝成其他類型。這項策略與管道上 FIDL 方法的安全屬性一致。

這似乎是傳輸一般化的絕佳案例,但結果會是相當奇怪的通訊協定,只能單向傳輸一次:用戶端只能傳送一種值,而且只能傳送一次。收件者無法回覆。這與我們在通訊協定層級新增的演進功能 (例如開放式和封閉式互動) 不相容。

替代方案 4:使用訊息類型資訊擴充線路格式中繼資料

相較於替代方案 3,這個步驟較不冒險,我們可以將保存訊息的完整類型名稱雜湊化,並新增至線路格式中繼資料,以識別保存訊息的類型,不必導入完整的傳輸概念。

這可減少串音,但仍有其他細微的複雜性:

  • 如何處理型別重新命名:持續性型別的名稱現在會成為 ABI 的一部分,因為這會影響中繼資料中的雜湊。
  • 如何與 FIDL 的交易式 IPC 用途協調一致:主要提案將獨立編碼/解碼 API 制定為 FIDL 的低階核心功能,交易式 IPC 功能可在此基礎上建構。這個替代方案會產生兩個不同的功能。具體來說,由於後者使用的序數雜湊方法與要求和回應類型相同,因此無法從交易訊息標頭衍生出線路格式中繼資料。

整體而言,我們認為這個替代方案防範的安全風險,不值得額外增加複雜度。

替代方案 5:將獨立 API 限制為非資源類型

主要提案建議兩種處理 FIDL 線路格式的公開 API:

  • 獨立編碼/解碼:可編碼資源類型並產生控制代碼。由 FIDL 交易訊息實作 (用戶端和伺服器繫結) 共用。
  • 持久性:不允許資源類型,結果為純資料。線路格式中繼資料一律會以單一單元的形式,出現在酬載之前。

這是因為我們觀察到,目前 FIDL 的所有獨立用途都適用於持續性慣例。

在未來的用途中,可能更適合分別傳輸或儲存線路格式中繼資料和酬載。他們可以採用獨立的編碼/解碼 API,但這類 API 允許使用控制代碼,可能造成不必要的風險。

另一種做法是提供三種 API:

  • 繫結內部獨立編碼/解碼:可能會編碼資源類型。用於交易訊息導入作業。
  • 公開獨立編碼/解碼:不允許資源類型。
  • 持續性:不允許資源類型。線路格式中繼資料一律會以一個單位的形式,出現在酬載之前。

如果使用案例需要分別傳輸或儲存線路格式中繼資料,這項做法可提升非資源保證,但會造成 API 混淆,因為我們最終會得到兩種編碼/解碼 API,兩者唯一的差異在於是否支援資源類型。目標語言的限制有時會使情況更加複雜,因為有時很難從公用介面隱藏 API。

主要提案採取簡化路徑,並合併這項替代方案中的前兩種 API。

既有技術和參考資料