RFC-0029:增加方法一般

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

將方法序數的大小增加至 64 位元 (從 32 位元起),同時維持最高位元集範圍,以供 Fuchsia 保留使用。將墓誌銘序數更新為 0xFFFFFFFFFFFFFFFF (從 0xFFFFFFFF 開始)。使用交易標頭的旗標欄位,封裝增加的序數大小。並停止使用 FragileBase 屬性。

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

摘要

我們建議:

  1. 將方法序數的大小增加至 64 位元 (從 32 位元起),同時維持最高位元集範圍,以供 Fuchsia 保留使用;
  2. 墓誌銘序數更新為 0xFFFFFFFFFFFFFFFF (從 0xFFFFFFFF 開始);
  3. 使用交易標頭的旗標欄位,封裝增加的序數大小;
  4. 停止使用 [FragileBase] 屬性

與其他 RFC 的關係

這項 RFC 已由下列項目取代:

提振精神

遠端解決中斷問題

我們已研究一段時間,發現組合模型會導致遠距離的物件斷裂。

簡而言之,當 Child 通訊協定組成 Parent 通訊協定時,Parent 可能會事後導入與 Child 中定義的方法衝突的方法。這項變更可能是在 Parent 中進行,但並未意識到「遠端」Child 通訊協定中發生的中斷。

以目前的序數雜湊配置而言,如果通訊協定有 1,000 個方法,發生衝突的機率約為 2/10,000。鑑於要建構及以 FIDL 表示的通訊協定數量龐大 (且預計會更多),這種情況很可能發生。我們已暫時使用 [FragileBase] 註解來標示這個問題,並通知開發人員。

增加專用於方法序數的位元數,可將碰撞機率降至令人滿意的程度,因此實際上可視為不存在這個中斷問題。

方法序數大小

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

如果我們將容許的最大碰撞機率設為百萬分之一,則方法數量上限為:

  • 31 位元:~60
  • 39 位元:約 1,000 個
  • 47 位元:約 16,000 個字元
  • 52 位元:約 95,000
  • 63 位元:約 430 萬

因此,47 位元以上是合理的選擇。 我們選擇 63 位元的原因如下:

  • 在實務上,使用標準剖析器 (例如 Go 或 Python) 時,序數以 JSON 格式的數字表示是安全的。(在 JSON IR 的第 2 版中,我們計畫將序數包裝為字串)。
  • 控制訊息仍有空間,因此日後如有需要,可分配其他標記。目前唯一現有的控制訊息是墓誌銘。

我們為保留的序數新增高位元,因此序數的結果大小為 64 位元

我們考慮將位元限制為 52 位元 (含控制訊息則為 53 位元),因為這是 IEEE754 中可表示的最大正整數,因此在僅支援雙精度浮點數的語言 (例如 JavaScript)。 不過,這些語言仍需處理序數,才能將序數放在連線上,因此需要其他機制來存取組成倍數的個別位元組。

設計

雜湊值計算

RFC-0020:介面序數雜湊中導入的雜湊計算方式略有變更。這應該會產生 64 位元數字,且計算雜湊的字串為 <library>/<top level declaration>.<member> (根據 RFC-0043:文件註解格式)。

雜湊序數是透過下列項目的 SHA-256 雜湊衍生而來:

  • 程式庫名稱 - 編碼為 UTF-8,沒有尾端 \0
  • 「/」:正斜線字元,ASCII 0x2f
  • 通訊協定名稱 - 編碼為 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、保留、旗標和目前序數。預留欄位用於墓誌銘的錯誤代碼。 因此,我們建議使用旗標欄位 (目前未使用),將序數欄位從 4 個位元組增加到 8 個位元組。

標頭的新定義如下:

  • zx_txid_t txid、交易 ID (32 位元)
    • txid,且高位元集保留供 zx_channel_call() 使用
    • txids with the high bit unset are reserved for use by userspace
    • 如要進一步瞭解 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 的雜湊值。

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

  • 我們預期會有多少通訊協定?10 萬左右的數量是合理的。
    • → 需要 P = 52 位元
  • 我們預期會有多少種方法?1,000 個左右的順序是合理的。
    • → 需要 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)))