CompileCache::YAML のキャッシュオプション不変性修正

rails/bootsnap

Bootsnap の YAML コンパイルキャッシュは、キャッシュミス時に storage_to_output が呼び出されます。このメソッドが受け取った kwargs ハッシュを直接書き換え、symbolize_names キーを symbolize_keys に置換していたため、psych が未知のキーワードで例外を投げていました。本 PR はその副作用を排除し、オプションハッシュの不変性を確保する変更です。

背景

キャッシュが存在するが安全性ミスマッチで利用できない場合、storage_to_output が実行されます。その過程で kwargs がミューテートされると、次回の安全ロードでも同様の不正オプションが渡り、ArgumentError: unknown keyword: :symbolize_keys が継続して発生します。この挙動はテスト CompileCacheYAMLTest#test_load_file_symbolize_names_after_unsafe_cache の失敗として顕在化しました。安全ロード機能全体の信頼性が低下することが根本的な課題です。

技術的な変更

msgpack_unpacker_options メソッドを新たに導入し、kwargs を破壊的に変更しない方針を実装しました。メソッドは kwargsnil または :symbolize_names を含まない場合はそのまま返し、含む場合は dup でコピーを作成し、キー名だけを変換した新しいハッシュを返します。

def msgpack_unpacker_options(kwargs)
  return kwargs unless kwargs&.key?(:symbolize_names)

  kwargs = kwargs.dup
  kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
  kwargs
end

Psych4::UnsafeLoadPsych4::SafeLoadstorage_to_output は、従来の直接変換ロジックを削除し、上記ユーティリティを呼び出すよう変更されました。これにより、unpacker へ渡すオプションは副作用のないハッシュになります。

def storage_to_output(data, kwargs)
  unpacker = CompileCache::YAML.msgpack_factory.unpacker(
    CompileCache::YAML.msgpack_unpacker_options(kwargs),
  )
  unpacker.feed(data)
  _safe_loaded = unpacker.unpack
  result = unpacker.unpack
  result
end

テストコードも新しい振る舞いを検証するために拡張されました。symbolize_names が保持されたまま二度の安全ロードが成功し、kwargs が変化しないことを assert_equal({symbolize_names: true}, kwargs) で確認しています。

def test_load_file_symbolize_names_after_unsafe_cache
  skip unless ::Bootsnap::CompileCache::YAML.supported_options.include?(:symbolize_names)

  Help.set_file("a.yml", "---\nfoo: bar", 100)

  kwargs = {symbolize_names: true}
  FakeYaml.unsafe_load_file("a.yml", **kwargs)

  2.times do
    assert_equal({foo: "bar"}, FakeYaml.load_file("a.yml", **kwargs))
    assert_equal({symbolize_names: true}, kwargs)
  end
end

設計判断

この修正は 不変性 (immutability) の原則に沿って、外部から渡されたハッシュを直接変更しない設計を採用しました。dup でコピーを作成してからキー置換を行うことで、呼び出し側が同一オブジェクトを再利用でき、副作用を防止します。また、変換ロジックを msgpack_unpacker_options に集約したことで重複コードが排除され、将来的に類似オプションが追加された際にも同様のパターンで拡張しやすくなります。外部 API は変更せず、既存の symbolize_names オプションを安全に扱えるようになりました。

まとめ

Bootsnap の YAML コンパイルキャッシュは、オプションハッシュの不変性を守ることで例外を回避し、信頼性を向上させました。新たに導入された msgpack_unpacker_options とそれを利用した storage_to_output の書き換えにより、symbolize_names が外部に漏れず psych への不正パラメータ渡しが解消されました。テスト追加により回帰も防止され、既存機能との互換性を維持しつつ安全性が高まりました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
d7c0bf76

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断(任意)、まとめの5要素が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライトが正しい形式 ```ruby:lib/bootsnap/compile_cache/yaml.rb``` で記述され、PRリンクは [#558](URL) の形式で正しく記載されています。

対象読者への適合性 ✓ PASS

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

Ruby/Bootsnap の内部実装に関する記述で、エンジニア向けの適切な技術レベルです。初心者向けの余分な説明はありません。

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

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

各セクションは総論・各論・結論の段落構成になり、トピックセンテンスで始まり、1段落1テーマで長さも適切です。段落間は空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは Diff の追加・変更内容と一致しており、ファイル名やロジックに齟齬はありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語(kwargs、symbolize_names、symbolize_keys、immutability など)は PR と合致しており、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

技術的説明は PR の背景と変更点を正しく裏付けており、因果関係や影響の記述に誤りはありません。

事実の突合 ✓ PASS

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

記事の主張はすべて PR のタイトル・説明・Diff で裏付けられており、推測や捏造はありません。

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

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

PR 番号やコード内のキーワードは正確です。数値的な主張はありません。

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

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

記事タイトルは PR の内容(キャッシュオプションのミューテーション修正)を適切に要約しています。

外部知識の正確性 ✓ PASS

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

バージョンサポートやリリース日程などの外部情報は含まれておらず、PR に基づく記述のみです。

時間表現の正確性 ✓ PASS

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

時間表現のずれはなく、PR の記述と一致しています。