RFC-0137:捨棄 FIDL 中的未知資料

RFC-0137:捨棄 FIDL 中的不明資料
狀態已接受
區域
  • FIDL
說明

讓 FIDL 繫結捨棄不明資料,而非保留並代理這些資料。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-08-25
審查日期 (年-月-日)2021-10-13

摘要

大多數 FIDL 繫結會保留不明的表格欄位和聯集變數,方便使用者程式碼檢查及重新編碼原始位元組和控制代碼。這種行為會造成安全和隱私權風險,大幅增加 FIDL 的複雜度,導致難以遷移線路格式,且無法在所有繫結中實作。我們建議改為讓繫結捨棄不明資料,產生表 1 所示的行為。

表 1:彈性類型 (資料不明) 的變更

類型 可以存取不明內容嗎? 可以重新編碼嗎? Proxy 不明?
位元
enum
桌子 是 → 是 →
聯集 是 → 僅限序數 是 → 是 →

背景

彈性型別是 FIDL 的重要功能,可編寫可演進的 API。RFC-0033:處理不明欄位和嚴格程度中已介紹過這些欄位,自 2020 年底起,所有繫結都提供這些欄位。使用彈性型別時,即使有不明成員,解碼作業仍會成功。FIDL 資料表一律為彈性,而位元、列舉和聯集則可標示為嚴格或彈性。使用彈性位元和列舉時,未知值只是整數。不過,如果是不明的資料表欄位或聯集變體,值會包含原始位元組和控制代碼,我們稱之為「不明資料」

目前大多數繫結都會在網域物件保留不明資料。例外狀況是 LLCPP,其設計限制導致難以支援這項功能。對於其他資料,保留不明資料可啟用下列行為。假設程序 A、B 和 C 透過 FIDL 通訊。如果 A 和 C 知道新的表格欄位,但 B 不知道,且該欄位是從 A 傳送至 B 再傳送至 C,則 C 會收到並瞭解該欄位,即使 B 不知道也一樣。換句話說,B 會代理未知資料。應用程式也可能根據對結構定義的假設 (例如前四個位元組一律為 ID),解讀不明資料。不過,這類情況是人為設計,最好直接使用 FIDL 型別建立模型。保留不明資料的唯一實際用途是 Proxying。

提振精神

設計 FIDL 時,我們盡量使用最少的必要功能解決實際問題。保留不明資料的功能並未符合這些原則。自實作以來,Fuchsia 幾乎沒有使用過這項功能,而且在其他 FIDL 工作中,這項功能也屢次成為複雜因素

這讓我們質疑代理未知資料的優點。這項做法是否合適?我們認為並非如此,至少不應是預設行為。對於真正要當做 Proxy 的 FIDL 伺服器來說,這項功能或許很有用。不過,在這些情況下,FIDL 專屬的 Proxy 支援會是更好的解決方案,因為這項支援可解決問題的所有層面,而不僅是未知資料。

即使我們假設預設的 Proxy 處理是可取的,但只有在直接重新編碼 FIDL 網域物件時,這項功能才有效。不過,建議您先將這些物件轉換為更豐富的應用程式專屬型別,再進行後續處理。這種做法與處理 Proxy 的方式相反,因為處理 Proxy 時,最好是傳遞未經變更的編碼訊息,或是直接重新編碼解碼訊息,並盡量減少處理作業。舉例來說,Rust Crate fidl_table_validation 會在將 FIDL 網域物件轉換為應用程式網域物件時提供驗證。因此,如果任何參與者使用此模式,在複雜系統中透過多個躍點傳送表格的對等互連裝置,就無法確保所有欄位都能抵達最終目的地。

無論是否需要 Proxy,保留不明資料都有幾項缺點。這會增加線路格式遷移的難度。在遷移期間,當所有對等互連裝置都能讀取新舊格式時,就可以開始寫入新格式。由於這項變更無法同時在所有地方生效,因此在過渡期間,對等互連裝置難免會同時收到舊格式和新格式的訊息。假設這兩個資料表都含有不明的資料表欄位,且系統嘗試將這兩個資料表編碼為單一訊息。如要保留這類不明資料,唯一的方法是在每個封包中加入線路格式中繼資料,但這會增加難以接受的複雜度和額外負荷。

另一個缺點是 FIDL 繫結之間的特性同位性。在支援就地解碼的繫結 (例如 LLCPP) 中,很難選擇可同時擁有控制代碼和代表不明控制代碼的網域物件表示法。對於已知資料,解碼器會覆寫存在指標,在網域物件中插入控制代碼。對於不明資料,解碼器只知道要在控制代碼表格中略過的控制代碼數量,不知道存在指標的位置。因此,除非同時傳回網域物件和重新封裝的控制代碼資料表,否則無法傳回擁有網域物件。相反地,這些繫結只是不支援保留不明資料。這可能會讓在其他繫結中依賴這項功能的開發人員感到意外,而且會增加我們的測試負擔,因為所有涉及不明資料的案例都需要進行兩項 GIDL 測試。

