[rails/bootsnap] opendir失敗時のerrno=0クラッシュを根本から修正
背景
bootsnapのC拡張において、opendirシステムコールが失敗した際にerrnoが0のままとなり、Rubyのsyserr_failが想定外のクラッシュを引き起こす問題が報告されていました。以前のバージョンでは、この問題に対してerrno == 0の場合にEINVALへ置き換える応急処置が施されていましたが、根本原因が特定されたため、より適切な修正が実施されました。
問題の根本原因
問題は、rb_ary_new*が引き起こすガベージコレクション(GC)にありました。具体的には以下の流れで発生していました:
-
opendirの前にrb_ary_new()を3回呼び出してRubyの配列を作成 - これらの配列作成がGCをトリガー
- GC中にファイルファイナライザーが実行される可能性がある
- ファイナライザーの処理が
errnoをリセット(0にする) - その後に実行される
opendirが失敗しても、errnoは既に0になっている -
errno == 0でsyserr_failを呼ぶとクラッシュ
修正内容
修正は極めてシンプルです。配列の初期化をopendirの後に移動することで、opendirの実行時点でのerrnoが他の処理によって上書きされないことを保証します。
変更前:
DIR *dirp = opendir(RSTRING_PTR(abspath));
VALUE dirs = rb_ary_new();
VALUE requirables = rb_ary_new();
VALUE result = rb_ary_new_from_args(2, requirables, dirs);
if (dirp == NULL) {
if (errno == ENOTDIR || errno == ENOENT) {
return result;
}
// 応急処置: errno == 0 を EINVAL に置き換え
if (errno == 0) {
errno = EINVAL;
}
bs_syserr_fail_path("opendir", errno, abspath);
return Qundef;
}
変更後:
VALUE dirs = rb_ary_new();
VALUE requirables = rb_ary_new();
VALUE result = rb_ary_new_from_args(2, requirables, dirs);
DIR *dirp = opendir(RSTRING_PTR(abspath));
if (dirp == NULL) {
if (errno == ENOTDIR || errno == ENOENT) {
return result;
}
bs_syserr_fail_path("opendir", errno, abspath);
return Qundef;
}
この変更により、opendirが失敗した場合、その直後にerrnoをチェックするため、他の処理によるerrnoの上書きが発生しません。したがって、errno == 0のチェックとEINVALへの置き換えは不要になります。
不要になったコード
根本原因が解決されたことで、以下のコードも削除されました。
C拡張側:
if (errno == 0) {
errno = EINVAL;
}
Ruby側のフォールバック処理:
rescue SystemCallError => error
if ENV["BOOTSNAP_DEBUG"]
raise
else
Bootsnap.logger&.call("Unexpected error: #{error.class}: #{error.message}")
ruby_call(root_path)
end
end
このrescue節は、C拡張で予期しないエラーが発生した際にRuby実装へフォールバックするためのものでしたが、今回の修正により不要となりました。
また、デバッグ用の環境変数設定も削除されています:
ENV["BOOTSNAP_DEBUG"] = "1"
技術的な意義
この修正は、C拡張とRubyのメモリ管理の相互作用を正しく理解することの重要性を示しています。Rubyの配列作成のような一見無害な操作でも、GCをトリガーし、それが他の処理(ファイナライザーなど)を実行する可能性があります。システムコールのerrnoのような、外部状態に依存する処理を行う場合は、その状態が他の処理によって変更される可能性を常に考慮する必要があります。
今回の修正により、応急処置的なエラーハンドリングコードを削除し、より信頼性の高いコードベースを実現できました。