參加你附近的 ,瞭解 VS Code 中的 AI 輔助開發。

將 VS Code 遷移到程序沙盒

對安全性與 VS Code 架構而言的雙贏

2022 年 11 月 28 日,作者 Benjamin Pasero,@BenjaminPasero

Electron 渲染器程序中啟用 沙盒 是 Visual Studio Code 等安全可靠的 Electron 應用程式的關鍵要求。沙盒透過限制對大多數系統資源的訪問來減少惡意程式碼可能造成的損害。在這篇博文中,我們將詳細介紹我們如何在 VS Code 中啟用程序沙盒,這是一段我們始於 2020 年初並計劃在 2023 年初結束的旅程。為了幫助理解程序沙盒的挑戰,這篇博文還描述了 VS Code 程序模型的細節以及它在這段旅程中的演變。

這是一項團隊合作,因為幾乎所有 VS Code 元件都需要進行基礎架構更改以及程式碼修改。VS Code 程序架構得到了全面檢修,並在此過程中得到了顯著加強。我們重點介紹了在此過程中取得的主要里程碑,希望為他人提供寶貴的見解以供學習。在過去的幾個月裡,程序沙盒模式已在 VS Code Insider 版本中成功執行,為我們提供了有關此更改影響的反饋。如果您發現問題、有改進體驗的建議或有一般性問題,請隨時聯絡我們

如果您不熟悉 VS Code、Electron 或沙盒,您可能需要先閱讀部落格文章末尾的術語部分。在那裡您會找到所用術語的解釋和背景材料的連結。

程序沙盒簡介

長期以來,Electron 允許在 HTML 和 JavaScript 中直接使用 Node.js API。下面的程式碼片段提供了一個簡單的網頁示例,該網頁不僅向用戶列印“Hello World”,還寫入本地磁碟上的檔案

HTML and Node.js code on a web page in Electron

負責向用戶呈現網頁的 Electron 程序稱為渲染器程序。為渲染器程序啟用沙盒模式可降低其功能以提高安全性並更符合 Web 模型:雖然 HTML 和 JavaScript 仍然允許,但 Node.js 的使用則不允許。渲染器程序中需要訪問系統資源的元件將不得不委託給未沙盒化的另一個程序。

以下程式碼不再依賴 Node.js,而是使用一個提供更新設定功能的 vscode 全域性變數。該方法的實現涉及向另一個可以訪問 Node.js 的程序傳送訊息。因此,它也不再同步執行,而是非同步執行

Removing Node.js by providing an asynchronous alternative in Electron

我們如何在渲染器程序中擁有 vscode 全域性變數以及它如何實現,將在下面的時間線部分中詳細介紹。

阻止 Node.js 從渲染器程序中執行是 Electron 安全建議鼓勵的做法。我們過去曾遇到過安全問題,攻擊者能夠從渲染器程序執行任意 Node.js 程式碼。沙盒渲染器程序大大降低了這些攻擊的風險。

我們是如何實現這一目標的?

移除渲染器程序中所有 Node.js 依賴項這樣的大規模更改,存在迴歸和 bug 的風險。以前在一個程序中執行的程式碼必須拆分並在多個程序中執行。本機且因此無法進行 Web 打包的 Node 模組也必須移出。某些全域性物件,例如 Node.js Buffer,必須替換為與瀏覽器相容的變體,例如 Uint8Array

下圖顯示了沙盒工作開始之前的程序架構。正如您所看到的,大多數程序都是從渲染器程序派生出來的 Node.js 子程序(綠色)。大多數(程序間通訊)IPC 都是透過 Node.js 套接字實現的,渲染器程序是 Node.js API 的主要客戶端——例如用於讀寫檔案。

VS Code process model before sandboxing in 2020

我們很快決定,我們希望在不釋出單獨沙盒版 VS Code 應用程式的情況下進行程序沙盒化。我們希望逐步讓 VS Code 渲染器程序做好沙盒準備,然後在最後“翻轉開關”。過去幾年,我們每月釋出了 VS Code 的穩定版本,其中包含有助於實現沙盒目標但未完全啟用的更改。想象一下,一架飛機正在空中被徹底重建。而在我們的案例中,使用者大多沒有意識到 VS Code 的變化。

我們的技術時間線

