RFC-0082:在 Fuchsia 上執行未修改的 Linux 程式

RFC-0082:在 Fuchsia 上執行未經修改的 Linux 程式
狀態已接受
區域
  • 外國 ABI 相容性
說明

本文件提議在 Fuchsia 上執行未經修改 Linux 程式的機制。

問題
變更
作者
審查人員
提交日期 (年/月)2021-02-11
審查日期 (年/月)2021-03-25

摘要

本文件提議在 Fuchsia 上執行未經修改的 Linux 程式的機制。程式會在使用者空間程序中執行,其系統介面與 Linux ABI 相容。我們不會使用 Linux kernel 實作這個介面,而是在 Fuchsia 使用者空間程式中實作介面,此程式稱為 starnix。大部分情況下,starnix 會做為相容性層,用於將 Linux 用戶端程式的要求轉譯至適當的 Fuchsia 子系統。其中許多子系統都必須經過複雜設定,才能支援 Linux 系統介面隱含的所有功能。

提振精神

如要立即在 Fuchsia 上執行,軟體必須從來源重新編譯至以 Fuchsia 為目標。為減少在 Fuchsia 上執行的來源修改作業,Fuchsia 提供這個軟體可指定的 POSIX 相容性層 POSIX Lite。POSIX Lite 做為用戶端程式庫,在基礎 Fuchsia 系統 ABI 上分層。

不過,POSIX Lite 並非 POSIX 的完整導入方式。舉例來說,POSIX Lite 不包含隱含可變動的全域狀態 (例如 kill 函式) 的 POSIX 部分,因為 Fuchsia 是以物件能力紀律為基礎設計,會避開可變動的全域狀態,以提供強大的安全保證。因此,使用 POSIX Lite 的軟體必須改為使用 Fuchsia 系統介面,以便直接用於這些用途 (例如 zx_task_kill 函式)。

這個方法目前為止成效良好,因為我們可以存取在 Fuchsia 上執行軟體所需的原始碼,進而重新編譯 Fuchsia 系統 ABI 的軟體,並修改需要適應物件能力系統的軟體部分。

隨著我們拓展想要在 Fuchsia 上執行的軟體,我們也在遇到希望在 Fuchsia 上執行軟體時,這個軟體無法重新編譯。舉例來說,Android 應用程式包含已針對 Linux 編譯的原生程式碼模組。為了在 Fuchsia 執行這個軟體,我們必須能夠在不修改的情況下執行二進位檔。

設計

如要在 Fuchsia 上執行 Linux 二進位檔,最直接的方式是在虛擬機器中以 Linux kernel 做為訪客核心執行這些二進位檔。但是,這種做法會難以將訪客程式與 Fuchsia 系統的其餘部分整合,因為訪客程式是在與系統其他部分不同的作業系統中執行。

Fuchsia 的設計宗旨是讓您使用自己的執行階段,也就是說,Fuchsia 系統不會對元件的內部結構表達意見。為了做為與 Fuchsia 系統的一流公民互動,元件只需要透過適當的 zx::channel 物件,收發格式正確的訊息。

starnix 不會在虛擬機器中執行 Linux 二進位檔,而是在 Fuchsia 中原生建立 Linux 執行階段。具體來說,Linux 程式可以包含元件資訊清單,將 starnix 識別為該元件的「執行器」。與其直接使用 ELF Runner,系統會將 Linux 程式的二進位檔指派給 starnix 以執行。

為了執行指定的 Linux 二進位檔,starnix 會手動建立具有與 Linux ABI 相符的初始記憶體版面配置的 zx::process。舉例來說,starnix 會將程式的 argvenviron 填入初始執行緒堆疊 (以及 aux 向量) 上的資料,而不是做為啟動管道上的訊息,因為系統會在 Fuchsia 系統 ABI 中填入這項資料。

系統呼叫

將二進位檔載入用戶端程序後,starnix 會進行註冊,以處理用戶端程序中的所有 Syscall 機制 (請參閱下方「Syscall 機制」一節)。每當用戶端發出 syscall 時,Zircon 核心就會將控制項轉移至 starnix,後者會根據 Linux 系統呼叫慣例將系統呼叫解碼,並執行系統呼叫的工作。

舉例來說,如果用戶端程式發出 brk 系統呼叫,starnix 會使用適當的 zx::vmarzx::vmo 作業操控用戶端程序的位址空間,變更用戶端程序程式中斷的地址。在某些情況下,我們可能會需要強化單一程序的功能 (即starnix) 來操控其他程序 (例如用戶端) 的位址空間,但早期實驗指出 Zircon 已包含大量遠端位址空間操控所需的機器。

