設計の判断
paludarium を作る中で決めた、大きな判断とその理由です。
既存の部品の使い方
外部の crate や道具は、基本的に使ってよいことにしています。ただし、ライセンスが Apache-2.0 と両立するかを先に確かめます。著作権やライセンスに関わるもの、不具合や 合わないところがあるものは、その部分を自作します。依存は crates.io からだけ取り、 cargo deny で確かめています。
ほかのエミュレータのコードは写しません。blink などの動作と設計は参考にしますが、 実装は一から書いています。履歴にマルウェアが見つかった blink の派生(lanmower/blink とその系列)は、参照も依存もしません。
デコーダは既存の crate を包む
最初は「デコーダも自作する」と決めていましたが、外部の部品の方針を見直したときに、 iced-x86(MIT)を使うことにしました。 paludarium-decoder で包み、ほかの crate には自前の命令の型だけを見せています。 合わないところが見つかったら、デコーダの中だけで差し替えたり一部を自作したりできます。
ソフトウェア MMU
WebAssembly(wasm32)のメモリは 4 GB までなので、ゲストの 64-bit のアドレス空間を そのまま置けません。そこで、4 KiB のページの 2 段のページテーブルを持つソフトウェア MMU を すべてのホストで使っています。アクセスはすべて検査し、ページがないときや権限が違うときは ゲストの例外にします。
待機の仕組みを自分で持つ
futex・epoll・eventfd・timerfd は、ホストの同じ機能を使わず、paludarium の Kernel の中で 実装しています。macOS と Windows には Linux の futex がなく、WebAssembly で待つ手段は Atomics.wait と notify しかないからです。ホストからは「待つ・起こす・期限つきで眠る」 という最小の道具だけを使います。代わりに、待ち行列の競合に気をつけて作っています。
CPU と Kernel を分ける
CPU は、システムコールや例外に当たると、止まった理由を返して止まるだけで、Kernel を 知りません。スレッドの状態は Kernel が持ちます。こうすると、CPU と Kernel をそれぞれ 単独でテストでき、JIT とインタプリタを入れ替えやすくなります。
Kernel とファイルシステムを分ける
「ゲストが指定したディレクトリの外に出られない」という保証を、paludarium-vfs だけで 確かめられるように、ファイルシステムを Kernel から分けています。ファジングの対象も 絞れます。ホストのディレクトリは、Session を作るときに開いたディレクトリの capability からの相対でだけ操作します(ネイティブでは cap-std を使います)。
WebAssembly のスレッド
いちばん大きな不確実性の 1 つだったので、最初の段階のうちに小さく試しました。 共有のメモリを持つ 2 つの Worker で memory.atomic.wait32 と notify が、Node.js・ Chromium・Firefox・本物の Safari で動くことを確かめてから、作り始めています。
- Worker を作るのは起動用の JavaScript です。WebAssembly から直接 Worker を作れず、 主スレッドでは待てず、止めるときにまとめて止められるからです。
- エミュレータ全体(Rust のヒープと std)を Worker の間で共有するために、std も atomics 付きで作り直し、Worker ごとにスタックと TLS を分けています。
JIT を独立した部品にする
JIT は、インタプリタとは別の部品にしています。設定で JIT の有無を切り替えられ、 ありとなしを同じテストで比べられます。命令の意味を CPU と JIT の 2 か所に持つことになるので、 差分テストでそろえています。
JIT の変換は Rust が行い、WebAssembly のバイト列を作ります。それをゲストの Worker の中の JavaScript がコンパイルして実行します。生成したコードは、MMU の権限と範囲の検査を保ち、 書き換えられたページの変換結果は捨てます。
ファイルシステムを作る方法は 1 つ
snapshot は、別の復元用の呼び出しではなく、createFileSystem の entries として 渡します。そうすると files と組み合わせられ、run() も同じように受け付けます。 aube のように更新時刻で状態の新しさを判断するプログラムがあるので、更新時刻は 引き継ぎます。inode の番号は引き継がず、どのパスが同じ inode かだけを保ちます。 Linux と同じく、ハードリンクはシンボリックリンクも指せるので、ゲストが作れる状態は すべて作り直せます。ディレクトリへのハードリンクは断ります。
3 つの入口
ライブラリ、コマンド、WebAssembly の 3 つの使い方を用意しました。使う人ごとに合う入口があり、 差分テストではコマンドを使えます。代わりに、互換を保つ対象が増えます。
CPU ごとの違いを明示する
REP の命令が途中でフォールトしたときのフラグは、Intel と AMD で違いました。 paludarium は、ホストの CPU を見て動きを変えることはせず、モデルを明示して扱います。 既定は Intel の設計どおりです。
ブラウザのネットワークは中継を通す
ブラウザは TCP の接続を張れず、名前も引けないので、ブラウザのゲストは WebSocket の 中継を通してネットワークを使います。接続 1 本ごと、名前を引くこと 1 回ごとに WebSocket を 1 本使い、多重化はしません。そのためどちらの側も、その間で状態を持たず、 流れの制御も接続ごとです。名前は中継が引き、渡すアドレスごとにチケットで署名します。 ゲストはそのあとアドレスに接続するので、中継(別の台かもしれません)は、それが どの許された名前から来たかを知る必要があるからです。最初に決めるのは今までどおり カーネルの許可リストです。ページにあるものは誰にでも見えるので、中継も自分の リストを持ちます。
ゲスト自身のループバック
ゲストは自分の TCP と UDP のサーバーを動かせますが、相手は自身のソケットだけです。 ゲストのソケットが、ゲストのソケットの待ち受けているループバックのアドレスに接続すると、 カーネルが中で、ホストのものと同じメモリの上のストリーム 2 本をつなぎます。 データグラムは、受け取るソケットにそのまま届きます。どちらもホストには届きません。ホストやほかのマシンにサービスすると、ゲストが利用者のマシンにポートを 開けることになり、それは許可リストでは決められないので、扱いません。何もゲストの外に 出ないので、ゲスト自身のループバックも許可リストでは決めません。ゲストのソケットが 待ち受けていないループバックのポートは、Linux のループバックと同じく拒みます。ただし、 リストがホストのものを許していれば、ホストに届きます。
公開は Trusted Publishing だけ
crates.io と npm への公開は、長く有効なトークンを保存しない Trusted Publishing(OIDC) だけで行います。npm のパッケージには provenance を付けます。公開済みの版は消せないので、 問題があれば crates.io の yank と npm の deprecate で対応し、修正版を出します。