接下來的章節將詳細介紹沙盒在過去幾年中是如何實現的。主要任務是移除渲染器程序中所有 Node.js 依賴項,但在此過程中出現了更多挑戰,例如在 MessagePort 的幫助下找出高效的沙盒就緒型 IPC 解決方案,或者為我們可從渲染器程序派生出來的各種 Node.js 子程序尋找新的宿主。

在很大程度上,主題的順序遵循實際的時間線。為了使每個部分都簡明扼要,我們連結到其他文件和教程,更詳細地解釋某個技術方面。儘管我們計劃在 2020 年初進行這項工作,但忽略一些有助於完成此任務的先前工作是不公平的。讓我們仔細看看……

站在巨人的肩膀上

當我們在 2020 年初開始考慮沙盒時,我們已經發布了一個可以在 Web 瀏覽器中執行的 VS Code 版本。您可以在瀏覽器中執行 vscode.dev,並檢視正在執行的 Web 版 Visual Studio Code。在建立 VS Code 的 Web 版本時,我們已經學會了如何從工作臺(VS Code 的主要使用者介面視窗)中刪除 Node.js 依賴項。

VS Code for Web running in the browser

移除對 Node.js 的依賴意味著尋找替代方案。例如,我們對 Node.js Buffer 型別的依賴被替換為 VSBuffer 等效項,該等效項在瀏覽器環境中將回退到 Uint8Array。我們還能夠將一些 Node.js 模組 (oniguruma, iconv-lite) 打包以在 Web 環境中執行。

VSBuffer utility class supporting both Node.js and web environments

但即使在 Web 版 VS Code 成為現實之前,我們已經啟用了對遠端開發的支援,這允許在遠端主機上編輯原始碼,例如透過 SSH 連線(後來甚至支援了 GitHub Codespaces)。對於遠端開發,我們必須實現一個解決方案,其中 VS Code 的 UI 部分在本地執行,而實際的檔案操作在遠端機器上執行。此模型也適用於沙盒化的工作臺,其中特權操作必須在不同的程序中執行。在這兩種情況下,渲染器程序都透過 IPC 與特權主機通訊以執行操作。

啟用從渲染器通訊的通道

當渲染器程序無法使用 Node.js 時,工作必須委託給可用的 Node.js 的另一個程序。在 Web 環境中,一個解決方案是依賴 HTTP 方法,由伺服器接受請求。然而,這對於桌面應用程式來說似乎不是最佳解決方案,因為出於安全原因,本地伺服器在某個埠上執行可能會被防火牆阻止。

Electron 提供將 預載入指令碼 注入渲染器程序的功能,這些指令碼在主指令碼執行之前執行。這些指令碼可以訪問 Electron 自己的 IPC 機制。預載入指令碼可以透過 上下文橋 API 豐富渲染器主指令碼可用的 API。雖然預載入指令碼可以直接使用 Electron 的 IPC,但主指令碼不能。因此,我們透過上下文橋向主指令碼公開了某些方法。在我們最初使用的示例中,以下是如何從預載入指令碼向主指令碼公開用於更新設定的方法

Exposing a method from preload script to the main script in Electron

預載入指令碼是我們將特權程式碼與非特權程式碼分離的基本構建塊。例如,寫入磁碟上的檔案意味著包含新內容的 IPC 訊息將從主指令碼傳到預載入指令碼,然後從那裡傳到可以訪問 Node.js 的主程序。

IPC flow when preload scripts are involved in Electron

透過訊息埠進行快速程序間通訊

隨著預載入指令碼的引入,我們有一種方法讓渲染器程序與 Electron 主程序通訊以排程工作。然而,在 Electron 應用程式中,不應讓主程序承擔過多工作,因為它也是負責處理使用者輸入(例如鍵盤和滑鼠)的程序,這一點至關重要。一個繁忙的主程序可能導致使用者介面無響應。

這在我們之前就遇到過。即使在進行沙盒工作之前,我們也對將效能密集型程式碼解除安裝到後臺程序,即 VS Code 共享程序感興趣。此程序是一個隱藏視窗,所有工作臺視窗和主程序都可以與之通訊。例如,當您安裝擴充套件時,會向共享程序傳送請求以執行整個操作。

然而,與共享程序的通訊是透過 Node.js 套接字實現的。這樣做的好處是主程序沒有任何開銷,因為它根本不參與通訊。缺點是 Node.js 套接字通訊在沙盒化渲染器中不可能實現,因為您不能使用任何 Node.js API。

