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

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

此 RFC 將要求在沒有傳輸的情況下使用 FIDL 傳輸格式 (亦即編碼和解碼) 的要求。也會說明繫結如何公開這項功能的評分量表。

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

摘要

此 RFC 將要求在沒有傳輸的情況下使用 FIDL 線路的要求 (也就是編碼和解碼)。也會指定繫結應如何公開這項功能的評分量表。我們引進「無線格式中繼資料」的概念,說明傳輸格式的修訂版本和功能,並要求在編碼和解碼 API 時使用此格式,例如:

  • 繫結已正式支援使用 FIDL 線路 (不需傳輸)。
  • 使用者必須一併傳送傳輸格式訊息和編碼訊息的中繼資料。
  • 繫結可能支援訊息開頭於中繼資料前置字串的持續性慣例。

提振精神

富奇西亞的核心原則是能夠更新。我們在處理序間通訊 (IPC) 環境中使用 FIDL 時,已投入大量心力改善 ABI 的相容性,例如兩個對等端透過 Zircon 管道使用 FIDL 通訊協定。另一方面,FIDL 傳輸格式的獨立使用案例相對罕見,相容性也較低。例如,系統有時會誤以為僅傳遞 FIDL 訊息的編碼位元組會產生變化的 ABI。

驅動程式中繼資料 RFCRFC-0109:Fast datagram 通訊端呼叫,都是透過位元組導向介面傳送 FIDL。現在是正式使用 FIDL 傳輸格式獨立使用情況的好時機,可為它們提供演變和互通性保證。

設計

繫結「必須」支援對 FIDL 傳輸格式進行編碼和解碼,且無需傳輸。這類 API 的要求詳情請見下文。請注意,許多繫結已具備某種形式的公開編碼/解碼 API (例如高階 C++ 繫結中的 fidl::Encode)。這些值應根據 RFC 加以調整。因此,這部分的 RFC 可視為核心功能的正式化,進而釐清 FIDL 的分層。

FIDL 線路格式

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

  • 魔術號碼:識別線路格式的修訂版本。如果接收器不支援這個修訂版本,可以確定會拒絕解碼,而不是將錯誤的解讀方式指派給不相符的線路格式。
  • 旗標:表示這則訊息已啟用的任何軟轉換。舉例來說,在聯集至 x 聯集遷移期間,標記中的其中一個位元會用來表示聯集是以可擴充表示法進行編碼。

獨立使用 FIDL 傳輸格式時,編碼結果中會缺少這項資訊。我們建議在編碼和解碼中加入一部分資訊。具體違規事項如下:

  • 編碼會將繫結/語言專屬網域物件轉換為 FIDL 編碼格式無線格式中繼資料的不透明 blob,說明使用的線路格式修訂版本和功能。
  • 解碼是指以編碼格式使用 FIDL 訊息和對應的傳輸格式中繼資料,產生繫結/語言專屬網域物件。

虛擬程式碼會有下列函式簽章:

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

EncodedMessage 包含已編碼的位元組以及訊息中的所有控點。大部分的繫結都會定義用途類似或同等用途的類型。

傳輸格式中繼資料本身將與 64 位元整數相容的 ABI。其版面配置如下所示:

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) 程式碼路徑中使用,則繫結「應該」提供向量變化版本。

繫結「可能」支援向量化 Unpersist 變化版本,並使用向量化輸入內容,例如使用 zx_iovec_t 連結至多個緩衝區,或與目標語言的慣用讀卡介面整合。

請注意,持續性會產生位元組,而不是可能處理的獨立編碼/解碼。

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

您應更新 FIDL 樣式指南和 API 評分量表,納入持續性注意事項:

  • 清楚指出二進位檔 blob 是否使用持續慣例或自訂/外頻機制來傳送中繼資料。

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 訊息是否包含前置字串的中繼資料標頭,或者中繼資料外傳遞而導致輸入剖析錯誤。我們認為,這些錯誤可能會在測試階段初期發現。加上清楚的說明文件,這種混淆的安全性風險應該會很小。

  • 惡意人士可能會利用路徑處理中的安全漏洞,誘騙程式覆寫類型為 Foo 的保留 FIDL 訊息,以及另一種不同類型的 Bar 訊息 (攻擊者能夠控制這類訊息)。這可讓惡意人士間接影響 Foo 訊息的內容。

