RFC-0261:快速且有效率的使用者空間核心模擬

RFC-0261:快速有效率的使用者空間核心模擬
狀態已接受
區域
  • Kernel
說明

讓 Fuchsia 程式成為不信任客體程式碼的安全高效監管員。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-06-07
審查日期 (年-月-日)2024-10-01

摘要

本文建議一組功能,讓 Fuchsia 程式能做為不受信任的訪客程式碼的安全高效監管員,而不使用 Hypervisor 技術。主要用途是啟用更快速有效率的 Starnix 執行元件。

這些功能有時統稱為「限制模式」。在 Starnix 的脈絡中,這些特徵是 Starnix 執行模型的基础。

本文著重於 Zircon 核心,以及新核心功能的介紹。Starnix 如何運用這些功能不在本文討論範圍內。

提振精神

如需背景資訊,請參閱 RFC-0082:在 Fuchsia 上執行未修改的 Linux 程式

我們希望開發出 Fuchsia 程式,有效模擬其他作業系統的介面。具體來說,我們希望在 Fuchsia 上執行未修改的 Linux 程式,且效能接近原生。

利害關係人

協助人員:davemoore@google.com

審查人員:abarth@google.com、adanis@google.com、cpu@google.com、 jamesr@google.com、lindkvist@google.com、mvanotti@google.com

諮詢對象:abarth@google.com、adanis@google.com、 rashaeqbal@google.come、lindkvist@google.com、mcgrathr@google.com、 mvanotti@google.com

社交化:這項提案已透過電子郵件、即時通訊和多個 Kernel Evolution Working Group (KEWG) 工作階段,與核心和 Starnix 團隊進行社交化。

目標

下列目標是設計這個系統的依據。

快速且有效率的模擬 - 這個新機制必須提供比 Zircon 例外狀況機制更快且更有效率的模擬。

強大的安全界線 - 這項機制必須做為強大的安全界線,並允許安全執行不受信任的程式碼。

自然且簡化的程式設計模型 - 這個機制提供的程式設計模型不僅應適用於模擬 Linux 核心介面,也應處理許多繁重的工作。就原生而言,大多數核心會將核心程式碼和資料放在一個共用的位址空間,因此如果這個新機制提供具有共用位址空間的程式設計模型,就會很方便 (詳情請參閱「共用狀態」一節)。就簡單性而言,我們偏好在較高層級的 OS 抽象層上建構 OS,而非在較低層級的硬體抽象層上建構。

複雜度分區 - 我們希望將 Zircon 核心的變更分區,以便更妥善地管理複雜度,並維持彈性。具體來說,這表示要盡量減少這個機制跨核心子系統的程度。

範圍

這項提案的範圍包括模型和一般方法 (如「設計」一節所述),以及系統呼叫介面 (如「實作」一節所述)。

設計

這個系統由三個設計元素組成,可搭配使用。

這項設計的主要元素包括受限模式、直接存取和共用狀態。兩者共同提供多程序共用記憶體程式設計模型,用於在 Zircon 抽象化之上實作使用者空間核心。

採用這種架構時,每個客體執行緒都會以一個 Zircon 執行緒模擬,每個客體程序則會以一個 Zircon 程序模擬。每個 Zircon 程序都會有所有程序共用的位址空間區域。對客層核心開發人員而言,共用區域會讓客層核心看起來更像單一多執行緒程式。

元素 1:嚴格篩選模式

這是核心元素。

目前執行緒有兩種執行模式:使用者模式或核心模式。當執行緒發出系統呼叫時,會透過模式切換,從使用者模式轉換為核心模式。系統呼叫完成後,會返回使用者模式。

注意:為求簡單起見,除非另有說明,否則本文一律使用 ARMv8a 術語。

透過系統呼叫切換模式

當然,系統呼叫並非唯一會導致模式切換的事件。核心可能需要處理的任何事件都會觸發模式切換至核心模式。包括頁面錯誤、例外狀況和中斷。

我們推出第三種模式 (選用),稱為嚴格篩選模式。這項新模式為選用性質,因為系統中並非所有執行緒都會進入限制模式。使用者模式的執行環境比核心模式更受限,而受限模式比使用者模式更受限。

核心模式和使用者模式會對應至硬體功能 (例如 ARMv8-A 中的 EL1 和 EL0),但受限模式不會。這項建構作業僅限於軟體。硬體不會知道嚴格篩選模式。從硬體的角度來看,這只是使用者模式。

術語:為避免混淆,當我們談論包含所有三種模式的執行緒時,會將非受限使用者模式稱為「一般模式」。

概念上,受限模式位於一般模式下方。就像在核心模式中執行的程式碼會做為使用者模式程式的監管程式一樣,在正常模式中執行的程式碼會做為受限模式程式的客體核心或使用者模式監管程式。

嚴格篩選模式低於一般模式

隔離

嚴格篩選模式的主要功能是隔離。在一般模式和受限模式之間切換時,變更可存取的位址空間範圍和暫存器狀態,即可達到隔離效果。

通常程序的位址空間會分成兩半,其中一半 (核心位址空間) 只能在執行緒處於核心模式時存取,另一半 (使用者位址空間) 則隨時可存取。在限制模式下,使用者位址空間會進一步劃分,總共提供三個區域。

只有在執行緒處於核心模式時,才能存取最上層的區域。中間區域只能從核心模式或正常模式存取。且隨時可存取下方區域 (詳情請參閱「直接存取」一節)。這個較低的區域稱為受限區域,因為受限模式中的執行緒只能存取這個區域。

