RFC-0120:使用獨立的 FIDL 線路格式 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 這份 RFC 將 FIDL 傳輸格式 (在沒有傳輸的情況下) 的使用 (即編碼和解碼) 要求正式化。並指定繫結應如何公開這項功能。 |
問題 | |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2021-07-02 |
審查日期 (年-月-日) | 2021-08-04 |
摘要
這份 RFC 將 FIDL 線路格式 (在沒有傳輸的情況下) 的使用 (即編碼和解碼) 需求正式化。並指定繫結應如何公開這項功能。我們引入線路格式中繼資料的概念,說明線路格式的修訂版本和功能,並要求在編碼和解碼 API 中使用這項資料,例如:
- 繫結正式支援在不使用傳輸工具的情況下使用 FIDL 線路格式。
- 使用者必須將電報格式中繼資料與已編碼的訊息一併傳送。
- 繫結可能會支援持久性慣例,其中訊息的前置字元為中繼資料。
提振精神
Fuchsia 的核心原則是可更新性。我們在使用 IPC 情境中的 FIDL 時,已投入大量心力來提升 ABI 相容性,例如兩個同級機器透過 Zircon 管道使用 FIDL 通訊協定。另一方面,由於 FIDL 線路格式的獨立用途相對較少,因此相容性受到的關注較少。舉例來說,有時會錯誤假設只要傳遞 FIDL 訊息的已編碼位元組,就能產生可進化的 ABI。
Driver metadata RFC 和 RFC-0109:Fast datagram sockets 都會透過以位元組為導向的介面傳送 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 支援大型訊息,並已用於解決管道訊息大小限制的問題,等待內建的 FIDL 支援大型訊息,方法是手動將大型值儲存在 VMO 中。
以下用途超出處理範圍:
- 內建支援功能,可對同類型訊息的序列進行編碼。應用程式可以定義自訂串流方法,以便更適合特定用途。
使用這個前置字串 API 版本可在多方面改善人體工學和安全性:
- 使用者不必手動追蹤資料和中繼資料之間的關聯。資料只會遵循中繼資料,且可傳送為一個單位。相較之下,將中繼資料傳遞至頻外會增加版本不相符的風險。如果接收器需要處理多個匯流格式版本,並將這些版本多重匯入相同的持久性媒體,則會產生額外的複雜性:
- 當串流 API 中的傳送端變更身分時,新傳送端可能會使用與原始傳送端不同的傳輸格式修訂版本。
- 請考慮使用不同線路格式修訂版本,從多個元件接收持久性訊息,然後將這些訊息儲存在資料庫中的 Proxy。為了保留不同的線路格式修訂版本,代理程式必須將頻外版本轉換回前置版本。
- 緩衝區管理功能會簡化,效能也可能提升,如果在熱門路徑中使用獨立線路格式,這會很有幫助。舉例來說,繫結可以分配一個緩衝區來保存中繼資料和酬載,或是描述單一向量化寫入作業,其中第一個元素會指向中繼資料。
- 由於 FIDL 已提供實作項目,因此使用者不必重新實作用於傳遞多種語言和用戶端程式庫的中繼資料的相同邏輯。
持久性 API 必須支援下列頂層類型:
- 非資源結構體
- 非資源表格
- 非資源聯結
在任何其他資料類型中,持久化作業都會失敗。失敗應盡可能發生在編譯期間。
在虛擬程式碼中,持久性 API 會有以下函式簽章:
function Persist<T>(object: T) -> vector<uint8>;
function Unpersist<T>(bytes: vector<uint8>) -> T;
繫結可使用其他命名/方法簽章,只要從資料流程的角度來說,這些簽章最適合目標語言即可。
繫結可能支援向量化 Persist
變化版本,該版本支援向量化輸出,例如產生連結至多個緩衝區的 zx_channel_iovec_t
或 zx_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 的範圍,因為本 RFC 著重於傳輸 FIDL 訊息的特定方法。
測試
我們將擴充 GIDL (FIDL 相容性測試套件),以測試持續性格式的編碼和解碼。
說明文件
建立參考頁面,說明用於獨立編碼/解碼和持久性的 FIDL,以及這兩個 API 之間的關係。
以所有語言新增至
//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、檔案、Socket)。我們之後也可能會新增串流或多訊息支援功能,方法是加入控制封包 (控制序號),以便提供訊息大小等資訊。
- 重複使用序列雜湊檢查可減少交叉干擾並提升安全性:攻擊者無法透過在資料平面 (酬載) 中修改部分位元組,將訊息偽裝成其他類型。這項策略與管道中 FIDL 方法的安全性屬性相符。
這似乎是運輸泛化功能的完美範例,但結果會是一種非常奇怪的通訊協定,只會單向傳送一次:用戶端只能傳送一種值一次。收件者無法回應。這與我們在通訊協定層級新增的演進功能 (例如開啟和關閉互動) 不相容。
替代方案 4:使用訊息類型資訊擴充線路格式中繼資料
相較於第 3 個替代方案,這個步驟較不具冒險性,我們可以對已儲存訊息的完整類型名稱進行雜湊,並將其加入線路格式中繼資料,以便識別要儲存的訊息類型,而不需要引入完整傳輸的概念。
這麼做可減少串音,但仍有其他微妙的複雜性:
- 如何處理型別的重新命名:已儲存型別的名稱現在會成為 ABI 的一部分,因為這會影響中繼資料中的雜湊。
- 如何將此與 FIDL 的交易 IPC 用途協調一致:主要提案將獨立的編碼/解碼 API 定義為 FIDL 的較低層級核心功能,以便在其上建構交易 IPC 功能。這個替代方案會產生兩種不同的功能。具體來說,傳輸格式中繼資料無法從交易訊息標頭衍生,因為後者使用的方法序號雜湊與要求和回應類型相同。
整體而言,我們認為這個替代方案所能防範的安全風險,不值得帶來額外的複雜性。
替代方案 5:將獨立 API 限制為非資源類型
主要提案建議兩種公開 API 來處理 FIDL 線路格式:
- 獨立編碼/解碼:可能會編碼資源類型並產生句柄。由 FIDL 交易訊息實作項目 (用戶端和伺服器繫結) 共用。
- 持久性:不允許資源類型,結果為純資料。線路格式中繼資料一律會以單一單位置於酬載之前。
這是因為我們發現,現今 FIDL 的所有獨立用途都已足以支援持久性慣例。
未來的用途可能更適合用於分別傳送或儲存線路格式中繼資料和酬載。他們可以使用獨立的編碼/解碼 API,但這會導致在 API 中允許句柄,這可能不必要。
另一種做法是提供三種 API:
- 繫結內部獨立的編碼/解碼:可能會編碼資源類型。交易訊息實作項目使用。
- 公開的獨立編碼/解碼:不允許資源類型。
- 持久性:不允許資源類型。線路格式中繼資料一律會以單一單位置於酬載之前。
當用途需要個別傳送或儲存線路格式中繼資料時,這項功能可改善非資源保證,但會導致 API 混淆,因為我們最終會提供兩種編碼/解碼 API,唯一的差異在於支援的資源類型。這與目標語言的限制有關,有時很難在公開途徑中隱藏 API。
主要提案採用簡化路徑,並將這個選項中前兩種 API 合併在一起。