替代方案部分呈現較深入的格式,可藉由擴充中繼資料標頭並加入訊息類型相關資訊,以降低這個風險。

隱私權注意事項

FIDL 傳輸格式的填充位元組必須設為零,這樣有助於避免機密資訊外洩。

相較於快速取用的處理序間通訊 (IPC) 中的臨時資料,永久性資料通常更能顧及隱私權,不過我們也已將處理序間通訊 (IPC) 資料傳送至元件,以保存這些資料或透過網路傳送資料。因此,處理序間通訊 (IPC) 和永久性 API 之間會有相似的隱私權疑慮。

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

FIDL API 元素上的隱私權註解將簡化隱私權審查作業,並啟動更完善的下游工具 (例如自動遮蓋);這些註解不在 RFC 範圍內,著重介紹特定傳送 FIDL 訊息的方法。

測試

我們將擴充 GIDL (FIDL 符合性測試套件),以測試永久格式的編碼和解碼。

說明文件

  • 擴充繫結規格,以包含這個 RFC 的新增規定 (例如 LLCPP)。

  • 為獨立編碼/解碼和永久性建立關於 FIDL 的參考頁面,以及兩個 API 之間的關係。

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

缺點、替代方案和未知

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

我們可以進一步解釋慣例,並說它是標準:所有中繼資料都必須放在訊息酬載之前。雖然這足以應付我們今天觀察到的用途,但這個方向可能會有未來變得過於嚴苛的風險。透過提供未經操作的獨立編碼/解碼 API 以及具有主觀性的持續性 API,使用者將可根據自己的設計選擇最適切的持續性 API。

替代方法 2:允許分享線路格式中繼資料

我們可以允許工作階段中的所有訊息都共用相同的中繼資料。 這可讓系統在開始時傳送中繼資料一次,然後在兩個對等點之間的其他通訊時省略中繼資料。這可用來在持續媒介的一個執行個體上,串流多個 FIDL 物件。例如,Fast datagram sockets 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 的較低層級核心功能,運用該 API 進行交易處理序間通訊 (IPC 功能) 的頂端。此替代會產生兩個獨立的功能。具體來說,線路格式中繼資料無法衍生自交易訊息標頭,因為後者使用的要求和回應類型都相同。

整體而言,我們認為這個替代方法能夠防範的安全風險,並不能滿足所需的額外複雜性。

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

主要提案會建議兩種處理 FIDL 傳輸方式的公開 API:

  • 獨立編碼/解碼:可能會將資源類型編碼並產生控點。由 FIDL 交易訊息實作 (用戶端和伺服器繫結) 共用。
  • 保留:不允許資源類型;結果為純資料。傳輸格式中繼資料一律會在酬載前方以一個單位呈現。

從觀察到的,持續慣例就足以滿足現今所有 FIDL 獨立用途的需求。

未來的用途可能更適合單獨傳輸或儲存線上格式中繼資料和酬載。他們可能會觸及獨立編碼/解碼 API,但擁有允許在 API 中處理的缺點,但這可能不失真。

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

  • Binding-internal 獨立編碼/解碼:可對資源類型進行編碼。用於實作交易訊息。
  • 公開獨立編碼/解碼:不允許資源類型。
  • 保留:不允許資源類型。傳輸格式中繼資料一律在酬載前一律以一個單位的形式呈現。

如此一來,當用途需要單獨傳輸或儲存傳輸格式中繼資料時,這可改善非資源的保證,但會導致 API 混淆,因為最終使用兩種編碼/解碼 API,系統只會支援資源類型。這可以像目標語言限制一樣,有時在公開途徑中隱藏 API 可能並不容易。

主要提案會採用簡化路徑,並在這個替代方法中將前兩種 API 合併在一起。

先前的圖片和參考資料