コンテンツにスキップ

バグ報告ドラフト — `prepare_callable_method_entry`での`Ruby::Box` SIGSEGV

bugs.ruby-lang.org向けのドラフト(How To Reportに従う)。ADR-39スライス5(プラグインのターゲットライブラリ分離のためにRigorのアナライザーをRUBY_BOX=1のもとで実行する)のプロトタイピング中に表面化した。


カテゴリ: core 対象バージョン: master / 4.0 ruby -v: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25]

実験的なRuby::Boxを有効化(RUBY_BOX=1)して大きなプログラムを実行すると、VMのメソッドルックアップパス(rb_vm_search_method_slowpathcallable_method_entry_or_negativeprepare_callable_method_entry)の内部でSIGSEGV0x0でのnullポインタ参照)でクラッシュする。同一のプログラムをRUBY_BOX=1なしで実行すると正常に完了する。つまりRuby::Boxの有効化は、十分に複雑なワークロードでNULLを参照しうる形でメソッドエントリの解決を変える。

自己完結した純粋Rubyのリプロデューサーはまだ切り出せていない(「メモ」を参照);最小の信頼できる再現は、rigor静的アナライザーの単一ファイル実行である(これは入力をパースして静的に解析するだけで、決して実行しない):

  1. ruby 4.0.5、arm64-darwin(macOS)。
  2. 十分に複雑なRubyファイルを1つ(ここではRedmineのapp/models/issue.rb、約2,140行)、ボックスを有効にして静的解析する:
    RUBY_BOX=1 bundle exec rigor check app/models/issue.rb # SIGSEGV
    bundle exec rigor check app/models/issue.rb # exit 0
    Ruby::Box.newのサブボックスは作成されない——プロセス全体のRUBY_BOX=1フラグが設定されるだけである;クラッシュはアナライザーの実行中の通常の(メインボックスの)メソッドディスパッチで起きる。

SIGSEGV。クラッシュレポート(抜粋):

... [BUG] Segmentation fault at 0x0000000000000000
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25]
-- C level backtrace information -------------------------------------------
libruby-4.0.5.dylib(rb_vm_bugreport+0xbc8)
libruby-4.0.5.dylib(rb_bug_for_fatal_signal)
libruby-4.0.5.dylib(sigsegv)
libsystem_platform.dylib(_sigtramp+0x38)
libruby-4.0.5.dylib(prepare_callable_method_entry) <-- crash
libruby-4.0.5.dylib(prepare_callable_method_entry)
libruby-4.0.5.dylib(callable_method_entry_or_negative)
libruby-4.0.5.dylib(rb_vm_search_method_slowpath)
libruby-4.0.5.dylib(vm_exec_core)
libruby-4.0.5.dylib(rb_vm_exec)
... (deep rb_yield / rb_ary_each / vm_exec_core recursion below)

Rubyレベルの制御フレームは深い再帰的なeach駆動の評価を示している;フォルトはその再帰中にメソッドを解決している際に発生する。終了ステータス139(SIGSEGV)。

プログラムは(RUBY_BOX=1なしのときのように)完了するか、通常のRubyレベルの例外を発生させるべきである。VMはprepare_callable_method_entryでNULLを参照してはならない;Ruby::Boxの有効化が、動作するプログラムをsegfaultに変えるべきではない。

二分探索によって確立されたこと:

  • RUBY_BOX=1がトリガー。上記のすべてのコマンドはそれなしでは0で終了する。
  • Ruby::Box.newのサブボックスとは独立 — プロセス全体のフラグだけを設定して再現する;ユーザーボックスは作成されない。
  • 単一の入力ファイルに二分探索。 Redmineのapp/全体でのクラッシュ → app/models/(86ファイル)に絞り込み → 単一ファイルapp/models/issue.rbまで二分探索した。その約2,140行のファイル1つだけがRUBY_BOX=1のもとでsegfaultする;アナライザー自身のソース(rigor check lib、248ファイル)はRUBY_BOX=1のもとでクラッシュしない。したがってトリガーはファイル数ではなく、RUBY_BOX=1 + 十分に複雑な1ファイルの解析の組み合わせである。
  • これは「劣化/RBS環境なし」パスではない: 意図的に不正な形式のsig/(RBS環境構築が失敗するように)で248ファイルをRUBY_BOX=1のもとで解析してもクラッシュしない。

それを再現しなかったもの(RUBY_BOX=1のもとでの純粋Rubyの試み):

  • 単純な深い再帰(def f = f; f → 通常のSystemStackError)。
  • 300個のモジュールをインクルードしたクラスにわたるメガモルフィックディスパッチ + 再帰。
  • メガモルフィックなノードごとのディスパッチを伴う、ネストされたeachブロック経由で再帰的に走査される131kノードのツリー。
  • 40段の深さのsuperミックスインチェイン × 120サブクラス + GC.stress
  • require "rbs" / Ruby::Box.new + box.require(問題なくロードされる)。

したがってフォルトは、単純な合成では捉えられないアナライザーのホットパスに固有のメソッド解決パターンを必要とする;自己完結した最小リプロデューサーはまだ未解決である。完全なmacOSクラッシュレポート(~/Library/Logs/DiagnosticReports)を添付できる。RUBY_BOXのもとでprepare_callable_method_entryが何をNULLとして参照するかについてのポインタを歓迎する——Cバックトレースはフォルトをrb_vm_search_method_slowpathcallable_method_entry_or_negativeprepare_callable_method_entryに位置づけている。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.