From 9e707ecfea1bc89697d7245d7daf94f399931d39 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 28 Sep 2026 14:48:01 +0900 Subject: [PATCH 1/7] [ruby/rubygems] Keep the output of Gem::Specification#to_ruby well-formed Dependency names were written into %q<> without escaping, and specification_version, dependency types and stub line values were interpolated as is, so a stray `\`, `>` or newline changed the structure of the generated gemspec. Dump dependency names with ruby_code and raise for values that cannot be written safely. https://github.com/ruby/rubygems/commit/07fce12110 Co-Authored-By: Claude Opus 5.5 --- lib/rubygems/specification.rb | 13 +++++- test/rubygems/test_gem_installer.rb | 6 ++- test/rubygems/test_gem_specification.rb | 55 ++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 4a423275c68e27..a5a3e37ab6102a 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -2420,6 +2420,10 @@ def to_ruby result << "#{Gem::StubSpecification::PREFIX}#{extensions.join "\0"}" unless extensions.empty? result << "#{Gem::StubSpecification::TARGET_PREFIX}platform=#{platform}" if content_addressed + # Comments can't be escaped, so a newline would end the stub line early. + result.each do |line| + raise Gem::Exception, "stub line #{line.dump} contains a newline" if line.include?("\n") + end result << nil result << "Gem::Specification.new do |s|" @@ -2467,13 +2471,20 @@ def to_ruby end unless dependencies.empty? + unless Integer === specification_version + raise Gem::Exception, "invalid specification_version: #{specification_version.inspect}" + end + result << nil result << " s.specification_version = #{specification_version}" result << nil dependencies.each do |dep| dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK - result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{ruby_code dep.requirements_list})" + unless Gem::Dependency::TYPES.include?(dep.type) + raise Gem::Exception, "invalid dependency type: #{dep.type.inspect}" + end + result << " s.add_#{dep.type}_dependency(#{ruby_code dep.name}, #{ruby_code dep.requirements_list})" end end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index f7402874b145bc..db9fcd655369d2 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -294,17 +294,19 @@ def test_ensure_dependency def test_ensure_loadable_spec a, a_gem = util_gem "a", 2 do |s| - s.add_dependency "garbage ~> 5" + s.add_dependency "b" end installer = Gem::Installer.at a_gem + requirement = installer.spec.dependencies.first.requirement + requirement.instance_variable_set :@requirements, [["garbage", Gem::Version.new(5)]] e = assert_raise Gem::InstallError do installer.ensure_loadable_spec end assert_equal "The specification for #{a.full_name} is corrupt " \ - "(SyntaxError)", e.message + "(Gem::Requirement::BadRequirementError)", e.message end def test_ensure_loadable_spec_security_policy diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 47a5d289fa10b3..275f9c78d2734f 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -2547,7 +2547,7 @@ def test_to_ruby s.specification_version = #{Gem::Specification::CURRENT_SPECIFICATION_VERSION} - s.add_runtime_dependency(%q.freeze, [\"= 1\".freeze]) + s.add_runtime_dependency(\"b\".freeze, [\"= 1\".freeze]) end SPEC @@ -2646,7 +2646,7 @@ def test_to_ruby_for_cache s.specification_version = #{Gem::Specification::CURRENT_SPECIFICATION_VERSION} - s.add_runtime_dependency(%q.freeze, ["= 1".freeze]) + s.add_runtime_dependency("b".freeze, ["= 1".freeze]) end SPEC @@ -2699,9 +2699,9 @@ def test_to_ruby_fancy s.specification_version = 4 - s.add_runtime_dependency(%q.freeze, [\"> 0.4\".freeze]) - s.add_runtime_dependency(%q.freeze, [\"> 0.0.0\".freeze]) - s.add_runtime_dependency(%q.freeze, [\"> 0.4\".freeze, \"<= 0.6\".freeze]) + s.add_runtime_dependency(\"rake\".freeze, [\"> 0.4\".freeze]) + s.add_runtime_dependency(\"jabber4r\".freeze, [\"> 0.0.0\".freeze]) + s.add_runtime_dependency(\"pqa\".freeze, [\"> 0.4\".freeze, \"<= 0.6\".freeze]) end SPEC @@ -2720,6 +2720,51 @@ def test_to_ruby_keeps_requirements_as_originally_specified assert_includes spec.to_ruby, '"~> 1.0".freeze, ">= 1.0.0".freeze' end + def test_to_ruby_dependency_name + name = "b\\>\n\#{raise}" + @a2.add_dependency name, "1" + + same_spec = eval @a2.to_ruby + + assert_equal [name], same_spec.dependencies.map(&:name) + end + + def test_to_ruby_invalid_dependency_type + @a2.add_dependency "b", "1" + @a2.dependencies.first.instance_variable_set :@type, :foo + + e = assert_raise Gem::Exception do + @a2.to_ruby + end + assert_equal "invalid dependency type: :foo", e.message + end + + def test_to_ruby_invalid_specification_version + @a2.add_dependency "b", "1" + @a2.specification_version = "4\n``" + + e = assert_raise Gem::Exception do + @a2.to_ruby + end + assert_equal 'invalid specification_version: "4\n``"', e.message + end + + def test_to_ruby_newline_in_stub_line + [ + proc {|s| s.name = "a\n``" }, + proc {|s| s.require_paths = ["lib\n``"] }, + proc {|s| s.extensions = ["ext\n``"] }, + ].each do |setup| + spec = @a2.dup + setup.call spec + + e = assert_raise Gem::Exception do + spec.to_ruby + end + assert_match(/\Astub line .* contains a newline\z/, e.message) + end + end + def test_to_ruby_legacy gemspec1 = Gem::Deprecate.skip_during do eval LEGACY_RUBY_SPEC From e3ed0c5121558a7b7a2cda2bee04187f0fb18dc5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 28 Sep 2026 14:48:01 +0900 Subject: [PATCH 2/7] [ruby/rubygems] Drop the verify_spec checks that only protected to_ruby Gem::Specification#to_ruby now escapes these values or raises for them, so such a spec fails in ensure_loadable_spec as corrupt. Call to_ruby inside its rescue so that failure is reported the same way. https://github.com/ruby/rubygems/commit/008e87008d Co-Authored-By: Claude Opus 5.5 --- lib/rubygems/installer.rb | 26 ++------------------------ test/rubygems/test_gem_installer.rb | 23 +++++++++-------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 79f4d3406b2fd1..20a2c3f9830b33 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -628,10 +628,10 @@ def shebang(bin_file_name) # installation. def ensure_loadable_spec - ruby = spec.to_ruby_for_cache + spec = self.spec begin - eval ruby + eval spec.to_ruby_for_cache rescue StandardError, SyntaxError => e raise Gem::InstallError, "The specification for #{spec.full_name} is corrupt (#{e.class})" @@ -723,30 +723,10 @@ def verify_spec raise Gem::InstallError, "#{spec} has an invalid version" end - if spec.raw_require_paths.any? {|path| path =~ /\R/ } - raise Gem::InstallError, "#{spec} has an invalid require_paths" - end - - if spec.extensions.any? {|ext| ext =~ /\R/ } - raise Gem::InstallError, "#{spec} has an invalid extensions" - end - unless /\A[\w.-]+\z/.match?(spec.platform.to_s) raise Gem::InstallError, "#{spec.platform} is an invalid platform" end - unless /\A\d+\z/.match?(spec.specification_version.to_s) - raise Gem::InstallError, "#{spec} has an invalid specification_version" - end - - if spec.dependencies.any? {|dep| dep.type != :runtime && dep.type != :development } - raise Gem::InstallError, "#{spec} has an invalid dependencies" - end - - if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ } - raise Gem::InstallError, "#{spec} has an invalid dependencies" - end - if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) } raise Gem::InstallError, "#{spec} has an invalid executable" end @@ -945,8 +925,6 @@ def gem def pre_install_checks verify_gem_home - # The name and require_paths must be verified first, since it could contain - # ruby code that would be eval'ed in #ensure_loadable_spec verify_spec ensure_loadable_spec diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index db9fcd655369d2..9982fc43f72e55 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2428,7 +2428,7 @@ def spec.validate(*args); end e = assert_raise Gem::InstallError do installer.pre_install_checks end - assert_equal "# has an invalid require_paths", e.message + assert_equal "The specification for malicious-1 is corrupt (Gem::Exception)", e.message end end @@ -2444,7 +2444,7 @@ def spec.validate(*args); end e = assert_raise Gem::InstallError do installer.pre_install_checks end - assert_equal "# has an invalid extensions", e.message + assert_equal "The specification for malicious-1 is corrupt (Gem::Exception)", e.message end end @@ -2455,6 +2455,7 @@ def spec.full_name # so the spec is buildable end def spec.validate(*args); end + spec.add_dependency "b" spec.specification_version = "malicious\n``" util_build_gem spec @@ -2466,30 +2467,24 @@ def spec.validate(*args); end e = assert_raise Gem::InstallError do installer.pre_install_checks end - assert_equal "# has an invalid specification_version", e.message + assert_equal "The specification for malicious-1 is corrupt (Gem::Exception)", e.message end end def test_pre_install_checks_malicious_dependencies_before_eval spec = util_spec "malicious", "1" - def spec.full_name # so the spec is buildable - "malicious-1" - end - def spec.validate(*args); end - spec.add_dependency "b\nfoo", "> 5" - - util_build_gem spec + spec.add_dependency "b", "> 5" + spec.dependencies.first.instance_variable_set :@type, :foo - gem = File.join(@gemhome, "cache", spec.file_name) + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome use_ui @ui do - installer = Gem::Installer.at gem - installer.ignore_dependencies = true e = assert_raise Gem::InstallError do installer.pre_install_checks end - assert_equal "# has an invalid dependencies", e.message + assert_equal "The specification for malicious-1 is corrupt (Gem::Exception)", e.message end end From 177587f5e17b91686f6c2b03061080e8a552350a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sun, 27 Sep 2026 10:56:57 +0900 Subject: [PATCH 3/7] Fix cycle(n).size for n beyond the Fixnum range LONG2FIX does not check the range, so `[1].cycle(2**30).size` returned -1073741824 on mswin. 64-bit platforms hit the same with 2**62. Co-Authored-By: Claude Opus 5.5 --- array.c | 4 ++-- enum.c | 2 +- test/ruby/test_enumerator.rb | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/array.c b/array.c index d8e8f2ac31cf12..c279f248c70a16 100644 --- a/array.c +++ b/array.c @@ -7133,8 +7133,8 @@ rb_ary_cycle_size(VALUE self, VALUE args, VALUE eobj) if (NIL_P(n)) return DBL2NUM(HUGE_VAL); mul = NUM2LONG(n); if (mul <= 0) return INT2FIX(0); - n = LONG2FIX(mul); - return rb_fix_mul_fix(rb_ary_length(self), n); + n = LONG2NUM(mul); + return rb_int_mul(rb_ary_length(self), n); } /* diff --git a/enum.c b/enum.c index 99fd8a712360a6..fcf139400000a0 100644 --- a/enum.c +++ b/enum.c @@ -3764,7 +3764,7 @@ enum_cycle_size(VALUE self, VALUE args, VALUE eobj) if (NIL_P(n)) return DBL2NUM(HUGE_VAL); if (mul <= 0) return INT2FIX(0); - n = LONG2FIX(mul); + n = LONG2NUM(mul); return rb_funcallv(size, '*', 1, &n); } diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb index 7a461ff2982d0c..fd0c485bfb9725 100644 --- a/test/ruby/test_enumerator.rb +++ b/test/ruby/test_enumerator.rb @@ -647,6 +647,9 @@ def test_size_for_cycle assert_equal 0, [].cycle(5).size assert_equal 0, {}.cycle.size assert_equal 0, {}.cycle(5).size + n = RbConfig::LIMITS["FIXNUM_MAX"] + 1 + assert_equal 2 * n, [:foo, :bar].cycle(n).size + assert_equal 2 * n, {foo: 1, bar: 2}.cycle(n).size assert_equal nil, @obj.cycle.size assert_equal nil, @obj.cycle(5).size From 390e274ca3b35b891df96f1896a1607ed97c77a4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 28 Sep 2026 16:30:08 +0900 Subject: [PATCH 4/7] Pend test_sending_hook_payloads_under_gc_stress on Windows arm64 The child dies with SIGSEGV in about a quarter of the Windows 11-arm check runs on master, and leaves no [BUG] report to go on. It has not shown up on x64 Windows, Linux or macOS. Co-Authored-By: Claude Opus 5.5 --- test/ruby/test_ractor.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index d0bff24579327d..ac52997bf7dcd8 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -373,6 +373,7 @@ def test_failed_send_leaves_receiver_usable end def test_sending_hook_payloads_under_gc_stress + pend "SIGSEGV intermittently on Windows arm64" if /\A(?:arm64|aarch64)-(?:mswin|mingw)/ =~ RUBY_PLATFORM # A dump hook's payload is garbage once captured. A later payload allocated into # its slot must not be taken for the one already seen. assert_ractor(<<~'RUBY', timeout: 60) From 1212cfb3187c9131f664766a1807622c758b8dc2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sat, 26 Sep 2026 19:21:59 +0900 Subject: [PATCH 5/7] win32: Stop splitting writes to the console Windows 7 and older could not write more than about 64KB to a console in one call, so console writes and binary writes to character devices were split into 31366-unit pieces [Bug #14942]. Windows older than 8 has not been supported since [Feature #20563], and a single 1MB write succeeds on Windows 11 with conhost, with and without VT processing, and with Windows Terminal. Co-Authored-By: Claude Opus 5.5 --- win32/win32.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/win32/win32.c b/win32/win32.c index 578d2a56a98f6b..2c6bb9ca422937 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -7219,10 +7219,6 @@ constat_apply(HANDLE handle, struct constat *s, WCHAR w) } } -/* get rid of console writing bug; assume WriteConsole and WriteFile - * on a console share the same limit. */ -static const long MAXSIZE_CONSOLE_WRITING = 31366; - /* License: Ruby's */ static long constat_parse(HANDLE h, struct constat *s, const WCHAR **ptrp, long *lenp) @@ -7277,7 +7273,7 @@ constat_parse(HANDLE h, struct constat *s, const WCHAR **ptrp, long *lenp) } rest = 0; } - else if ((rest = *lenp - len) < MAXSIZE_CONSOLE_WRITING) { + else { continue; } *ptrp = ptr; @@ -7554,7 +7550,7 @@ rb_w32_write_internal(int fd, const void *buf, size_t size, rb_off_t *offset) ret = 0; retry: - len = (_osfile(fd) & FDEV) ? min(MAXSIZE_CONSOLE_WRITING, size) : size; + len = size; size -= len; retry2: From 64ff11dfd6e5ab04da488b81b8d37afc6383a9e8 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 28 Sep 2026 03:57:01 -0500 Subject: [PATCH 6/7] [DOC] Harmonize writable? methods --- file.c | 42 +++++++++++++++++++++++++++++++++--------- pathname_builtin.rb | 19 +++++++++++-------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/file.c b/file.c index eb352896c7fe87..daba8ec7a92bdb 100644 --- a/file.c +++ b/file.c @@ -2232,11 +2232,23 @@ rb_file_world_readable_p(VALUE obj, VALUE fname) * call-seq: * File.writable?(object) -> true or false * - * Returns +true+ if the named file is writable by the effective user and - * group id of this process. See eaccess(3). + * Returns whether given `object` exists and is writable by the owner and group + * in the current process: * - * Note that some OS-level security features may cause this to return true - * even though the file is not writable by the effective user/group. + * ```ruby + * filepath = '/tmp/secret.txt' + * File.writable?(filepath) # => false # Non-existent. + * File.write(filepath, 'foo') # Create file. + * File.writable?(filepath) # => true # Writable. + * File.chmod(0o000, filepath) # Make non-writable. + * File.writable?(filepath) # => false # Not writable. + * File.delete(filepath) # Clean up. + * File.writable?('/etc') # => false # Directory. + * File.writable?($stdin) # => false # IO object. + * ``` + * + * Note that filesystem security features may cause this method to return `true` + * even when the file is not writable by the owner and group. */ static VALUE @@ -7210,14 +7222,26 @@ rb_stat_wr(VALUE obj) } /* - * call-seq: - * stat.writable? -> true or false + * :markup: markdown + + * call-seq: + * writable? -> true or false * - * Returns +true+ if stat is writable by the effective user id of this - * process. + * Returns whether the entry at the path in `self` exists and is writable + * by the effective owner and group in the current process: * - * File.stat("testfile").writable? #=> true + * ```ruby + * filepath = '/tmp/secret.txt' + * File.write(filepath, 'foo') + * File.stat(filepath).writable? # => true # Writable. + * File.chmod(0o000, filepath) # Make non-writable. + * File.stat(filepath).writable? # => false # Not writable. + * File.delete(filepath) # Clean up. + * File.stat('/etc').writable? # => false # Directory. + * ``` * + * Note that filesystem security features may cause this method to return `true` + * even when the file is not writable by the effective owner and group. */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index e3d4174b2fdb60..65834942148bcc 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2742,17 +2742,20 @@ def symlink?() FileTest.symlink?(@path) end # writable? => true or false # # Returns whether entry at the path in `self` - # is writable by the owner and group of the current process: + # exists and is writable by the effective owner and group of the current process: # # ```ruby # pn = Pathname('/tmp/secret.txt') - # pn.write('foo') - # pn.writable? # => true - # pn.chmod(0o000) - # pn.writable? # => false - # pn.delete - # Pathname('nosuch').writable? # => false - # ``` + # pn.writable? # => false # Non-existent. + # pn.write('foo') # Create the file. + # pn.writable? # => true # Writable. + # pn.chmod(0o000) # Make non-writable. + # pn.writable? # => false # Not writable. + # pn.delete # Clean up. + # Pathname('/etc/').writable? # => false # Directory. + # ``` + # Note that filesystem security features may cause this method to return true + # even when the entry is not writable by the effective owner and group. # def writable?() FileTest.writable?(@path) end From d939272e7dd89d9d75a1d848963a6cd26f87df51 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 26 Sep 2026 13:23:09 +0900 Subject: [PATCH 7/7] Fix out-of-bounds in Array#sample when array modified The fix in commit 11f57c6 contained a bug where the calculated index should be `len - i` and not `len - 1`. This script reproduces the crash: ary = (1..100).to_a gen = Object.new gen.define_singleton_method(:rand) { |lim| 8 } nv = Object.new nv.define_singleton_method(:to_int) { ary.replace(Array.new(10) { :x }); 10 } p ary.sample(nv, random: gen) --- array.c | 2 +- test/ruby/test_array.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/array.c b/array.c index c279f248c70a16..0eab5acbe249c7 100644 --- a/array.c +++ b/array.c @@ -7009,7 +7009,7 @@ ary_sample(rb_execution_context_t *ec, VALUE ary, VALUE randgen, VALUE nv, VALUE len = RARRAY_LEN(ary); if (len < k && n <= numberof(idx)) { for (i = 0; i < n; ++i) { - if (rnds[i] >= len - 1) return rb_ary_new_capa(0); + if (rnds[i] >= len - i) return rb_ary_new_capa(0); } } if (n > len) n = len; diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 935a56452eb3d8..d659c7400f4036 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -3417,6 +3417,16 @@ def test_sample_modify_array_out_of_bounds # 49 will be out-of-bounds when ary.replace is called def gen.rand(lim) = 49 assert_equal([], ary.sample(obj, random: gen)) + + ary = (1..100).to_a + obj = Object.new + obj.define_singleton_method(:to_int) do + ary.replace(Array.new(10) { :x }) + 10 + end + gen = Object.new + gen.define_singleton_method(:rand) { |lim| 8 } + assert_equal([], ary.sample(obj, random: gen)) end def test_cycle