Categories
程式開發

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


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

對於像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寫道

確保沙箱正確

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

例如,有個沙箱函數:

/* 返回0到16之间的值。  */

int return_the_value();

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

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

extern const char* do_the_thing();

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

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

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

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

污染數據

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

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

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

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

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

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

下一步工作

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

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

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

關於作者

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

英文原文:

Securing Firefox with WebAssembly