跳到主要內容

Electron 與 V8 記憶體隔離區

·7 分鐘閱讀

Electron 21 及更高版本將啟用 V8 記憶體隔離區,這對某些原生模組有影響。


更新(2022/11/01)

若要追蹤關於 Electron 21+ 中原生模組使用的持續討論,請參閱 electron/electron#35801

在 Electron 21 中,我們將啟用 V8 沙箱指標 在 Electron 中,遵循 Chrome 在 Chrome 103 中執行相同操作的決定。這對原生模組有一些影響。此外,我們先前在 Electron 14 中啟用了一項相關技術 指標壓縮。當時我們沒有太多討論它,但指標壓縮對 V8 堆積大小上限有影響。

啟用這兩項技術後,對安全性、效能和記憶體使用量都非常有利。然而,啟用它們也有一些缺點。

啟用沙箱指標的主要缺點是,不再允許指向外部(「堆外」)記憶體的 ArrayBuffer。這表示依賴 V8 中此功能的原生模組需要重構,才能在 Electron 20 及更高版本中繼續運作。

啟用指標壓縮的主要缺點是,V8 堆積大小上限為 4GB。確切細節有點複雜——例如,ArrayBuffer 與 V8 堆積的其餘部分分開計算,但有其 自身限制

Electron 升級工作小組 認為,指標壓縮和 V8 記憶體隔離區的優點大於缺點。這樣做的主要原因有三個

  1. 它使 Electron 更接近 Chromium。Electron 在 V8 組態等複雜內部細節上與 Chromium 的差異越小,我們意外引入錯誤或安全性漏洞的可能性就越小。Chromium 的安全性團隊非常強大,我們希望確保我們充分利用他們的工作。此外,如果錯誤僅影響 Chromium 中未使用的組態,則修復它不太可能是 Chromium 團隊的優先事項。
  2. 它效能更好。指標壓縮將 V8 堆積大小減少高達 40%,並將 CPU 和 GC 效能提高 5%–10%。對於絕大多數不會遇到 4GB 堆積大小限制,且不使用需要外部緩衝區的原生模組的 Electron 應用程式來說,這些都是顯著的效能提升。
  3. 它更安全。有些 Electron 應用程式執行不受信任的 JavaScript(希望遵循我們的 安全性建議!),對於這些應用程式,啟用 V8 記憶體隔離區可以保護它們免受大量惡意的 V8 漏洞侵害。

最後,對於確實需要更大堆積大小的應用程式,有一些解決方案。例如,可以將 Node.js 的副本包含在您的應用程式中,該副本是在停用指標壓縮的情況下建置的,並將記憶體密集型工作移至子程序。雖然有點複雜,但如果您認為您希望為您的特定用例採用不同的權衡方案,也可以建置停用指標壓縮的自訂 Electron 版本。最後,在不久的將來,wasm64 將允許使用 WebAssembly 建置的應用程式(無論是在 Web 上還是在 Electron 中)使用遠遠超過 4GB 的記憶體。


常見問題

我如何知道我的應用程式是否受到此變更的影響?

嘗試使用 ArrayBuffer 包裝外部記憶體將在 Electron 20+ 中於運行時崩潰。

如果您在應用程式中未使用任何原生 Node 模組,則您是安全的——沒有辦法從純 JS 觸發此崩潰。此變更僅影響在 V8 堆積之外分配記憶體(例如,使用 mallocnew),然後使用 ArrayBuffer 包裝外部記憶體的原生 Node 模組。這是一個相當罕見的用例,但有些模組確實使用了此技術,並且此類模組將需要重構,以便與 Electron 20+ 相容。

我如何測量我的應用程式正在使用的 V8 堆積記憶體量,以了解我是否接近 4GB 限制?

在渲染程序中,您可以使用 performance.memory.usedJSHeapSize,它將以位元組為單位傳回 V8 堆積使用量。在主程序中,您可以使用 process.memoryUsage().heapUsed,這是可比較的。

