テストと検証
paludarium の正しさは、「本物の x86-64 Linux と同じ結果になるか」で決めます。
差分テスト
同じゲストのプログラムを、本物の x86-64 Linux と paludarium の両方で動かし、結果を比べます。 期待する結果はリポジトリに保存せず、CI を動かすたびに、x86-64 Linux のランナーで 新しく作ります。
- 命令の単位:レジスタ、フラグ、メモリを比べます。
- プログラムの単位:標準出力、標準エラー、終了状態をバイト単位で比べます。
- x86 で結果が決まっていないフラグや値(論理演算の後の AF、
divの後のフラグなど)は、 命令ごとの表に従って比べません。 - pid、時刻、乱数、アドレスのように実行ごとに変わる値は、比べる前にそろえるか、 比べる対象から外します。
- 新しい命令やシステムコールは、実装の前にテストのケースを用意して、本物の Linux での 結果を先に決めてから作ります。
- 各ケースには 30 秒の時間切れがあります。
- コアダンプのケースでは、Linux がカーネルの既定の
core_pattern(coreで、core_uses_pidは 0)のままである必要があります。CI ではsysctlで設定します。 それ以外の環境では、クラッシュの処理系に送られたコアと比べず、そう伝えて止まります。 - native の側は、本物の x86-64 Linux でなければなりません。Arm の Mac の Docker は、 x86-64 のプログラムを QEMU のユーザーモードのエミュレーションで動かします。QEMU は シグナルを自分で扱うので、そこでは native の結果も Linux と違うことがあります。 たとえば、
SA_NOCLDSTOPを付けた親にも、止めて再開した子の SIGCHLD が届きます。 Linux なら届きません。そのような実行は、どちらの向きにも証拠になりません。 tests/guests/processes/cases.jsonに載っているプロセスのケースと、diff_processesが動かすケースがちょうど同じかをテストで確かめます。載せたのに動かないケースは 作れません。- ネットワークのケース(
diff_network)では、テスト自身が 127.0.0.1 でゲストの相手を します。そのため、native の Linux とエミュレータは同じサーバーと話します。 エミュレータに許すのは、そのサーバーのポートと、何も待っていないポートだけです。listenとudpのケースはゲスト自身のループバックで待ち受け、Linux はネットワークの スタックで、エミュレータはカーネルの中で、それぞれ動かします。 Linux での名前解決は、実行するマシンのファイルとサーバーによって変わるので、 エミュレータの上でだけ、localhostを相手に確かめます。UDP は musl のリゾルバを 通して、TCP は自前の問い合わせで確かめます。 - WebAssembly のパッケージのネットワークは、Node.js(
wasm-network.test.mjs)と 中継 を通したもの(wasm-relay.test.mjs)で確かめます。どちらも、 単独で動くゲストが、テストが 127.0.0.1 で動かすサーバーに届き、localhostを 引きます。中継のテストは、中継の許可リストとチケット、両方向の流れの制御 (すべて書いてからでないと読まない相手も含む)、カーネルの規則と同じであることを 確かめます。CI ではwasm-browser-network.mjsが、Chromium、Firefox、WebKit、Safari で ゲストに中継を通させます。
ファジング
decode、ELF の読み込み、システムコールの引数、ファイルシステムのパス、MMU の操作、CPU、 スレッド、futex、イベント、ソケット、JIT の 13 の対象を、AddressSanitizer と LeakSanitizer を付けて、それぞれ 600 秒動かします。夜間の CI で毎日動かしています。
決まった時間のファジングは、どんな入力でも安全だという証明ではありません。それでも、 ゲストからのどんな入力でも panic しないことを、継続して確かめる手段にしています。
カバレッジと静的な検査
- 行カバレッジの下限は 80% です。公開の前の測定では、workspace 全体で約 94% でした。
cargo fmt --check、clippy(警告はエラー)、cargo deny(ライセンス、取得元、 既知の脆弱性)を、プルリクエストごとに動かします。- GitHub Actions の action は、すべてコミットの SHA で固定しています。
CI
| いつ | 何を |
|---|---|
| プルリクエストごと | Linux・macOS・Windows の単体テスト、lint、依存の検査、WebAssembly のビルド、x86-64 Linux での差分テスト、カバレッジ、probe・aube・ripgrep の差分テスト、4 つの WebAssembly の環境と x86-64 Linux の WebKit での受け入れのテスト、ファジングの一部 |
| 毎晩 | 13 の対象のファジング、macOS と Windows での差分テスト、4 つの WebAssembly の環境と x86-64 Linux の WebKit での JIT ありとなし、速さの測定 |
| 毎日 | 定期実行のワークフローが時間どおりに始まったかを確かめます。始まらなかった、動かなかった、無効になっていたときは Issue を立てます |
| 毎週 | 固定した nightly の Rust と、aube と ripgrep のゲストを最新のリリースへ上げます。それぞれのプルリクエストは、検査が通ると自分でマージされます |
| 公開の前 | 公開する同じコードで、上のすべて(下を参照) |
ゲストのプログラムは、ビルドの前に、依存がルートの方針の cargo deny を通らなければ なりません。例外があるのは aube だけです。2 つのクレートのライセンスと、今のところ zstd 0.13.3 と zstd-safe 7.3.0 への 3 つの RustSec のアドバイザリ(RUSTSEC-2026-0391、 RUSTSEC-2026-0398、RUSTSEC-2026-0403)です。aube はこの版を、zstd を版まで固定している decmpfs から受け取るので、自分では上げられません。アドバイザリの例外は、decmpfs が zstd 0.14 にしたリリースを出したら外します (SocketDev/decmpfs#16)。どのクレートにも 当たらなくなった例外は監査を失敗させるので、例外が要らなくなった aube の更新は、例外を 外すまでマージされません。例外はすべて tests/guests/programs/evidence.py に並んでいます。
公開の前の受け入れの確かめ
新しい版を公開する前に、公開するのとまったく同じコードで、次のことがそろっていることを 確かめます。そろわなければ公開しません(リリースの流れ)。
- probe の 8 項目が、Linux・macOS・Windows と、Node.js・Chromium・Firefox・Safari の すべてで通る。
- aube の 4 つの操作が、すべての環境で、本物の x86-64 Linux と同じ標準出力と終了コードになる。 4 つの WebAssembly の環境では、操作のたびにファイルシステムを snapshot から作り直しても 一致し、作り直したファイルシステムの snapshot は元と同じになる。
- 4 つの WebAssembly の環境で、JIT ありとなしの両方の結果が一致する。
- JIT ありのほうが、統計的に意味のある差で速い(下を参照)。
JIT とインタプリタ
JIT は、インタプリタと同じ意味でなければなりません。JIT だけの意味論は持たず、同じ差分テストを JIT ありとなしの両方で動かして比べます。JIT が変換できない命令はインタプリタで動き、 その実行は「JIT の成功」に数えません。4 つの WebAssembly の環境では、probe、aube、 ripgrep の検索をすべて両方で動かし、JIT ありの実行では、変換したコードが実際に動いたことも 確かめます。
速さの測り方
Node.js の Worker で、probe 全体を JIT ありとなしで 10 回ずつ、決まった順番で交互に動かします。 毎回新しい Session を使い、JIT の変換の時間も含めます。1024 通りの符号の入れ替えによる 片側の正確な検定で p < 0.05 であり、平均で JIT ありのほうが速いことを求めます。 外れ値を外したり、合格するまで測り直したりはしません。
公開の前の測定(Node.js 26.12.0)では、平均で JIT なしが 5.75 秒、JIT ありが 4.55 秒 (約 21% 速い)、p = 1/1024 でした。ほかの CPU やブラウザでも速くなるとは主張していません。