RFC-0029:增加方法一般

RFC-0029:增加方法序號
狀態已接受
區域
  • FIDL
說明

將方法序號的大小從 32 位元增加至 64 位元,同時保留最高順序位元集合,以便 Fuchsia 保留用途。將墓誌銘序列更新為 0xFFFFFFFFFFFFFFFF (從 0xFFFFFFFF)。使用交易標頭的旗標欄位,將遞增序號大小加以封裝。並停止使用 FragileBase 屬性。

作者
提交日期 (年-月-日)2019-02-14
審查日期 (年-月-日)2019-02-28

摘要

我們建議:

  1. 將方法序號的大小增加到 64 位元 (從 32 位元增加),同時保留範圍,並將最高順序位元集指定為 Fuchsia 保留用途。
  2. 墓誌銘序數更新為 0xFFFFFFFFFFFFFFFF (從 0xFFFFFFFF 更新);
  3. 使用交易標頭的旗標欄位,將序數大小增加。
  4. 停止使用 [FragileBase] 屬性

與其他 RFC 的關係

此 RFC 已由下列 RFC 取代:

提振精神

從遠端對抗破損

我們花了一段時間研究組合模型引發的斷裂問題。

簡而言之,當 Child 通訊協定組合 Parent 通訊協定時,Parent 可能會在後續引入與 Child 中定義的函式相衝突的函式。這項變更可能在 Parent 中進行,但未察覺在 Child 通訊協定中造成的「遠端」中斷。

以目前的序號雜湊法為例,如果有 1,000 個方法,就會有 10,000 分之 2 的機率發生協定衝突。考量到 FIDL 中需要建構及表達的通訊協定 (和預期的通訊協定) 數量眾多,這很有可能會發生。我們已使用暫時性註解 [FragileBase] 來指出這個問題,並讓開發人員瞭解這個問題。

透過增加方法序數專用的位元數量,我們可以將衝突的機率降低到可接受的程度,以便考慮在實際上不存在的距離中發生破損的問題。

方法序號的大小

一般生日問題可清楚說明衝突的機率。我們有 D = 2^k 種可能性 (「天」),並且在 N 種方法 (「人」) 中,找出兩種方法發生碰撞的機率 (「相同的生日」)。

如果我們將百萬分之一的門檻視為願意容許的最大衝突機率,那麼方法的最大數量會是:

  • 31 位元:約 60
  • 39 位元:約 1 千
  • 47 位元:約 16k
  • 52 位元:約 95k
  • 63 位元:約 4.3M

考量上述因素,47 位元以上的選擇是合理的。我們選擇 63 位元還有其他幾個原因:

  • 實際上,在 JSON 中使用標準剖析器 (例如 Go 或 Python) 將序數設為數字是安全的。(在 JSON IR 的 2 版中,我們打算將序數包裝為字串)。
  • 由於控制訊息有空位,因此如果日後需要分配其他標記,就會有足夠的空間。目前唯一的控制訊息是墓誌銘文。

我們為保留的序號新增高位元,因此序號的產生大小為 64 位元

我們考慮將位元數限制為 52 位元 (含控制訊息共 53 位元),因為這是 IEEE754 中可表示的最大正整數,可在僅支援雙精度 (例如 JavaScript)。不過,這些語言仍需要操控序數,才能將序數放入網路,因此需要進一步的機制才能存取組成雙精度的個別位元組。

設計

雜湊計算

RFC-0020:介面序號雜湊中所述的雜湊計算方式稍有變動。它應該會產生 64 位元數字,且計算雜湊的字串為 <library>/<top level declaration>.<member> (依據 RFC-0043:文件註解格式)。

雜湊序號是根據下列項目的 SHA-256 雜湊值衍生而來:

  • library name - 以 UTF-8 編碼;沒有尾隨的 \0
  • 「/」— 斜線字元,ASCII 0x2f
  • protocol name:採用 UTF-8 編碼;沒有尾隨的 \0
  • 「.」:ASCII 的 0x2e 點字元
  • 方法名稱 - 以 UTF-8 編碼;沒有尾隨的 \0

