概要
Rust WorkersはRustをWebAssemblyにコンパイルしてCloudflare Workersプラットフォーム上で動作しますが、Wasmには従来鋭いエッジがありました。panicや予期しないabortが発生するとランタイムが未定義状態に陥ることがあり、これまでRust Workersではpanicが致命的でインスタンスを汚染(poison)し、場合によっては一定期間Workerをブリックしてしまうことがありました。
本記事では、最新のRust Workersがどのようにして包括的なWasmエラー回復(abortによるサンドボックス汚染を解決するもの)を実現したかを説明します。この作業はwasm-bindgenプロジェクトへ還元され、下記のような進化をもたらしました。
panic=unwind サポート: 単一の失敗したリクエストが他のリクエストを汚染しないようにする
- abort回復機構: Rustコードがabort後に再実行されることを保証しない(再実行を防ぐ)
初期の回復緩和策
まずはプロダクションのRust Workersでpanicやabortによる故障がどう発生し、広がるかを理解して封じ込めることに注力しました。行った対策の概要は次の通りです。
- カスタムのRust panicハンドラを導入し、Worker内で失敗状態を追跡して、以降のリクエストを処理する前にアプリケーション全体を再初期化する
- JavaScript側ではRust⇄JS呼び出し境界をProxyベースの間接化でラップして、すべてのエントリポイントが一貫してカプセル化されるようにする
- 生成されたバインディングに対して、障害後にWebAssemblyモジュールを正しく再初期化するための修正を加える
このアプローチはカスタムJSロジックに依存していましたが、信頼性のある回復が可能であることを示し、実際の持続的な失敗モードを排除しました。この解決策はworkers-rsユーザー向けにデフォルトでバージョン0.6から出荷され、後述する上流向けの一般的なabort回復機構の基盤となりました。
WebAssembly Exception Handlingを使ったpanic=unwindの実装
上記のabort回復機構は失敗後にアプリケーション全体を再初期化することで問題を避けます。これはステートレスなリクエストハンドラでは許容できますが、Durable Objectsのようにメモリ内に意味のある状態を保持するワークロードでは、再初期化するとその状態を完全に失ってしまいます。ネイティブなRust環境の多くではpanicはunwind可能で、デストラクタを走らせて状態を保ったまま回復できますが、Wasmではかつて状況が異なっていました。
wasm32-unknown-unknown にコンパイルされたRustはデフォルトで panic=abort であり、Rust Worker内のpanicは突然のtrapになって unreachable 命令で止まり、WebAssembly.RuntimeError としてJSに戻りました。インスタンス状態を破棄せずにpanicから回復するには、wasm-bindgenでwasm32-unknown-unknown向けのpanic=unwindサポートが必要で、これはWebAssembly Exception Handling提案(2023年以降に広くエンジンでサポート)によって可能になりました。
まずは以下のように標準ライブラリをunwind対応で再ビルドしてコンパイルします:
RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-std
これにより、標準ライブラリがunwind対応で再構築され、panicのアンワインド用コードが生成されます。例として以下のようなRustコード:
struct HasDropA;
struct HasDropB;
extern "C" { fn imported_func(); }
fn some_func() {
let a = HasDropA;
let b = HasDropB;
imported_func();
}
はWebAssemblyで次のようにコンパイルされます:
try call <imported_func>
catch_all
call <drop_b>
call <drop_a>
rethrow
end
call <drop_b>
call <drop_a>
これによりimported_func()がpanicしてもデストラクタが実行されます。類似して、std::panic::catch_unwind(|| some_func()) は次のように変換されます:
try call <some_func> ;; set result to Ok(return value)
catch
try call <std::panicking::catch_unwind::cleanup> ;; set result to Err(panic payload)
catch_all
call <core::panicking::cannot_unwind>
unreachable
end
end
これをエンドツーエンドで動作させるには、wasm-bindgenツールチェインにいくつかの変更が必要でした。
- Walrus(Wasmパーサ)が
try/catch命令を扱えなかったため、これをサポートする追加を行った
- ディスクリプタ実行系(descriptor interpreter)も例外処理ブロックを含むコードの評価ができるようにした
- wasm-bindgenが生成するエクスポートを修正して、Rust⇄JS境界でpanicをキャッチし、JavaScriptの
PanicError例外として表現するようにした
ひとつの細かい点として、Rustは外部例外をキャッチし、extern "C"関数を通してのアンワインドの際にabortするため、エクスポートはアンワインドを許可するために extern "C-unwind" としてマークする必要がありました。非同期(futures)ではpanicはJavaScriptのPromiseをPanicErrorでrejectします。クロージャはアンワインド安全性のチェックが必要で、新しいMaybeUnwindSafeトレイトによりpanic=unwindでビルドされた場合にのみUnwindSafeのチェックを行うようにしました。
ただし、多くのクロージャはアンワインド後も残る参照をキャプチャしており、アンワインド安全ではないため問題が明らかになりました。ユーザーがコンパイラを満たすためだけに誤ってAssertUnwindSafeでラップするような状況を避けるため、新たにClosure::new_abortingのようなバリアントを導入し、アンワインド安全が保証できない場合はpanicでアンワインドせずプロセス的に終了(abort相当)する挙動を提供しました。
panic=unwindを有効にした結果:
- エクスポートされたRust関数内のpanicはwasm-bindgenによってキャッチされる
- panicはJavaScript側では
PanicError例外として表出する
- 非同期エクスポートは
PanicErrorで返却されるPromiseをrejectする
- Rustのデストラクタが正しく実行される
- WebAssemblyインスタンスは有効なまま再利用可能である
このアプローチおよびwasm-bindgenでの使い方の詳細は、wasm-bindgenのガイド「Catching Panics」で解説されています。
abort回復
panic=unwindサポートがあっても、out-of-memoryなどによるabortは依然として発生します。abortはunwindしないため状態の回復は不可能ですが、少なくともabortを検出して今後の操作のために回復処理(例えばインスタンスの再初期化)を行うことで、無効な状態が後続のリクエストを壊すのを防げます。
panicのアンワインド対応を導入したことで、新たな問題も生じました。Wasmからエラーを受け取ったときに、それがextern "C-unwind"由来の外部例外によるものか、真のabortによるものかを判別できないケースが生じたのです。Wasmにおけるabortは様々な形を取りえます。技術的な解決策としては「確実にabortであるエラーにマークを付ける」か「確実にunwindであるエラーにマークを付ける」かの二択がありましたが、後者を採用しました。
我々の外部例外処理は既にWATレベルの例外処理命令(Exception Handling)を直接使っていたため、外部例外を区別するための例外タグ(Exception.Tag)を実装する方が容易でした。これにより、回復可能なエラー(unwind)と回復不可能なエラー(abort等)を明確に区別できるようになりました。
区別が可能になったことで、次の仕組みを統合しました:
- 初期化時に設定できる新しいabortフック
set_on_abort を導入し、ランタイム埋め込み側の要件に応じて回復処理を登録できるようにした
- abort再入(reentrancy)ガードを導入して、深く入り組んだ呼び出しスタックや複数タスクの同インスタンス利用において、あるタスクのabortがJS経由で高位のスタックを未定義な形で汚染してしまうことを防いだ
Wasmは深く入り組んだコールスタックやJSとWasmの再入を許すため、ある深いネストでabortが発生しても高位のスタックが無効化されることを確実に保証するためには細心の注意が必要です。abortは理想的ではありませんが、失敗時に再初期化を最後の防衛線として実装することで、実行の整合性を保ち、今後の操作が成功できるようにします。無効状態が持続せず、単一の失敗が複数の失敗に連鎖することを防ぎます。
拡張: wasm-bindgenライブラリ向けのabort再初期化
この作業を進める中で、wasm-bindgenでビルドされたJSから使われるライブラリでも同様の問題を抱えることに気づきました。ESモジュールとしてWasmをビルドして直接インポート(例: import { func } from 'wasm-dep')するケースだと、ライブラリが既にリンク・初期化済みの状態でfunc()呼び出し中にWasmがabortしたときに、アプリケーション側でどのように回復すべきかが明確ではありません。
我々のチームはRust-backed Wasmライブラリを使うJSベースのWorkersユーザーもサポートしており、この問題を同時に解決できればCloudflare Workers上のWasm利用にも間接的に利益があると考えました。
そのため、wasm-bindgenに実験的な再初期化機構 --reset-state-function を追加しました。これにより、Rustアプリケーションは内部のWasmインスタンスを次回の呼び出し時に初期状態に戻すことを要求でき、生成されたバインディングを再インポートしたり再作成したりすることなく回復できるようになります。
- 古いインスタンスから生成されたクラスのインスタンスはハンドルが孤立するため例外を投げるようになる
- その後、新しいインスタンスからクラスを再構築できる
- JSアプリケーションはエラーになるがブリックされない
この機能の技術的詳細とwasm-bindgenでの使い方は、新しいガイドセクション「Wasm Bindgen: Handling Aborts」で説明されています。
RustのWasm Exception Handlingエコシステムの成熟
この作業の上流への貢献はwasm-bindgenだけに留まりませんでした。panic=unwindでWasm向けにビルドするにはまだ実験的なnightly Rustターゲットが必要であり、WebAssembly Exception Handlingのサポートを安定したRustへ広げるための作業も進めています。
開発中に仕様の後期段階で変更が入り、レガシーな例外処理と最終的なモダンな例外処理("with exnref")の2種類が生じました。現時点でRustのWasmターゲットはデフォルトでレガシーなバリアントのコードを出力しています。レガシーは広くサポートされていますが非推奨になっており、モダンなWebAssembly Exception Handlingへの移行が望まれます。
以下のJSプラットフォームのリリースでモダンなException Handlingがサポートされています:
- v8
13.8.1 — April 28, 2025
- workerd
v1.20250620.0 — June 19, 2025
- Chrome
138 — June 28, 2025
- Firefox
131 — October 1, 2024
- Safari
18.4 — March 31, 2025
- Node.js
25.0.0 — October 15, 2025
我々がサポートマトリクスを調査したところ、最大の懸念はNode.js 24のLTSリリーススケジュールで、これだとエコシステム全体が2028年4月までレガシーException Handlingに縛られてしまう恐れがありました。この不整合を発見したことで、Node.js 24向けにモダンなException Handlingをバックポートし、さらにNode.js 22系でも動作するように必要な修正をバックポートしました。これにより、モダンなException Handling提案をデフォルトに移行できる道筋が開けます。
まとめ
- wasm-bindgenと関連ツールチェインへの変更により、Rust Workersでのpanicとabortに対する回復力が大きく向上しました。
panic=unwindにより、単一リクエストのpanicがインスタンス全体を汚染することを防ぎ、デストラクタが正しく動作します。
- abortは回復不能ですが、abortを検出してインスタンスを再初期化することで、失敗が連鎖するのを防げます。
- wasm-bindgenに導入した
--reset-state-functionなどの機構は、直接インポートされるWasmライブラリにも恩恵を与えます。
- エコシステム側でもモダンなException Handlingのサポートを広げるための作業が進められており、プラットフォーム側のバックポート対応により移行が現実的になっています。
詳細な使い方や実装の細部については、wasm-bindgenのガイド「Catching Panics」および「Wasm Bindgen: Handling Aborts」を参照してください。