[rails/bootsnap] opendir失敗時のerrno=0クラッシュを根本から修正

rails/bootsnap

背景

bootsnapのC拡張において、opendirシステムコールが失敗した際にerrnoが0のままとなり、Rubyのsyserr_failが想定外のクラッシュを引き起こす問題が報告されていました。以前のバージョンでは、この問題に対してerrno == 0の場合にEINVALへ置き換える応急処置が施されていましたが、根本原因が特定されたため、より適切な修正が実施されました。

問題の根本原因

問題は、rb_ary_new*が引き起こすガベージコレクション(GC)にありました。具体的には以下の流れで発生していました:

  1. opendirの前にrb_ary_new()を3回呼び出してRubyの配列を作成
  2. これらの配列作成がGCをトリガー
  3. GC中にファイルファイナライザーが実行される可能性がある
  4. ファイナライザーの処理がerrnoをリセット(0にする)
  5. その後に実行されるopendirが失敗しても、errnoは既に0になっている
  6. errno == 0syserr_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のような、外部状態に依存する処理を行う場合は、その状態が他の処理によって変更される可能性を常に考慮する必要があります。

今回の修正により、応急処置的なエラーハンドリングコードを削除し、より信頼性の高いコードベースを実現できました。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

ガイドライン準拠 ✓ PASS

記事構成とDiffDaily Styleへの準拠状況

記事構成、カスタムMarkdown構文、対象読者への適合性のすべての点でガイドラインを完全に満たしています。特に、問題の根本原因から修正内容までを論理的に説明する構成が優れています。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ✓ PASS

技術的な正確性と表現の適切性

PRのDiff内容を正確に反映しており、技術的な説明に誤りはありません。C拡張、GC、errnoが絡む複雑な問題を、根本原因と解決策を対比させることで非常に分かりやすく解説しています。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ✓ PASS

元のPR情報との一致度

PRのTitle, Description, Diffから得られる情報に完全に忠実で、ハルシネーションは一切見られません。事実に基づいた信頼性の高い記事です。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除