再舉一個例子,假設用戶端程式發出 write 系統呼叫。如要實作檔案相關功能,starnix 會為每個用戶端程序保留一個檔案描述元資料表。收到 write 系統呼叫後,starnix 會針對用戶端程序查詢檔案描述元資料表中找到的檔案描述元。一般來說,該檔案描述元會由實作 fuchsia.io.File FIDL 通訊協定的 zx::channel 提供支援。為了執行 writestarnix 會格式化 fuchsia.io.File#Write 訊息,其中包含來自用戶端位址空間的資料 (請參閱「記憶體存取權」),並透過管道傳送訊息,類似 POSIX Lite 在用戶端程式庫中實作 write 的方式。

全域狀態

為了處理隱含可變動的全域狀態的系統呼叫,starnix 會維持用戶端程序之間共用的一些可變動狀態。舉例來說,starnix 會指派 pid_t 給執行的每個用戶端程序,並維持資料表,將 pid_t 對應至該程序的基礎 zx::process 控制代碼。為了實作 kill 系統呼叫,starnix 會查詢這個資料表中的特定 pid_t,並對相關聯的 zx::process 處理常式發出 zx_task_kill 系統呼叫。

如此一來,每個 starnix 執行個體都會做為相關 Linux 程序的容器。如果希望在兩個 Linux 程序之間取得強式隔離保證,可以在個別的 starnix 執行個體中執行這些程序,而不會產生執行多個虛擬機器的負擔 (例如排程複雜作業)。

每個 starnix 執行個體也會公開自己的全域狀態,以供其他 Fuchsia 程序使用。舉例來說,starnix 會保留 AF_UNIX 通訊端的命名空間。這個命名空間可從 starnix 執行的 Linux 二進位檔,以及透過 FIDL 與 starnix 通訊的 Fuchsia 二進位檔存取。

Linux 系統介面也意味著全域檔案系統。由於 Fuchsia 沒有全域檔案系統,因此 starnix 會從自身的命名空間,為用戶端程序合成「全域」檔案系統。舉例來說,starnix 會在代表用戶端程序的全域檔案系統中,將 /data/root 從自身的命名空間掛接為 /。其他掛接點 (例如 /proc) 可透過 starnix 於內部實作,例如諮詢執行中程序的資料表。

安全性

starnix 會盡可能以基礎 Fuchsia 系統的安全性機制為基礎進行建構。舉例來說,與系統服務 (例如檔案系統、網路和圖形) 互動時,starnix 主要做為轉譯層,並將來自 Linux ABI 的要求重新格式化至 Fuchsia 系統 ABI。系統服務將負責強制執行自己的安全性不變,就像其他所有用戶端一樣。不過,starnix 必須實作一些安全性機制,才能保護自家服務的存取權。舉例來說,starnix 需要判斷是否允許某個用戶端程序對其他用戶端程序執行 kill

為了做出這些安全性決策,starnix 將追蹤每個用戶端程序的安全性情境,包括 uid_tgid_t、有效的 uid_t 和有效的 gid_t。需要安全性檢查的作業會使用此安全性情境來做出適當的存取權控管決策。起初,我們預期這個機制不會經常使用,但隨著用途越來越複雜,我們對於存取控制的需求可能也會變得更加複雜。

她在討論時

當需要選擇 starnix 在特定情況下的行為時,設計考量的到與 Linux 的可行性相近。這麼做的用意是建立 Linux 介面實作,該介面可執行未經修改的現有 Linux 二進位檔。只要 starnix 與 Linux 語意不同,我們就會產生風險,其中部分 Linux 二進位檔會發現差異和行為異常。

為了更輕鬆地討論這項設計原則,我們假設 starnix 會在她叫出時實作 Linux,也就是實際 Linux 系統的所有美妝、小瑕疵、共識和稀疏度。

在某些情況下,在輪輻一併實作 Linux 介面時,需要為 Fuchsia 服務新增功能,才能提供需要的語意。舉例來說,實作 inotify 需要基礎檔案系統實作的支援,才能有效運作。我們的目標是在 Fuchsia 服務中新增這項功能,使其與服務公開的其餘功能完美整合。

實作

我們打算將 starnix 實作為 Fuchsia 元件,特別是實作runner通訊協定的一般使用者空間元件。我們打算在 Rust 中實作 starnix,以避免從用戶端程序提升到 starnix 程序的權限。

管理階層

執行starnix 的核心之一,會在 Linux 系統介面中實作語意概念。舉例來說,高階主管會有代表執行緒、程序和檔案說明的物件。

