VS Code をプロセスサンドボックスに移行する
セキュリティと VS Code アーキテクチャにとってのウィンウィン
2022 年 11 月 28 日 Benjamin Pasero 著、@BenjaminPasero
Electron レンダラープロセスでサンドボックスを有効にすることは、Visual Studio Code のような安全で信頼性の高い Electron アプリケーションにとって不可欠な要件です。サンドボックスは、ほとんどのシステムリソースへのアクセスを制限することで、悪意のあるコードが引き起こす可能性のある損害を軽減します。このブログ記事では、2020 年初頭に開始し、2023 年初頭に完了する予定の VS Code でのプロセスサンドボックスの有効化にどのように成功したかについて詳しく説明します。プロセスサンドボックスの課題を理解するために、このブログ記事では VS Code のプロセスモデルの詳細と、この道のりでどのように進化してきたかについても説明します。
これは、ほぼすべての VS Code コンポーネントで根本的なアーキテクチャの変更とコードの修正が必要だったため、チームでの取り組みでした。VS Code のプロセスアーキテクチャは全面的に見直され、その過程で大幅に強化されました。私たちは、その道のりの主要なマイルストーンを強調し、他の人が学ぶための貴重な洞察を提供することを願っています。過去数ヶ月間、プロセスサンドボックスモードは VS Code Insiders で正常に実行されており、この変更の影響についてフィードバックを得ています。問題を見つけたり、エクスペリエンスを改善するための提案があったり、一般的な質問がある場合は、お気軽にご連絡ください。
VS Code または Electron またはサンドボックスに慣れていない場合は、まずこのブログ記事の最後にある用語セクションを確認することをお勧めします。そこには、使用されている用語の説明と背景資料へのリンクがあります。
プロセスサンドボックスの概要
長い間、Electron は HTML と JavaScript でNode.js API を直接使用することを許可してきました。以下のコードスニペットは、ユーザーに「Hello World」を出力するだけでなく、ローカルディスクにファイルを書き込む Web ページの簡単な例を示しています。

Web ページをユーザーに表示する責任を負う Electron プロセスは、レンダラープロセスと呼ばれます。レンダラープロセスのサンドボックスモードを有効にすると、セキュリティが向上し、Web モデルにさらに近づくために、その機能が低下します。HTML と JavaScript は引き続き許可されますが、Node.js の使用は許可されません。システムリソースへのアクセスを必要とするレンダラープロセスのコンポーネントは、サンドボックス化されていない別のプロセスに委任する必要があります。
以下のコードは Node.js に依存せず、設定を更新する機能を提供する `vscode` グローバル変数を使用します。このメソッドの実装には、Node.js にアクセスできる別のプロセスにメッセージを送信することが含まれます。そのため、同期的に実行されなくなり、非同期的に実行されます。

レンダラープロセスで `vscode` グローバルがどのようにして使用されるようになったか、そしてその実装方法は、以下のタイムラインセクションで詳しく説明します。
レンダラープロセスからの Node.js のブロックは、推奨される Electron セキュリティ推奨事項です。過去には、攻撃者がレンダラープロセスから任意の Node.js コードを実行できるセキュリティ上の問題がありました。サンドボックス化されたレンダラープロセスは、これらの攻撃のリスクを大幅に軽減します。
どうやってここまで来たのか?
レンダラープロセスからすべての Node.js 依存関係を削除するような大規模な変更は、回帰やバグのリスクを伴います。以前は 1 つのプロセスで実行されていたコードは、分割されて複数のプロセスで実行される必要があります。ネイティブで Web パッケージ化できない Node モジュールも移動する必要があります。Node.js Buffer などの特定のグローバルオブジェクトは、Uint8Array などのブラウザ互換のバリアントに置き換える必要があります。
下の図は、サンドボックス化の取り組みが始まる前のプロセスアーキテクチャを示しています。ご覧のとおり、ほとんどのプロセスはレンダラープロセスからフォークされた Node.js 子プロセス (緑色) です。ほとんどの (プロセス間通信) IPC は Node.js ソケットを介して実装されており、レンダラープロセスは Node.js API の主要なクライアントです。たとえば、ファイルの読み書きなどです。

