Zircon & 中的 SafeStack;Fuchsia

說明

LLVM 的安全堆疊功能是一種編譯器模式,可強化產生的程式碼,防範因緩衝區過度執行攻擊等堆疊攻擊。

上方連結的 Clang/LLVM 說明文件頁面說明瞭一般配置。膠囊摘要是指每個執行緒都有兩個堆疊,而非一般的堆疊:「安全堆疊」和「不安全的堆疊」。不安全的堆疊會用於所有目的,其中可能會使用指標指向堆疊記憶體,而安全堆疊僅用於特定用途,也就是禁止程式碼查看堆疊記憶體的指標。因此,不安全的堆疊會用於由其他函式參照傳遞的陣列或變數,或將其位址儲存在堆積中,這類記憶體可能會因為緩衝區過度執行,或未釋放錯誤和記憶體遭到利用。安全堆疊會用於編譯器的暫存器和函式呼叫的傳回位址。因此,舉例來說,無法利用簡易緩衝區超載錯誤來覆寫含有函式的傳回位址,這就是使用所謂的 ROP (「傳回導向程式設計」) 技術發動攻擊和攻擊的基礎。

該頁面的「相容性」部分不適用於 Zircon (或 Fuchsia)。在 Zircon 使用者模式程式碼 (包括所有 Fuchsia) 中,SafeStack 的執行階段支援會直接納入標準 C 執行階段程式庫中,而在共用程式庫 (DSO) 中一切運作正常。

安全堆疊陰影呼叫堆疊檢測配置和 ABI 彼此相關且相近,但也有關係。每個函式都可單獨啟用或停用,供任何函式使用。Fuchsia 的編譯器 ABI 和 libc 一律會與使用或未採用任一檢測設備建構的程式碼互通,無論檢測作業在特定 libc 版本中為何都不考慮。

互通性與 ABI 效果

一般來說,安全堆疊不會影響 ABI。機器專屬的呼叫慣例維持不變。它可以在使用安全堆疊 (而非安全堆疊) 建構的程式中附帶特定功能。如果合併兩者來自於直接編譯的 .o 檔案、封存程式庫 (.a 檔案) 或來自任何組合的共用資料庫 (.so 檔案),則不會影響兩者。

雖然每個執行緒會有額外的狀態 (不安全的堆疊指標,請見下文「實作詳細資料」底下),但如果程式碼未使用安全堆疊,則不需要針對此狀態執行任何相關動作,即可在呼叫或使用安全堆疊的程式碼呼叫正確狀態。其中唯一可能的例外狀況,是程式碼實作自己的非本機離開或情境切換 (例如協同程式)。Zircon C 程式庫的 setjmp/longjmp 程式碼可自動儲存及還原這個額外狀態,因此即使呼叫 setjmplongjmp 的程式碼不知道安全堆疊,任何以 longjmp 為基礎的項目都已正確處理所有內容。

用於 Zircon 和福奇亞

Clang 編譯器可透過 -fsanitize=safe-stack 指令列選項啟用這項功能。這是 *-fuchsia 目標的編譯器預設模式。如要針對特定編譯停用此功能,請使用 -fno-sanitize=safe-stack 選項。

Zircon 支援使用者模式和核心程式碼的安全堆疊。在 x86 Zircon 版本中,使用 Clang (將 variants = [ "clang" ] 傳遞至 GN) 進行建構時,系統一律會啟用安全堆疊。

導入作業詳細資料

如要支援安全堆疊程式碼,還有不安全的堆疊指標。在抽象層中,這可以視為額外的註冊,就像機器的一般堆疊指標登錄一樣。機器的堆疊指標暫存器會用於安全堆疊,與以往相同。系統會將不安全的堆疊指標當做在 ABI 中具有固定用途的其他註冊,但當然,機器實際上並沒有新的登錄,此外,相容性安全堆疊不會變更將用於所有機器註冊的基本機器專屬呼叫慣例。

Zircon 和 Fuchsia 的 C 和 C++ ABI 會將不安全的堆疊指標儲存在記憶體中,堆疊指標位於執行緒指標的固定偏移處。<zircon/tls.h> 標頭會定義每部機器的偏移值。

對於 x86 使用者模式,執行緒指標為 fsbase,這表示組合程式碼中的存取權看起來類似 %fs:ZX_TLS_UNSAFE_SP_OFFSET。對於 x86 核心,執行緒指標為 gsbase,這表示組合程式碼中的存取權看起來類似 %gs:ZX_TLS_UNSAFE_SP_OFFSET

如果是 Aarch64 (ARM64),在 C 或 C++ 程式碼中,__builtin_thread_pointer() 會傳回執行緒指標。在使用者模式中,執行緒指標位於 TPIDR_EL0 特殊註冊檔中,必須在一般註冊器 (使用 mrs *reg*, TPIDR_EL0) 中擷取後才能存取記憶體,因此這並不是組合程式碼中的單一指示。核心中的程序都相同,但改用 TPIDR_EL1 特殊暫存器。

低層級和組合程式碼的注意事項

大部分程式碼甚至不需要考慮安全堆疊問題,就連組件也不例外。呼叫慣例維持不變。使用儲存註冊、尋找傳回地址等堆疊時,無論是否使用安全堆疊,都大致相同。主要例外狀況是程式碼會實作類似非本機離開或內容切換功能的程式碼。這類程式碼可能需要儲存或還原不安全的堆疊指標。longjmp 函式和 C++ throw 均已直接處理這個問題,因此使用這些建構函式的 C 或 C++ 程式碼不需要執行任何新功能。

核心中的情境切換程式碼會處理切換不安全的堆疊指標。在 x86 中,程式碼會明確指出:%gs 指向 struct x86_percpu,後者在 ZX_TLS_UNSAFE_SP_OFFSET 中有一個成員 kernel_unsafe_sparch_context_switch 會將此內容複製到舊執行緒 struct arch_threadunsafe_sp 欄位,然後將新執行緒的 unsafe_sp 複製到 kernel_unsafe_sp 中。在 ARM64 中,set_current_thread 會自動完成這項操作,因為 TPIDR_EL1 特殊註冊會變更直接指向每個執行緒的 struct thread,而非像 x86 所示每個 CPU 的結構。

導入某些新類型非本機結束或內容切換功能的新程式碼,必須處理不安全的堆疊指標,做法與處理傳統機器堆疊指標註冊的方式類似。這類程式碼都應在編譯期間使用 #if __has_feature(safe_stack) 進行測試,指出特定建構作業是否使用安全堆疊。這個預先處理工具建構可在 C、C++ 或組件 (.S) 來源檔案中使用。