進入受限模式後,Zircon 核心會載入一般用途的暫存器,並使用一般模式提供的值。清除或載入一般用途暫存器以外的暫存器,是正常模式程式碼的責任。舉例來說,Zircon 核心不會清除或載入浮點或向量暫存器。x86 的 fs.basegs.base、arm64 的 tpidr_el0,以及 riscv64 的 tp 都視為一般用途的暫存器。詳情請參閱「輸入」一節。

執行緒進入受限模式後,只能存取其暫存器狀態和受限區域中的對應。

處於受限模式時,Zircon 核心會將所有系統呼叫路徑導向一般模式,無論這些呼叫是否從 Zircon vDSO 發出 (詳情請參閱下一個章節)。也就是說,即使 vDSO 對應到受限模式,系統也會將系統呼叫重新導向至正常模式,不會由 Zircon 核心處理。

依模式存取位址空間

模式切換和攔截

本節說明執行緒如何在一般模式和受限模式之間轉換。

在轉場期間傳達模式狀態

為提升效能,Zircon 核心和訪客核心會使用共用記憶體 (即模式狀態 VMO),在一般模式和受限模式之間轉換時,傳達執行緒的暫存器狀態和其他詳細資料。詳情請參閱「準備」一節。

進入嚴格篩選模式

Zircon 核心會使用核心執行緒結構上的旗標,追蹤執行緒是否處於受限模式或一般模式。瞭解執行緒是處於受限模式還是正常模式後,核心就能正確「將」系統呼叫、錯誤和例外狀況「路徑」導向正確位置 (核心或正常模式)。

從一般模式進入受限模式時,會使用新的專屬系統呼叫 (zx_thread_enter_restricted)。呼叫前,使用者模式管理員會先填入包含要在受限模式中使用的暫存器值的結構體。這個結構體位於模式狀態 VMO 中。

進入程序包括將執行緒的可存取位址空間,從程序的完整位址空間變更為程序的受限區域 (使用者位址空間的下半部),從模式狀態 VMO 載入暫存器狀態,然後繼續執行 (可視為分支到還原的 PC)。

為提升效能,Zircon 核心不會自動儲存執行緒的一般模式暫存器狀態。呼叫端可自行儲存感興趣的狀態。此外,Zircon 核心不會嘗試還原任何受限模式的暫存器狀態,一般用途 (整數) 暫存器除外。呼叫端可自行決定是否要還原更多項目。如要瞭解通用暫存器包含的確切內容,請參閱「輸入」zx_restricted_state_t部分中的

離開嚴格篩選模式

在討論執行緒如何離開受限模式並返回正常模式之前,我們先簡單說明在受限模式執行期間發生三種執行緒事件時,系統的處理方式,建立一些背景知識:

中斷和可解決的錯誤 - 中斷和可解決的錯誤不會導致執行緒離開受限模式。當執行緒處於受限模式時,硬體和軟體中斷,以及可解決的頁面錯誤,基本上對執行緒 (包括其使用者模式監管程式) 都是不可見的。舉例來說,假設 CPU 的平台計時器觸發,且 CPU 進入核心模式。或裝置中斷觸發。或者,CPU 發生頁面錯誤,並由 Zircon 核心解決。執行緒不會觀察到任何這類事件 (一般模式程式碼或限制模式程式碼)。受限模式中的執行緒完全可搶占,與在一般模式中執行的執行緒相同。

工作暫停 - 工作暫停作業不會導致執行緒離開受限模式。嚴格篩選模式不會「保護」執行緒免於暫停作業 (例如 zx_task_suspend)。如果執行緒在收到暫停信號時處於嚴格篩選模式,系統會以嚴格篩選模式暫停執行緒,並在恢復執行時繼續以嚴格篩選模式執行,彷彿什麼都沒發生。

偵錯工具例外狀況 - 如果附加了偵錯工具,且以受限模式執行的執行緒產生例外狀況 (例如達到中斷點),系統會照常產生 Zircon 偵錯例外狀況,不會強制執行緒退出受限模式。不過,如果沒有附加任何偵錯工具,例外狀況會導致執行緒離開受限模式,並由使用者模式監管程式處理。另請參閱「例外狀況離職」。

瞭解背景資訊後,我們來討論如何讓執行緒退出限制模式,並返回正常模式。有兩種方式,分別是透過系統呼叫離開,以及透過例外狀況離開。Zircon 核心會提供受限通用暫存器狀態的副本,以及指出執行緒傳回原因的理由代碼,這點適用於所有情況。視離開嚴格篩選模式的原因而定,Zircon 核心可能會提供額外資訊。這個狀態會寫入模式狀態 VMO,然後執行緒會返回正常模式。

透過系統呼叫離開

在受限模式下執行系統呼叫指令,會強制執行緒返回正常模式,以便使用者模式管理員處理系統呼叫。

執行系統呼叫指令時,CPU 會先陷入核心模式。核心中的一般系統呼叫路徑會測試執行緒是否處於受限模式。如果是,系統會採取替代路徑,包括儲存受限的一般用途暫存器狀態,並直接返回正常模式,回到原始系統呼叫中傳遞的位址。接著,一般模式必須復原儲存的暫存器狀態,然後處理系統呼叫。從邏輯上來看,控制權會從受限模式傳遞至正常模式,但實際上,控制權會在旅程的每個階段透過核心模式彈跳。範例如下:

受限模式轉換

  1. 受限模式發出系統呼叫
  2. syscall came from restricted mode so kernel returns to normal mode to allow it to handle the call
  3. 正常模式會處理呼叫,並 (邏輯上) 透過系統呼叫返回受限模式
  4. 核心會處理「進入受限模式系統呼叫」,並跳回受限模式

