Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
20c8223
[DOC] Update bundled gems list at ae3ad5e13d8e4a2532c69f251b92a7
matzbot Mar 10, 2026
3071dcf
Bump zizmorcore/zizmor-action
dependabot[bot] Mar 10, 2026
56bfa5d
[ruby/rubygems] Add yaml_load/yaml_dump helpers and adapt tests
hsbt Mar 10, 2026
f7e6eae
[ruby/rubygems] Return nil for empty YAML and raise on nil
hsbt Mar 10, 2026
25b82e7
[ruby/rubygems] Support YAML 1.2 nulls and fix nil emission
hsbt Mar 10, 2026
009acc7
[ruby/rubygems] Raise on unknown YAML object tags
hsbt Mar 10, 2026
cccb275
[ruby/rubygems] Do not sanitize dependency requirements from YAML
hsbt Mar 10, 2026
5f44db8
[ruby/rubygems] Construct Gem::Platform from cpu/os/version fields
hsbt Mar 10, 2026
01396cd
[ruby/rubygems] Treat rdoc_options as Hash instead of Array
hsbt Mar 10, 2026
b84ed32
[ruby/rubygems] Stop normalizing requirements to Array
hsbt Mar 10, 2026
c648235
[ruby/rubygems] Handle malformed/unknown YAML Platform fields
hsbt Mar 10, 2026
05dbf2a
[ruby/rubygems] Add YAML roundtrip tests for specs
hsbt Mar 10, 2026
9205a6a
[ruby/rubygems] Add test for gem specification metadata roundtrip
hsbt Mar 10, 2026
9211794
[ruby/rubygems] Restrict platform ivars when deserializing YAML
hsbt Mar 10, 2026
f79f618
[ruby/rubygems] Limit YAML nesting and alias resolutions
hsbt Mar 10, 2026
ca215b7
[ruby/rubygems] bin/rubocop -A
hsbt Mar 10, 2026
83571ba
[ruby/rubygems] Load rdoc_options and requirements from YAML
hsbt Mar 10, 2026
386ad8b
[ruby/rubygems] Skip test when Psych unsafe_load is used
hsbt Mar 10, 2026
5f725bf
[ruby/rubygems] Treat nil deserialized config as empty
hsbt Mar 10, 2026
78d6c9b
Use sort.exe located in the same directory as comm.exe
nobu Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check_sast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!(".", "__")
Expand Down
11 changes: 9 additions & 2 deletions lib/rubygems/specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

##
Expand Down Expand Up @@ -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
Expand Down
100 changes: 45 additions & 55 deletions lib/rubygems/yaml_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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?

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -270,7 +279,7 @@ def coerce(val)
true
elsif val == "false"
false
elsif val == "nil"
elsif ["~", "null"].include?(val)
nil
elsif val == "{}"
Mapping.new
Expand Down Expand Up @@ -388,7 +397,8 @@ 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 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|
Expand All @@ -397,10 +407,11 @@ def initialize(permitted_classes: [], permitted_symbols: [], aliases: true)
@permitted_symbols = permitted_symbols
@aliases = aliases
@anchor_values = {}
@alias_count = 0
end

def build(node)
return {} if node.nil?
return nil if node.nil?

result = build_node(node)

Expand Down Expand Up @@ -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

Expand All @@ -451,10 +466,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)
Expand All @@ -480,20 +497,30 @@ def build_version(node)
Gem::Version.new((hash["version"] || hash["value"]).to_s)
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)
if hash["value"]
Gem::Platform.new(hash["value"])
else
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
plat = Gem::Platform.allocate
hash.each do |k, v|
plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k)
end
plat
end
end

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 = []
Expand Down Expand Up @@ -524,8 +551,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
Expand All @@ -541,7 +567,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)
Expand All @@ -557,26 +582,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)
Expand Down Expand Up @@ -609,26 +614,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
Expand Down Expand Up @@ -662,7 +649,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"
Expand Down Expand Up @@ -697,9 +686,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)
Expand Down Expand Up @@ -788,10 +777,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,
Expand Down
6 changes: 5 additions & 1 deletion test/rubygems/test_gem_commands_owner_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <<EOF
---
Expand Down
Loading