訊息埠提供了一種強大的方式,透過在兩個程序之間建立 IPC 通道來相互連線。即使是完全沙盒化的渲染器程序也可以使用訊息埠,因為它們在瀏覽器中作為 Web API 提供。用訊息埠替換 Node.js 套接字通訊,使我們能夠擁有一個沙盒相容的 IPC 解決方案,同時仍然保留了不涉及主程序的效能方面。

跨程序邊界傳遞訊息埠是複雜的,尤其是在具有預載入指令碼的沙盒渲染器程序中。序列如下所示

  • 共享程序建立訊息埠 P1 和 P2 並保留 P1。
  • P2 透過 Electron IPC 傳送給主程序。
  • 主程序將 P2 轉發給請求的渲染器程序。
  • P2 最終位於該渲染器程序的預載入指令碼中。
  • 預載入指令碼將 P2 轉發到渲染器主指令碼。
  • 主指令碼接收 P2 並可以使用它直接傳送訊息。

Message ports exchange between shared and renderer process in VS Code

更改渲染器的源

在 Web 瀏覽器中,您輸入 URL,然後載入並呈現內容。在 Electron 中,您不輸入 URL,而是由應用程式為您決定載入和呈現哪個內容。因此,當您開啟 VS Code 時,會載入一個帶有預配置 URL 的視窗,以顯示工作臺的內容。