如先前所述,Zircon 核心執行的暫存器狀態儲存和還原作業會盡量減少,以提升效能,讓一般模式只儲存/還原所需內容。轉換至/離開限制模式的最低費用取決於架構。舉例來說,在 x86 上,我們需要交換頁面資料表根 (CR3),而在 arm64 上,我們可以使用 TCR_EL1.T0SZ 對位址空間的正常一半執行較便宜的「遮罩/取消遮罩」作業,從而保留更多正常模式和受限模式共用的 TLB 項目。

例外狀況下離開

如先前所述,在受限模式下執行時,Zircon 核心會以透明方式處理可解決的頁面錯誤。也就是說,雖然執行緒可能會實際進入核心模式來解決錯誤,但不會返回正常模式。從程序的角度來看,就好像從未發生故障。

不過,如果沒有偵錯工具,處理架構例外狀況 (包括未處理的頁面錯誤) 的責任就落在使用者模式管理員身上,Zircon 核心不會處理這類狀況。舉例來說,如果在受限模式下執行時,執行緒存取沒有有效對應的位址,系統會產生 Zircon 例外狀況,執行緒也會返回正常模式。從 Zircon 例外狀況處理機制的角度來看,返回正常模式「會處理例外狀況」,且不會諮詢其他例外狀況處理常式 (例如執行緒/程序/工作處理常式)。系統會產生標準 zx_exception_report_t 物件,並將其放在模式狀態 VMO 中,以便在傳回時,使用者模式監管員能取得例外狀況詳細資料。

遭踢出而離開

為支援實作正常終止工作和 POSIX 信號等功能,我們新增了 Zircon 系統呼叫 zx_thread_kick_restricted,可強制受限模式中的執行緒返回正常模式。「踢除」是閂鎖作業,也就是說,如果執行緒在正常模式下遭到踢除,下次嘗試進入受限模式時,會立即傳回表示「已遭踢除」的狀態碼 ZX_RESTRICTED_REASON_KICK。傳回這個狀態碼可有效解除啟動鎖定。

踢腿不會「堆疊」。舉例來說,假設目標執行緒處於正常模式,而另一個執行緒發出五次踢除要求。目標執行緒進入限制模式時,系統會立即傳回,有效處理所有五項啟動作業。

一般來說,zx_thread_enter_restricted 的呼叫端應編寫為處理虛假踢除。

元素 2:直接存取

由於 OS 核心經常需要存取使用者模式資料,因此有效率地完成這項作業非常重要。作業系統核心通常會直接存取使用者程序的程式碼和資料。舉例來說,Linux 核心和 Zircon 核心都能直接存取使用者程序的位址空間。

直接存取是這項設計的關鍵要素。

如前所述,在正常模式下,Zircon 執行緒可存取其程序的完整位址空間;在受限模式下,則只能存取位址空間的子集。因此,在正常模式下執行時,Zircon 執行緒可做為客層核心,並存取受限區域中的客層使用者程式碼和資料。

使用者位址空間實際上會分成兩部分。將位址空間分成兩半,我們就能在某些架構上運用一些重要的位址空間切換效率 (也就是遮蓋一半,而非實際切換,TCR_EL1.T0SZ)。

由於在 Starnix 下執行的程式可能需要「接近」零位址的位址空間 (請參閱 MAP_32BIT),因此我們選擇將最低區域設為受限區域。

但這種做法有一些限制。主要原因是受限區域是完整區域的連續子集,因此不得有衝突的對應。用於訪客程式碼和資料的位址,必須與用於使用者模式監管員程式碼和資料的位址不相交。由於受限區域位於使用者位址空間的「下半部」,因此用來實作使用者模式監管程式的 Fuchsia 執行階段必須具備彈性,且不需要任何對應項目位於下半部。為達成這個目的,我們必須對 Fuchsia 程序建構工具進行一些變更。有時,清除工具對對應項目的位置有額外要求,因此日後可能需要進行作業,才能支援特定清除工具。

元素 3:共用狀態

核心通常具有在使用者工作之間共用,且由系統呼叫操控的資料結構。對於這類系統,核心程式設計模型類似於多執行緒程式,其中每個使用者工作都是一個執行緒,可存取共用 (核心) 位址空間,以及存取某些私人的使用者工作專屬區域。共用區域中的程式碼和資料會形成共用系統映像檔。

為了方便使用共用系統映像檔開發使用者空間核心,我們將擴充 Zircon 程序模型,支援與其他程序共用部分位址空間的新型程序。這些程序集合會代管訪客使用者工作和訪客核心。

我們先前說明過如何遮蓋程序的位址空間,完整與受限。所有代管相同系統映像檔客層工作的 Zircon 程序,都會共用非受限區域。每個程序都會有自己的獨立受限區域,該區域僅供該程序使用。因此,當其中一個程序的執行緒以正常模式執行時,將可存取完整位址空間,包括共用對應,以及受限區域中的自有私人對應。

除了共用這些非受限的對應項,Zircon 處理序 (主機代管訪客工作) 也都會共用單一控制代碼表和單一 futex 環境。共用區域中的任何控制代碼值 (例如 zx_handle_t) 都可供任何程序使用,無須進行明確的控制代碼複製或轉移作業。透過共用 (部分) 對應、控制代碼和 futex 內容,一組程序看起來會更像單一程序中的一組執行緒,進而簡化程式設計模型。

在共用或受限區域存取對應時,請務必小心,避免建立資料競爭,就像在多執行緒程序中存取記憶體時一樣。

雖然共用程序群組中的執行緒具有不同的程序 ID,但它們共用相同的控制代碼表、futex 環境和位址空間的共用區域。這種做法的其中一個影響是,對於 Starnix 而言,一般模式的程序當機與核心恐慌類似,因為當機程序可能已將某些共用資料置於不一致的狀態。