例如,下列 FIDL 宣告:

library foo;

protocol Science {
    Hypothesize();
    Investigate();
    Explode();
    Reproduce();
};

會使用下列位元組模式計算序數雜湊:

foo/Science.Hypothesize
foo/Science.Investigate
foo/Science.Explode
foo/Science.Reproduce

計算 SHA-256 雜湊後,系統會擷取 SHA-256 雜湊的上半 63 位元。我們只會擷取 63 位元,因為第 64 位元是保留給系統使用的。

在虛擬程式碼中:

full_hash = sha256(library_name + "/" +
                   protocol_name + "." +
                   method_name)
ordinal = full_hash[0]       |
          full_hash[1] << 8  |
          full_hash[2] << 16 |
          full_hash[3] << 24 |
          full_hash[4] << 32 |
          full_hash[5] << 40 |
          full_hash[6] << 48 |
          full_hash[7] << 56;
ordinal &= 0x7fff ffff ffff ffff; // i.e., 63 ones

在標頭中打包雜湊

目前的交易訊息標頭包含四個 4 個位元組欄位:交易 ID、保留、標記和目前的序號。預留欄位會用於 Epitaph 的錯誤代碼。因此,我們建議使用標記欄位 (目前未使用),將序數欄位從 4 個位元組增加到 8 個位元組。

標頭的新定義如下:

  • zx_txid_t txid、交易 ID (32 位元)
    • 已設為高位元的 txid 保留給 zx_channel_call() 使用
    • 高位元未設為 txid 的值會保留給使用者空間使用
    • 如要進一步瞭解 txid 分配作業,請參閱 zx_channel_call()
  • uint32 reserved0,保留供日後使用。
  • uint64 ordinal
    • 零序數無效。
    • 0xfffffffffffff 以上的序數為保留字。

JSON IR

沒有變動,且由於開發人員定義的序數最多可達 52 位元,因此標準 JSON 剖析器可將其剖析為 64 位元浮點數,且不會損失精確度。

其他序數

我們在表格中使用序數,並提供可擴充的聯集。我們不建議變更這些項目:在上述兩種情況下,目前在遠端情境中並未發生破壞 (例如,沒有可延伸的組合會組合其他可延伸的組合)。

導入策略

與明確轉雜湊序數採用的策略類似。

人體工學

沒有變更。

說明文件和範例

需要修改線路格式。

回溯相容性

不具回溯相容性。

成效

預期不會受到影響。方法和事件調度現在必須在 64 位元整數 (而非 32 位元整數) 上執行,這應該不會造成任何差異。

安全性

不會影響安全性。如要瞭解安全性為導向的用途,請參閱「替代做法:找出通訊協定和方法」,瞭解如何透過其他企劃書改善成效。

測試

排序計算的單元測試。遵循 RFC-0020:介面序號雜湊 的類似模式。

替代做法:識別通訊協定和方法

我們認為沙箱服務可保護其他服務免於遭到未經授權的使用,也就是類似 FIDL 訊息的防火牆。在建構這類沙箱服務時,有效參照一組訊息 (「允許的訊息」) 會很有幫助。您可以想像使用通訊協定定義這組訊息。

在這種情況下,建議您使用兩個 ID,一個用於通訊協定,另一個用於方法 (與上述建議的方案不同,後者只提供一個 ID)。

請考慮以下替代方案:

  • P 位元,用於產生通訊協定名稱的雜湊值
  • 方法名稱雜湊的 M 位元

因此,序數的總大小會是:

  • P + M + 1

因為我們需要為系統序號保留 1 位元。

舉例來說,如果使用範例程式庫:

library alternative;

protocol Parent {
    Get(); // alternative/Parent.Get
}

protocol Child {
    compose Parent;
    Get(); // alternative/Child.Get
}