對於 VS Code,此 URL 曾使用指向磁碟上實際檔案的本地檔案協議載入 (file://<path to file on disk>)。作為沙盒工作的一部分,我們重新審視了此方法,因為它具有嚴重的安全隱患。Chromium 對本地檔案協議做出了一些安全假設,這些假設與 HTTPS 協議相比不那麼嚴格。例如,對於本地檔案協議 URL,不應用嚴格的來源檢查。

使用 Electron,您可以註冊自定義協議,用於將內容載入到渲染器程序中。自定義協議可以配置為在安全性方面與 HTTPS 協議的行為相同。我們使用這種方法來避免執行服務內容的本地 Web 伺服器。

透過為所有渲染器程序引入自定義的 vscode-file 協議,我們得以放棄所有檔案協議的使用。它配置為與 HTTPS 行為相同,這意味著我們更接近於 Web 版 VS Code 的實際工作方式。

調整我們的程式碼載入器

從歷史上看,我們所有的 TypeScript 程式碼都編譯為 AMD 模組,並使用我們多年來一直維護的 自定義載入器 載入。我們計劃擺脫 AMD 並採用 ESM,但這項工作仍處於早期階段

我們的程式碼載入器透過探測一些定義良好的變數來識別實際執行環境,從而支援 Node.js 和 Web 環境。沙盒渲染器本質上就像一個 Web 環境,因此我們的載入器只需很少的更改即可支援沙盒。

這些更改完成後,我們能夠執行啟用沙盒模式的早期版本 VS Code。然而,由於我們尚未將渲染器程序從其 Node.js 依賴項中解放出來,因此只顯示一個空白頁面以及控制檯輸出的錯誤。

有助於採用的工具

既然我們有辦法在啟用沙盒的情況下執行 VS Code,我們希望投資於工具,以便更輕鬆地從依賴 Node.js 的原始碼過渡到“沙盒就緒”程式碼。鑑於我們對 Web 版 VS Code 的投資,我們已經擁有靜態分析工具,可以阻止 Node.js 程式碼被髮布到 Web 版本。此工具定義了一組目標環境及其執行時要求。我們的工具可以檢測並報告在不允許 Node.js 的目標環境中使用的 Node.js 全域性物件(例如 Buffer)、Node.js API 或 Node 模組。為了進行沙盒化工作,我們添加了一個新的目標環境 electron-sandbox,該環境不允許使用任何 Node.js。透過將程式碼移到此環境中,我們能夠逐步使程式碼沙盒就緒。

在下面的截圖中,編輯器中出現一個警告標記,表示來自 browser 目標環境的檔案依賴於 Node.js 的 API。此警告將導致我們的構建失敗,並防止意外地將此程式碼推送到釋出版本。

A warning in VS Code informing about a target environment violation

我們的程序資源管理器和問題報告工具是首批符合 electron-sandbox 目標要求的工具。我們能夠在工作臺視窗完成適配之前,完全沙盒化地執行這些視窗。

將程序移出渲染器

正如前面主題詳細解釋的那樣,將 Node.js 功能片段移到另一個程序並使用 IPC 排程工作和接收結果可能很簡單。

然而,工作臺中一些依賴於 Node.js 的元件更為複雜,特別是那些派生子程序的元件,例如:

  • 擴充套件主機
  • 整合終端
  • 檔案監視
  • 全文搜尋
  • 任務執行
  • 除錯

鑑於 VS Code 可以在遠端場景中執行,我們已經有機制可以遠端執行一些任務,即:搜尋、除錯和任務執行。這些元件可以在擴充套件主機程序中執行,該程序自然執行在程式碼所在的本地位置。因此,即使 VS Code 在本地執行而沒有連線遠端,我們也能夠將這些子程序的所有權從渲染器程序轉移到擴充套件主機。

對於擴充套件主機,我們有更宏偉的計劃。我們將在後面的章節中介紹這些更改,因為它需要向 Electron 新增新的“實用程式程序”API。

整合終端和檔案監視器已移至共享程序的子程序。任何需要檔案監視或整合終端的視窗都將透過訊息埠與共享程序通訊以獲取這些服務。

下圖顯示了我們在 2022 年末的程序架構,當時我們已在渲染器程序中啟用了沙盒。所有 Node.js 程序都已移至共享程序的子程序或主程序的實用程式程序。訊息埠用於高效的直接程序間通訊,而不會給主程序帶來負擔。

VS Code process model after sandboxing in late 2022

調整 Chromium 的程式碼快取

我們還希望確保啟用沙盒不會導致任何效能退化。我們測量了從啟動到編輯器中顯示閃爍游標所需的時間,發現大量時間花在 V8 JavaScript 引擎載入、解析和執行主工作臺指令碼(大約 11.5 MB 的縮減程式碼)上。除非安裝更新,否則每次啟動都會載入相同的指令碼。考慮到這種行為,V8 可以將指令碼的最佳化版本儲存在磁碟上,下次使用程式碼快取載入時速度更快。

Chromium 本身使用程式碼快取來加速網頁載入時間。它觸發與我們解決方案相同的 V8 引擎最佳化,但是 Chromium 的實現僅針對在特定時間內頻繁訪問的網頁執行此操作。我們希望有一種始終使用程式碼快取的解決方案,因為我們的應用程式是桌面應用程式而不是網頁。

我們在啟動時啟用了程式碼快取,它很快成為我們改進啟動時間的首選解決方案。不幸的是,我們的解決方案依賴於 Node.js,不適用於沙盒渲染器程序。

透過在 Electron 中公開程式碼快取選項,我們可以在使用 bypassHeatCheck 選項時強制觸發 Chromium 中的程式碼快取。此外,當檢測到使用者執行的是更新版本的 VS Code 時,我們透過丟棄之前生成的程式碼快取,增加了一層額外的保護。

一個全新的 Electron API:UtilityProcess

最後,可能也是最複雜的任務是找到一個解決方案,來移動擴充套件主機。像共享程序一樣,通訊是透過 Node.js 套接字實現的。每個視窗都有一個擴充套件主機程序,擴充套件程式可以隨意生成任意數量的子程序。

我們曾考慮過將擴充套件主機移動到我們的共享程序中,就像檔案監視器和整合終端一樣,但我們覺得我們應該抓住機會構建一個更靈活的解決方案,而無需隱藏視窗作為主機。

為此,我們想要一個強大且可擴充套件的解決方案,它可以在沙盒渲染器中工作,但保留大部分當前行為:

  • 支援生成子程序的隔離程序
  • 完整的 Node.js 支援
  • 使用訊息埠與沙盒程序進行直接 IPC

當時,Electron 無法為我們提供支援這些要求的 API,因此我們為 Electron 貢獻了一個新的實用程式程序 API。此 API 使我們能夠將擴充套件主機從渲染器程序移至從主程序建立的實用程式程序。透過使用訊息埠,我們可以在渲染器和擴充套件主機之間直接通訊,而不會影響任何其他程序,例如處理所有使用者輸入的主程序。

脫離 Electron 的 webview 元素

雖然不一定需要啟用沙盒,但我們藉此機會重新審視了在 VS Code 中使用 Electron webview 標籤 的情況,並將其替換為 iframe 標籤,以更緊密地與 VS Code 在 Web 中的工作方式保持一致。這兩個標籤的相似之處在於它們都允許工作臺託管來自擴充套件程式的非受信任程式碼,同時將工作臺與執行此程式碼的影響隔離開來。例如,當您開啟 Markdown 檔案的預覽時,內容將在此類元素中呈現,由內建的 Markdown 擴充套件程式提供。

在大多數情況下,我們只需將 webview 標籤替換為 iframe 標籤。然而,iframe 缺少一項功能,即在內容中執行和突出顯示文字搜尋的能力。此功能對於在預覽 Markdown 文件時支援搜尋至關重要。雖然 Chromium 內部實現了此功能,但它並未作為 Web API 匯出供使用。我們進行了必要的更改,以在 Electron 中公開此 API,並得以放棄所有 webview 元素的使用。

啟用渲染器程序重用

沙盒化渲染器程序的一個性能優勢是它們在 Electron 中的生命週期行為。傳統上,每當導航到另一個 URL 時,渲染器程序就會終止並重新啟動。對於 VS Code 而言,這意味著更改工作區或重新載入視窗會重新建立渲染器程序,這在某些環境和設定中可能會很慢。

沙盒化渲染器程序保持活動狀態,即使在導航 URL 時也是如此。開啟另一個工作區或重新載入當前工作區會快得多。然而,要實現這一點,需要使在渲染器程序中執行的本地 Node.js 模組具有上下文感知能力。儘管我們最終將所有本地模組移出渲染器程序以啟用沙盒,但我們仍然希望儘早測試渲染器程序重用,因此使我們所有的本地模組都具有上下文感知能力。

萬事俱備

最後一步是透過使用者設定有條件地啟用沙盒模式。我們不希望為所有使用者啟用沙盒模式,而是給它一些時間在我們的Insiders版本中進行驗證。透過 window.experimental.useSandbox 設定,沙盒在 Insiders 中預設啟用,並可在 Stable 版本中啟用。

我們計劃在 2023 年初利用我們的實驗基礎設施,逐步向穩定版推出沙盒功能。這將使我們能夠在不斷增長的使用者群中測試和驗證沙盒模式,同時檢查是否存在問題。

一旦實驗階段結束,沙盒模式將對所有使用者預設啟用,非沙盒模式將被移除。未來迭代仍有一些工作計劃,例如,我們希望將共享程序轉換為實用程式程序,因為它是一個隱藏視窗,並且佔用的資源比必要的更多。

這是一段非凡的旅程,只有在整個 VS Code 團隊的幫助和激勵下才得以實現。很高興看到我們能夠逐步釋出這些更改,併為需要程序沙盒的新 Electron 版本做好準備。我們成功地大大改進了我們的程序架構,並與 Web 模型更緊密地對齊,為未來奠定了堅實的基礎。

術語

Electron 是支援 VS Code 桌面版在所有受支援平臺(Windows、macOS 和 Linux)上執行的主要框架。它結合了 Chromium 及其瀏覽器 API、V8 JavaScript 引擎和 Node.js API,以及平臺整合 API,用於構建跨平臺桌面應用程式。

在本博文中,我們將 Electron 程序沙盒 簡稱為“沙盒”。

瞭解 Chromium 和 Electron 提供的程序模型非常重要。在這篇博文中,我們經常提到以下程序:

  • 主程序 - 應用程式的主要入口點。
  • 渲染器程序 - 使用者可以與之互動的視窗。

雖然主程序始終只有一個,但每開啟一個視窗就會建立一個渲染器程序。您可以在 Electron 程序模型文件和這篇 Chrome 開發者部落格文章中瞭解有關程序模型的更多資訊。

“共享程序”並非 Electron 特有,而是 VS Code 的實現細節。它是一個隱藏的、啟用了 Node.js 的 Electron 視窗,所有其他視窗都可以與之通訊以執行復雜任務,例如擴充套件安裝。

“擴充套件主機”是一個程序,執行所有已安裝的擴充套件,與渲染器程序隔離。每個開啟的視窗都有一個擴充套件主機。

VS Code“工作臺”視窗是使用者用於編輯檔案、搜尋或除錯的主視窗。在本博文中,我們將其簡稱為“工作臺”。其他視窗是“程序資源管理器”和“問題報告器”,可以透過幫助選單訪問。

我們使用“IPC”一詞指代程序間通訊。IPC 是一種程序與其他程序通訊的方式。

我們釋出了名為“Insiders”的 VS Code 每晚構建版本,以在部分使用者中測試最新更改。VS Code 團隊中的每個人都使用Insiders版本,我們希望您也能嘗試並報告任何問題

編碼愉快!

Benjamin Pasero,@BenjaminPasero