當然,受限模式的當機情況非常不同,因為受限模式無法存取一般模式狀態。由於在一般模式下執行的程式碼會充當受限模式程式碼的監管程式,因此在大多數情況下 (例如 Starnix),系統會妥善處理受限模式的「當機」問題,例如架構例外狀況和未處理的頁面錯誤。

實作

我們首先建構 x86 支援,因為這是 Starnix 最初的目標。後來我們新增了 arm64 和 riscv64 支援。

雖然設計和實作的重點是 Starnix 用途,但開發這些功能時,完全不需使用 (和測試) Starnix。

@next vDSO 中新增了系統呼叫。

系統呼叫介面和語意

程序建立

如要進入受限模式,執行緒必須屬於「共用程序」,且具有受限的位址空間區域。共用程序可選擇與群組中的其他程序共用部分位址空間 (共用區域)、控制代碼表和 futex 環境。我們說「選擇性」,是因為共用程序可以存在於單一群組中。

程序是屬於一般程序還是共用程序,取決於程序建立時的設定。如要建立共用程序,我們會擴充現有的 zx_process_create 系統呼叫,並導入新的 zx_process_create_shared

現有的 zx_process_create 系統呼叫會變更為接受新選項 ZX_PROCESS_SHARED。如果存在,結果程序的位址空間 (以傳回的 VMAR 模擬) 只會涵蓋共用區域 (上半部)。以 ZX_PROCESS_SHARED 建立的程序可做為原型,用於建立一組程序,這些程序都會共用部分位址空間。

新增的系統呼叫 zx_process_create_shared 會與 ZX_PROCESS_SHARED 選項搭配運作。

zx_process_create_shared(zx_handle_t shared_proc,
                         uint32_t options,
                         const char* name,
                         size_t name_size,
                         zx_handle_t* proc_handle,
                         zx_handle_t* restricted_vmar_handle);

這項新通話功能與 zx_process_create 類似。透過 zx_process_create_shared 建立程序的權限,取決於呼叫端的工作政策和 shared_proc 引數。

zx_process_create_shared 會建立新的程序 proc_handle 和新的 VMAR restricted_vmar_handle。新程序的位址空間包含兩個區域 (共用和私人),並分別備份至兩個 VMAR。共用區域是由先前 ZX_PROCESS_SHARED 建立呼叫傳回的現有 VMAR 支援。私有區域會由新建立的 VMAR 支援,並透過 restricted_vmar_handle 傳回。

這樣一來,Starnix 執行元件就能建立 Starnix 程序集合,這些程序會在半個位址空間中共用對應項,但每個程序都有獨立的下半部,可代管受限模式的程式碼和資料。

進入/離開嚴格篩選模式

只有共用程序中具有受限區域的執行緒,才能進入受限模式。

執行緒必須先建立模式狀態 VMO 並使用 zx_thread_prepare_restricted 繫結至自身,才能進入。執行緒和 Zircon 核心會使用模式狀態 VMO,儲存/還原受限模式一般用途的暫存器狀態。VMO 會由執行緒 (以及任何明確建立的使用者對應) 保留,直到執行緒終止為止。

成功呼叫準備作業後,執行緒可能會使用 zx_thread_enter_restricted 進入受限模式。離開受限模式後,執行緒可能會重新進入,不需重新準備,無論是因 ZX_RESTRICTED_REASON_SYSCALLZX_RESTRICTED_REASON_EXCEPTIONZX_RESTRICTED_REASON_KICK (zx_thread_kick_restricted) 而離開都一樣。

準備
zx_status_t zx_thread_prepare_restricted(uint32_t options, zx_handle_t* out_vmo);

準備建立作業,並將模式狀態 VMO 繫結至目前執行緒。產生的 VMO 會用於在進入/離開嚴格篩選模式時,儲存/還原嚴格篩選模式狀態。

傳回的 VMO 與使用 zx_vmo_create 建立的 VMO 類似,且產生的控制代碼具有與 VMO 由 zx_vmo_create 建立時相同的權限 (不含任何選項)。請注意,系統不支援取消提交、調整大小或建立子 VMO。支援透過 zx_vmo_read/zx_vmo_write 對應、取消對應及讀取/寫入。

一次只能將一個模式狀態 VMO 繫結至執行緒。嘗試繫結另一個 VMO 時,系統會取代已繫結的 VMO。只有在最後一個使用者控制代碼關閉、最後一個使用者對應移除,且 VMO 遭另一個 zx_thread_restricted_prepare 呼叫取代或執行緒終止時,繫結的 VMO 才會遭到毀損。與任何其他 VMO 相同,VMO 對應後會由對應保留,因此呼叫端可以關閉控制代碼,並透過對應直接存取記憶體。

呼叫者的工作政策必須允許 ZX_POL_NEW_VMO

這個呼叫可能會失敗,並顯示以下錯誤訊息:

  • ZX_ERR_INVALID_ARGS - out_vmo 是無效的指標或 NULL,或 options 是 0 以外的任何值。
  • ZX_ERR_NO_MEMORY - 記憶體不足,導致作業失敗。

注意:如果 out_vmo 是無效指標,即使呼叫傳回 ZX_ERR_INVALID_ARGS,新建立的 VMO 控制代碼仍可能繫結至執行緒。系統會產生政策例外狀況 (ZX_EXCP_POLICY_CODE_HANDLE_LEAK)。假設例外狀況已處理完畢,呼叫端可以再次呼叫 prepare 並提供有效的 out_vmo,從這個狀態復原。