兩個「Get」方法都會具有相同的 M 位元 (這是「Get」的雜湊值)。不過,P 位元會有所不同;一個是 alternative/Parent 的雜湊,另一個則是 alternative/Child 的雜湊。

從可行性角度來看,使用與上述相似的數值方法,我們有:

  • 我們預期有幾個通訊協定?100k 的順序是合理的。
    • → 需要 P = 52 位元
  • 我們預期有幾種方法?1k 的訂單規模是合理的。
    • → 需要 M = 39 位元
  • 因此,這個方案需要 92 位元

因此,我們認為這個替代方案不可行。

此外,進一步考量沙箱用途,由於通訊協定組合 (方法可能來自多個來源通訊協定),因此比對一個通訊協定 ID 是不夠的。因此,雖然最佳化路徑可能會因單一 ID 而受益,但一般情況下,您必須透過某些資料結構進行查詢,才能提高效率。

附錄:衝突機率

結果

size | num_methods | r | p(collision)
-----+-------------+---+-------------
  31 |        1000 |   | 0.0002325707643
  39 |        1000 | x | 0.0000009085847943
  47 |        1000 | x | 0.000000003549160959
  52 |        1000 | x | 0.0000000001109112802
  63 |        1000 | x | 0.00000000000005415589852
  31 |       10000 |   | 0.02301183054
  39 |       10000 |   | 0.00009093624028
  47 |       10000 | x | 0.0000003552357776
  52 |       10000 | x | 0.00000001110111996
  63 |       10000 | x | 0.000000000005420468761
  31 |       50000 |   | 0.4412566126
  39 |       50000 |   | 0.002271108402
  47 |       50000 |   | 0.00000888156712
  52 |       50000 | x | 0.0000002775501665
  63 |       50000 | x | 0.000000000135522561
  31 |      100000 |   | 0.9025370676
  39 |      100000 |   | 0.009053622963
  47 |      100000 |   | 0.00003552615045
  52 |      100000 |   | 0.000001110211306
  63 |      100000 | x | 0.0000000005420956651
  31 |     1000000 |   | 1.0
  39 |     1000000 |   | 0.5972719635
  47 |     1000000 |   | 0.003546406718
  52 |     1000000 |   | 0.0001110160287
  63 |     1000000 | x | 0.00000005421005294

size: max. num_methods
31: 66
39: 1049
47: 16777
52: 94906
63: 4294968

程式碼

使用 http://mpmath.org 計算各種機率。

from mpmath import mp
mp.dps = 50
mp.pretty = True

# Given n random integers drawn from a discrete uniform distribution with
# range [1,d], what is the probability p(n; d) that at least two numbers are
# the same?
def p_collision(n, d):
    # 1 - ((d - 1) / d) ^ (n * (n - 1) / 2)
    base = mp.fdiv(mp.fsub(d, 1), d)
    expo = mp.fdiv(mp.fmul(n, mp.fsub(n, 1)), 2)
    return mp.fsub(1, mp.power(base, expo))

print("size | num_methods | r | p(collision)")
for num_methods in [1000, 10000, 50000, 100000, 1000000]:
    print("-----+-------------+---+-------------")
    for size in [31, 39, 47, 52, 63]:
        p = p_collision(num_methods, mp.power(2, size))
        # 1 in 1,000,000
        result = " "
        if p < mp.fdiv(1, 1000000):
            result = "x"
        print("%4d | %11d | %s | %s" % (size, num_methods, result, mp.nstr(p, 10, min_fixed = -mp.inf)))

def find_max_num_methods(size):
    low = 1
    target = 1
    upper = 10000000
    while True:
        p = p_collision(target, mp.power(2, size))
        if p < mp.fdiv(1, 1000000):
            low = target
        else:
            upper = target
        target = ((upper - low) / 2) + low
        if upper - low < 2:
            return low

print("size: max. num_methods")
for size in [31, 39, 47, 52, 63]:
    print("%d: %s" % (size, find_max_num_methods(size)))