Copy-on-Writeで実現するサブミリ秒VMサンドボックス
コピーオンライトのメモリフォークにより、1ミリ秒未満で起動するVMサンドボックスの仕組みと、サーバーレスやセキュリティ分離への影響を解説します。

Dockerコンテナの起動には約500ミリ秒かかります。Firecrackerのmicro VMは約125ミリ秒、V8アイソレートは約5ミリ秒です。しかし、コピーオンライトのメモリフォークを使う新しい軽量サンドボックスなら、分離された実行環境を1ミリ秒未満で立ち上げられます。多くは50〜200マイクロ秒程度です。これなら関数呼び出しのたびに新しいサンドボックスを作ることも現実的です。
これは単なる漸進的な改善ではありません。サンドボックスにできることの質が変わるのです。作成に500msかかるなら、数を絞って使い回すのが普通です。50μsで作れるなら、信頼できない入力、プラグインの呼び出し、ユーザーのリクエストごとに作れます。セキュリティモデルは「テナントを隔離する」から「個々の処理を隔離する」へと移行するのです。
Copy-on-Writeの実際
Copy-on-Write(CoW)は、データを実際にコピーすることなくメモリ領域の「コピー」を作るOSの手法です。元のメモリとコピーは同じ物理メモリページを指し、読み取り専用としてマークされます。両者は区別がつかず、どちらも同じデータが見えます。実際のコピーが発生するのは、どちらかがページへ 書き込もう としたときだけです。その時点でカーネルが書き込みを横取りし、そのページ1枚だけをコピーして、書き込みはコピー側で進められます。
Unixの fork() システムコールは1990年代からこの仕組みを使っています。プロセスをforkすると子プロセスは親のメモリの完全なコピーを得ますが、CoWのおかげで実際にはデータはコピーされません。子が(典型的な使い方のように)すぐ exec() を呼ぶと、メモリ全体が置き換えられ、CoWのページは単に解放されます。forkはほぼ無料だったのです。
// Classic fork() — copy-on-write in action
pid_t pid = fork();
// At this point:
// - Parent and child share ALL physical memory pages
// - Both have identical virtual address spaces
// - Pages are marked read-only in both
// - Zero data has been copied
if (pid == 0) {
// Child process
// Reading shared_data is free — same physical page
int value = shared_data[0];
// Writing triggers CoW — only THIS page gets copied
shared_data[0] = 42;
// Now child has its own copy of this one page
// Parent's page is unchanged
}
forkからサンドボックスへ
CoWサンドボックスの発想はこうです。VMやコンテナをゼロから起動する代わりに、ランタイム、ライブラリ、初期状態を読み込んだ「テンプレート」環境を事前に起動しておきます。そして、それをCoWでforkして即座にコピーを作ります。各コピーはテンプレートが止まった正確な状態から始まります。初期化は完了済みで実行準備ができていますが、それぞれ独立した分離メモリ空間で動きます。
性能差は桁違いです。従来のVM起動では、カーネルの読み込み、ハードウェアの初期化、ファイルシステムのマウント、initシステムの起動、アプリケーションコードの読み込み、ランタイムの初期化を行います。Firecrackerのように大幅に削ぎ落としても、まだ数百ミリ秒の初期化処理が残ります。
CoW forkはそれをすべて飛ばします。初期化処理はテンプレートがすでに済ませています。forkは初期化済みのコピーをマイクロ秒単位で作ります。「起動コスト」は、新しいアドレス空間の作成とページテーブルエントリの複製というカーネル側の処理だけです。テンプレートのメモリ量に関係なく、数千回程度の操作で済みます。
何が変わるのか
サーバーレス関数
コールドスタートはサーバーレスコンピューティングの厄介な問題です。AWS Lambdaのコールドスタートには100〜500msかかり、JVMベースのランタイムではさらに長くなります。レイテンシに敏感なワークロードではこれは許容できず、ユーザーはインスタンスを温め続けるか(サーバーレスの意味が薄れます)、予測できないレイテンシを受け入れるしかありません。
CoWサンドボックスなら、コールドスタートはミリ秒未満に収まります。コールドスタートがほぼ無料になるので、すべての呼び出しをコールドスタートにできます。ウォームプールも不要で、アイドルのインスタンスによるメモリの無駄もなく、呼び出し間で古い状態が残ることもありません。各関数の実行は、初期化コストを払うことなく、まっさらで隔離された環境を得られます。
プラグインと拡張機能の仕組み
信頼できないプラグインを安全に動かすことは、ソフトウェアで最も難しい問題の一つです。ブラウザはJavaScriptについてV8アイソレートで解決しました。しかし、コンパイル済みの拡張、スクリプト言語、バイナリプラグインといった任意のコードについては、分離の選択肢はコンテナ(リクエストごとの分離には遅すぎる)かWebAssembly(エコシステムや言語サポートが限られる)に限られてきました。
CoWサンドボックスは中間の道を提供します。ホスト環境の分離されたコピーの中で任意のコードを実行でき、セットアップと後片付けはサブミリ秒で済みます。プラグインからはファイルシステム、ネットワーク、ライブラリを含むOS環境全体が見えますが、その変更は閉じ込められます。サンドボックスが終了すると、すべての変更は消えます。CIシステム、ノートブック環境、ビルドツールでユーザー投入のコードを扱うのに理想的です。
セキュリティ分離
アップロードされたPDFの解析、ユーザー提供のHTMLのレンダリング、データベースクエリの実行など、信頼できない入力を処理するときに、その処理を分離されたサンドボックスで実行すれば、脆弱性を突かれた場合の被害範囲を限定できます。PDFパーサーにバッファオーバーフローがあっても、攻撃者が制御を得るのはまもなく破棄される使い捨てのサンドボックスであり、アプリケーションサーバーではありません。
信頼できない処理ごとにプロセス分離を行うというこの手法は、従来のサンドボックス技術ではオーバーヘッドが処理時間を上回るため、現実的ではありませんでした。PDFの解析に10msかかるのに、コンテナの作成に500msを費やすのは意味がありません。しかし、CoWサンドボックスの作成に100μsかけるのは、ほとんどコストになりません。
実装の詳細
実用的なCoWサンドボックスシステムを作るには、単に fork() を呼ぶ以外にもいくつかの問題を解決する必要があります。
- メモリ会計。 CoWはメモリ使用量を分かりにくくします。テンプレートが1GBを使い、それを100個forkしてそれぞれ10MBを変更する場合、物理メモリ使用量は100GBではなく約2GB(共有の1GB + 100 × 10MBの固有領域)です。カーネルは共有ページと専有ページを追跡していますが、サンドボックスごとの正確な使用量を得るには
/proc/[pid]/smapsを解析する必要があります。 - ファイルシステムの分離。 CoWはメモリを扱いますが、ファイルシステムへの書き込みには別途分離が必要です。オーバーレイファイルシステム(overlayfs)は、ファイルに対してCoWの振る舞いを提供します。サンドボックスからはテンプレートのファイルシステムが見えますが、書き込みは別のレイヤーに行われます。サンドボックスの終了時にそのオーバーレイは破棄されます。
- ネットワークの分離。 各サンドボックスは干渉を防ぐために独自のネットワーク名前空間を必要とします。Linuxの名前空間がこれを提供しますが、ネットワーク名前空間の作成には計測可能なオーバーヘッドがあります。事前に作成した名前空間のプールを再利用するシステムもあります。
- リソース制限。 無制限にメモリを確保したり、CPUを無制限に消費したりするサンドボックスは、サービス拒否攻撃の経路になります。cgroupsはメモリ、CPU、I/Oのリソース制限を提供しますが、cgroupの作成と破棄にはオーバーヘッドがあります。ここでもプーリングが役立ちます。
- 確実なクリーンアップ。 サンドボックスが終了するとき、メモリ、ファイルディスクリプタ、ネットワーク接続、IPCオブジェクトなどのすべてのリソースを確実に解放しなければなりません。PID名前空間が役立ちます。名前空間のinitプロセスを終了させれば、その子孫はすべて終了します。
CoWとWebAssemblyサンドボックスの比較
WebAssembly(Wasm)はもう一つの主要な軽量サンドボックス技術です。両者は根本的に異なるトレードオフを取るため、比較する価値があります。
Wasmサンドボックスは、線形メモリモデルを持つメモリ安全な仮想マシンでコードを実行します。線形メモリの外にあるものには一切アクセスできません。ファイルシステムもネットワークもシステムコールもありません(WASIで明示的に提供された場合を除く)。非常に安全ですが制約も大きく、既存のコードをWasmに再コンパイルする必要があり、すべての言語がWasmに効率よくコンパイルできるわけではありません。
CoWサンドボックスは、分離されたOS環境でネイティブコードを実行します。サンドボックスはフルのOSインターフェースにアクセスでき(seccompフィルタで制限される場合もあります)、任意のバイナリを実行でき、通常のシステムライブラリを使えます。制約は緩いものの、セキュリティ面では劣ります。分離の境界はOSのプロセスモデルであり、これはWasmの最小限のVMに比べて攻撃対象領域が広いためです。
Wasmを選ぶべきとき:サンドボックス化するコードを自分で管理しており、ワークロードがきれいにWasmへコンパイルでき、可能な限り強力な分離が必要なとき。CoWサンドボックスを選ぶべきとき:既存の任意のバイナリを実行する必要があり、ワークロードがOSレベルの機能(ファイルシステム、ネットワーク、子プロセス)を必要とし、最小限の攻撃対象領域より互換性を優先したいとき。
落とし穴:マルチスレッドプログラムでのfork
fork() には有名な落とし穴があります。呼び出したスレッドしかコピーしないのです。親に20個のスレッドがあれば、子に残るのは1つだけです。他の19個のスレッドが保持していたミューテックスは、子のメモリ上では依然としてロックされたままですが、それを保持していたスレッドはもう存在しません。子は、そのミューテックスを取得しようとした最初の時点でデッドロックします。
CoWサンドボックスシステムは、forkする瞬間にテンプレートプロセスがシングルスレッドであるようにしてこの問題を回避します。典型的には、テンプレートですべてを初期化し(ライブラリの読み込み、ランタイムのセットアップ、初期状態の準備)、メインスレッド以外のすべてのスレッドを停止してforkします。その後、各子プロセスが必要に応じてスレッドを生成し直します。初期化コストは一度だけ払われ、forkはスレッドの危険性を回避します。
新しいアプローチの中には、userfaultfd やカスタムのページフォルトハンドラを使い、fork() にまったく頼らずにCoWに似た振る舞いを実現するものもあります。これらはマルチスレッドの問題を避けられますが、複雑さが増し、カーネルレベルでのより多くの調整が必要です。
注目すべき点
サブミリ秒のサンドボックスはまだ初期段階ですが、その基盤技術は堅実です(fork、名前空間、cgroups、overlayfsはいずれも成熟しています)。その上に構築されているシステム(サーバーレスコンピューティング、CI/CD、安全なコード実行向け)は、処理単位ごとの分離が大規模でも実用的であることを示しています。これらのツールが成熟するにつれ、「サンドボックスは高くつく」という前提は、「ガベージコレクションはリアルタイムアプリケーションには遅すぎる」という前提と同じく時代遅れになるでしょう。オーバーヘッドは消えつつあり、「すべてをサンドボックスに入れる」ことのセキュリティ上の利点は、無視しにくくなっています。