什麼是 V8 記憶體隔離區?

有些文件將其稱為「V8 沙箱」,但該術語很容易與 Chromium 中發生的 其他種類的沙箱 混淆,因此我將堅持使用術語「記憶體隔離區」。

有一種相當常見的 V8 漏洞利用方式,大致如下

  1. 在 V8 的 JIT 引擎中找到錯誤。JIT 引擎分析程式碼,以便能夠省略緩慢的運行時類型檢查並產生快速的機器碼。有時邏輯錯誤意味著它的分析出錯,並省略了它實際上需要的類型檢查——例如,它認為 x 是一個字串,但實際上它是一個物件。
  2. 濫用這種混淆來覆寫 V8 堆積內部的某些記憶體位元,例如,指向 ArrayBuffer 開頭的指標。
  3. 現在您有一個指向您想要的任何位置的 ArrayBuffer,因此您可以讀取和寫入程序中的任何記憶體,甚至是 V8 通常無法存取的記憶體。

V8 記憶體隔離區是一種旨在完全阻止此類攻擊的技術。實現此目的的方式是不在 V8 堆積中儲存任何指標。相反,對 V8 堆積內部其他記憶體的所有引用都儲存為距某些保留區域開頭的偏移量。然後,即使攻擊者設法損壞 ArrayBuffer 的基底位址,例如透過利用 V8 中的類型混淆錯誤,他們最壞的情況也只能讀取和寫入隔離區內的記憶體,而他們可能無論如何已經可以這樣做了。關於 V8 記憶體隔離區如何運作還有很多可讀的內容,因此我在此不再贅述——最好的入門讀物可能是 Chromium 團隊的 高階設計文件

我想重構 Node 原生模組以支援 Electron 21+。我該怎麼做?

有兩種方法可以重構原生模組以與 V8 記憶體隔離區相容。第一種是在將外部建立的緩衝區傳遞給 JavaScript 之前,將其複製到 V8 記憶體隔離區中。這通常是一個簡單的重構,但當緩衝區很大時,可能會很慢。另一種方法是使用 V8 的記憶體分配器來分配您打算最終傳遞給 JavaScript 的記憶體。這有點複雜,但可讓您避免複製,這意味著大型緩衝區的效能更好。

為了使這更具體,以下是一個使用外部陣列緩衝區的 N-API 模組範例

// Create some externally-allocated buffer.
// |create_external_resource| allocates memory via malloc().
size_t length = 0;
void* data = create_external_resource(&length);
// Wrap it in a Buffer--will fail if the memory cage is enabled!
napi_value result;
napi_create_external_buffer(
env, length, data,
finalize_external_resource, NULL, &result);

當啟用記憶體隔離區時,這將崩潰,因為資料是在隔離區之外分配的。重構為將資料複製到隔離區中,我們得到

size_t length = 0;
void* data = create_external_resource(&length);
// Create a new Buffer by copying the data into V8-allocated memory
napi_value result;
void* copied_data = NULL;
napi_create_buffer_copy(env, length, data, &copied_data, &result);
// If you need to access the new copy, |copied_data| is a pointer
// to it!

這會將資料複製到 V8 記憶體隔離區內新分配的記憶體區域中。您可以選擇性地讓 N-API 也提供指向新複製資料的指標,以防您需要在事後修改或引用它。

重構為使用 V8 的記憶體分配器有點複雜,因為它需要修改 create_external_resource 函數以使用 V8 分配的記憶體,而不是使用 malloc。這可能或多或少是可行的,具體取決於您是否控制 create_external_resource 的定義。想法是首先使用 V8 建立緩衝區,例如使用 napi_create_buffer,然後將資源初始化到 V8 分配的記憶體中。重要的是為 資源的生命週期 保留對 Buffer 物件的 napi_ref,否則 V8 可能會垃圾回收 Buffer,並可能導致釋放後使用錯誤。