Firefox 是怎樣解決內存安全的?

Firefox 是怎樣解決內存安全的?

對於像 Firefox 這樣複雜且高度優化的系統,內存安全是最大的安全挑戰之一。Firefox 主要是用 C 和 C++ 編寫的。眾所周知,這些語言很難安全地使用,因為任何錯誤都有可能導致程序完全崩潰。

Firefox 軟件工程師 Nathan Froyd 寫道,“我們努力尋找並消除內存風險,但也在改進 Firefox 代碼庫,以便在更深的層次上解決這些問題。”

截至目前,Firefox 主要關注兩項技術:

  1. 將代碼分解成多個沙箱進程,減少特權
  2. 用一門安全的語言去重寫代碼,比如 Rust

一種新方法

“雖然我們繼續在 Firefox 中使用沙箱和 Rust,但它們各有侷限性。對已有的大型組件,進程級沙箱很有效,但這會消耗大量系統資源,因此必須謹慎使用。”Nathan Froyd 寫道。

雖然 Rust 是輕量級的,但是重寫現有的數百萬行 C++ 代碼是一件“勞神費力”的事。

以 Graphite 字形庫為例,Firefox 用它來正確呈現某些複雜字體。它太小了,不適合“放入”自己的進程中。

然而,如果發現內存風險,即使是站點隔離的進程架構也無法阻止惡意字體破壞加載它的頁面。同時,重寫和維護這種領域專用的代碼並不是 Firefox 有限工程資源的理想用法。

如今,Firefox 在“軍火庫”中加入第三種方法。

加利福尼亞大學、聖地亞哥大學、德克薩斯大學、奧斯汀分校和斯坦福大學的研究人員開發出一種新的沙箱技術,叫 RLBox 。

Nathan Froyd 表示,“它讓我們能快速有效地將現有 Firefox 組件轉換為在一個 WebAssembly 沙箱中運行。我們已經成功地將該技術集成到我們的代碼庫中,並將其用於沙箱化 Graphite。”

據悉,這種隔離將提供給 Firefox 74 的 Linux 用戶和 Firefox 75 的 Mac 用戶,不久之後還將提供 Windows 支持。

構建一個 wasm 沙箱

Wasm 沙箱背後的核心實現思想是,你可以將 C/C++ 編譯成 wasm 代碼,然後將該 wasm 代碼編譯成實際運行程序的機器的本機代碼。

這些步驟與在瀏覽器中運行 C/C++ 應用程序的步驟類似,但是,“我們在構建 Firefox 本身之前,就執行本地代碼到 wasm 的轉換。這兩個步驟都各自依賴於重要的軟件,我們還添加了第三個步驟,以使沙箱轉換更簡單、更不易出錯。”Nathan Froyd 寫道。

首先,你要將 C/C++ 編譯成 wasm 代碼。作為 WebAssembly 工作的一部分,在 Clang 和 LLVM 中添加一個 wasm 後端。光有一個編譯器是不夠的;你還需一個 C/C++ 的標準庫。該組件是通過 wsi -sdk 提供的。一旦擁有這些組件,你就有足夠能力將 C/C++ 轉化成 wasm 代碼。

其次,你需要將 wasm 代碼轉換為本機對象文件。Nathan Froyd 說,“當我們第一次實現 wasm 沙箱時,經常有人問我們,‘為什麼需要這個步驟?’你可以分發 wasm 代碼,並在 Firefox 啟動時在用戶的機器上動態編譯它。我們本可以做到這一點,但這種方法要求針對每個沙箱實例重新編譯 wasm 代碼。“

在每個源都位於單獨進程中的情況下,每個沙箱都編譯代碼是不必要的重複。他們選擇的方法支持在多個進程間共享已編譯的本機代碼,從而能節省大量內存。

這種方法還提高了沙箱的啟動速度,這對於細粒度的沙箱非常重要,例如,將每次字體訪問或圖像加載的相關聯代碼置入沙箱。

通過 Cranelift 實現預編譯

這種方法並不意味著必須自己編寫將 wasm 代碼編譯成本機代碼的編譯器。

“我們用相同的編譯器後端實現這種提前編譯”,它最終將通過字節碼聯盟的 Lucet 編譯器和運行時來支持 Firefox JavaScript 引擎的 wasm 組件: Cranelift 。

