`BOOTSNAP_READONLY`時にRubyソースのエンコーディングが崩れるバグを修正

rails/bootsnap

BOOTSNAP_READONLY=1環境でキャッシュからRubyソースを読み込む際、文字列エンコーディングがASCII-8BITに変化してしまうバグが修正されました。1.24.0で導入されたこの問題により、純粋なASCII範囲外のリテラル文字列を扱う際にエンコーディングエラーが発生していました。

背景

bootsnap 1.24.0へのアップグレード後、本番環境でエンコーディング関連のエラーが報告されていました(#537)。

報告されたエラーの特徴は、文字列のエンコーディングがUTF-8からASCII-8BITに変化してしまうというものでした。この変化により、たとえばgsub呼び出し時にincompatible encoding regexp match例外が発生するなど、バリデーション処理が失敗するケースが確認されていました。影響範囲はRackリクエストのrequest.paramsに含まれるcontrolleraction_nameといった文字列にも及んでおり、BOOTSNAP_READONLY=1でプリコンパイル済みキャッシュを使用する環境で再現していました。

問題の根本原因は、ISeq.compileが引数として渡されたソース文字列のエンコーディングをそのまま使用するという挙動にあります。キャッシュから読み込んだバイナリデータをASCII-8BITのままcompileに渡すと、コンパイル結果のエンコーディング情報もそれに引きずられてしまいます。

技術的な変更

修正はlib/bootsnap/compile_cache/iseq.rbinput_to_outputメソッド1箇所に集中しており、Encoding.default_externalを使ってソース文字列のエンコーディングを明示的に設定するようになりました。

変更前:

def input_to_output(source, path, _kwargs)
  RubyVM::InstructionSequence.compile(source, path, path, nil, @compile_options)
end

変更後:

def input_to_output(source, path, _kwargs)
  RubyVM::InstructionSequence.compile(
    source.force_encoding(Encoding.default_external),
    path,
    path,
    nil,
    @compile_options,
  )
end

force_encoding はバイト列を変換せずエンコーディングラベルだけを付け替えるメソッドです。キャッシュから読み込んだバイナリデータのバイト列は変えずに、Rubyが「このソースはどのエンコーディングで書かれているか」を判断する際にEncoding.default_external(通常はUTF-8)を正しく参照させることができます。

回帰テストとしてtest/compile_cache/iseq_cache_test.rbにも検証が追加されました。

def test_input_to_output_encoding
  compiler = Bootsnap::CompileCache::ISeq::DEFAULT
  source = "_a = 'fée'.encoding".b
  iseq = compiler.input_to_output(source, "a.rb", nil)
  assert_equal Encoding.default_external, iseq.eval
end

テストでは'fée'(ASCII範囲外の文字を含む文字列)のソースコードをバイナリエンコーディング(.b)で渡し、コンパイル・評価後の文字列エンコーディングがEncoding.default_externalと一致することを確認しています。

設計判断

force_encodingを用いてコンパイル直前にエンコーディングを付与するという最小限の修正が採用されました。

PR本文では「ISeq.compileはソース文字列のエンコーディングを使用するため、エンコーディングを変更する必要がある」と明確に説明されています。キャッシュから読み込むバイト列自体はUTF-8として有効なデータであり、バイト変換は不要です。force_encodingはバイト列を保持したままラベルだけを変えるため、この用途に適しています。修正箇所をinput_to_outputの呼び出し1点に限定したことで、変更の影響範囲が最小化されています。

まとめ

本修正は、キャッシュ読み込みパスでASCII-8BITのバイト列をEncoding.default_externalとして解釈させる1行の追加によって、1.24.0で混入したエンコーディング崩れを解消しています。force_encodingという既存のRuby APIを的確に用いることで、バイト列を変えずにエンコーディングの誤認識のみを修正した、影響範囲の小さな実装といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2f293f7e

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的変更・設計判断(各論)、まとめ(結論)の3部構成が明確に適用されており、模範的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とGitHubのIssue/PRリンク記法([#番号](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

`ISeq.compile`や`force_encoding`といったRubyの内部実装に関するトピックを扱っており、専門知識を持つエンジニアという対象読者に完全に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落がトピックセンテンスで始まるため、非常に高い可読性を確保しています。1段落1トピックの原則も守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている`lib/bootsnap/compile_cache/iseq.rb`と`test/compile_cache/iseq_cache_test.rb`のコードは、提供されたDiffの内容と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`ASCII-8BIT`, `ISeq.compile`, `force_encoding`, `Encoding.default_external`など、使用されている技術用語はすべて文脈に即して正確です。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「`ISeq.compile`がソース文字列のエンコーディングを使用する」という原因の説明や、「`force_encoding`がバイト列を変えずにエンコーディングラベルのみを付け替える」という解説は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(バグの発生バージョン1.24.0、関連Issue #537、`BOOTSNAP_READONLY`環境での発生など)は、PRのDescriptionやCHANGELOGのDiff内容で裏付けられており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#538)、Issue番号(#537)、バージョン番号(1.24.0)など、記事に含まれる数値や固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRが修正した「`BOOTSNAP_READONLY`環境でのエンコーディングバグ」というユーザー影響に焦点を当てており、技術的なPRタイトルよりも分かりやすく、内容を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はすべてPR情報およびそこから参照されるIssueに基づいており、PRに記載のない外部知識(サポート状況やリリース予定など)の追加はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「1.24.0で導入された」といった過去の事象に関する時間表現は、PRのCHANGELOGの記載と一致しており正確です。