一般來說,保留不明資料的需求會大幅增加 FIDL 的複雜度。這種複雜性不僅限於實作,也會影響使用者與其他功能的互動。舉例來說,值類型和資源類型之間的差異只會影響 API 相容性,不會影響 ABI。不過,後來發現,如果收到彈性值型別的不明控制代碼,就會對 ABI 造成無法避免的影響。只有在需要保留網域物件中的不明資料時,才會出現這種特殊情況。

利害關係人

誰會受到這項 RFC 是否通過的影響?(這個部分為選填,但建議填寫)。

主持人:pascallouis@google.com

審查者:abarth@google.com、yifeit@google.com、ianloic@google.com

諮詢對象:bryanhenry@google.com

社交化:這份 RFC 草案已送交 FIDL 團隊徵詢意見。

設計

彈性位元和列舉的不明值處理方式維持不變。

解碼資料表和彈性聯集時:

  • 除非繫結專為 Proxy 設計,否則繫結「不得」在網域物件中儲存不明位元組和控制代碼。

  • 繫結「必須」關閉所有不明控制代碼。

重新編碼先前解碼的資料表和彈性聯集時:

  • 繫結「必須」成功重新編碼資料表的已知欄位,且「不得」包含不明欄位 (這表示會儲存這些欄位)。

  • 當編碼具有不明變體的彈性聯集時,繫結必須失敗並傳回錯誤。

表格和彈性聯集適用的網域物件:

  • 繫結「不應」提供任何機制,用來區分沒有不明欄位的資料表,以及已捨棄不明欄位的資料表。如果繫結提供深層相等函式,則應視為相等。

  • 繫結「必須」提供機制,判斷彈性聯集是否含有不明變體,且「應」提供不明序數的存取權 (也就是說,網域物件的不明變體應只儲存序數)。如果繫結提供深層相等函式,則不明變數的行為應類似 NaN,即使序數相同,比較結果也不相等。

在 Rust 中,後者表示要從彈性聯集和遞移包含一項特徵的型別中移除 Eq 特徵,就像浮點數一樣。

實作

實作方式主要是刪除負責在所有繫結中保留不明項目的程式碼。我們認為不明資料存取者不會用於任何生產用途。如有,我們必須瞭解使用案例,並嘗試找出解決方法。

目前,LLCPP 無法重新編碼含有不明欄位的表格。這項設定必須根據設計變更,才能成功編碼已知欄位。

安全性考量

這項提案可減少隱含傳遞的資訊和功能,因此有助於提升安全性。保留不明資料時,很容易透過毫無戒心的元件傳遞任意位元組和控制代碼。捨棄後,資料界線會由 FIDL 架構準確編碼,方便系統稽核。

隱私權注意事項

這項提案會限制傳輸不明資料 (可能包含私密資訊),因此有助於提升隱私權。

測試

測試大多會在 GIDL 中進行。涉及不明資料的 success 測試會分成兩部分:decode_successencode_success (僅編碼已知資料表欄位) 或 encode_failure (聯集無法編碼)。不明資料的值表示方式也會變更。GIDL 不應再剖析不明位元組和控制碼,而是使用 123: unknown 語法,在序數 123 指出不明封包。

您可以移除 LLCPP 和非 LLCPP 分開的允許清單和拒絕清單。所有繫結都會針對不明資料採用相同的編碼/解碼行為。此外,fxrev.dev/428410 中新增的 LLCPP 專屬單元測試可以移除,改用 GIDL 測試。

測試應會繼續執行嚴格/彈性和值/資源的所有組合,但使用彈性值類型的控點解碼不明資料時,不會再失敗。

說明文件

需要更新下列文件:

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

替代做法:選擇性保留未知值

我們不會完全移除保留不明資料的支援,而是可以繼續支援,只是預設不會保留。舉例來說,這可能是選擇加入,並在類型上加上屬性,或許會限制值類型,以減輕對不明控制代碼的代理疑慮。不過,這種做法會讓很少使用的功能更加複雜,而且無法解決線路格式遷移問題。

缺點:彈性類型不一致

這項提案的缺點是會導致彈性型別的行為較不一致,或許也較不直觀。為說明這點,請根據兩個軸向分類位元、列舉、表格和聯集,如表 2 所示:代數型別 (產品或總和) 和酬載 (含或不含酬載)。

表 2:彈性類型分類

產品類型 加總類型
沒有酬載 位元 enum
酬載 桌子 聯集

目前,所有彈性型別都會將不明資訊設為 Proxy。這項提案打破了兩個軸的對稱性。舉例來說,請參考下列 FIDL 型別:

// Product types (multiple fields set)
type Bits  = bits  {    A = 1;         B = 2; };
type Table = table { 1: a struct{}; 2: b struct{}; };

// Sum types (one variant selected)
type Enum  = enum  {    A = 1;         B = 2; };
type Union = union { 1: a struct{}; 2: b struct{}; };

首先,有效負載軸會失去一致性。目前從 BitsTable,或從 EnumUnion,都會增加功能,允許每個成員攜帶酬載。但這項提案的代價是無法再保留未知數。