執行階層的結構,使其可以從 starnix 系統的其餘部分獨立進行單元測試。例如,我們可以對複製檔案描述元分享基礎檔案說明的單元測試,而無需與 Linux ABI 執行程序。

Linux syscall 定義

如要實作 Linux syscall,starnix 需要每個 Linux syscall 的說明,以及任何相關聯輸入或輸出參數的使用者空間記憶體版面配置。這些項目可在 Linux uapi 中定義,該 Linux 是獨立式 C 標頭集合。如要在 Rust 中使用這些定義,我們會使用 Rust bindgen 產生 Rust 宣告。

Linux uapi 會隨著時間不斷演進。一開始,我們會指定 Linux 5.10 LTS 中的 Linux uapi,但可能需要調整我們支援的 Linux uapi 確切版本。

Syscall 機制

starnix 的初始實作會使用 Zircon 例外狀況,藉此截斷用戶端程序中的系統呼叫。具體來說,每當用戶端程序嘗試發出系統呼叫時,Zircon 會拒絕系統呼叫,這是因為 Zircon 需要從 Zircon vDSO 發出系統呼叫 (且用戶端程序無感知)。

Zircon 會產生 ZX_EXCP_POLICY_CODE_BAD_SYSCALL 例外狀況,藉此拒絕這些 syscall。starnix 程序會為每個用戶端程序安裝例外狀況處理常式,藉此擷取這些例外狀況。為了接收 syscall 的參數,starnix 會使用 zx_thread_read_state 從產生例外狀況的執行緒讀取註冊資料。處理 syscall 後,starnix 會使用 zx_thread_write_state 設定系統呼叫的傳回值,然後在用戶端程序中繼續執行執行緒。

這項機制可以運作,但效能可能不足以實用。在我們建構足夠的 starnix 來執行 Linux 基準測試後,可能會希望將這個系統呼叫機制替換為更有效率的機制。舉例來說,starnix 可能會建立 zx::port 以處理來自用戶端程序的系統呼叫,而 Zircon 會將封包排入 zx::port 佇列,並提供用戶端程序的註冊狀態。取得基準測試後,我們就能設計各種方法的原型,並選擇當下最適合的設計。

記憶體存取權

初始實作 starnix 會使用 zx_process_read_memoryzx_process_write_memory,從用戶端程序的位址空間讀取及寫入資料。這種機制可以運作,但不理想,原因如下:

  1. 有安全疑慮,因此正式環境版本會停用這些系統呼叫。
  2. 這些系統呼叫比直接讀取及寫入記憶體高上許多。

建構足夠的 starnix 來執行 Linux 基準測試後,我們會想以更有效率的方式取代這項機制。舉例來說,starnix 可能會限制用戶端位址空間的大小,並在某些用戶端專屬的偏移值中將每個用戶端的位址空間對應至自己的位址空間。或者,當 starnix 處理來自用戶端的系統呼叫時,Zircon 會安排該執行緒來顯示該用戶端的位址空間 (例如,就像從這些程序服務系統呼叫時,核心執行緒對使用者空間程序位址空間的瀏覽權限類似)。

與系統呼叫機制一樣,我們可以設計各種方法的原型,並提供更多用於評估方法的執行程式碼,然後選取最佳設計。

互通性

我們將採用測試驅動的方法開發 starnix。一開始,我們會使用簡易的實作方式,以便執行基本 Linux 二進位檔。我們已設計可執行 hello_world.c 程式 -static-pie 版本的實作原型設計。下一步將清除該原型,並教導 starnix 如何執行動態連結的 hello_world.c 二進位檔。

執行這些基本二進位檔後,我們將從不同的程式碼集取得單元測試二進位檔。這些二進位檔有助於確保 Linux ABI 的實作正確無誤 (也就是說,因為 Linux 是輪輻的 Linux)。例如,我們會從 Android 來源樹狀結構和 Linux Test Project 執行一些低階測試二進位檔。

效能

效能是這項專案的關鍵層面。一開始,starnix 的效能會相當不佳,因為我們會使用效率不彰的機制來轉移系統呼叫和存取用戶端記憶體。不過,只要我們擁有充分功能,可在 Linux 執行環境中執行基準測試,這些測試領域就應該可以大幅改善。

除了將這些機制最佳化,我們也有機會將高頻率的作業卸載給用戶端。舉例來說,我們可以先將程式碼載入用戶端程序,然後再將控制權轉移到 Linux 二進位檔,藉此直接在用戶端位址空間中實作 gettimeofday。舉例來說,如果 Linux 二進位檔透過 Linux vDSO 叫用 gettimeofdaystarnix 可透過呼叫 Zircon vDSO 直接呼叫 Zircon vDSO,以提供共用程式庫取代實作 gettimeofday 的 Linux vDSO。