私たちは、サンドボックス化された別の VS Code アプリケーションを出荷することなく、プロセスサンドボックスに取り組むことをすぐに決定しました。VS Code レンダラープロセスを段階的にサンドボックス対応にし、最後にスイッチを切り替えることを望んでいました。過去数年間、私たちはサンドボックスの目標に貢献する変更を加えながらも、完全に有効にすることなく、VS Code の毎月の安定版リリースを出荷してきました。飛行中に根本的に再構築されている飛行機を操縦することを想像してみてください。そして私たちのケースでは、ユーザーは VS Code への変更をほとんど認識していませんでした。
私たちのテクノロジータイムライン
次のセクションでは、過去数年間でサンドボックスがどのように実現したかについて詳しく説明します。主なタスクは、レンダラープロセスからすべての Node.js 依存関係を削除することでしたが、その過程で、`MessagePort` を使用した効率的なサンドボックス対応 IPC ソリューションの考案や、レンダラープロセスからフォークできるさまざまな Node.js 子プロセスの新しいホストの検索など、さらに多くの課題が発生しました。
ほとんどの場合、トピックの順序は実際のタイムラインに従っています。各セクションを簡潔に保つために、特定の技術的側面をより詳細に説明する他のドキュメントやチュートリアルへのリンクを掲載しています。そして、この作業を 2020 年初頭に計画したにもかかわらず、このタスクに役立った以前の作業のいくつかを省略するのは公平ではありません。さらに詳しく見てみましょう…
巨人の肩に乗る
2020 年初頭にサンドボックス化を検討し始めたとき、すでに Web ブラウザで実行できるバージョンの VS Code を出荷していました。vscode.dev をブラウザで実行して、動作中のVisual Studio Code for the Web を見ることができます。VS Code の Web バージョンを作成する過程で、ワークベンチ (VS Code のメインユーザーインターフェイスウィンドウ) から Node.js の依存関係を削除する方法を学びました。

Node.js への依存関係を削除することは、代替手段を見つけることを意味しました。たとえば、Node.js の `Buffer` 型への依存関係は、ブラウザ環境では `Uint8Array` にフォールバックする VSBuffer 相当のものに置き換えられました。また、一部の Node.js モジュール (oniguruma、iconv-lite) を Web 環境で実行できるようにパッケージ化することもできました。

しかし、VS Code for the Web が現実になる前にも、私たちはリモート開発のサポートを有効にしていました。これにより、SSH 接続などを介してリモートホストでソースコードを編集できるようになりました (そして後にGitHub Codespacesも強化されました)。リモート開発では、VS Code の UI 側の部分をローカルで実行し、実際のファイル操作はリモートマシンで実行するソリューションを実装する必要がありました。このモデルは、特権操作が別のプロセスで実行されなければならないサンドボックス化されたワークベンチにも適用されます。どちらの場合も、レンダラープロセスは IPC を介して特権ホストと通信して操作を実行します。
レンダラーからの通信チャネルの有効化
レンダラープロセスが Node.js を使用できない場合、Node.js が利用可能な別のプロセスに作業を委任する必要があります。Web コンテキストでの解決策の 1 つは、サーバーがリクエストを受け入れる HTTP メソッドに依存することです。しかし、これはデスクトップアプリケーションにとって最善の解決策とは感じられませんでした。デスクトップアプリケーションでは、ポートでローカルサーバーを実行すると、セキュリティ上の理由からファイアウォールによってブロックされる可能性があるためです。
Electron は、メインスクリプトが実行される前に実行されるプリロードスクリプトをレンダラープロセスに注入する機能を提供します。これらのスクリプトは、Electron 独自のIPC メカニズムにアクセスできます。プリロードスクリプトは、コンテキストブリッジ API を介して、レンダラーのメインスクリプトで利用可能な API を拡張できます。プリロードスクリプトは Electron の IPC を直接使用できますが、メインスクリプトはできません。そのため、特定のメソッドをコンテキストブリッジを介してメインスクリプトに公開しています。冒頭で使用した例では、設定を更新するメソッドをプリロードスクリプトからメインスクリプトに公開する方法は次のとおりです。