輸入
status_t zx_thread_enter_restricted(uint32_t options,
                                    uintptr_t return_vector,
                                    uintptr_t context);

將呼叫執行緒可存取的位址空間變更為共用程序的受限區域,並載入模式切換 VMO 中包含的暫存器狀態,進入受限模式。呼叫前,執行緒必須使用 zx_thread_prepare_restricted 準備模式狀態 VMO,並在偏移量為零的位置填入 zx_restricted_state_t 結構體。

與一般函式呼叫不同,成功的 zx_thread_enter_restricted 呼叫不會返回呼叫內容。相反地,離開受限模式後,執行緒只會跳至 return_vector 中指定的位址,並在兩個通用暫存器中載入 context 和原因代碼。所有其他暫存器 (包括堆疊指標) 都會處於未指定狀態。return_vector 的一般模式程式碼有責任使用 context、原因代碼和模式狀態 VMO 的內容,重建系統呼叫前狀態。

可能的原因代碼為 ZX_RESTRICTED_REASON_SYSCALLZX_RESTRICTED_REASON_EXCEPTIONZX_RESTRICTED_REASON_KICK。傳回時,模式狀態 VMO 的偏移量 0 會包含下列其中一個結構體 (視原因代碼而定):zx_restricted_syscall_tzx_restricted_exception_tzx_restricted_kick_t

在 x64 上,context 會放在 rdi 中,而原因代碼會放在 rsi 中。

在 arm64 上,context 會放在 x0 中,而原因代碼會放在 x1 中。

在 riscv64 上,context 會放在 a0 中,原因代碼則會放在 a1 中。

通話失敗時,系統可能會傳回:

  • ZX_ERR_INVALID_ARGS - return_vector 不是有效的使用者地址或選項。
  • ZX_ERR_BAD_STATE - 模式狀態 VMO 中的受限模式註冊狀態無效,或沒有繫結至呼叫執行緒的模式狀態 VMO。

結構體的定義如下:

typedef struct zx_restricted_syscall {
  zx_restricted_state_t state;
} zx_restricted_syscall_t;

typedef struct zx_restricted_exception {
  zx_restricted_state_t state;
  zx_exception_report_t exception;
} zx_restricted_exception_t;

typedef struct zx_restricted_kick {
  zx_restricted_state_t state;
} zx_restricted_kick_t;

請注意,這三個結構體的第一個元素都是 zx_restricted_state_t 物件。返回時,這個狀態物件會包含執行緒返回正常模式時,受限模式的一般用途暫存器狀態。儲存任何其他受限的暫存器狀態 (例如偵錯或 FPU/向量暫存器) 是 return_vector 正常模式的責任。

受限模式一般用途暫存器狀態 zx_restricted_state_t 的定義如下:

在 arm64 上,

typedef struct zx_restricted_state {
  uint64_t x[31];
  uint64_t sp;
  uint64_t pc;
  uint64_t tpidr_el0;
  // Contains only the user-controllable upper 4-bits (NZCV).
  uint32_t cpsr;
  uint8_t padding1[4];
} zx_restricted_state_t;

在 x64 上,

typedef struct zx_restricted_state {
  uint64_t rdi, rsi, rbp, rbx, rdx, rcx, rax, rsp;
  uint64_t r8, r9, r10, r11, r12, r13, r14, r15;
  uint64_t ip, flags;
  uint64_t fs_base, gs_base;
} zx_restricted_state_t;

在 riscv64 上,這只是 zx_riscv64_thread_state_general_regs_t 的 typedef。

typedef zx_riscv64_thread_state_general_regs_t zx_restricted_state_t;

zx_riscv64_thread_state_general_regs_t 的搜尋範圍:

typedef struct zx_riscv64_thread_state_general_regs {
  uint64_t pc;
  uint64_t ra;   // x1
  uint64_t sp;   // x2
  uint64_t gp;   // x3
  uint64_t tp;   // x4
  uint64_t t0;   // x5
  uint64_t t1;   // x6
  uint64_t t2;   // x7
  uint64_t s0;   // x8
  uint64_t s1;   // x9
  uint64_t a0;   // x10
  uint64_t a1;   // x11
  uint64_t a2;   // x12
  uint64_t a3;   // x13
  uint64_t a4;   // x14
  uint64_t a5;   // x15
  uint64_t a6;   // x16
  uint64_t a7;   // x17
  uint64_t s2;   // x18
  uint64_t s3;   // x19
  uint64_t s4;   // x20
  uint64_t s5;   // x21
  uint64_t s6;   // x22
  uint64_t s7;   // x23
  uint64_t s8;   // x24
  uint64_t s9;   // x25
  uint64_t s10;  // x26
  uint64_t s11;  // x27
  uint64_t t3;   // x28
  uint64_t t4;   // x29
  uint64_t t5;   // x30
  uint64_t t6;   // x31
} zx_riscv64_thread_state_general_regs_t;
zx_status_t zx_thread_kick_restricted(zx_handle_t thread, uint32_t options);

如果執行緒目前處於受限模式,則會將執行緒移出受限模式;如果不是,則會儲存待處理的移出作業。如果目標執行緒以受限模式執行,系統會透過 return_vector 提供給 zx_thread_enter_restrictedZX_RESTRICTED_REASON_KICK,將執行緒結束並切換為一般模式,且原因代碼會設為 ZX_RESTRICTED_REASON_KICK。否則,下一次呼叫 zx_thread_enter_restricted 時不會進入受限模式,而是會以原因代碼 ZX_RESTRICTED_REASON_KICK 分派至提供的進入點。

