CompileCache::YAML のキャッシュオプション不変性修正
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 を破壊的に変更しない方針を実装しました。メソッドは kwargs が nil または :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::UnsafeLoad と Psych4::SafeLoad の storage_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 への不正パラメータ渡しが解消されました。テスト追加により回帰も防止され、既存機能との互換性を維持しつつ安全性が高まりました。