プリロードスクリプトは、特権コードと非特権コードを分割するための基本的な構成要素です。たとえば、ディスクにファイルを書き込むことは、新しいコンテンツを含む IPC メッセージがメインスクリプトからプリロードスクリプトに、そしてそこから Node.js にアクセスできるメインプロセスに送信されることを意味します。

メッセージポートによる高速プロセス間通信
プリロードスクリプトの導入により、レンダラープロセスが Electron メインプロセスと通信して作業をスケジュールする方法が得られました。しかし、Electron アプリケーションでは、メインプロセスに過度な作業負荷をかけないことが重要です。メインプロセスは、キーボードやマウスからのユーザー入力の処理も担当するプロセスであるためです。メインプロセスがビジー状態になると、ユーザーインターフェイスが応答しなくなる可能性があります。
これは以前にも見た問題でした。サンドボックスに取り組む前でさえ、パフォーマンス集約型のコードをバックグラウンドプロセスである VS Code 共有プロセスにオフロードすることに興味がありました。このプロセスは、すべてのワークベンチウィンドウとメインプロセスが通信できる隠しウィンドウです。たとえば、拡張機能をインストールすると、操作全体を実行するために共有プロセスにリクエストが送信されます。
しかし、共有プロセスへの通信は Node.js ソケットを介して実装されていました。これは、メインプロセスが通信にまったく関与しないため、オーバーヘッドがゼロであるという利点がありました。欠点は、Node.js API を使用できないため、サンドボックス化されたレンダラーでは Node.js ソケット通信が不可能であることです。
メッセージポートは、2 つのプロセス間に IPC チャネルを確立することで、2 つのプロセスを相互に接続する強力な方法を提供します。完全にサンドボックス化されたレンダラープロセスでさえ、メッセージポートを使用できます。メッセージポートはブラウザでWeb APIとして提供されているためです。Node.js ソケット通信をメッセージポートに置き換えることで、メインプロセスを関与させる必要がないというパフォーマンス面を維持しながら、サンドボックス互換の IPC ソリューションを実現できました。
特にプリロードスクリプトを使用するサンドボックス化されたレンダラープロセスにメッセージポートをプロセス境界を越えて渡すことは複雑です。シーケンスは以下の図に示されています。
- 共有プロセスはメッセージポート P1 と P2 を作成し、P1 を保持します。
- P2 は Electron IPC を介してメインプロセスに送信されます。
- メインプロセスは P2 を要求元のレンダラープロセスに転送します。
- P2 はそのレンダラープロセスのプリロードスクリプトに送られます。
- プリロードスクリプトは P2 をレンダラーメインスクリプトに転送します。
- メインスクリプトは P2 を受信し、それを使用して直接メッセージを送信できます。