其次,我們在代數型別軸上失去一致性。目前 TableUnion 都允許在解碼含有不明資料的物件後重新編碼。有了這項提案,Table 可以重新編碼,但 Union 無法。

我們認為,為了避免「動機」一節所述的複雜情況,這種實用性與一致性之間的取捨是值得的。不過,下文介紹的替代設計可維持較高的一致性。

替代做法:捨棄所有不明資訊

為提升一致性,我們可以捨棄所有不明資訊,包括容易儲存的不明整數。也就是說,位元和列舉的狀態不明,且會捨棄不明序數,以及聯集酬載。表 3 顯示產生的行為。

表 3:調整表 1:捨棄所有不明資訊

類型 可以存取不明內容嗎? 可以重新編碼嗎? Proxy 不明?
位元 是 → 是 →
enum 是 → 是 → 是 →
桌子 是 → 是 →
聯集 是 → 是 → 是 →

替代方案:選用彈性聯集

為提升一致性,我們可以規定彈性聯集一律為選用,然後將不明變數解碼為缺少的聯集。這樣一來,就能重新編碼聯集,使其與資料表保持一致。表 4 顯示產生的行為。

表 4:表 1的調整項目:彈性聯盟 (選用)

類型 可以存取不明內容嗎? 可以重新編碼嗎? Proxy 不明?
位元
enum
桌子 是 → 是 →
聯集 是 → 是 →

替代方法:記住是否捨棄不明欄位

在建議的設計中,繫結使用者無法判斷解碼資料表時是否捨棄了不明欄位。另一種做法是在資料表網域物件中儲存布林值,或一組不明序數。使用者隨後可透過 has_unknown_fields() 等函式查詢這項資料。舉例來說,儲存服務可能想在這種情況下失敗,以避免資料遺失。

這個替代方案的缺點是,它會將額外的隱藏狀態新增至表格網域物件。不再是簡單的值型別,而是欄位的總和。 舉例來說,這會引發一個問題:== 運算子是否應將這類布林值旗標納入考量。

替代方案:重要欄位

先前所述,檢查 had_unknown_fields() 的唯一實際用途是傳回 true 時失敗。我們可以在繫結中提供該存取子,也可以接受表格和彈性聯集成員的屬性,選擇加入該行為:

type Data = table {
  1: foo string;
  @important
  2: bar string;
};

這個屬性的作用是為該欄位在信封標頭中設定新預留的位元。解碼器遇到重要位元已設定的不明欄位時,必須失敗。換句話說,@important 屬性會選擇不採用向前相容性,運作方式類似於我們允許在位元、列舉和聯集上使用的靜態 strict 修飾符動態版本。

這個替代方案可能需要自己的 RFC,才能在此 RFC 的基礎上運作。

致謝:這個構想來自 yifeit@google.com。

替代方案:保留值/資源的 ABI 影響

這項提案可消除 RFC-0057 的 ABI 影響,並將此視為捨棄不明資料後可實現的改善。不過,有人認為 ABI 影響是可取的,應該保留。

降低 ABI 影響的優點 (本提案):

  • 因此嚴格/彈性和值/資源會成為更獨立的功能。交集不再是特例。我們在撰寫相關 RFC 很久之後才注意到這個案例,因此使用者可能也會感到驚訝。
  • 這樣一來,從值到資源的型別轉換會更加容易,因為這只會中斷 API,不會中斷 ABI。如果沒有程式碼中斷 (要求和回應類型可能發生這種情況,因為某些繫結無法直接參照這些類型),這項轉換作業就不會再默默變更行為。

保留 ABI 影響 (這個替代方案) 的優點:

  • 可更準確地模擬介面的意圖。如果您表示不希望有控制代碼 (使用值類型),但在執行階段收到控制代碼,這表示有落差,因此失敗是適當的。
  • 如果改變心意,我們稍後可以放棄 ABI 影響。反向切換較可能導致中斷。

既有技術和參考資料

Protobuf

通訊協定緩衝區的設計在這方面反覆變更。在 proto2 中,系統會保留並代理未知欄位 (如同現今的 FIDL)。在 proto3 中,行為已變更為在解碼期間捨棄不明欄位 (如這項提案)。不過,這項決定後來遭到撤銷,因此在 3.5 以上版本中,proto3 會再次保留不明欄位。

這引發了一個問題:如果我們接受這項提案,FIDL 是否會走上相同的道路?我們認為答案是否定的,因為 FIDL 和 Protobuf 佔據不同的設計空間。由於有兩個用途 (中介伺服器和讀取 - 修改 - 寫入模式),Protobuf 必須還原為舊的保留行為。這兩者在 Fuchsia 中都不常見。Fuchsia 的安全與隱私權原則鼓勵直接通訊,而非透過中介 Proxy 伺服器。FIDL API 評分標準建議採用部分更新模式,而非讀取-修改-寫入模式。

Thrift

Apache Thrift 會捨棄不明欄位