安全性考量

本提案有多項細微的安全性考量。starnix 程序和用戶端程序之間有信任界線。具體來說,starnix 程序可以保留未完全公開給用戶端的物件功能。舉例來說,starnix 程序會為每個用戶端程序維護一個檔案描述元資料表。一個用戶端程序應能夠存取儲存在檔案描述元資料表中,但無法存取儲存在檔案描述元資料表中,用於其他程序。同樣地,starnix 會維持共用的可變動狀態,用戶端只能與受存取權控管情況互動。

為了提供此信任界線,starnix 會在用戶端程序以外的使用者空間程序中執行。為了避免權限提升,我們計劃在 Rust 中實作 starnix,並使用 Rust 的類型系統來避免類型混淆。我們也打算使用 Rust 的類型系統,明確區分用戶端資料 (例如用戶端位址空間中的位址,以及從用戶端位址空間讀取的資料),以及由 starnix 自行維護的可靠資料。

此外,我們必須考量 Linux 二進位檔的來源,因為 starnix 會直接執行這些二進位檔,而不是在虛擬機器或 SFI 容器中執行。我們必須針對涉及 Linux 二進位檔的特定端對端產品用途,重新審視這項考量。

starnix 中的存取權控管機制需要進行詳細的安全性評估,在設計上最好能直接參與資安團隊的設計,甚至可能實作。起初,我們期望的是簡易的存取權控管機制隨著這個機制的需求越來越複雜,我們將需要進一步審查安全性。

最後,高效能系統呼叫和用戶端記憶體機制的設計需要審慎謹慎,尤其在最終針對 starnix 使用外質位址空間設定,或嘗試將註冊狀態直接從用戶端執行緒轉移至 starnix 執行緒時。

隱私權注意事項

這項設計不需立即考量隱私權。不過,一旦我們有涉及 Linux 二進位檔的特定端對端產品使用案例後,就需要評估該使用案例的隱私權影響。

測試

測試是建構 starnix 的核心環節。我們會直接對 starnix 高階主管進行單元測試。我們也會嘗試傳遞在 Linux 上執行的測試二進位檔,藉此建構 Linux 系統介面實作項目。我們會在持續整合中執行這些二進位檔,確保 starnix 不會迴歸。

我們也會比較在 starnix 中執行 Linux 二進位檔,以及在 Fuchsia 虛擬機器中執行這些相同二進位檔。我們預期能夠在 starnix 中更有效率地執行 Linux 二進位檔,但我們應驗證這項假設。

說明文件

在這個階段,我們打算透過此 RFC 記錄 starnix。展開複雜的二進位檔後,我們會需要在 Fuchsia 上執行 Linux 二進位檔的來龍去脈。

缺點、替代方案和未知

有大型的設計空間,您可以探索如何在 Fuchsia 上執行未經修改的 Linux 二進位檔。本節將概述主要的設計決策。

Linux 核心

您必須選用 Linux kernel 來實作 Linux 系統介面,這是一項重要的設計選擇。除了建構 starnix 之外,我們也會建構在 Machina 虛擬機器中執行 Linux kernel 的機制,用於執行未經修改的 Linux 二進位檔。這個方法包含少量實作負擔,因為 Linux kernel 原本是在虛擬機器中執行,而 Linux kernel 已包含構成 Linux 系統介面的數百個系統呼叫實作項目。

使用 Linux kernel 的方式有很多種。舉例來說,我們可以在虛擬機器中執行 Linux kernel,可以使用 User-Mode Linux (UML),也可以使用 Linux 核心程式庫 (LKL)。然而,無論執行方式為何,執行整個 Linux kernel 要執行 Linux 二進位檔都會產生大筆費用。Linux kernel 的核心工作是減少高階作業 (例如write) 改為低層級作業 (例如將 DMA 資料傳送到基礎硬體)這個核心函式對於將 Linux 二進位檔整合到 Fuchsia 系統中具有副生作用。我們不會將 write 作業縮減至指定行銷區域,而是希望將 write 運算轉譯為具有相同語意等級的 fuchsia.io/File.Write 作業。

同樣地,Linux kernel 附有排程器,可控管其管理程序中的執行緒。這項功能的目的是為了將高階作業 (例如執行十個並行執行緒) 縮減為低層級作業 (例如在本處理器上執行這個時間片段)。再次強調,這項核心功能會造成反效果。如果每個 Linux 二進位檔執行的執行緒實際上是與系統中所有其他執行緒相同的排程器所排定的 Zircon 執行緒,我們可以計算出整體的更佳時間表。