レンダラーのオリジンの変更
ウェブブラウザでは、URL を入力するとコンテンツが読み込まれて表示されます。Electron では、URL を入力するのではなく、アプリケーションがどのコンテンツを読み込んで表示するかを決定します。したがって、VS Code を開くと、ワークベンチのコンテンツを表示するために事前に構成された URL でウィンドウが読み込まれます。
VS Code の場合、この URL は、読み込むディスク上の実際のファイルを指すローカルファイルプロトコル ( `file://<ディスク上のファイルへのパス>` ) を使用していました。サンドボックス化の作業の一環として、重大なセキュリティ上の問題があったため、このアプローチを見直しました。Chromium は、HTTPS プロトコルと比較して厳密ではないローカルファイルプロトコルに対して特定のセキュリティ上の仮定をしています。たとえば、ローカルファイルプロトコル URL には厳密なオリジンチェックは適用されません。
Electron では、レンダラープロセスにコンテンツを読み込むために使用できるカスタムプロトコルを登録できます。カスタムプロトコルは、セキュリティに関して HTTPS プロトコルと同じように動作するように構成できます。このアプローチを使用して、コンテンツを提供するローカル Web サーバーを実行する必要を回避しました。
すべてのレンダラープロセスにカスタムの `vscode-file` プロトコルを導入することで、ファイルプロトコルのすべての使用を廃止することができました。これは HTTPS と同じように動作するように構成されており、VS Code for the Web が実際にどのように機能するかにより近づきました。
コードローダーの適応
これまで、すべての TypeScript コードはAMD モジュールにコンパイルされ、長年にわたって維持してきたカスタムローダーで読み込まれていました。AMD から離れてESM を採用する予定ですが、その作業は初期段階にあります。
当社のコードローダーは、よく定義されたいくつかの変数をプローブして実際の実行環境を特定することにより、Node.js 環境と Web 環境の両方をサポートしています。サンドボックス化されたレンダラーは本質的に Web 環境のようなものであるため、ローダーがサンドボックスをサポートするために必要な変更はごくわずかでした。
これらの変更が加えられると、サンドボックスモードを有効にした VS Code の初期バージョンを実行できるようになりました。しかし、レンダラープロセスから Node.js の依存関係をまだ解放していなかったため、空白のページが表示され、コンソールにエラーが出力されるだけでした。
採用を支援するツール
サンドボックスを有効にして VS Code を実行する方法ができたので、Node.js に依存するソースコードから「サンドボックス対応」のコードへの移行を容易にするためのツールに投資したいと考えました。VS Code for the Web への投資を考えると、Node.js コードが Web バージョンにリリースされるのをブロックする静的解析ツールがすでに導入されていました。このツールは、そのランタイム要件を持つ一連のターゲット環境を定義していました。当社のツールは、Node.js グローバルオブジェクト ( `Buffer` など)、Node.js API、またはそれらを許可しないターゲット環境でのノードモジュールの使用を検出して報告できます。サンドボックス化の作業のために、Node.js の使用を一切許可しない新しいターゲット環境 **electron-sandbox** を追加しました。この環境にコードを移動することで、コードを徐々にサンドボックス対応にすることができました。
下のスクリーンショットでは、エディターに警告マーカーが表示され、**ブラウザ** ターゲット環境のファイルが Node.js の API に依存していることを示しています。この警告により、ビルドが失敗し、このコードが誤ってリリースにプッシュされるのを防ぎます。

私たちのプロセスエクスプローラーと問題報告ユーティリティは、**electron-sandbox** のターゲット要件に準拠した最初のものの一つでした。ワークベンチウィンドウが採用を終えるずっと前に、これらのウィンドウを完全にサンドボックス化して実行することができました。
プロセスをレンダラーから移動する
前のトピックで詳しく説明したように、Node.js 機能の一部を別のプロセスに移動し、IPC を使用して作業をスケジュールし、結果を受け取るのは簡単です。
ただし、Node.js に依存するワークベンチのコンポーネントの中には、より複雑なものがあります。特に、以下のような子プロセスをフォークするものです。
- 拡張機能ホスト
- 統合ターミナル
- ファイル監視
- 全文検索
- タスク実行
- デバッグ
VS Code はリモートシナリオで実行できることを考えると、すでに一部のタスクをリモートで実行するメカニズム (検索、デバッグ、タスク実行) を導入していました。これらのコンポーネントは、コードが置かれている場所にローカルで実行される拡張機能ホストプロセス内で動作できます。そのため、リモートが接続されていないローカルで VS Code が実行されている場合でも、これらの子プロセスの所有権をレンダラープロセスから拡張機能ホストに移動することができました。
拡張機能ホストについては、より野心的な計画がありました。これには Electron に新しい「ユーティリティプロセス」API を追加する必要があったため、これらの変更については後で独自のセクションで説明します。
統合ターミナルとファイル監視は、共有プロセスの子プロセスになりました。ファイル監視または統合ターミナルを必要とするすべてのウィンドウは、メッセージポートを介して共有プロセスと通信してこれらのサービスを取得します。
下の図は、レンダラープロセスでサンドボックスを有効にした 2022 年後半のプロセスアーキテクチャを示しています。すべての Node.js プロセスは、共有プロセスの子プロセスまたはメインプロセスからのユーティリティプロセスに移動されました。メッセージポートは、メインプロセスに負担をかけることなく、効率的な直接プロセス間通信に使用されます。