如果同一執行緒物件有多個踢除要求,系統會將這些要求合併。因此,如果多個執行緒在同一目標執行或進入受限模式時,對該目標呼叫 zx_thread_kick_restricted,則會觀察到至少一個 (可能多個) ZX_RESTRICTED_REASON_KICK 傳回值。建議使用這個系統呼叫的方式是先記錄啟動同步資料結構的原因,然後呼叫 zx_thread_kick_restricted。呼叫 zx_thread_enter_restricted 的執行緒應在觀察到 ZX_RESTRICTED_REASON_KICK 時查閱這個資料結構,並在重新進入受限模式前處理任何待處理的狀態。

thread 控制代碼必須具有正確的 ZX_RIGHT_MANAGE_THREAD

這類通話可能會失敗:

  • ZX_ERR_WRONG_TYPE - thread 不是執行緒。
  • ZX_ERR_ACCESS_DENIED - thread 沒有 ZX_RIGHT_MANAGE_THREAD。
  • ZX_ERR_BAD_STATE - thread已過世。

其他系統呼叫變更

以 VMAR 為導向的系統呼叫 - zx_vmar_mapzx_vmar_op_range 等系統呼叫不受這項提案影響。雖然 zx_vmar_root_self 不是系統呼叫,但會受到這項提案影響。詳情請參閱後續章節的討論內容。

zx_process_read_memoryzx_process_write_memory - 這些系統呼叫會取得程序控制代碼,並提供程序的記憶體存取權。目前 (本提案之前),這些呼叫不會跨越對應邊界。也就是說,如果您要求讀取跨越兩個對應項的 8KiB 區域,系統只會傳回前 4KiB。根據這項提案,這些呼叫的 API 和語意不會變更。您需要稍微調整導入方式,才能根據提供的 vaddr,在適當的 VmAspace (共用或私有) 上執行 FindRegion 呼叫。呼叫端仍可繼續讀取及寫入程序完整位址空間,包括共用和私有區域。

ZX_INFO_TASK_STATSZX_INFO_PROCESS_MAPS - 實作項目已更新,同時包含一般模式和受限模式對應,但 API 保持不變。也就是說,結果不會區分正常和受限的內容。

其他變更

zx_vmar_root_self - 這項提案最大的語意變更之一,就是透過 zx_process_create_shared 建立的程序不會有單一根 VMAR,封裝整個位址空間。而是會有兩個 VMAR。一個用於共用區域,另一個用於私人區域。語言執行階段會決定是否向程式公開這項語意變更,以及公開方式。「zx_vmar_root_self」用於 C、C++ 和 Rust 執行階段。 根據這項提案,zx_vmar_root_self 的行為更像是「取得預設 VMAR」,會傳回共用 VMAR,讓現有程式庫 (例如 scudo)「預設」在共用區域運作。