環境

決定直接使用 Fuchsia 系統實作 Linux 系統介面後,就需要選擇執行該實作的位置。

處理中

我們可以利用與 Linux 二進位檔相同的程序執行實作。舉例來說,POSIX Lite 會使用這個方法將 POSIX 作業轉譯為 Fuchsia 作業。但是,當您執行未經修改的 Linux 二進位檔時,此方法較不理想,原因有二:

  1. 如果我們在處理期間執行實作,就必須「隱藏」 Linux 二進位檔中的實作,因為 Linux 二進位檔預期系統無法在其程序中執行 (大部分) 程式碼。例如,如果實作使用了執行緒本機儲存空間,就必須小心避免與 Linux 二進位檔 C 執行階段代管的執行緒本機儲存空間衝突。

  2. Linux 系統介面的許多部分就是可變動的全域狀態。程序中的實作仍需與程序外伺服器協調,才能正確實作介面的這些部分。

基於上述原因,我們選擇先從程序外的伺服器實作著手。不過,為了取得效能,我們可能會從伺服器卸載部分作業至用戶端。

使用者空間

在此方法中,實作會在獨立的使用者空間程序中執行,而不是 Linux 程序。這是我們為 starnix 選擇的方法。這種方法的主要挑戰在於,我們必須審慎設計用於系統呼叫和用戶端記憶體存取權的機制,才能提供足夠的效能。涉及第二個使用者空間程序會產生一些無可避免的負擔,因為我們需要執行額外的內容切換來進入程序,但有來自其他系統的證據可以達到極佳的效能。

核心

最後,我們可在核心中執行實作。這是為作業系統提供外國人士的傳統方法。不過,我們想避免這個做法降低核心的複雜度。如果核心遵循明確的物件能力準則,就更容易推斷核心的行為,從而提高安全性。

相較於使用者空間實作,核心實作提供的主要優勢是效能。舉例來說,核心可以直接接收系統呼叫,且已有高效能機制與用戶端位址空間互動。如果我們可以透過使用者空間方法達到絕佳效能,就不太可能在核心中執行實作。

非同步信號

Linux 二進位檔預期核心會在非同步信號處理常式中執行部分程式碼。Fuchsia 目前未在程序中直接叫用程式碼的機制,這代表叫用非同步信號處理常式的機制沒有明顯的機制。一旦遇到需要支援非同步信號處理常式的 Linux 二進位檔後,就需要設計出支援該功能的方法。

家具行

Futexes 在 Fuchsia 和 Linux 上的運作方式有所不同。在 Fuchsia 上,虛設常式會取出虛擬位址的位置,而 Linux 可讓您選擇移除實體位址的金鑰。此外,Linux futexe 提供各種不適用於 Fuchsia futexe 的選項和作業。

如要實作 Linux futex 介面,我們需要在 starnix 中實作 futexe,或將 Zircon 核心新增功能,以支援 Linux 二進位檔所需的功能。

先前的圖片和參考資料

在非 POSIX 系統上執行 Linux (或 POSIX) 二進位檔時,需要先執行大量的程式。本節說明兩個相關系統。

WSL1

本文件的設計與第一個 Windows Subsystem for Linux (WSL1) 類似,後者是在 Windows 上實作的 Linux 系統介面,並可執行未經修改的 Linux 二進位檔,包括整個 GNU/Linux 發行版,例如 Ubuntu、Debian 和 openSUSE。與 starnix 不同,WSL1 會在核心中執行,並為 NT kernel 提供 Linux 個人化功能。

很遺憾,WSL1 受到 NTFS 效能特性的阻礙,這與 Linux 軟體的期望不符。自此之後,Microsoft 已經將 WSL1 取代為 WSL2,在虛擬機器中執行 Linux 核心可以提供類似功能。在 WSL2 中,Linux 軟體會在 ext4 檔案系統上執行,而非 NTFS 檔案系統。

我們應從 WSL1 學到一個重要的概念,其中 starnix 的效能將取決於 starnix 對用戶端程式公開的基礎系統服務效能。舉例來說,如果希望 Linux 軟體在 Fuchsia 上順利運作,我們需要提供與 ext4 效能相同的檔案系統實作。

QNX 新竹諾

QNX Neutrino 是提供高品質 POSIX 實作的商用微核心作業系統。本文件中說明 starnix 的方法與 QNX 中的 proc 伺服器類似,後者會服務來自用戶端程序的 POSIX 呼叫,並維持 POSIX 介面隱含的可變動全域狀態。procstarnix 類似,是 QNX 上的使用者空間程序。