Chromium のコードキャッシュの調整
サンドボックスを有効にしても、パフォーマンスの低下が発生しないことも確認したいと考えました。私たちは、起動からエディターにカーソルが点滅するまでの時間を測定し、メインワークベンチスクリプト (約 11.5 MB のミニファイされたコード) の読み込み、解析、実行に V8 JavaScript エンジンでかなりの時間が費やされていることを確認しました。更新がインストールされていない限り、起動ごとに同じスクリプトが読み込まれます。この動作を考慮すると、V8 は、コードキャッシュを使用して、次回より高速に読み込めるように、最適化されたバージョンのスクリプトをディスクに保存できます。
Chromium 自体は、Web ページの読み込み時間を高速化するためにコードキャッシュを使用しています。当社のソリューションと同じ最適化を V8 エンジンでトリガーしますが、Chromium の実装では、特定の期間に頻繁にアクセスされる Web ページに対してのみ行われます。当社のアプリケーションはデスクトップアプリケーションであり、Web ページではないため、常にコードキャッシュを使用するソリューションを求めていました。
起動時にコードキャッシュを有効にしたところ、起動時間を短縮するための最良のソリューションになりました。残念ながら、当社のソリューションはNode.js に依存していたため、サンドボックス化されたレンダラープロセスでは適用できませんでした。
Electron でコードキャッシュオプションを公開することで、bypassHeatCheck オプションを使用する際に Chromium でコードキャッシュを強制的にトリガーできます。さらに、ユーザーが新しいバージョンの VS Code を実行していることを検出した場合、以前に生成されたコードキャッシュを破棄することで、追加の保護層を追加しました。
新しい Electron API: UtilityProcess
最後の、そしておそらく最も複雑なタスクは、拡張機能ホストをどこに移動するかについての解決策を見つけることでした。共有プロセスと同様に、通信は Node.js ソケットを介して実装されていました。ウィンドウごとに 1 つの拡張機能ホストプロセスがあり、拡張機能は必要に応じて任意数の子プロセスを生成できます。
私たちは拡張機能ホストをファイルウォッチャーや統合ターミナルと同様に共有プロセスに移動することを検討しましたが、この機会を利用して、隠しウィンドウをホストとして必要としない、より柔軟なものを作成すべきだと感じました。
そのため、サンドボックス化されたレンダラーで動作し、現在の動作のほとんどを維持する、堅牢でスケーラブルなソリューションを求めていました。
- 子プロセスの生成をサポートする分離されたプロセス
- 完全な Node.js サポート
- サンドボックス化されたプロセスとの直接 IPC にメッセージポートを使用する
当時、Electron はこれらの要件をサポートする API を提供できませんでした。そこで、新しいユーティリティプロセス API を Electron に貢献しました。この API により、拡張機能ホストをレンダラープロセスからメインプロセスから作成されるユーティリティプロセスに移動することができました。メッセージポートを使用することで、レンダラーと拡張機能ホストの間で直接通信でき、ユーザー入力処理などの他のプロセスに影響を与えることはありません。
Electron の webview 要素からの移行
サンドボックスを有効にするために必ずしも必要というわけではありませんでしたが、この機会に VS Code での Electron のwebview タグの使用を見直し、iframe タグに置き換え、VS Code が Web でどのように機能するかにより密接に合わせました。どちらのタグも、ワークベンチをこのコードを実行する影響から分離しながら、拡張機能からの信頼できないコードをホストできる点で似ています。たとえば、Markdown ファイルのプレビューを開くと、コンテンツは組み込みの Markdown 拡張機能によって提供されるこのような要素でレンダリングされます。
ほとんどの場合、`webview` タグを `iframe` タグに置き換えるだけで済みました。しかし、`iframes` にはコンテンツ内のテキスト検索を実行して強調表示する機能が欠けていました。この機能は、Markdown ドキュメントをプレビューするときに検索をサポートするために不可欠でした。Chromium は内部的にこの機能を実装していましたが、Web API としてはエクスポートされていませんでした。私たちは Electron で API を公開するために必要な変更を行い、`webview` 要素の使用をすべて廃止することができました。
レンダラープロセスの再利用の有効化
サンドボックス化されたレンダラープロセスのパフォーマンス上の利点の 1 つは、Electron でのライフサイクル動作です。従来、レンダラープロセスは、別の URL にナビゲーションが発生するたびに終了して再起動していました。VS Code の場合、これはワークスペースを変更したり、ウィンドウをリロードしたりすると、レンダラープロセスが再作成されることを意味し、一部の環境や設定では遅くなる可能性があります。
サンドボックス化されたレンダラープロセスは、URL をナビゲートしても存続します。別のワークスペースを開いたり、現在のワークスペースをリロードしたりするのがはるかに速くなります。ただし、これには、レンダラープロセスで実行されるネイティブ Node.js モジュールをコンテキスト対応にする必要があります。サンドボックス化を有効にするためにすべてのネイティブモジュールをレンダラープロセスから移動することになりましたが、レンダラープロセスの再利用を早期にテストしたかったため、すべてのネイティブモジュールをコンテキスト対応にしました。
すべてをまとめる
最後のステップは、ユーザー設定を介してサンドボックスモードを条件付きで有効にすることでした。すべてのユーザーに対してサンドボックスモードを有効にするのではなく、Insiders エディションで検証する時間を設けたかったのです。window.experimental.useSandbox 設定により、Insiders ではサンドボックスがデフォルトで有効になり、Stable でも有効にできます。
2023 年初頭に、実験インフラストラクチャを使用して、サンドボックスの有効化を Stable エディションに段階的に展開する予定です。これにより、問題がないかを確認しながら、ユーザーセットを増やしてサンドボックスモードをテストおよび検証できるようになります。
実験フェーズが終了すると、サンドボックスモードはすべてのユーザーに対してデフォルトで有効になり、非サンドボックスモードは削除されます。隠しウィンドウであり、必要以上のリソースを使用するため、共有プロセスをユーティリティプロセスに変換するなど、後のイテレーションのためにまだいくつかの作業が計画されています。
これは、VS Code チーム全体の助けとモチベーションがなければ不可能だった素晴らしい旅でした。これらの変更を段階的にリリースし、プロセスサンドボックスを必要とする新しい Electron バージョンに備えることができたのは素晴らしいことでした。プロセスアーキテクチャを大幅に改善し、Web モデルにさらに近づけることができ、将来のための堅牢な基盤を築きました。
使用された用語
Electron は、VS Code デスクトップ版をサポートするすべてのプラットフォーム (Windows、macOS、Linux) で実行できるようにする主要なフレームワークです。Chromium とブラウザ API、V8 JavaScript エンジン、Node.js API、およびクロスプラットフォームデスクトップアプリケーションを構築するためのプラットフォーム統合 API を組み合わせています。
このブログ記事では、Electron のプロセスサンドボックスを単に「サンドボックス」と呼びます。
Chromium、したがって Electron が提供するプロセスモデルを理解することが重要です。このブログ記事では、以下のプロセスを頻繁に参照します。
- メインプロセス - アプリケーションのメインエントリポイント。
- レンダラープロセス - ユーザーが操作できるウィンドウ。
メインプロセスは常に 1 つだけですが、開かれたウィンドウごとにレンダラープロセスが作成されます。プロセスモデルの詳細については、Electron のプロセスモデルドキュメントとこのChrome Developers のブログ記事を参照してください。
「共有プロセス」は Electron に固有のものではなく、VS Code の実装の詳細です。これは、Node.js が有効になっている隠し Electron ウィンドウであり、他のすべてのウィンドウが拡張機能のインストールなどの複雑なタスクを実行するために通信できます。
「拡張機能ホスト」は、すべてのインストール済み拡張機能をレンダラープロセスから隔離して実行するプロセスです。開かれたウィンドウごとに 1 つの拡張機能ホストがあります。
VS Code の「ワークベンチ」ウィンドウは、ユーザーがファイルを編集したり、検索したり、デバッグしたりするために操作するメインウィンドウです。このブログ記事では、これを単に「ワークベンチ」と呼びます。その他のウィンドウは、**ヘルプ** メニューからアクセスできるプロセスエクスプローラーと問題報告です。
「IPC」という用語は、プロセス間通信を指すために使用されます。IPC は、あるプロセスが別のプロセスと通信する方法です。
私たちは、ユーザーの一部で最新の変更をテストするために、「Insiders」と呼ばれる VS Code の nightly バージョンをリリースしています。VS Code チームの全員がInsiders エディションを使用しており、皆様にも試していただき、何か問題があれば報告していただければ幸いです。
ハッピーコーディング!
Benjamin Pasero, @BenjaminPasero