這種代碼共享可確保 JavaScript 引擎和 wasm 沙箱編譯器共享改進所帶來的好處。由於工程原因,這兩段代碼目前使用不同版本的 Cranelift。

然而,隨著沙箱技術的成熟,“我們希望修改它們以使用完全相同的代碼庫”。

現在,Firefox 工程師已將 wasm 代碼轉換為本機對象代碼,“我們需要能從 C++ 調用沙箱代碼”。如果沙箱代碼在單獨的虛擬機中運行,這一步將涉及到在運行時查找函數名以及管理與虛擬機相關的狀態。

但是,通過上面設置,沙箱代碼是符合 wasm 安全模型的本機編譯代碼。因此,可以使用與調用常規本機代碼相同的機制來調用沙箱函數。

“我們必須注意所涉及的不同機器模型:wasm 代碼使用 32 位指針,而我們最初的目標平臺 x86-64 Linux 使用 64 位指針。但是,還有其他障礙需要克服,這就把我們帶到轉換過程的最後一步。”Nathan Froyd 寫道

確保沙箱正確

使用與常規本機代碼相同的機制調用沙箱代碼很方便,但它隱藏了一個重要細節。“我們不能相信任何來自沙箱的東西,因為對手可能已經損害沙箱”。

例如,有個沙箱函數:

複製代碼

<code>/* 返回 0 到 16 之間的值。  */ int return_the_value();/<code>

不能保證這個沙箱函數遵循它的契約。因此,“要確保返回的值落在我們期望範圍內”。

類似地,對於一個返回指針的沙箱函數:

複製代碼

<code>extern const char* do_the_thing();/<code>

Nathan Froyd 表示,“我們不能保證返回的指針實際上指向沙箱控制的內存。對手可能會強迫返回的指針指向應用程序沙箱之外的某個地方。因此,我們在使用指針前要驗證它。”

在閱讀源代碼時,還有一些其他的運行時約束並不明顯。

例如,上面返回的指針可能指向沙箱中動態分配的內存。在這種情況下,應該由沙箱釋放指針,而不是由主機應用程序釋放。“我們可以依靠開發人員始終記住哪些值是應用程序值,哪些值是沙箱值”。

經驗表明,這種方法是不可行的。

汙染數據

上面兩個例子說明一個普遍原則:從沙箱返回的數據應該被明確標識。有了這個標識,我們就可以確保以適當方式處理數據。

我們將與沙箱相關的數據標記為“汙染”。汙染數據可以自由地操作(例如指針運算、訪問字段),生成更多汙染數據。

但是,當我們將汙染數據轉換為非汙染數據時,我們希望這些操作儘可能明確。汙染數據不僅對管理從沙箱返回的內存很有價值,它對於識別從沙箱中返回的可能需要額外驗證的數據也很有價值,例如指向某個外部數組的索引。

因此,我們將沙箱中所有公開的函數建模為返回汙染數據。這些函數還將汙染數據作為參數,因為它們所操作的任何東西在某種程度上都必須屬於沙箱。

一旦函數調用有了這個接口,編譯器就變成了一個汙染檢查器。當汙染數據在需要非汙染數據的上下文中使用時,編譯器將發生錯誤,反之亦然。

這些上下文正是需要傳播汙染數據和 / 或需要驗證數據的地方。 RLBox 處理汙染數據的所有細節,並提供一些特性,可以直接將庫的接口增量轉換為沙箱接口。

下一步工作

有了 wasm 沙箱的核心基礎結構,我們就可以集中精力提高它在 Firefox 代碼庫中的影響力了——既可以將它帶到所有支持的平臺上,也可以將它應用到更多的組件上。

由於這種技術是輕量級的,並且易於使用,我們希望在接下來的幾個月裡對 Firefox 的更多部件進行快速沙箱化。

我們最初的努力集中在與 Firefox 綁定的第三方庫上。此類庫通常具有定義良好的入口點,並且不會與系統的其他部分廣泛共享內存。然而,在未來,我們也計劃將這項技術應用於甲方代碼。

關於作者

Nathan Froyd 是 Firefox 的軟件工程師。在業餘時間,他喜歡奧林匹克舉重和閱讀。


關注我並轉發此篇文章,私信我“領取資料”,即可免費獲得InfoQ價值4999元迷你書!


分享到:


相關文章: