From 20c822340a84c5c783a5a04ff22367f1d8931586 Mon Sep 17 00:00:00 2001 From: git Date: Tue, 10 Mar 2026 07:05:58 +0000 Subject: [PATCH 01/20] [DOC] Update bundled gems list at ae3ad5e13d8e4a2532c69f251b92a7 --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 82ea31a303135a..f9a56d3075647b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -77,6 +77,7 @@ releases. * 3.1.6 to [v3.1.7][strscan-v3.1.7] * syntax_suggest 2.0.3 * timeout 0.6.1 + * 0.6.0 to [v0.6.1][timeout-v0.6.1] * zlib 3.2.3 * 3.2.2 to [v3.2.3][zlib-v3.2.3] @@ -141,6 +142,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [prism-v1.9.0]: https://github.com/ruby/prism/releases/tag/v1.9.0 [resolv-v0.7.1]: https://github.com/ruby/resolv/releases/tag/v0.7.1 [strscan-v3.1.7]: https://github.com/ruby/strscan/releases/tag/v3.1.7 +[timeout-v0.6.1]: https://github.com/ruby/timeout/releases/tag/v0.6.1 [zlib-v3.2.3]: https://github.com/ruby/zlib/releases/tag/v3.2.3 [test-unit-3.7.4]: https://github.com/test-unit/test-unit/releases/tag/3.7.4 [test-unit-3.7.5]: https://github.com/test-unit/test-unit/releases/tag/3.7.5 From 3071dcf4e7197e399b01141369bbba7aa1150c22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:01:29 +0000 Subject: [PATCH 02/20] Bump zizmorcore/zizmor-action Bumps the github-actions group with 1 update in the / directory: [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action). Updates `zizmorcore/zizmor-action` from 0.5.1 to 0.5.2 - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f...71321a20a9ded102f6e9ce5718a2fcec2c4f70d8) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check_sast.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 6e2b9099e87570..7192f878810607 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -45,7 +45,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1 + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 continue-on-error: true analyze: From 56bfa5de0330b115fed091f26935e2041472f274 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:28:23 +0900 Subject: [PATCH 03/20] [ruby/rubygems] Add yaml_load/yaml_dump helpers and adapt tests https://github.com/ruby/rubygems/commit/5a422dafb1 --- test/rubygems/test_gem_safe_yaml.rb | 378 +++++++++++++++------------- 1 file changed, 199 insertions(+), 179 deletions(-) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index c0d26162007264..2f1afa2d843e36 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -5,6 +5,28 @@ Gem.load_yaml class TestGemSafeYAML < Gem::TestCase + def yaml_load(input, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES, + permitted_symbols: Gem::SafeYAML::PERMITTED_SYMBOLS, + aliases: true) + if Gem.use_psych? + Psych.safe_load(input, permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + aliases: aliases) + else + Gem::YAMLSerializer.load(input, permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + aliases: aliases) + end + end + + def yaml_dump(obj) + if Gem.use_psych? + obj.to_yaml + else + Gem::YAMLSerializer.dump(obj) + end + end + def test_aliases_enabled_by_default assert_predicate Gem::SafeYAML, :aliases_enabled? assert_equal({ "a" => "a", "b" => "a" }, Gem::SafeYAML.safe_load("a: &a a\nb: *a\n")) @@ -23,7 +45,6 @@ def test_aliases_disabled end def test_specification_version_is_integer - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -39,7 +60,6 @@ def test_specification_version_is_integer end def test_disallowed_class_rejected - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:SomeDisallowedClass @@ -53,7 +73,6 @@ def test_disallowed_class_rejected end def test_disallowed_symbol_rejected - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:Gem::Dependency @@ -79,7 +98,6 @@ def test_disallowed_symbol_rejected end def test_yaml_serializer_aliases_disabled - pend "Psych mode" if Gem.use_psych? aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = false @@ -95,7 +113,6 @@ def test_yaml_serializer_aliases_disabled end def test_real_gemspec_fileutils - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -157,7 +174,6 @@ def test_real_gemspec_fileutils end def test_yaml_anchor_and_alias_enabled - pend "Psych mode" if Gem.use_psych? aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = true @@ -184,7 +200,6 @@ def test_yaml_anchor_and_alias_enabled end def test_real_gemspec_rubygems_bundler - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -269,7 +284,6 @@ def test_real_gemspec_rubygems_bundler end def test_empty_requirements_array - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -293,14 +307,18 @@ def test_empty_requirements_array assert_equal "foo", dep.name assert_kind_of Gem::Requirement, dep.requirement - # Requirements should be empty array, not nil reqs = dep.requirement.instance_variable_get(:@requirements) - assert_kind_of Array, reqs - assert_equal [], reqs + if Gem.use_psych? + # Psych sets nil for empty value + assert_nil reqs + else + # YAMLSerializer normalizes empty requirements to [] + assert_kind_of Array, reqs + assert_equal [], reqs + end end def test_requirements_hash_converted_to_array - pend "Psych mode" if Gem.use_psych? # Malformed YAML where requirements is a Hash instead of Array yaml = <<~YAML @@ -309,20 +327,22 @@ def test_requirements_hash_converted_to_array foo: bar YAML - req = Gem::YAMLSerializer.load(yaml, permitted_classes: ["Gem::Requirement"]) + req = yaml_load(yaml, permitted_classes: ["Gem::Requirement"]) assert_kind_of Gem::Requirement, req - # Requirements should be converted from Hash to empty Array reqs = req.instance_variable_get(:@requirements) - assert_kind_of Array, reqs - assert_equal [], reqs - - # Should not raise error when used - assert req.satisfied_by?(Gem::Version.new("1.0")) + if Gem.use_psych? + # Psych assigns the Hash directly + assert_kind_of Hash, reqs + else + # YAMLSerializer normalizes Hash to empty Array + assert_kind_of Array, reqs + assert_equal [], reqs + assert req.satisfied_by?(Gem::Version.new("1.0")) + end end def test_rdoc_options_hash_converted_to_array - pend "Psych mode" if Gem.use_psych? # Some gemspecs incorrectly have rdoc_options: {} instead of rdoc_options: [] yaml = <<~YAML @@ -337,48 +357,60 @@ def test_rdoc_options_hash_converted_to_array assert_kind_of Gem::Specification, spec assert_equal "test-gem", spec.name - # rdoc_options should be converted from Hash to Array - assert_kind_of Array, spec.rdoc_options - assert_equal [], spec.rdoc_options + if Gem.use_psych? + # Psych assigns the empty Hash directly + assert_kind_of Hash, spec.rdoc_options + else + # YAMLSerializer normalizes Hash to Array + assert_kind_of Array, spec.rdoc_options + assert_equal [], spec.rdoc_options + end end def test_load_returns_hash_for_comment_only_yaml - pend "Psych mode" if Gem.use_psych? # Bundler config files may contain only comments after deleting all keys - result = Gem::YAMLSerializer.load("---\n# BUNDLE_FOO: \"bar\"\n") - assert_kind_of Hash, result - assert_empty result + result = yaml_load("---\n# BUNDLE_FOO: \"bar\"\n") + if Gem.use_psych? + # Psych returns nil for comment-only documents + assert_nil result + else + assert_kind_of Hash, result + assert_empty result + end end def test_load_returns_hash_for_empty_document - pend "Psych mode" if Gem.use_psych? - assert_equal({}, Gem::YAMLSerializer.load("---\n")) - assert_equal({}, Gem::YAMLSerializer.load("")) - assert_equal({}, Gem::YAMLSerializer.load(nil)) + if Gem.use_psych? + # Psych returns nil for empty documents + assert_nil yaml_load("---\n") + assert_nil yaml_load("") + assert_raise(TypeError) { yaml_load(nil) } + else + assert_equal({}, yaml_load("---\n")) + assert_equal({}, yaml_load("")) + assert_equal({}, yaml_load(nil)) + end end def test_load_returns_hash_for_flow_empty_hash - pend "Psych mode" if Gem.use_psych? - # Gem::YAMLSerializer.dump({}) produces "--- {}\n" - result = Gem::YAMLSerializer.load("--- {}\n") + # yaml_dump({}) produces "--- {}\n" + result = yaml_load("--- {}\n") assert_kind_of Hash, result assert_empty result end def test_load_parses_flow_empty_hash_as_value - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("metadata: {}\n") + result = yaml_load("metadata: {}\n") assert_kind_of Hash, result assert_kind_of Hash, result["metadata"] assert_empty result["metadata"] end def test_yaml_non_specific_tag_stripped - pend "Psych mode" if Gem.use_psych? # Legacy RubyGems (1.x) generated YAML with ! non-specific tags like: # - ! '>=' @@ -410,7 +442,6 @@ def test_yaml_non_specific_tag_stripped end def test_legacy_gemspec_with_anchors_and_non_specific_tags - pend "Psych mode" if Gem.use_psych? aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = true @@ -467,29 +498,26 @@ def test_legacy_gemspec_with_anchors_and_non_specific_tags end def test_non_specific_tag_on_plain_value - pend "Psych mode" if Gem.use_psych? # ! tag on a bracketed value like rubyforge_project: ! '[none]' - result = Gem::YAMLSerializer.load("key: ! '[none]'\n") + result = yaml_load("key: ! '[none]'\n") assert_equal({ "key" => "[none]" }, result) end def test_dump_quotes_dollar_sign_values - pend "Psych mode" if Gem.use_psych? # Values starting with $ should be quoted to preserve them as strings - yaml = Gem::YAMLSerializer.dump({ "BUNDLE_FOO" => "$BUILD_DIR", "BUNDLE_BAR" => "baz" }) + yaml = yaml_dump({ "BUNDLE_FOO" => "$BUILD_DIR", "BUNDLE_BAR" => "baz" }) assert_include yaml, 'BUNDLE_FOO: "$BUILD_DIR"' assert_include yaml, "BUNDLE_BAR: baz" # Round-trip: ensure the quoted value is parsed back correctly - result = Gem::YAMLSerializer.load(yaml) + result = yaml_load(yaml) assert_equal "$BUILD_DIR", result["BUNDLE_FOO"] assert_equal "baz", result["BUNDLE_BAR"] end def test_dump_quotes_special_characters - pend "Psych mode" if Gem.use_psych? # Various special characters that should trigger quoting special_values = { @@ -502,31 +530,36 @@ def test_dump_quotes_special_characters "percent" => "%encoded", } - yaml = Gem::YAMLSerializer.dump(special_values) + yaml = yaml_dump(special_values) special_values.each do |key, value| assert_include yaml, "#{key}: #{value.inspect}", "Value #{value.inspect} for key #{key} should be quoted" end # Round-trip - result = Gem::YAMLSerializer.load(yaml) + result = yaml_load(yaml) special_values.each do |key, value| assert_equal value, result[key], "Round-trip failed for key #{key}" end end def test_load_ambiguous_value_with_colon - pend "Psych mode" if Gem.use_psych? # "invalid: yaml: hah" is ambiguous YAML - our parser treats it as # {"invalid" => "yaml: hah"}, but the value looks like a nested mapping. # config_file.rb's load_file should detect this and reject it. - result = Gem::YAMLSerializer.load("invalid: yaml: hah") - assert_kind_of Hash, result - assert_equal "yaml: hah", result["invalid"] + if Gem.use_psych? + # Psych raises a syntax error for this ambiguous YAML + assert_raise(Psych::SyntaxError) do + yaml_load("invalid: yaml: hah") + end + else + result = yaml_load("invalid: yaml: hah") + assert_kind_of Hash, result + assert_equal "yaml: hah", result["invalid"] + end end def test_nested_anchor_in_array_item - pend "Psych mode" if Gem.use_psych? # Ensure aliases are enabled for this test aliases_enabled = Gem::SafeYAML.aliases_enabled? @@ -570,7 +603,6 @@ def test_nested_anchor_in_array_item end def test_roundtrip_specification - pend "Psych mode" if Gem.use_psych? spec = Gem::Specification.new do |s| s.name = "round-trip-test" @@ -587,7 +619,7 @@ def test_roundtrip_specification s.add_dependency "rake", ">= 1.0" end - yaml = Gem::YAMLSerializer.dump(spec) + yaml = yaml_dump(spec) loaded = Gem::SafeYAML.safe_load(yaml) assert_kind_of Gem::Specification, loaded @@ -608,22 +640,20 @@ def test_roundtrip_specification end def test_roundtrip_version - pend "Psych mode" if Gem.use_psych? ver = Gem::Version.new("1.2.3") - yaml = Gem::YAMLSerializer.dump(ver) - loaded = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + yaml = yaml_dump(ver) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Version, loaded assert_equal ver, loaded end def test_roundtrip_platform - pend "Psych mode" if Gem.use_psych? plat = Gem::Platform.new("x86_64-linux") - yaml = Gem::YAMLSerializer.dump(plat) - loaded = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + yaml = yaml_dump(plat) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Platform, loaded assert_equal plat.cpu, loaded.cpu @@ -632,22 +662,20 @@ def test_roundtrip_platform end def test_roundtrip_requirement - pend "Psych mode" if Gem.use_psych? req = Gem::Requirement.new(">= 1.0", "< 2.0") - yaml = Gem::YAMLSerializer.dump(req) - loaded = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + yaml = yaml_dump(req) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Requirement, loaded assert_equal req.requirements.sort_by(&:to_s), loaded.requirements.sort_by(&:to_s) end def test_roundtrip_dependency - pend "Psych mode" if Gem.use_psych? dep = Gem::Dependency.new("foo", ">= 1.0", :development) - yaml = Gem::YAMLSerializer.dump(dep) - loaded = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + yaml = yaml_dump(dep) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Dependency, loaded assert_equal "foo", loaded.name @@ -656,27 +684,24 @@ def test_roundtrip_dependency end def test_roundtrip_nested_hash - pend "Psych mode" if Gem.use_psych? obj = { "a" => { "b" => "c", "d" => [1, 2, 3] } } - yaml = Gem::YAMLSerializer.dump(obj) - loaded = Gem::YAMLSerializer.load(yaml) + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) assert_equal obj, loaded end def test_roundtrip_block_scalar - pend "Psych mode" if Gem.use_psych? obj = { "text" => "line1\nline2\n" } - yaml = Gem::YAMLSerializer.dump(obj) - loaded = Gem::YAMLSerializer.load(yaml) + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) assert_equal "line1\nline2\n", loaded["text"] end def test_roundtrip_special_characters - pend "Psych mode" if Gem.use_psych? obj = { "dollar" => "$HOME", @@ -689,8 +714,8 @@ def test_roundtrip_special_characters "braces" => "{key}", "comma" => "a,b,c", } - yaml = Gem::YAMLSerializer.dump(obj) - loaded = Gem::YAMLSerializer.load(yaml) + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) obj.each do |key, value| assert_equal value, loaded[key], "Round-trip failed for key #{key}" @@ -698,11 +723,10 @@ def test_roundtrip_special_characters end def test_roundtrip_boolean_nil_integer - pend "Psych mode" if Gem.use_psych? obj = { "flag" => true, "count" => 42, "empty" => nil, "off" => false } - yaml = Gem::YAMLSerializer.dump(obj) - loaded = Gem::YAMLSerializer.load(yaml) + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) assert_equal true, loaded["flag"] assert_equal 42, loaded["count"] @@ -711,12 +735,11 @@ def test_roundtrip_boolean_nil_integer end def test_roundtrip_time - pend "Psych mode" if Gem.use_psych? time = Time.utc(2024, 6, 15, 12, 30, 45) obj = { "created" => time } - yaml = Gem::YAMLSerializer.dump(obj) - loaded = Gem::YAMLSerializer.load(yaml) + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) assert_kind_of Time, loaded["created"] assert_equal time.year, loaded["created"].year @@ -725,111 +748,106 @@ def test_roundtrip_time end def test_roundtrip_empty_collections - pend "Psych mode" if Gem.use_psych? obj = { "arr" => [], "hash" => {} } - yaml = Gem::YAMLSerializer.dump(obj) - loaded = Gem::YAMLSerializer.load(yaml) + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) assert_equal [], loaded["arr"] assert_equal({}, loaded["hash"]) end def test_load_double_quoted_escape_sequences - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("newline: \"hello\\nworld\"") + result = yaml_load("newline: \"hello\\nworld\"") assert_equal "hello\nworld", result["newline"] - result = Gem::YAMLSerializer.load("tab: \"col1\\tcol2\"") + result = yaml_load("tab: \"col1\\tcol2\"") assert_equal "col1\tcol2", result["tab"] - result = Gem::YAMLSerializer.load("cr: \"line\\rend\"") + result = yaml_load("cr: \"line\\rend\"") assert_equal "line\rend", result["cr"] - result = Gem::YAMLSerializer.load("quote: \"say\\\"hi\\\"\"") + result = yaml_load("quote: \"say\\\"hi\\\"\"") assert_equal "say\"hi\"", result["quote"] end def test_load_double_quoted_backslash_before_escape_chars - pend "Psych mode" if Gem.use_psych? # \\r in YAML should become literal backslash + r, not carriage return - result = Gem::YAMLSerializer.load('path: "D:\\\\ruby-mswin\\\\lib"') + result = yaml_load('path: "D:\\\\ruby-mswin\\\\lib"') assert_equal "D:\\ruby-mswin\\lib", result["path"] # \\n should become literal backslash + n, not newline - result = Gem::YAMLSerializer.load('path: "C:\\\\new_folder"') + result = yaml_load('path: "C:\\\\new_folder"') assert_equal "C:\\new_folder", result["path"] # \\t should become literal backslash + t, not tab - result = Gem::YAMLSerializer.load('path: "C:\\\\tmp\\\\test"') + result = yaml_load('path: "C:\\\\tmp\\\\test"') assert_equal "C:\\tmp\\test", result["path"] # \\\\ should become two literal backslashes - result = Gem::YAMLSerializer.load('val: "a\\\\\\\\b"') + result = yaml_load('val: "a\\\\\\\\b"') assert_equal "a\\\\b", result["val"] end def test_load_single_quoted_escape - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: 'it''s'") + result = yaml_load("key: 'it''s'") assert_equal "it's", result["key"] - result = Gem::YAMLSerializer.load("key: 'no escape \\n here'") + result = yaml_load("key: 'no escape \\n here'") assert_equal "no escape \\n here", result["key"] end def test_load_quoted_numeric_stays_string - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: \"42\"") + result = yaml_load("key: \"42\"") assert_equal "42", result["key"] assert_kind_of String, result["key"] - result = Gem::YAMLSerializer.load("key: '99'") + result = yaml_load("key: '99'") assert_equal "99", result["key"] assert_kind_of String, result["key"] end def test_load_empty_string_value - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: \"\"") + result = yaml_load("key: \"\"") assert_equal "", result["key"] end def test_load_unquoted_integer - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: 42") + result = yaml_load("key: 42") assert_equal 42, result["key"] assert_kind_of Integer, result["key"] - result = Gem::YAMLSerializer.load("key: -7") + result = yaml_load("key: -7") assert_equal(-7, result["key"]) end def test_load_boolean_values - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("a: true\nb: false") + result = yaml_load("a: true\nb: false") assert_equal true, result["a"] assert_equal false, result["b"] end def test_load_nil_value - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: nil") - assert_nil result["key"] + result = yaml_load("key: nil") + if Gem.use_psych? + # Psych treats "nil" as a string (not a YAML 1.1 null) + assert_equal "nil", result["key"] + else + assert_nil result["key"] + end end def test_load_time_value - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("date: 2024-06-15 12:30:45.000000000 Z") + result = yaml_load("date: 2024-06-15 12:30:45.000000000 Z") assert_kind_of Time, result["date"] assert_equal 2024, result["date"].year assert_equal 6, result["date"].month @@ -837,87 +855,76 @@ def test_load_time_value end def test_load_block_scalar_keep_trailing_newline - pend "Psych mode" if Gem.use_psych? yaml = "text: |\n line1\n line2\n" - result = Gem::YAMLSerializer.load(yaml) + result = yaml_load(yaml) assert_equal "line1\nline2\n", result["text"] end def test_load_block_scalar_strip_trailing_newline - pend "Psych mode" if Gem.use_psych? yaml = "text: |-\n no trailing newline\n" - result = Gem::YAMLSerializer.load(yaml) + result = yaml_load(yaml) assert_equal "no trailing newline", result["text"] refute result["text"].end_with?("\n") end def test_load_flow_array - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("items: [a, b, c]") + result = yaml_load("items: [a, b, c]") assert_equal ["a", "b", "c"], result["items"] end def test_load_flow_empty_array - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("items: []") + result = yaml_load("items: []") assert_equal [], result["items"] end def test_load_mapping_key_with_no_value - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key:") + result = yaml_load("key:") assert_kind_of Hash, result assert_nil result["key"] end def test_load_sequence_item_as_mapping - pend "Psych mode" if Gem.use_psych? yaml = "items:\n- name: foo\n ver: 1\n- name: bar\n ver: 2" - result = Gem::YAMLSerializer.load(yaml) + result = yaml_load(yaml) assert_equal [{ "name" => "foo", "ver" => 1 }, { "name" => "bar", "ver" => 2 }], result["items"] end def test_load_nested_sequence - pend "Psych mode" if Gem.use_psych? yaml = "matrix:\n- - a\n - b\n- - c\n - d" - result = Gem::YAMLSerializer.load(yaml) + result = yaml_load(yaml) assert_equal [["a", "b"], ["c", "d"]], result["matrix"] end def test_load_comment_stripped_from_value - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: value # this is a comment") + result = yaml_load("key: value # this is a comment") assert_equal "value", result["key"] end def test_load_comment_in_quoted_string_preserved - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: \"value # not a comment\"") + result = yaml_load("key: \"value # not a comment\"") assert_equal "value # not a comment", result["key"] - result = Gem::YAMLSerializer.load("key: 'value # not a comment'") + result = yaml_load("key: 'value # not a comment'") assert_equal "value # not a comment", result["key"] end def test_load_crlf_line_endings - pend "Psych mode" if Gem.use_psych? - result = Gem::YAMLSerializer.load("key: value\r\nother: data\r\n") + result = yaml_load("key: value\r\nother: data\r\n") assert_equal "value", result["key"] assert_equal "data", result["other"] end def test_load_version_requirement_old_tag - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML !ruby/object:Gem::Version::Requirement @@ -927,33 +934,36 @@ def test_load_version_requirement_old_tag version: "1.0" YAML - req = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + req = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Requirement, req assert_equal [[">=", Gem::Version.new("1.0")]], req.requirements end def test_load_platform_from_value_field - pend "Psych mode" if Gem.use_psych? yaml = "!ruby/object:Gem::Platform\nvalue: x86-linux\n" - plat = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Platform, plat - assert_equal "x86", plat.cpu - assert_equal "linux", plat.os + if Gem.use_psych? + # Psych doesn't interpret the "value" field specially + assert_nil plat.cpu + else + # YAMLSerializer parses the "value" field as a platform string + assert_equal "x86", plat.cpu + assert_equal "linux", plat.os + end end def test_load_platform_from_cpu_os_version_fields - pend "Psych mode" if Gem.use_psych? yaml = "!ruby/object:Gem::Platform\ncpu: x86_64\nos: darwin\nversion: nil\n" - plat = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Platform, plat assert_equal "x86_64", plat.cpu assert_equal "darwin", plat.os end def test_load_dependency_missing_requirement_uses_default - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML !ruby/object:Gem::Dependency @@ -961,15 +971,20 @@ def test_load_dependency_missing_requirement_uses_default type: :runtime YAML - dep = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + dep = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Dependency, dep assert_equal "foo", dep.name assert_equal :runtime, dep.type - assert_kind_of Gem::Requirement, dep.requirement + if Gem.use_psych? + # Psych doesn't set a default requirement + assert_nil dep.instance_variable_get(:@requirement) + else + # YAMLSerializer sets a default Gem::Requirement + assert_kind_of Gem::Requirement, dep.requirement + end end def test_load_dependency_missing_type_defaults_to_runtime - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML !ruby/object:Gem::Dependency @@ -981,12 +996,11 @@ def test_load_dependency_missing_type_defaults_to_runtime version: '0' YAML - dep = Gem::YAMLSerializer.load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + dep = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_equal :runtime, dep.type end def test_specification_version_non_numeric_string_not_converted - pend "Psych mode" if Gem.use_psych? yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -1003,63 +1017,63 @@ def test_specification_version_non_numeric_string_not_converted end def test_unknown_permitted_tag_returns_hash_with_tag - pend "Psych mode" if Gem.use_psych? yaml = "!ruby/object:MyCustomClass\nfoo: bar\n" - result = Gem::YAMLSerializer.load(yaml, permitted_classes: ["MyCustomClass"]) - assert_kind_of Hash, result - assert_equal "bar", result["foo"] - assert_equal "!ruby/object:MyCustomClass", result[:tag] + if Gem.use_psych? + # Psych raises ArgumentError for undefined classes + assert_raise(ArgumentError) do + yaml_load(yaml, permitted_classes: ["MyCustomClass"]) + end + else + # YAMLSerializer returns a Hash with the tag stored + result = yaml_load(yaml, permitted_classes: ["MyCustomClass"]) + assert_kind_of Hash, result + assert_equal "bar", result["foo"] + assert_equal "!ruby/object:MyCustomClass", result[:tag] + end end def test_dump_block_scalar_with_trailing_newline - pend "Psych mode" if Gem.use_psych? - yaml = Gem::YAMLSerializer.dump({ "text" => "line1\nline2\n" }) + yaml = yaml_dump({ "text" => "line1\nline2\n" }) assert_include yaml, " |\n" refute_includes yaml, " |-\n" end def test_dump_block_scalar_without_trailing_newline - pend "Psych mode" if Gem.use_psych? - yaml = Gem::YAMLSerializer.dump({ "text" => "line1\nline2" }) + yaml = yaml_dump({ "text" => "line1\nline2" }) assert_include yaml, " |-\n" end def test_dump_nil_value - pend "Psych mode" if Gem.use_psych? - yaml = Gem::YAMLSerializer.dump({ "key" => nil }) - assert_include yaml, "key: nil\n" + yaml = yaml_dump({ "key" => nil }) - loaded = Gem::YAMLSerializer.load(yaml) + loaded = yaml_load(yaml) assert_nil loaded["key"] end def test_dump_symbol_keys_quoted - pend "Psych mode" if Gem.use_psych? - yaml = Gem::YAMLSerializer.dump({ foo: "bar" }) + yaml = yaml_dump({ foo: "bar" }) # Symbol keys should use inspect format assert_include yaml, ":foo:" # Symbol values in hash with symbol keys should be quoted - yaml = Gem::YAMLSerializer.dump({ type: ":runtime" }) + yaml = yaml_dump({ type: ":runtime" }) assert_include yaml, "\":runtime\"" end def test_regression_flow_empty_hash_as_root - pend "Psych mode" if Gem.use_psych? # Previously returned Mapping struct instead of Hash - result = Gem::YAMLSerializer.load("--- {}") + result = yaml_load("--- {}") assert_kind_of Hash, result assert_empty result end def test_regression_alias_check_in_builder_not_parser - pend "Psych mode" if Gem.use_psych? # Previously aliases were resolved in Parser, bypassing Builder's policy check. # The Builder must enforce aliases: false. @@ -1068,19 +1082,18 @@ def test_regression_alias_check_in_builder_not_parser # Alias in mapping value assert_raise(Psych::AliasesNotEnabled) do - Gem::YAMLSerializer.load("a: &x val\nb: *x", aliases: false) + yaml_load("a: &x val\nb: *x", aliases: false) end # Alias in sequence item assert_raise(Psych::AliasesNotEnabled) do - Gem::YAMLSerializer.load("items:\n- &x val\n- *x", aliases: false) + yaml_load("items:\n- &x val\n- *x", aliases: false) end ensure Gem::SafeYAML.aliases_enabled = aliases_enabled end def test_regression_anchored_mapping_stored_for_alias_resolution - pend "Psych mode" if Gem.use_psych? # Previously build_mapping didn't call store_anchor, so anchored # Gem types (Requirement, etc.) couldn't be resolved via aliases. @@ -1105,7 +1118,6 @@ def test_regression_anchored_mapping_stored_for_alias_resolution end def test_regression_register_anchor_sets_node_anchor - pend "Psych mode" if Gem.use_psych? # Previously register_anchor only stored node in @anchors hash but # didn't set node.anchor, so Builder couldn't track anchored values. @@ -1130,19 +1142,17 @@ def test_regression_register_anchor_sets_node_anchor end def test_regression_coerce_empty_hash_not_wrapped_in_scalar - pend "Psych mode" if Gem.use_psych? # Previously coerce("{}") returned Mapping but parse_plain_scalar # wrapped it in Scalar.new(value: Mapping), causing type mismatch. - result = Gem::YAMLSerializer.load("--- {}") + result = yaml_load("--- {}") assert_kind_of Hash, result - result = Gem::YAMLSerializer.load("key: {}") + result = yaml_load("key: {}") assert_kind_of Hash, result["key"] end def test_regression_rdoc_options_normalized_to_array - pend "Psych mode" if Gem.use_psych? # rdoc_options as Hash (malformed gemspec) yaml = <<~YAML @@ -1156,14 +1166,18 @@ def test_regression_rdoc_options_normalized_to_array YAML spec = Gem::SafeYAML.safe_load(yaml) - assert_kind_of Array, spec.rdoc_options - # Hash rdoc_options: normalize_rdoc_options! extracts values - assert_include spec.rdoc_options, "MyGem" - assert_include spec.rdoc_options, "README" + if Gem.use_psych? + # Psych assigns the Hash directly + assert_kind_of Hash, spec.rdoc_options + else + # YAMLSerializer normalizes Hash rdoc_options to Array + assert_kind_of Array, spec.rdoc_options + assert_include spec.rdoc_options, "MyGem" + assert_include spec.rdoc_options, "README" + end end def test_regression_requirements_field_normalized_to_array - pend "Psych mode" if Gem.use_psych? # The "requirements" field in a Specification (not Requirement) # should be normalized from Hash to Array if malformed @@ -1177,6 +1191,12 @@ def test_regression_requirements_field_normalized_to_array YAML spec = Gem::SafeYAML.safe_load(yaml) - assert_kind_of Array, spec.requirements + if Gem.use_psych? + # Psych assigns the Hash directly + assert_kind_of Hash, spec.requirements + else + # YAMLSerializer normalizes Hash to Array + assert_kind_of Array, spec.requirements + end end end From f7e6eae3d3f39127aa7b0f57de1ac92a0c0759a0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:32:28 +0900 Subject: [PATCH 04/20] [ruby/rubygems] Return nil for empty YAML and raise on nil https://github.com/ruby/rubygems/commit/c091444047 --- lib/rubygems/yaml_serializer.rb | 7 ++++--- test/rubygems/test_gem_safe_yaml.rb | 25 ++++++------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 0a7ecaca10a50b..681e852c667cec 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -400,7 +400,7 @@ def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) end def build(node) - return {} if node.nil? + return nil if node.nil? result = build_node(node) @@ -788,10 +788,11 @@ def dump(obj) end def load(str, permitted_classes: [], permitted_symbols: [], aliases: true) - return {} if str.nil? || str.empty? + raise TypeError, "no implicit conversion of nil into String" if str.nil? + return nil if str.empty? ast = Parser.new(str).parse - return {} if ast.nil? + return nil if ast.nil? Builder.new( permitted_classes: permitted_classes, diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 2f1afa2d843e36..4e2602d117a054 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -367,31 +367,18 @@ def test_rdoc_options_hash_converted_to_array end end - def test_load_returns_hash_for_comment_only_yaml + def test_load_returns_nil_for_comment_only_yaml # Bundler config files may contain only comments after deleting all keys result = yaml_load("---\n# BUNDLE_FOO: \"bar\"\n") - if Gem.use_psych? - # Psych returns nil for comment-only documents - assert_nil result - else - assert_kind_of Hash, result - assert_empty result - end + assert_nil result end - def test_load_returns_hash_for_empty_document + def test_load_returns_nil_for_empty_document - if Gem.use_psych? - # Psych returns nil for empty documents - assert_nil yaml_load("---\n") - assert_nil yaml_load("") - assert_raise(TypeError) { yaml_load(nil) } - else - assert_equal({}, yaml_load("---\n")) - assert_equal({}, yaml_load("")) - assert_equal({}, yaml_load(nil)) - end + assert_nil yaml_load("---\n") + assert_nil yaml_load("") + assert_raise(TypeError) { yaml_load(nil) } end def test_load_returns_hash_for_flow_empty_hash From 25b82e72a3294d3adc7da98b81b0b6d9cb62a982 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:37:06 +0900 Subject: [PATCH 05/20] [ruby/rubygems] Support YAML 1.2 nulls and fix nil emission https://github.com/ruby/rubygems/commit/940358f2c2 --- lib/rubygems/yaml_serializer.rb | 12 +++++++----- test/rubygems/test_gem_safe_yaml.rb | 14 ++++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 681e852c667cec..bea9d82499da4b 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -270,7 +270,7 @@ def coerce(val) true elsif val == "false" false - elsif val == "nil" + elsif val == "~" || val == "null" nil elsif val == "{}" Mapping.new @@ -662,7 +662,9 @@ def emit_node(obj, indent, quote: false) when Array then emit_array(obj, indent) when Time then emit_time(obj) when String then emit_string(obj, indent, quote: quote) - when Numeric, Symbol, TrueClass, FalseClass, nil + when NilClass + "\n" + when Numeric, Symbol, TrueClass, FalseClass " #{obj.inspect}\n" else " #{obj.to_s.inspect}\n" @@ -697,9 +699,9 @@ def emit_version(ver, indent) def emit_platform(plat, indent) " !ruby/object:Gem::Platform\n" \ - "#{pad(indent)}cpu: #{plat.cpu.inspect}\n" \ - "#{pad(indent)}os: #{plat.os.inspect}\n" \ - "#{pad(indent)}version: #{plat.version.inspect}\n" + "#{pad(indent)}cpu:#{emit_node(plat.cpu, indent + 2)}" \ + "#{pad(indent)}os:#{emit_node(plat.os, indent + 2)}" \ + "#{pad(indent)}version:#{emit_node(plat.version, indent + 2)}" end def emit_requirement(req, indent) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 4e2602d117a054..7c95ab734e82aa 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -823,13 +823,15 @@ def test_load_boolean_values def test_load_nil_value + # YAML 1.2: "nil" is not a null value, only ~ and null are result = yaml_load("key: nil") - if Gem.use_psych? - # Psych treats "nil" as a string (not a YAML 1.1 null) - assert_equal "nil", result["key"] - else - assert_nil result["key"] - end + assert_equal "nil", result["key"] + + result = yaml_load("key: ~") + assert_nil result["key"] + + result = yaml_load("key: null") + assert_nil result["key"] end def test_load_time_value From 009acc746fd13845494c48410b35ea59a944235d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:41:52 +0900 Subject: [PATCH 06/20] [ruby/rubygems] Raise on unknown YAML object tags https://github.com/ruby/rubygems/commit/c29b3b2240 --- lib/rubygems/yaml_serializer.rb | 4 +++- test/rubygems/test_gem_safe_yaml.rb | 15 +++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index bea9d82499da4b..73cb1268dc515c 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -451,10 +451,12 @@ def build_mapping(node) build_dependency(node) when nil build_hash(node) - else + when "!ruby/object:Gem::Specification" hash = build_hash(node) hash[:tag] = node.tag hash + else + raise ArgumentError, "undefined class/module #{node.tag.sub("!ruby/object:", "")}" end store_anchor(node.anchor, result) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 7c95ab734e82aa..c4f133d8d76c0a 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -1005,20 +1005,11 @@ def test_specification_version_non_numeric_string_not_converted assert_equal "abc", spec.specification_version end - def test_unknown_permitted_tag_returns_hash_with_tag + def test_unknown_permitted_tag_raises_argument_error yaml = "!ruby/object:MyCustomClass\nfoo: bar\n" - if Gem.use_psych? - # Psych raises ArgumentError for undefined classes - assert_raise(ArgumentError) do - yaml_load(yaml, permitted_classes: ["MyCustomClass"]) - end - else - # YAMLSerializer returns a Hash with the tag stored - result = yaml_load(yaml, permitted_classes: ["MyCustomClass"]) - assert_kind_of Hash, result - assert_equal "bar", result["foo"] - assert_equal "!ruby/object:MyCustomClass", result[:tag] + assert_raise(ArgumentError) do + yaml_load(yaml, permitted_classes: ["MyCustomClass"]) end end From cccb275a982e1ce11f8a4f733a4168753fc78b09 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:43:50 +0900 Subject: [PATCH 07/20] [ruby/rubygems] Do not sanitize dependency requirements from YAML https://github.com/ruby/rubygems/commit/cbe57bbbd7 --- lib/rubygems/yaml_serializer.rb | 23 +---------------------- test/rubygems/test_gem_safe_yaml.rb | 8 +------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 73cb1268dc515c..5c777944a44b31 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -526,8 +526,7 @@ def build_dependency(node) d = Gem::Dependency.allocate d.instance_variable_set(:@name, hash["name"]) - requirement = build_safe_requirement(hash["requirement"]) - d.instance_variable_set(:@requirement, requirement) + d.instance_variable_set(:@requirement, hash["requirement"]) type = hash["type"] type = type ? type.to_s.sub(/^:/, "").to_sym : :runtime @@ -559,26 +558,6 @@ def pairs_to_hash(node) result end - def build_safe_requirement(req_value) - return Gem::Requirement.default unless req_value - - converted = req_value - return Gem::Requirement.default unless converted.is_a?(Gem::Requirement) - - reqs = converted.instance_variable_get(:@requirements) - if reqs&.is_a?(Array) - valid = reqs.all? do |item| - next true if item == Gem::Requirement::DefaultRequirement - item.is_a?(Array) && item.size >= 2 && VALID_OPS.include?(item[0].to_s) - end - valid ? converted : Gem::Requirement.default - else - converted - end - rescue StandardError - Gem::Requirement.default - end - def validate_tag!(tag) unless @permitted_tags.include?(tag) if defined?(Psych::VERSION) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index c4f133d8d76c0a..a1ebff63edb6e1 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -964,13 +964,7 @@ def test_load_dependency_missing_requirement_uses_default assert_kind_of Gem::Dependency, dep assert_equal "foo", dep.name assert_equal :runtime, dep.type - if Gem.use_psych? - # Psych doesn't set a default requirement - assert_nil dep.instance_variable_get(:@requirement) - else - # YAMLSerializer sets a default Gem::Requirement - assert_kind_of Gem::Requirement, dep.requirement - end + assert_nil dep.instance_variable_get(:@requirement) end def test_load_dependency_missing_type_defaults_to_runtime From 5f44db88fb0338dd3481b0f17015af4309841e56 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:44:41 +0900 Subject: [PATCH 08/20] [ruby/rubygems] Construct Gem::Platform from cpu/os/version fields https://github.com/ruby/rubygems/commit/cf566c3079 --- lib/rubygems/yaml_serializer.rb | 6 +----- test/rubygems/test_gem_safe_yaml.rb | 9 +-------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 5c777944a44b31..223445378a5726 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -484,11 +484,7 @@ def build_version(node) def build_platform(node) hash = pairs_to_hash(node) - if hash["value"] - Gem::Platform.new(hash["value"]) - else - Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]]) - end + Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]]) end def build_requirement(node) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index a1ebff63edb6e1..a8029c8dbdd5e0 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -933,14 +933,7 @@ def test_load_platform_from_value_field yaml = "!ruby/object:Gem::Platform\nvalue: x86-linux\n" plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Platform, plat - if Gem.use_psych? - # Psych doesn't interpret the "value" field specially - assert_nil plat.cpu - else - # YAMLSerializer parses the "value" field as a platform string - assert_equal "x86", plat.cpu - assert_equal "linux", plat.os - end + assert_nil plat.cpu end def test_load_platform_from_cpu_os_version_fields From 01396cd2c24cf88454ff7dcebc0c214791074c3f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:47:13 +0900 Subject: [PATCH 09/20] [ruby/rubygems] Treat rdoc_options as Hash instead of Array https://github.com/ruby/rubygems/commit/074e3f7f62 --- lib/rubygems/yaml_serializer.rb | 21 +-------------------- test/rubygems/test_gem_safe_yaml.rb | 19 ++----------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 223445378a5726..ada2810711302b 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -388,7 +388,7 @@ def strip_comment(val) class Builder VALID_OPS = %w[= != > < >= <= ~>].freeze - ARRAY_FIELDS = %w[rdoc_options files test_files executables requirements extra_rdoc_files].freeze + ARRAY_FIELDS = %w[files test_files executables requirements extra_rdoc_files].freeze def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) @permitted_tags = Array(permitted_classes).map do |c| @@ -538,7 +538,6 @@ def build_specification(hash) spec = Gem::Specification.allocate normalize_specification_version!(hash) - normalize_rdoc_options!(hash) normalize_array_fields!(hash) spec.yaml_initialize("!ruby/object:Gem::Specification", hash) @@ -586,26 +585,8 @@ def normalize_specification_version!(hash) hash["specification_version"] = val.to_i if val.is_a?(String) && /\A\d+\z/.match?(val) end - def normalize_rdoc_options!(hash) - opts = hash["rdoc_options"] - if opts.is_a?(Hash) - hash["rdoc_options"] = opts.values.flatten.compact.map(&:to_s) - elsif opts.is_a?(Array) - hash["rdoc_options"] = opts.flat_map do |opt| - if opt.is_a?(Hash) - opt.flat_map {|k, v| [k.to_s, v.to_s] } - elsif opt.is_a?(String) - opt - else - opt.to_s - end - end - end - end - def normalize_array_fields!(hash) ARRAY_FIELDS.each do |field| - next if field == "rdoc_options" # already handled hash[field] = normalize_array_field(hash[field]) if hash[field] end end diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index a8029c8dbdd5e0..63dbb5f459e8aa 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -357,14 +357,7 @@ def test_rdoc_options_hash_converted_to_array assert_kind_of Gem::Specification, spec assert_equal "test-gem", spec.name - if Gem.use_psych? - # Psych assigns the empty Hash directly - assert_kind_of Hash, spec.rdoc_options - else - # YAMLSerializer normalizes Hash to Array - assert_kind_of Array, spec.rdoc_options - assert_equal [], spec.rdoc_options - end + assert_kind_of Hash, spec.rdoc_options end def test_load_returns_nil_for_comment_only_yaml @@ -1133,15 +1126,7 @@ def test_regression_rdoc_options_normalized_to_array YAML spec = Gem::SafeYAML.safe_load(yaml) - if Gem.use_psych? - # Psych assigns the Hash directly - assert_kind_of Hash, spec.rdoc_options - else - # YAMLSerializer normalizes Hash rdoc_options to Array - assert_kind_of Array, spec.rdoc_options - assert_include spec.rdoc_options, "MyGem" - assert_include spec.rdoc_options, "README" - end + assert_kind_of Hash, spec.rdoc_options end def test_regression_requirements_field_normalized_to_array From b84ed328f331817dfdcc8a9ef12e567b69c32100 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 13:51:30 +0900 Subject: [PATCH 10/20] [ruby/rubygems] Stop normalizing requirements to Array https://github.com/ruby/rubygems/commit/6b0aa2a2d3 --- lib/rubygems/yaml_serializer.rb | 3 +-- test/rubygems/test_gem_safe_yaml.rb | 27 +++------------------------ 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index ada2810711302b..8588a8b39f3f7c 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -388,7 +388,7 @@ def strip_comment(val) class Builder VALID_OPS = %w[= != > < >= <= ~>].freeze - ARRAY_FIELDS = %w[files test_files executables requirements extra_rdoc_files].freeze + ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) @permitted_tags = Array(permitted_classes).map do |c| @@ -491,7 +491,6 @@ def build_requirement(node) r = Gem::Requirement.allocate hash = pairs_to_hash(node) reqs = hash["requirements"] || hash["value"] - reqs = [] unless reqs.is_a?(Array) if reqs.is_a?(Array) && !reqs.empty? safe_reqs = [] diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 63dbb5f459e8aa..9482f19a450bdf 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -308,14 +308,7 @@ def test_empty_requirements_array assert_kind_of Gem::Requirement, dep.requirement reqs = dep.requirement.instance_variable_get(:@requirements) - if Gem.use_psych? - # Psych sets nil for empty value - assert_nil reqs - else - # YAMLSerializer normalizes empty requirements to [] - assert_kind_of Array, reqs - assert_equal [], reqs - end + assert_nil reqs end def test_requirements_hash_converted_to_array @@ -331,15 +324,7 @@ def test_requirements_hash_converted_to_array assert_kind_of Gem::Requirement, req reqs = req.instance_variable_get(:@requirements) - if Gem.use_psych? - # Psych assigns the Hash directly - assert_kind_of Hash, reqs - else - # YAMLSerializer normalizes Hash to empty Array - assert_kind_of Array, reqs - assert_equal [], reqs - assert req.satisfied_by?(Gem::Version.new("1.0")) - end + assert_kind_of Hash, reqs end def test_rdoc_options_hash_converted_to_array @@ -1143,12 +1128,6 @@ def test_regression_requirements_field_normalized_to_array YAML spec = Gem::SafeYAML.safe_load(yaml) - if Gem.use_psych? - # Psych assigns the Hash directly - assert_kind_of Hash, spec.requirements - else - # YAMLSerializer normalizes Hash to Array - assert_kind_of Array, spec.requirements - end + assert_kind_of Hash, spec.requirements end end From c648235eeb76b0deb96841c5c36fe57698731f0d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 14:03:37 +0900 Subject: [PATCH 11/20] [ruby/rubygems] Handle malformed/unknown YAML Platform fields https://github.com/ruby/rubygems/commit/e5bcde365f --- lib/rubygems/yaml_serializer.rb | 15 ++++++++++++++- test/rubygems/test_gem_safe_yaml.rb | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 8588a8b39f3f7c..23477a6694c5e0 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -482,9 +482,22 @@ def build_version(node) Gem::Version.new((hash["version"] || hash["value"]).to_s) end + PLATFORM_FIELDS = %w[cpu os version].freeze + def build_platform(node) hash = pairs_to_hash(node) - Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]]) + if (hash.keys & PLATFORM_FIELDS).any? + Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]]) + elsif hash["value"].is_a?(Array) + # Malformed platform (e.g. sequence instead of mapping). + # Return the raw value so yaml_initialize handles it like Psych does. + hash["value"] + else + # Unknown fields: set as instance variables like Psych's init_with + plat = Gem::Platform.allocate + hash.each {|k, v| plat.instance_variable_set(:"@#{k}", v) } + plat + end end def build_requirement(node) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 9482f19a450bdf..d89cbeff82fa57 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -923,6 +923,14 @@ def test_load_platform_from_cpu_os_version_fields assert_equal "darwin", plat.os end + def test_load_platform_malicious_sequence + + yaml = "!ruby/object:Gem::Platform\n- \"x86-mswin32\\n system('id')#\"\n" + result = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + refute_kind_of Gem::Platform, result + assert_kind_of Array, result + end + def test_load_dependency_missing_requirement_uses_default yaml = <<~YAML From 05dbf2aa71f534fe030e7d550c553c2849f2333b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 14:54:09 +0900 Subject: [PATCH 12/20] [ruby/rubygems] Add YAML roundtrip tests for specs Cover native extensions and Windows-style paths Verify files, extensions, and backslash fields are preserved by SafeYAML https://github.com/ruby/rubygems/commit/5adce1d26d --- test/rubygems/test_gem_safe_yaml.rb | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index d89cbeff82fa57..78d6d1e888a467 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -604,6 +604,47 @@ def test_roundtrip_specification assert_equal :runtime, dep.type end + def test_roundtrip_specification_with_extensions + + spec = Gem::Specification.new do |s| + s.name = "native-ext-test" + s.version = "1.0.0" + s.authors = ["Test"] + s.summary = "A gem with native extensions" + s.files = ["lib/native.rb", "ext/native/extconf.rb", "ext/native/native.c"] + s.extensions = ["ext/native/extconf.rb"] + s.require_paths = ["lib"] + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_equal ["ext/native/extconf.rb"], loaded.extensions + assert_equal ["ext/native/extconf.rb", "ext/native/native.c", "lib/native.rb"], loaded.files + end + + def test_roundtrip_specification_with_windows_paths + + spec = Gem::Specification.new do |s| + s.name = "win-path-test" + s.version = "1.0.0" + s.authors = ["Test"] + s.summary = "A gem with Windows-style paths" + s.files = ["lib/foo.rb", "lib/foo/bar.rb"] + s.require_paths = ["lib"] + s.description = 'Installed in D:\ruby\lib\ruby\gems' + s.post_install_message = "Installed to C:\\Program Files\\Ruby\\lib\\rdoc" + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_equal 'Installed in D:\ruby\lib\ruby\gems', loaded.description + assert_equal "Installed to C:\\Program Files\\Ruby\\lib\\rdoc", loaded.post_install_message + end + def test_roundtrip_version ver = Gem::Version.new("1.2.3") From 9205a6a77378151dd2af44240304517c9b0c00c0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 14:55:22 +0900 Subject: [PATCH 13/20] [ruby/rubygems] Add test for gem specification metadata roundtrip https://github.com/ruby/rubygems/commit/de53449860 --- test/rubygems/test_gem_safe_yaml.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 78d6d1e888a467..e363473a15ea7b 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -645,6 +645,35 @@ def test_roundtrip_specification_with_windows_paths assert_equal "Installed to C:\\Program Files\\Ruby\\lib\\rdoc", loaded.post_install_message end + def test_roundtrip_specification_with_metadata + + spec = Gem::Specification.new do |s| + s.name = "metadata-test" + s.version = "1.0.0" + s.authors = ["Test"] + s.summary = "A gem with metadata" + s.files = ["lib/foo.rb"] + s.require_paths = ["lib"] + s.metadata = { + "changelog_uri" => "https://example.com/CHANGELOG.md", + "source_code_uri" => "https://github.com/example/metadata-test", + "bug_tracker_uri" => "https://github.com/example/metadata-test/issues", + "allowed_push_host" => "https://rubygems.org", + } + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_kind_of Hash, loaded.metadata + assert_equal 4, loaded.metadata.size + assert_equal "https://example.com/CHANGELOG.md", loaded.metadata["changelog_uri"] + assert_equal "https://github.com/example/metadata-test", loaded.metadata["source_code_uri"] + assert_equal "https://github.com/example/metadata-test/issues", loaded.metadata["bug_tracker_uri"] + assert_equal "https://rubygems.org", loaded.metadata["allowed_push_host"] + end + def test_roundtrip_version ver = Gem::Version.new("1.2.3") From 92117949607507defb05263eacb5bdc1a694cf91 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 15:01:14 +0900 Subject: [PATCH 14/20] [ruby/rubygems] Restrict platform ivars when deserializing YAML https://github.com/ruby/rubygems/commit/b8f77e3860 --- lib/rubygems/yaml_serializer.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 23477a6694c5e0..ebbd8793f57f82 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -483,6 +483,7 @@ def build_version(node) end PLATFORM_FIELDS = %w[cpu os version].freeze + PLATFORM_ALLOWED_IVARS = %w[cpu os version value].freeze def build_platform(node) hash = pairs_to_hash(node) @@ -493,9 +494,10 @@ def build_platform(node) # Return the raw value so yaml_initialize handles it like Psych does. hash["value"] else - # Unknown fields: set as instance variables like Psych's init_with plat = Gem::Platform.allocate - hash.each {|k, v| plat.instance_variable_set(:"@#{k}", v) } + hash.each do |k, v| + plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k) + end plat end end From f79f61864bbae05e313a1d1d1218d61c4847638c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 15:07:14 +0900 Subject: [PATCH 15/20] [ruby/rubygems] Limit YAML nesting and alias resolutions https://github.com/ruby/rubygems/commit/d4c5fc58e7 --- lib/rubygems/yaml_serializer.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index ebbd8793f57f82..556fb4f734c0d5 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -30,10 +30,12 @@ def initialize(items: [], tag: nil, anchor: nil) class Parser MAPPING_KEY_RE = /^((?:[^#:]|:[^ ])+):(?:[ ]+(.*))?$/ + MAX_NESTING_DEPTH = 1_000 def initialize(source) @lines = source.split("\n") @anchors = {} + @depth = 0 strip_document_prefix end @@ -69,6 +71,11 @@ def strip_document_prefix end def parse_node(base_indent) + @depth += 1 + if @depth > MAX_NESTING_DEPTH + raise Psych::SyntaxError, "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})" + end + skip_blank_and_comments return nil if @lines.empty? @@ -99,6 +106,8 @@ def parse_node(base_indent) else parse_plain_scalar(indent, anchor) end + ensure + @depth -= 1 end def parse_sequence(indent, anchor) @@ -389,6 +398,7 @@ def strip_comment(val) class Builder VALID_OPS = %w[= != > < >= <= ~>].freeze ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze + MAX_ALIAS_RESOLUTIONS = 1_000 def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) @permitted_tags = Array(permitted_classes).map do |c| @@ -397,6 +407,7 @@ def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) @permitted_symbols = permitted_symbols @aliases = aliases @anchor_values = {} + @alias_count = 0 end def build(node) @@ -428,6 +439,10 @@ def build_node(node) def resolve_alias(node) raise Psych::AliasesNotEnabled unless @aliases + @alias_count += 1 + if @alias_count > MAX_ALIAS_RESOLUTIONS + raise Psych::BadAlias, "exceeded maximum alias resolutions (#{MAX_ALIAS_RESOLUTIONS})" + end @anchor_values.fetch(node.name, nil) end From ca215b764f8dfaf9b26cc4aeb05ccff71d852a7b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 15:09:50 +0900 Subject: [PATCH 16/20] [ruby/rubygems] bin/rubocop -A https://github.com/ruby/rubygems/commit/ba8e4ed9ed --- lib/rubygems/yaml_serializer.rb | 2 +- test/rubygems/test_gem_safe_yaml.rb | 77 +---------------------------- 2 files changed, 3 insertions(+), 76 deletions(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 556fb4f734c0d5..1721519c153fea 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -279,7 +279,7 @@ def coerce(val) true elsif val == "false" false - elsif val == "~" || val == "null" + elsif ["~", "null"].include?(val) nil elsif val == "{}" Mapping.new diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index e363473a15ea7b..e1b04674bd5cb7 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -6,8 +6,8 @@ class TestGemSafeYAML < Gem::TestCase def yaml_load(input, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES, - permitted_symbols: Gem::SafeYAML::PERMITTED_SYMBOLS, - aliases: true) + permitted_symbols: Gem::SafeYAML::PERMITTED_SYMBOLS, + aliases: true) if Gem.use_psych? Psych.safe_load(input, permitted_classes: permitted_classes, permitted_symbols: permitted_symbols, @@ -45,7 +45,6 @@ def test_aliases_disabled end def test_specification_version_is_integer - yaml = <<~YAML --- !ruby/object:Gem::Specification name: test @@ -60,7 +59,6 @@ def test_specification_version_is_integer end def test_disallowed_class_rejected - yaml = <<~YAML --- !ruby/object:SomeDisallowedClass foo: bar @@ -73,7 +71,6 @@ def test_disallowed_class_rejected end def test_disallowed_symbol_rejected - yaml = <<~YAML --- !ruby/object:Gem::Dependency name: test @@ -98,7 +95,6 @@ def test_disallowed_symbol_rejected end def test_yaml_serializer_aliases_disabled - aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = false refute_predicate Gem::SafeYAML, :aliases_enabled? @@ -113,7 +109,6 @@ def test_yaml_serializer_aliases_disabled end def test_real_gemspec_fileutils - yaml = <<~YAML --- !ruby/object:Gem::Specification name: fileutils @@ -174,7 +169,6 @@ def test_real_gemspec_fileutils end def test_yaml_anchor_and_alias_enabled - aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = true @@ -200,7 +194,6 @@ def test_yaml_anchor_and_alias_enabled end def test_real_gemspec_rubygems_bundler - yaml = <<~YAML --- !ruby/object:Gem::Specification name: rubygems-bundler @@ -284,7 +277,6 @@ def test_real_gemspec_rubygems_bundler end def test_empty_requirements_array - yaml = <<~YAML --- !ruby/object:Gem::Specification name: test @@ -312,7 +304,6 @@ def test_empty_requirements_array end def test_requirements_hash_converted_to_array - # Malformed YAML where requirements is a Hash instead of Array yaml = <<~YAML !ruby/object:Gem::Requirement @@ -328,7 +319,6 @@ def test_requirements_hash_converted_to_array end def test_rdoc_options_hash_converted_to_array - # Some gemspecs incorrectly have rdoc_options: {} instead of rdoc_options: [] yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -346,21 +336,18 @@ def test_rdoc_options_hash_converted_to_array end def test_load_returns_nil_for_comment_only_yaml - # Bundler config files may contain only comments after deleting all keys result = yaml_load("---\n# BUNDLE_FOO: \"bar\"\n") assert_nil result end def test_load_returns_nil_for_empty_document - assert_nil yaml_load("---\n") assert_nil yaml_load("") assert_raise(TypeError) { yaml_load(nil) } end def test_load_returns_hash_for_flow_empty_hash - # yaml_dump({}) produces "--- {}\n" result = yaml_load("--- {}\n") assert_kind_of Hash, result @@ -368,7 +355,6 @@ def test_load_returns_hash_for_flow_empty_hash end def test_load_parses_flow_empty_hash_as_value - result = yaml_load("metadata: {}\n") assert_kind_of Hash, result assert_kind_of Hash, result["metadata"] @@ -376,7 +362,6 @@ def test_load_parses_flow_empty_hash_as_value end def test_yaml_non_specific_tag_stripped - # Legacy RubyGems (1.x) generated YAML with ! non-specific tags like: # - ! '>=' # The ! prefix should be ignored. @@ -407,7 +392,6 @@ def test_yaml_non_specific_tag_stripped end def test_legacy_gemspec_with_anchors_and_non_specific_tags - aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = true @@ -463,14 +447,12 @@ def test_legacy_gemspec_with_anchors_and_non_specific_tags end def test_non_specific_tag_on_plain_value - # ! tag on a bracketed value like rubyforge_project: ! '[none]' result = yaml_load("key: ! '[none]'\n") assert_equal({ "key" => "[none]" }, result) end def test_dump_quotes_dollar_sign_values - # Values starting with $ should be quoted to preserve them as strings yaml = yaml_dump({ "BUNDLE_FOO" => "$BUILD_DIR", "BUNDLE_BAR" => "baz" }) assert_include yaml, 'BUNDLE_FOO: "$BUILD_DIR"' @@ -483,7 +465,6 @@ def test_dump_quotes_dollar_sign_values end def test_dump_quotes_special_characters - # Various special characters that should trigger quoting special_values = { "dollar" => "$HOME", @@ -508,7 +489,6 @@ def test_dump_quotes_special_characters end def test_load_ambiguous_value_with_colon - # "invalid: yaml: hah" is ambiguous YAML - our parser treats it as # {"invalid" => "yaml: hah"}, but the value looks like a nested mapping. # config_file.rb's load_file should detect this and reject it. @@ -525,7 +505,6 @@ def test_load_ambiguous_value_with_colon end def test_nested_anchor_in_array_item - # Ensure aliases are enabled for this test aliases_enabled = Gem::SafeYAML.aliases_enabled? Gem::SafeYAML.aliases_enabled = true @@ -568,7 +547,6 @@ def test_nested_anchor_in_array_item end def test_roundtrip_specification - spec = Gem::Specification.new do |s| s.name = "round-trip-test" s.version = "2.3.4" @@ -605,7 +583,6 @@ def test_roundtrip_specification end def test_roundtrip_specification_with_extensions - spec = Gem::Specification.new do |s| s.name = "native-ext-test" s.version = "1.0.0" @@ -625,7 +602,6 @@ def test_roundtrip_specification_with_extensions end def test_roundtrip_specification_with_windows_paths - spec = Gem::Specification.new do |s| s.name = "win-path-test" s.version = "1.0.0" @@ -646,7 +622,6 @@ def test_roundtrip_specification_with_windows_paths end def test_roundtrip_specification_with_metadata - spec = Gem::Specification.new do |s| s.name = "metadata-test" s.version = "1.0.0" @@ -675,7 +650,6 @@ def test_roundtrip_specification_with_metadata end def test_roundtrip_version - ver = Gem::Version.new("1.2.3") yaml = yaml_dump(ver) loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) @@ -685,7 +659,6 @@ def test_roundtrip_version end def test_roundtrip_platform - plat = Gem::Platform.new("x86_64-linux") yaml = yaml_dump(plat) loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) @@ -697,7 +670,6 @@ def test_roundtrip_platform end def test_roundtrip_requirement - req = Gem::Requirement.new(">= 1.0", "< 2.0") yaml = yaml_dump(req) loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) @@ -707,7 +679,6 @@ def test_roundtrip_requirement end def test_roundtrip_dependency - dep = Gem::Dependency.new("foo", ">= 1.0", :development) yaml = yaml_dump(dep) loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) @@ -719,7 +690,6 @@ def test_roundtrip_dependency end def test_roundtrip_nested_hash - obj = { "a" => { "b" => "c", "d" => [1, 2, 3] } } yaml = yaml_dump(obj) loaded = yaml_load(yaml) @@ -728,7 +698,6 @@ def test_roundtrip_nested_hash end def test_roundtrip_block_scalar - obj = { "text" => "line1\nline2\n" } yaml = yaml_dump(obj) loaded = yaml_load(yaml) @@ -737,7 +706,6 @@ def test_roundtrip_block_scalar end def test_roundtrip_special_characters - obj = { "dollar" => "$HOME", "exclamation" => "!important", @@ -758,7 +726,6 @@ def test_roundtrip_special_characters end def test_roundtrip_boolean_nil_integer - obj = { "flag" => true, "count" => 42, "empty" => nil, "off" => false } yaml = yaml_dump(obj) loaded = yaml_load(yaml) @@ -770,7 +737,6 @@ def test_roundtrip_boolean_nil_integer end def test_roundtrip_time - time = Time.utc(2024, 6, 15, 12, 30, 45) obj = { "created" => time } yaml = yaml_dump(obj) @@ -783,7 +749,6 @@ def test_roundtrip_time end def test_roundtrip_empty_collections - obj = { "arr" => [], "hash" => {} } yaml = yaml_dump(obj) loaded = yaml_load(yaml) @@ -793,7 +758,6 @@ def test_roundtrip_empty_collections end def test_load_double_quoted_escape_sequences - result = yaml_load("newline: \"hello\\nworld\"") assert_equal "hello\nworld", result["newline"] @@ -808,7 +772,6 @@ def test_load_double_quoted_escape_sequences end def test_load_double_quoted_backslash_before_escape_chars - # \\r in YAML should become literal backslash + r, not carriage return result = yaml_load('path: "D:\\\\ruby-mswin\\\\lib"') assert_equal "D:\\ruby-mswin\\lib", result["path"] @@ -827,7 +790,6 @@ def test_load_double_quoted_backslash_before_escape_chars end def test_load_single_quoted_escape - result = yaml_load("key: 'it''s'") assert_equal "it's", result["key"] @@ -836,7 +798,6 @@ def test_load_single_quoted_escape end def test_load_quoted_numeric_stays_string - result = yaml_load("key: \"42\"") assert_equal "42", result["key"] assert_kind_of String, result["key"] @@ -847,13 +808,11 @@ def test_load_quoted_numeric_stays_string end def test_load_empty_string_value - result = yaml_load("key: \"\"") assert_equal "", result["key"] end def test_load_unquoted_integer - result = yaml_load("key: 42") assert_equal 42, result["key"] assert_kind_of Integer, result["key"] @@ -863,14 +822,12 @@ def test_load_unquoted_integer end def test_load_boolean_values - result = yaml_load("a: true\nb: false") assert_equal true, result["a"] assert_equal false, result["b"] end def test_load_nil_value - # YAML 1.2: "nil" is not a null value, only ~ and null are result = yaml_load("key: nil") assert_equal "nil", result["key"] @@ -883,7 +840,6 @@ def test_load_nil_value end def test_load_time_value - result = yaml_load("date: 2024-06-15 12:30:45.000000000 Z") assert_kind_of Time, result["date"] assert_equal 2024, result["date"].year @@ -892,14 +848,12 @@ def test_load_time_value end def test_load_block_scalar_keep_trailing_newline - yaml = "text: |\n line1\n line2\n" result = yaml_load(yaml) assert_equal "line1\nline2\n", result["text"] end def test_load_block_scalar_strip_trailing_newline - yaml = "text: |-\n no trailing newline\n" result = yaml_load(yaml) assert_equal "no trailing newline", result["text"] @@ -907,46 +861,39 @@ def test_load_block_scalar_strip_trailing_newline end def test_load_flow_array - result = yaml_load("items: [a, b, c]") assert_equal ["a", "b", "c"], result["items"] end def test_load_flow_empty_array - result = yaml_load("items: []") assert_equal [], result["items"] end def test_load_mapping_key_with_no_value - result = yaml_load("key:") assert_kind_of Hash, result assert_nil result["key"] end def test_load_sequence_item_as_mapping - yaml = "items:\n- name: foo\n ver: 1\n- name: bar\n ver: 2" result = yaml_load(yaml) assert_equal [{ "name" => "foo", "ver" => 1 }, { "name" => "bar", "ver" => 2 }], result["items"] end def test_load_nested_sequence - yaml = "matrix:\n- - a\n - b\n- - c\n - d" result = yaml_load(yaml) assert_equal [["a", "b"], ["c", "d"]], result["matrix"] end def test_load_comment_stripped_from_value - result = yaml_load("key: value # this is a comment") assert_equal "value", result["key"] end def test_load_comment_in_quoted_string_preserved - result = yaml_load("key: \"value # not a comment\"") assert_equal "value # not a comment", result["key"] @@ -955,14 +902,12 @@ def test_load_comment_in_quoted_string_preserved end def test_load_crlf_line_endings - result = yaml_load("key: value\r\nother: data\r\n") assert_equal "value", result["key"] assert_equal "data", result["other"] end def test_load_version_requirement_old_tag - yaml = <<~YAML !ruby/object:Gem::Version::Requirement requirements: @@ -977,7 +922,6 @@ def test_load_version_requirement_old_tag end def test_load_platform_from_value_field - yaml = "!ruby/object:Gem::Platform\nvalue: x86-linux\n" plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Platform, plat @@ -985,7 +929,6 @@ def test_load_platform_from_value_field end def test_load_platform_from_cpu_os_version_fields - yaml = "!ruby/object:Gem::Platform\ncpu: x86_64\nos: darwin\nversion: nil\n" plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) assert_kind_of Gem::Platform, plat @@ -994,7 +937,6 @@ def test_load_platform_from_cpu_os_version_fields end def test_load_platform_malicious_sequence - yaml = "!ruby/object:Gem::Platform\n- \"x86-mswin32\\n system('id')#\"\n" result = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) refute_kind_of Gem::Platform, result @@ -1002,7 +944,6 @@ def test_load_platform_malicious_sequence end def test_load_dependency_missing_requirement_uses_default - yaml = <<~YAML !ruby/object:Gem::Dependency name: foo @@ -1017,7 +958,6 @@ def test_load_dependency_missing_requirement_uses_default end def test_load_dependency_missing_type_defaults_to_runtime - yaml = <<~YAML !ruby/object:Gem::Dependency name: bar @@ -1033,7 +973,6 @@ def test_load_dependency_missing_type_defaults_to_runtime end def test_specification_version_non_numeric_string_not_converted - yaml = <<~YAML --- !ruby/object:Gem::Specification name: test @@ -1049,7 +988,6 @@ def test_specification_version_non_numeric_string_not_converted end def test_unknown_permitted_tag_raises_argument_error - yaml = "!ruby/object:MyCustomClass\nfoo: bar\n" assert_raise(ArgumentError) do yaml_load(yaml, permitted_classes: ["MyCustomClass"]) @@ -1057,20 +995,17 @@ def test_unknown_permitted_tag_raises_argument_error end def test_dump_block_scalar_with_trailing_newline - yaml = yaml_dump({ "text" => "line1\nline2\n" }) assert_include yaml, " |\n" refute_includes yaml, " |-\n" end def test_dump_block_scalar_without_trailing_newline - yaml = yaml_dump({ "text" => "line1\nline2" }) assert_include yaml, " |-\n" end def test_dump_nil_value - yaml = yaml_dump({ "key" => nil }) loaded = yaml_load(yaml) @@ -1078,7 +1013,6 @@ def test_dump_nil_value end def test_dump_symbol_keys_quoted - yaml = yaml_dump({ foo: "bar" }) # Symbol keys should use inspect format assert_include yaml, ":foo:" @@ -1089,7 +1023,6 @@ def test_dump_symbol_keys_quoted end def test_regression_flow_empty_hash_as_root - # Previously returned Mapping struct instead of Hash result = yaml_load("--- {}") assert_kind_of Hash, result @@ -1097,7 +1030,6 @@ def test_regression_flow_empty_hash_as_root end def test_regression_alias_check_in_builder_not_parser - # Previously aliases were resolved in Parser, bypassing Builder's policy check. # The Builder must enforce aliases: false. aliases_enabled = Gem::SafeYAML.aliases_enabled? @@ -1117,7 +1049,6 @@ def test_regression_alias_check_in_builder_not_parser end def test_regression_anchored_mapping_stored_for_alias_resolution - # Previously build_mapping didn't call store_anchor, so anchored # Gem types (Requirement, etc.) couldn't be resolved via aliases. aliases_enabled = Gem::SafeYAML.aliases_enabled? @@ -1141,7 +1072,6 @@ def test_regression_anchored_mapping_stored_for_alias_resolution end def test_regression_register_anchor_sets_node_anchor - # Previously register_anchor only stored node in @anchors hash but # didn't set node.anchor, so Builder couldn't track anchored values. aliases_enabled = Gem::SafeYAML.aliases_enabled? @@ -1165,7 +1095,6 @@ def test_regression_register_anchor_sets_node_anchor end def test_regression_coerce_empty_hash_not_wrapped_in_scalar - # Previously coerce("{}") returned Mapping but parse_plain_scalar # wrapped it in Scalar.new(value: Mapping), causing type mismatch. result = yaml_load("--- {}") @@ -1176,7 +1105,6 @@ def test_regression_coerce_empty_hash_not_wrapped_in_scalar end def test_regression_rdoc_options_normalized_to_array - # rdoc_options as Hash (malformed gemspec) yaml = <<~YAML --- !ruby/object:Gem::Specification @@ -1193,7 +1121,6 @@ def test_regression_rdoc_options_normalized_to_array end def test_regression_requirements_field_normalized_to_array - # The "requirements" field in a Specification (not Requirement) # should be normalized from Hash to Array if malformed yaml = <<~YAML From 83571ba814a511351d284256a1fca18f7b3c05de Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 15:23:52 +0900 Subject: [PATCH 17/20] [ruby/rubygems] Load rdoc_options and requirements from YAML https://github.com/ruby/rubygems/commit/20153ebc78 --- lib/rubygems/specification.rb | 11 +++++++++-- test/rubygems/test_gem_safe_yaml.rb | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index d852332db7f92f..cf96348620262c 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -2190,10 +2190,13 @@ def raise_if_conflicts # :nodoc: end ## - # Sets rdoc_options to +value+, ensuring it is an array. + # Sets rdoc_options to +value+, ensuring it is a flat array of strings. + # Handles malformed gemspecs where rdoc_options may be a Hash or contain Hashes. def rdoc_options=(options) - @rdoc_options = Array options + @rdoc_options = Array(options).flat_map do |opt| + opt.is_a?(Hash) ? opt.to_a.flatten.map(&:to_s) : opt + end end ## @@ -2553,6 +2556,10 @@ def yaml_initialize(tag, vals) # :nodoc: self.date = val when "platform" self.platform = val + when "rdoc_options" + self.rdoc_options = val + when "requirements" + self.requirements = val else instance_variable_set "@#{ivar}", val end diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index e1b04674bd5cb7..47e6dd68774ca6 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -332,7 +332,7 @@ def test_rdoc_options_hash_converted_to_array assert_kind_of Gem::Specification, spec assert_equal "test-gem", spec.name - assert_kind_of Hash, spec.rdoc_options + assert_equal [], spec.rdoc_options end def test_load_returns_nil_for_comment_only_yaml @@ -1117,7 +1117,7 @@ def test_regression_rdoc_options_normalized_to_array YAML spec = Gem::SafeYAML.safe_load(yaml) - assert_kind_of Hash, spec.rdoc_options + assert_equal ["--title", "MyGem", "--main", "README"], spec.rdoc_options end def test_regression_requirements_field_normalized_to_array @@ -1133,6 +1133,6 @@ def test_regression_requirements_field_normalized_to_array YAML spec = Gem::SafeYAML.safe_load(yaml) - assert_kind_of Hash, spec.requirements + assert_equal [["foo", "bar"]], spec.requirements end end From 386ad8b977754adcdf6f718c41df667604af74a4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Mar 2026 16:07:19 +0900 Subject: [PATCH 18/20] [ruby/rubygems] Skip test when Psych unsafe_load is used https://github.com/ruby/rubygems/commit/2636b4d24a --- test/rubygems/test_gem_commands_owner_command.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index 80b1497c415ae9..8cd40e0eedde14 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -55,7 +55,11 @@ def test_show_owners end def test_show_owners_dont_load_objects - pend "testing a psych-only API" unless defined?(::Psych::DisallowedClass) + Gem.load_yaml + + # Gem::SafeYAML.load uses Psych.unsafe_load when Psych is enabled, + # which does not restrict classes. Only YAMLSerializer restricts object tags. + pend "Gem::SafeYAML.load uses Psych.unsafe_load which does not restrict classes" if Gem.use_psych? response = < Date: Tue, 10 Mar 2026 16:30:54 +0900 Subject: [PATCH 19/20] [ruby/rubygems] Treat nil deserialized config as empty https://github.com/ruby/rubygems/commit/368fc29ce1 --- lib/bundler/settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index d00a4bb916f692..6c12ca946602d7 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -476,7 +476,7 @@ def load_config(config_file) SharedHelpers.filesystem_access(config_file, :read) do |file| valid_file = file.exist? && !file.size.zero? return {} unless valid_file - serializer_class.load(file.read).inject({}) do |config, (k, v)| + (serializer_class.load(file.read) || {}).inject({}) do |config, (k, v)| k = k.dup k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}") k.gsub!(".", "__") From 78d6c9bf1b5fa795d15b9e94d951189d2295f70c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 10 Mar 2026 16:47:40 +0900 Subject: [PATCH 20/20] Use sort.exe located in the same directory as comm.exe Windows does not have comm.exe, but it does have sort.exe. However, the default Windows sort.exe is always case-insensitive, so its outputs is not suitable for input to UNIX-like `comm` tool. --- .github/workflows/windows.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d3a83852e0d1be..302c6e81cf638c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -114,7 +114,12 @@ jobs: # https://github.com/actions/virtual-environments/issues/712#issuecomment-613004302 run: | ::- Set up VC ${{ matrix.vc }} - set | coreutils sort > old.env + + ::- Using sort.exe located in the same directory as comm.exe + ::- should probably work just fine. + for %%I in (comm.exe) do set "sort=%%~dp$PATH:I\sort.exe" + + set | "%sort%" > old.env call ..\src\win32\vssetup.cmd ^ -arch=${{ matrix.target || 'amd64' }} ^ ${{ matrix.vcvars && '-vcvars_ver=' || '' }}${{ matrix.vcvars }} @@ -124,8 +129,8 @@ jobs: set MAKEFLAGS=l set /a TEST_JOBS=(15 * %NUMBER_OF_PROCESSORS% / 10) > nul set RUBY_OPT_DIR=%GITHUB_WORKSPACE:\=/%/src/vcpkg_installed/%VCPKG_DEFAULT_TRIPLET% - set | coreutils sort > new.env - coreutils comm -13 old.env new.env >> %GITHUB_ENV% + set | "%sort%" > new.env + comm -13 old.env new.env >> %GITHUB_ENV% del *.env - name: baseruby version