zx_process_self - 由於 zx_process_self 會傳回全域變數的值,且在共用程序中,該全域變數是由共用區域支援,因此實際上並未傳回自身的控制代碼。而是傳回群組中第一個共用程序的控制代碼 (原型程序)。我們探討了將 zx_process_self 改為使用執行緒本機儲存空間 (而非全域變數) 的想法,但最終決定暫緩處理 (請參閱 https://fxbug.dev/352827964),改為變更 zx_process_self 的問題用法。詳情請參閱下文。

pthread_create - 這是最有趣的案例,說明 zx_process_self 的使用方式有問題。為確保 pthread_create 在正確的程序中建立執行緒,需要呼叫程序的控制代碼。為解決這個問題,我們擴充了 pthread 結構體,納入程序控制代碼,並變更 pthread_create,改為使用結構體的控制代碼,而非 zx_process_self 傳回的控制代碼。

偵錯工具 - 偵錯工具 (例如 zxdb 和 fidlcat) 可能需要變更,才能瞭解執行緒的有效對應不一定與程序中的所有執行緒相同。換句話說,我們可能需要建立方法,讓偵錯工具查詢暫停執行緒的位址空間。

Fuchsia 程序建構工具 - 我們變更了程序位址空間中可存取的部分,因此必須對程序建構工具進行小幅變更。舉例來說,原型程序不會有下半部。

啟動 Starnix 元件 - Starnix 元件由 Component Manager 啟動。與任何 ELF 元件一樣,zx_process_create 用於在星系中建立第一個程序。與其他 ELF 元件不同,第一個 Starnix 程序的 VMAR 必須是共用 VMAR。為此,我們定義了新的元件資訊清單選項 is_shared_process,指示元件管理服務傳遞 ZX_PROCESS_SHARED。這個新程序會做為原型,負責在星系中建立後續程序。這個元件資訊清單選項可盡量減少元件管理服務的變更,同時避免 Starnix 本身需要參與 Fuchsia 程序建構業務。原型建立的每個程序只會共用原型的程式碼、資料和控制代碼,不會進行任何修改。

效能

對現有系統呼叫的影響極小

某些系統呼叫可能會受到小幅度的效能影響,但不會降至零。Zircon 微基準測試套件會確保影響微不足道。除了影響執行階段效能,部分核心物件可能會稍微變大,導致記憶體用量略微增加。與執行階段效能一樣,我們預期記憶體用量增加的幅度微不足道,但仍會進行測量和驗證。

模式切換效能

受限模式的模式切換效能會比原生模式切換更耗費資源,因為 CPU 必須在一般模式和受限模式之間轉換時,實際通過 EL1。不過,我們預期這項費用在 Starnix 的背景下不會太高。

我們會新增 Zircon 微基準,用於測量及追蹤模式切換成本。

安全性考量

這項機制旨在做為安全界線,因此需要由 Fuchsia 安全團隊徹底審查。

一般

使用這些新 Zircon 設施的使用者模式程式本身會做為核心,因此必須注意保護自己的資料和執行作業,以免受限於受限模式執行的程式碼影響,這與 Zircon 核心防範有錯誤或可能惡意的使用者模式 Fuchsia 程式碼的方式非常相似。

雖然本文其他部分會詳細說明,但仍值得強調幾個重點:

  • 共用控制代碼 - 共用程序群組中的所有程序都會使用單一控制代碼表格,因此在群組中任何程序的正常模式下執行的任何執行緒,都可以使用表格中的任何控制代碼。
  • 共用對應 - 共用程序群組中的所有程序會彼此共用部分位址空間 (共用 VMAR)。在正常模式下執行時,群組中的所有執行緒都可以存取這個共用區域中的對應。
  • 一般模式可存取受限區域 - 以一般模式執行時,可存取程序的受限區域。由於這個區域含有不受信任的程式碼和資料,請務必不要信任受限區域中的任何程式碼或資料 (例如複製出來後再驗證,避免 TOCTOU)。
  • 必須在進入時清除/還原 - 進入受限模式時,程式必須注意清除、還原或以其他方式取代超出 zx_restricted_state_t 指定範圍的暫存器狀態。包括浮點、向量和偵錯暫存器狀態。否則可能會將資料洩漏給不受信任的程式碼,或發生更糟的情況。
  • 必須在返回時清除/還原 - 返回正常模式時,程式必須注意清除、還原或以其他方式取代 zx_restricted_state_t 指定以外的暫存器狀態。否則,不受信任的程式碼可能會影響或控制使用者模式監管程式的執行作業,導致逸出。

推測執行安全漏洞

執行不受信任的訪客程式碼時,使用者模式核心必須防範推測執行攻擊,就像 Zircon 核心一樣。不過,由於每次切換至或切換出受限模式,都必須經過 Zircon 核心,因此我們有機會簡化客體核心,並可能忽略某些推測執行漏洞。這個概念是讓客體核心在核心進入和退出點,依賴 Zircon 核心執行的推測執行緩解措施。當然,這項策略需要經過 Fuchsia 安全性團隊審查,且每當發現新的推測執行漏洞時,都必須通知客體核心擁有者。

有限存取權

處於受限模式時,執行緒只能存取受限的位址空間,且無法發出 Zircon 系統呼叫。有兩種機制可防止受限模式執行緒發出 Zircon 系統呼叫。首先,系統呼叫攔截實作會將所有系統呼叫路由至正常模式。其次,即使系統呼叫已路由至 Zircon 核心,受限區域中缺少 Zircon vDSO,也會導致系統無法提供服務。

值得注意的是,在進入受限模式前,呼叫端應注意不要透過暫存器,無意間將資訊「洩漏」到受限模式。

當然,如果受限模式中的程式碼能夠以某種方式,危害同一程序中的一般模式程式碼 (例如透過一般模式程式碼中的錯誤),那麼它也可能危害構成共用系統映像檔的所有 Zircon 程序,因為這些程序會透過共用對應項共用部分程式碼和資料。

監督員模式存取權和執行保護

x86 的 SMAP 和 SMEP,以及 arm64 的 PAN 和 PXN,都是硬體功能,可限制核心程式碼可存取或執行的對應。這類功能可提高利用核心錯誤的難度。由於受限模式是軟體建構,且訪客核心在使用者模式下執行,因此我們無法在使用者模式管理員中直接運用這些功能。另請參閱替代方案一節中對替代對應的討論。

隱私權注意事項

這些新核心功能本身不會比其他核心功能有更多隱私權考量。不過,在以 Fuchsia 為基礎的產品上執行 Linux 二進位檔時,可能需要接受隱私權審查。

如果我們有涉及 Linux 二進位檔的特定端對端產品用途,就必須評估該用途的隱私權影響。

未來發展方向

這項設計的幾個方面可能會隨著時間改變。 本節說明我們近期可能重新審視的幾個方面。我們會在後續文件中詳細說明。

API 分解

這項提案整合了多項功能,並以 Starnix 用途為重點。舉例來說,以目前的形式來說,執行緒必須位於共用程序中,才能限制其位址空間。雖然 Starnix 沒問題,但這種耦合會限制這個系統的適用性。我們應在有更多這項技術的用途後,再考慮將 API 和功能集分解成更多獨立的建構區塊。

一個根 VMAR

與分解相關的附註:在未來的提案中,我們應重新探討程序可能有多個根 VMAR 的概念。根據目前的提案,共用和私有 VMAR 都視為根 VMAR,因為兩者都不包含在另一個 VMAR 中。這種多根方法是實用的設計決策,其動機是 VmAspaceVmAddressRegionVmMapping 類別的現有實作項目。重新設計/重構這些類別,即可讓我們不必讓程序擁有多個根 VMAR (雖然我們最終可能基於其他原因而想支援多個根)。

改善暫存器儲存功能

我們可能需要改善的其中一個領域,是在進入/離開受限模式時,如何儲存、還原或歸零 FPU/向量暫存器狀態。

Starnix 程式碼或其中一個依附元件可能會使用 FPU/向量暫存器 (例如 memcpy),因此 Starnix 必須小心儲存/還原受限的 FPU/向量狀態。目前,Starnix 會在每次受限的進入和退出時,儲存/還原所有 FPU/向量狀態。為提升效能,我們可能會編譯在受限結束點執行的部分 Starnix 邏輯,以延遲或減少需要儲存的暫存器集。

即使 Starnix 會小心儲存/還原 FPU/向量暫存器狀態,但從技術上來說,Zircon vDSO 仍可能會覆寫呼叫端在進入受限模式前建立的狀態 (請參閱 https://fxbug.dev/42062491)。目前,測試會驗證受限模式 FPU/向量暫存器狀態,以減輕這項脆弱性。

此外,在某些情況下,我們可能會提供特殊的系統呼叫,利用某些架構上可用的具備權限儲存/還原指令,或使用硬體輔助的延遲儲存/還原機制,進而提升效能。

測試

與核心介面的其他方面一樣,核心測試和核心內單元測試會實作,以驗證正確性。Zircon 微基準將用於評估效能。

說明文件

系統會新增/更新新版和修改版系統呼叫的樹狀結構內說明文件。

Zircon 例外狀況的樹內說明文件將會更新。

我們將撰寫樹狀結構內概覽文件,說明這項新機制。

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

缺點

這些變更會影響多個核心元件,並增加核心的複雜度。雖然設計目標是區隔並盡量減少這項新複雜度,但仍會產生非零成本。

替代方案

管理程序 - 除了純軟體設計外,您也可以利用硬體的管理程序功能,讓「客體核心」直接發出 Zircon 系統呼叫。這有點像是極端的半虛擬化版本。詳情請參閱 RFC:虛擬化直接模式。採用純軟體方法的其中一個原因是,確保我們可以在無法存取硬體輔助虛擬化的架構和/或裝置上套用此方法。

所有程序都在一個程序中完成 - 設計呈現的模型會為每個訪客程序提供一個 Zircon 程序。這種做法的優點之一是,每個客體程序都「可見」於 Zircon 工具,例如 pskill。替代設計是將每個訪客程序視為單一 Zircon 程序中的執行緒集合。這個替代設計會「隱藏」訪客程序,不讓 Zircon 使用者模式的其餘部分看到,但除此之外,兩者差異不大。

監督員與監督員之間的隔離 - 這是建議設計的替代方案,可消除共用區域,並要求每個監督員程序透過 IPC 或某些「手動管理」的明確對應,彼此協調。這個替代方案的優點是可提高隔離程度,讓一個程序更難以利用另一個程序遭到入侵,但會增加實作客體核心許多層面的難度 (請參閱「目標」一節的「自然且簡化的程式設計模型」)。跨程序的協調管理負擔也可能影響效能。

在不同程序中執行訪客和監管員 - 除了導入新模式,您也可以將不受信任的程式碼放入沒有 Zircon vDSO 的獨立程序。訪客系統呼叫接著會包含從不受信任的程序到實作訪客核心的 Zircon 程序的環境切換。這種方法雖然可行,但可能需要為每個系統呼叫進行「更完整的」內容切換,而且就執行階段效能而言,成本會更高。此外,與受限模式中每個客端工作一個 Zircon 執行緒 (一個 Zircon 執行緒執行客端工作,另一個則代表客端工作實作系統呼叫) 相比,這可能會導致每個客端工作需要兩個 Zircon 執行緒。與受限模式相比,針對不受信任的程式碼/資料使用個別程序時,在客體與監管程式之間的 IPC 和內容切換方面,效能會有些許劣勢。

專屬讀取/寫入與模式狀態 VMO - 這項設計的早期疊代版本使用專屬系統呼叫來存取模式狀態 (zx_restricted_state_readzx_restricted_state_write)。不過,這些呼叫很快就造成效能問題,因此遭到淘汰,改用模式狀態 VMO。

取消繫結 - 在先前的疊代中,有一個明確的取消繫結系統呼叫,可「復原」zx_thread_prepare_restricted 執行的繫結作業。我們發現解除繫結是不必要的,且只用於部分測試程式碼。我們移除了這項功能,以簡化 API。

存取和執行保護的替代對應 - 使用者模式管理員可用的安全漏洞緩解工具組,與核心模式管理員可用的工具組不同。一般來說,兩者都可以運用各種軟體技術 (例如 CFI),但使用者模式監管程式無法使用硬體功能,例如 x86 的 SMAP 或 SMEP,或是 arm64 的 PAN 或 PXN。這些硬體功能提供標記方式,可在核心模式中執行時,標記可在不同環境中存取的對應。如有需要,可以進一步瞭解 SMEP/PXN 和 SMAP/PAN 的替代方案。這個概念是維護一組具有不同保護位元的替代對應,並在切換一般和受限制模式,或在共用和受限制區域之間執行「使用者複製」作業時,在這類對應之間轉換。當然,維護及使用替代對應項目會產生一些顯著的效能和複雜度成本,因此這個替代方案可能可行,也可能不可行。

不明

核心核心物件關係的變更 - 這項提案會變更部分核心核心物件關係。舉例來說,這會在控制代碼表和程序之間建立一對多關係,程序不再只有一個位址空間 (或根 VMAR),futex 可在程序之間共用,特定控制代碼可能「同時存在於多個程序中」等等。這些變更可能會造成潛在的設計缺陷。

偵錯和診斷 - 這項提案會影響偵錯工具和其他以程序為中心的工具 (例如記憶體監控器 (fx mem)) 的模型。我們需要與 zxdb 和記憶體監控器的擁有者合作,深入瞭解相關影響。此外,我們還需要處理現有系統呼叫的詳細資料,這些系統呼叫假設程序只有一個作用中的位址空間 (例如 zx_process_read_memory)。

既有技術和參考資料

請參閱 RFC-0082:在 Fuchsia 上執行未修改的 Linux 程式,以及其中的「Prior art and references」一節。

這項設計是根據多份 Google 內部文件和許多對話內容而來。 另請參閱

嚴格篩選模式的靈感來自 ARMv8-A 例外狀況層級 (「如果我們有 EL-1 呢?」)。