VS Code のプロセスサンドボックスへの移行
セキュリティと VS Code アーキテクチャにとってWin-Win
2022年11月28日 Benjamin Pasero 著, @BenjaminPasero
Electron レンダラープロセスで サンドボックス を有効にすることは、Visual Studio Code のような安全で信頼性の高い Electron アプリケーションにとって重要な要件です。サンドボックスは、ほとんどのシステムリソースへのアクセスを制限することにより、悪意のあるコードが引き起こす可能性のある損害を軽減します。このブログ記事では、VS Code でプロセスサンドボックスを有効にするためにどのように取り組んだかの詳細な概要を提供します。これは、私たちが 2020年初頭に開始 し、2023年初頭に完了する予定の取り組みです。プロセスサンドボックス化の課題を理解するために、このブログ記事では、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 ソケット通信が不可能であることです。
メッセージポート は、プロセス間に IPC チャネルを確立することにより、2 つのプロセスを相互に接続する強力な方法を提供します。完全にサンドボックス化されたレンダラープロセスでも、ブラウザーで Web API として提供されているため、メッセージポートを使用できます。Node.js ソケット通信をメッセージポートに置き換えることで、メインプロセスを関与させる必要がないというパフォーマンスの側面を維持しながら、サンドボックス互換の IPC ソリューションを実現できました。
プロセス境界を越えてメッセージポートを渡すことは、特にプリロードスクリプトを使用したサンドボックス化されたレンダラープロセスでは 複雑 です。シーケンスを以下の図に示します。
- 共有プロセスはメッセージポート P1 と P2 を作成し、P1 を保持します。
- P2 は Electron IPC 経由でメインプロセスに送信されます。
- メインプロセスは P2 を要求元のレンダラープロセスに転送します。
- P2 はそのレンダラープロセスのプリロードスクリプトに到達します。
- プリロードスクリプトは P2 をレンダラーメインスクリプトに転送します。
- メインスクリプトは P2 を受信し、それを使用してメッセージを直接送信できます。
レンダラーのオリジンの変更
Web ブラウザーでは、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 モジュールの使用を検出して報告できます。サンドボックス化の作業のために、Node.js の使用を一切許可しない新しいターゲット環境 electron-sandbox を追加しました。コードをこの環境に移行することで、コードを徐々にサンドボックス対応にすることができました。
以下のスクリーンショットでは、browser ターゲット環境のファイルが Node.js の API に依存していることを示す警告マーカーがエディターに表示されています。警告により、ビルドが失敗し、このコードが誤ってリリースにプッシュされるのを防ぎます。
プロセスエクスプローラーと問題レポーターユーティリティは、electron-sandbox ターゲット要件に準拠した最初のユーティリティでした。ワークベンチウィンドウの採用が完了するずっと前に、これらのウィンドウを完全にサンドボックス化して実行することができました。
レンダラーからプロセスを移動する
前のトピックで詳しく説明したように、Node.js 機能の一部を別のプロセスに移動し、IPC を使用して作業をスケジュールし、結果を受信することは簡単です。
ただし、Node.js に依存するワークベンチの一部のコンポーネントは、より複雑です。具体的には、子プロセスをフォークするものなどです。
- 拡張機能ホスト
- 統合ターミナル
- ファイル監視
- 全文検索
- タスク実行
- デバッグ
VS Code はリモートシナリオで実行できるため、検索、デバッグ、タスク実行など、一部のタスクをリモートで実行するメカニズムがすでに整っていました。これらのコンポーネントは、コードが存在する場所にローカルで自然に実行される拡張機能ホストプロセス内で動作できます。そのため、VS Code がリモート接続なしでローカルで実行されている場合でも、これらの子プロセスの所有権をレンダラープロセスから拡張機能ホストに移動することができました。
拡張機能ホストについては、より野心的な計画がありました。これらの変更については、後で専用の セクション で説明します。Electron に新しい「ユーティリティプロセス」API を追加する必要があったためです。
統合ターミナルとファイル監視は、共有プロセスの子プロセスになるように移動しました。ファイル監視または統合ターミナルを必要とするウィンドウは、メッセージポートを介して共有プロセスと通信して、これらのサービスを取得します。
以下の図は、2022 年後半のレンダラープロセスでサンドボックスを有効にした後のプロセスアーキテクチャを示しています。すべての Node.js プロセスは、共有プロセスの子プロセスまたはメインプロセスのユーティリティプロセスのいずれかになるように移動しました。メッセージポートは、メインプロセスに負担をかけることなく、効率的なプロセス間直接通信に使用されます。
Chromium のコードキャッシュの調整
また、サンドボックスを有効にしてもパフォーマンスの低下が発生しないようにしたいと考えました。エディターで点滅するカーソルが表示されるまでの起動時間を 測定 しました。重要な時間は、V8 JavaScript エンジンでメインワークベンチスクリプト (約 11.5 MB の圧縮コード) をロード、解析、および実行するために費やされます。更新プログラムがインストールされていない限り、起動ごとに同じスクリプトがロードされます。この動作を考慮すると、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 を提供できなかったため、Electron に新しい ユーティリティプロセス API をコントリビューションしました。この API により、拡張機能ホストをレンダラープロセスからメインプロセスから作成されるユーティリティプロセスに移動することができました。メッセージポートを使用すると、ユーザー入力を処理するメインプロセスなど、他のプロセスに影響を与えることなく、レンダラーと拡張機能ホスト間で直接通信できます。
Electron webview 要素からの移行
サンドボックスを有効にするために必ずしも必要ではありませんでしたが、VS Code での Electron webview タグ の使用を再検討し、VS Code が Web でどのように動作するかにより近づけるために、iframe タグに置き換える機会を得ました。どちらのタグも、ワークベンチが拡張機能からの信頼されていないコードをホストできるようにすると同時に、このコードの実行の影響からワークベンチを分離するという点で似ています。たとえば、Markdown ファイルのプレビューを開くと、コンテンツは組み込みの Markdown 拡張機能によって提供されるこのような要素でレンダリングされます。
ほとんどの場合、webview
タグを iframe
タグに置き換えるだけで済みました。ただし、iframes
に欠落していた機能が 1 つありました。それは、コンテンツ内でテキスト検索を実行して強調表示する機能です。この機能は、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 のナイトリーバージョンをリリースしています。VS Code チームの全員が Insiders エディションを使用しており、皆様もぜひ試して 問題 を報告していただければ幸いです。
ハッピーコーディング!
Benjamin Pasero, @BenjaminPasero