From cbcbbb2fbe57fb17b3bd6b93244c4f37f3626c7e Mon Sep 17 00:00:00 2001 From: Daisuke Aritomo Date: Mon, 15 Dec 2025 18:04:57 +0900 Subject: [PATCH 01/23] Let Ractor::IsolationError report correct constant path Before this patch, Ractor::IsolationError reported an incorrect constant path when constant was found through `rb_const_get_0()`. In this code, Ractor::IsolationError reported illegal access against `M::TOPLEVEL`, where it should be `Object::TOPLEVEL`. ```ruby TOPLEVEL = [1] module M def self.f TOPLEVEL end end Ractor.new { M.f }.value ``` This was because `rb_const_get_0()` built the "path" part referring to the module/class passed to it in the first place. When a constant was found through recursive search upwards, the module/class which the constant was found should be reported. This patch fixes this issue by modifying rb_const_search() to take a VALUE pointer to be filled with the module/class where the constant was found. [Bug #21782] --- bootstraptest/test_ractor.rb | 14 ++++++++++++++ variable.c | 23 ++++++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 575b96e48d501a..a1169f9d29c54f 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1089,6 +1089,20 @@ def str; STR; end end RUBY +# The correct constant path shall be reported +assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false + STR = "hello" + module M + def self.str; STR; end + end + + begin + Ractor.new{ M.str }.join + rescue Ractor::RemoteError => e + e.cause.message + end +RUBY + # Setting non-shareable objects into constants by other Ractors is not allowed assert_equal 'can not set constants with non-shareable objects by non-main Ractors', <<~'RUBY', frozen_string_literal: false class C diff --git a/variable.c b/variable.c index 7a015a51e397e0..686056fdb47c15 100644 --- a/variable.c +++ b/variable.c @@ -63,7 +63,7 @@ static VALUE autoload_mutex; static void check_before_mod_set(VALUE, ID, VALUE, const char *); static void setup_const_entry(rb_const_entry_t *, VALUE, VALUE, rb_const_flag_t); -static VALUE rb_const_search(VALUE klass, ID id, int exclude, int recurse, int visibility); +static VALUE rb_const_search(VALUE klass, ID id, int exclude, int recurse, int visibility, VALUE *found_in); static st_table *generic_fields_tbl_; typedef int rb_ivar_foreach_callback_func(ID key, VALUE val, st_data_t arg); @@ -473,7 +473,7 @@ rb_path_to_class(VALUE pathname) if (!id) { goto undefined_class; } - c = rb_const_search(c, id, TRUE, FALSE, FALSE); + c = rb_const_search(c, id, TRUE, FALSE, FALSE, NULL); if (UNDEF_P(c)) goto undefined_class; if (!rb_namespace_p(c)) { rb_raise(rb_eTypeError, "%"PRIsVALUE" does not refer to class/module", @@ -3352,11 +3352,12 @@ rb_const_warn_if_deprecated(const rb_const_entry_t *ce, VALUE klass, ID id) static VALUE rb_const_get_0(VALUE klass, ID id, int exclude, int recurse, int visibility) { - VALUE c = rb_const_search(klass, id, exclude, recurse, visibility); + VALUE found_in; + VALUE c = rb_const_search(klass, id, exclude, recurse, visibility, &found_in); if (!UNDEF_P(c)) { if (UNLIKELY(!rb_ractor_main_p())) { if (!rb_ractor_shareable_p(c)) { - rb_raise(rb_eRactorIsolationError, "can not access non-shareable objects in constant %"PRIsVALUE"::%s by non-main Ractor.", rb_class_path(klass), rb_id2name(id)); + rb_raise(rb_eRactorIsolationError, "can not access non-shareable objects in constant %"PRIsVALUE"::%s by non-main Ractor.", rb_class_path(found_in), rb_id2name(id)); } } return c; @@ -3365,7 +3366,7 @@ rb_const_get_0(VALUE klass, ID id, int exclude, int recurse, int visibility) } static VALUE -rb_const_search_from(VALUE klass, ID id, int exclude, int recurse, int visibility) +rb_const_search_from(VALUE klass, ID id, int exclude, int recurse, int visibility, VALUE *found_in) { VALUE value, current; bool first_iteration = true; @@ -3402,13 +3403,17 @@ rb_const_search_from(VALUE klass, ID id, int exclude, int recurse, int visibilit if (am == tmp) break; am = tmp; ac = autoloading_const_entry(tmp, id); - if (ac) return ac->value; + if (ac) { + if (found_in) { *found_in = tmp; } + return ac->value; + } rb_autoload_load(tmp, id); continue; } if (exclude && tmp == rb_cObject) { goto not_found; } + if (found_in) { *found_in = tmp; } return value; } if (!recurse) break; @@ -3420,17 +3425,17 @@ rb_const_search_from(VALUE klass, ID id, int exclude, int recurse, int visibilit } static VALUE -rb_const_search(VALUE klass, ID id, int exclude, int recurse, int visibility) +rb_const_search(VALUE klass, ID id, int exclude, int recurse, int visibility, VALUE *found_in) { VALUE value; if (klass == rb_cObject) exclude = FALSE; - value = rb_const_search_from(klass, id, exclude, recurse, visibility); + value = rb_const_search_from(klass, id, exclude, recurse, visibility, found_in); if (!UNDEF_P(value)) return value; if (exclude) return value; if (BUILTIN_TYPE(klass) != T_MODULE) return value; /* search global const too, if klass is a module */ - return rb_const_search_from(rb_cObject, id, FALSE, recurse, visibility); + return rb_const_search_from(rb_cObject, id, FALSE, recurse, visibility, found_in); } VALUE From 0fe111caa6885407960541bdb6de7ba3cd6aad73 Mon Sep 17 00:00:00 2001 From: Daisuke Aritomo Date: Tue, 16 Dec 2025 15:15:28 +0900 Subject: [PATCH 02/23] Respect encoding of ID in exception messages --- variable.c | 2 +- vm_insnhelper.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/variable.c b/variable.c index 686056fdb47c15..085ba240e412ee 100644 --- a/variable.c +++ b/variable.c @@ -3357,7 +3357,7 @@ rb_const_get_0(VALUE klass, ID id, int exclude, int recurse, int visibility) if (!UNDEF_P(c)) { if (UNLIKELY(!rb_ractor_main_p())) { if (!rb_ractor_shareable_p(c)) { - rb_raise(rb_eRactorIsolationError, "can not access non-shareable objects in constant %"PRIsVALUE"::%s by non-main Ractor.", rb_class_path(found_in), rb_id2name(id)); + rb_raise(rb_eRactorIsolationError, "can not access non-shareable objects in constant %"PRIsVALUE"::%"PRIsVALUE" by non-main Ractor.", rb_class_path(found_in), rb_id2str(id)); } } return c; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index d103146d1f601f..aa67e54d0ac44b 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1145,7 +1145,7 @@ vm_get_ev_const(rb_execution_context_t *ec, VALUE orig_klass, ID id, bool allow_ if (UNLIKELY(!rb_ractor_main_p())) { if (!rb_ractor_shareable_p(val)) { rb_raise(rb_eRactorIsolationError, - "can not access non-shareable objects in constant %"PRIsVALUE"::%s by non-main ractor.", rb_class_path(klass), rb_id2name(id)); + "can not access non-shareable objects in constant %"PRIsVALUE"::%"PRIsVALUE" by non-main ractor.", rb_class_path(klass), rb_id2str(id)); } } return val; @@ -7575,4 +7575,3 @@ rb_vm_lvar_exposed(rb_execution_context_t *ec, int index) const rb_control_frame_t *cfp = ec->cfp; return cfp->ep[index]; } - From f483484fc610589a6faf828a77fd76f15fb4ebb3 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 16 Dec 2025 17:49:55 -0500 Subject: [PATCH 03/23] [DOC] Fix call-seq of Method#box --- proc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proc.c b/proc.c index cdc453f50092d2..aa52cd4a550793 100644 --- a/proc.c +++ b/proc.c @@ -2157,7 +2157,7 @@ method_owner(VALUE obj) } /* - * call-see: + * call-seq: * meth.box -> box or nil * * Returns the Ruby::Box where +meth+ is defined in. From 74a365310cfd150af6b45920d84920d17d6b3946 Mon Sep 17 00:00:00 2001 From: Misaki Shioi <31817032+shioimm@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:11:39 +0900 Subject: [PATCH 04/23] Fix: Recalculate the timeout duration considering `open_timeout` (#15596) This change updates the behavior so that, when there is only a single destination and `open_timeout` is specified, the remaining `open_timeout` duration is used as the connection timeout. --- ext/socket/ipsocket.c | 41 ++++++++++++++++++++++++++++++++-------- ext/socket/lib/socket.rb | 19 +++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/ext/socket/ipsocket.c b/ext/socket/ipsocket.c index a95f5767d21263..ba1b81b3ad864d 100644 --- a/ext/socket/ipsocket.c +++ b/ext/socket/ipsocket.c @@ -606,6 +606,7 @@ init_fast_fallback_inetsock_internal(VALUE v) struct timeval user_specified_open_timeout_storage; struct timeval *user_specified_open_timeout_at = NULL; struct timespec now = current_clocktime_ts(); + VALUE starts_at = current_clocktime(); if (!NIL_P(open_timeout)) { struct timeval open_timeout_tv = rb_time_interval(open_timeout); @@ -619,7 +620,14 @@ init_fast_fallback_inetsock_internal(VALUE v) arg->getaddrinfo_shared = NULL; int family = arg->families[0]; - unsigned int t = NIL_P(resolv_timeout) ? 0 : rsock_value_timeout_to_msec(resolv_timeout); + unsigned int t; + if (!NIL_P(open_timeout)) { + t = rsock_value_timeout_to_msec(open_timeout); + } else if (!NIL_P(open_timeout)) { + t = rsock_value_timeout_to_msec(resolv_timeout); + } else { + t = 0; + } arg->remote.res = rsock_addrinfo( arg->remote.host, @@ -833,14 +841,22 @@ init_fast_fallback_inetsock_internal(VALUE v) status = connect(fd, remote_ai->ai_addr, remote_ai->ai_addrlen); last_family = remote_ai->ai_family; } else { - if (!NIL_P(connect_timeout)) { - user_specified_connect_timeout_storage = rb_time_interval(connect_timeout); - user_specified_connect_timeout_at = &user_specified_connect_timeout_storage; + VALUE timeout = Qnil; + + if (!NIL_P(open_timeout)) { + VALUE elapsed = rb_funcall(current_clocktime(), '-', 1, starts_at); + timeout = rb_funcall(open_timeout, '-', 1, elapsed); + } + if (NIL_P(timeout)) { + if (!NIL_P(connect_timeout)) { + user_specified_connect_timeout_storage = rb_time_interval(connect_timeout); + user_specified_connect_timeout_at = &user_specified_connect_timeout_storage; + } + timeout = + (user_specified_connect_timeout_at && is_infinity(*user_specified_connect_timeout_at)) ? + Qnil : tv_to_seconds(user_specified_connect_timeout_at); } - VALUE timeout = - (user_specified_connect_timeout_at && is_infinity(*user_specified_connect_timeout_at)) ? - Qnil : tv_to_seconds(user_specified_connect_timeout_at); io = arg->io = rsock_init_sock(arg->self, fd); status = rsock_connect(io, remote_ai->ai_addr, remote_ai->ai_addrlen, 0, timeout); } @@ -1305,13 +1321,22 @@ rsock_init_inetsock( * Maybe also accept a local address */ if (!NIL_P(local_host) || !NIL_P(local_serv)) { + unsigned int t; + if (!NIL_P(open_timeout)) { + t = rsock_value_timeout_to_msec(open_timeout); + } else if (!NIL_P(open_timeout)) { + t = rsock_value_timeout_to_msec(resolv_timeout); + } else { + t = 0; + } + local_res = rsock_addrinfo( local_host, local_serv, AF_UNSPEC, SOCK_STREAM, 0, - 0 + t ); struct addrinfo *tmp_p = local_res->ai; diff --git a/ext/socket/lib/socket.rb b/ext/socket/lib/socket.rb index f47b5bc1d17a54..49fb3fcc6d9834 100644 --- a/ext/socket/lib/socket.rb +++ b/ext/socket/lib/socket.rb @@ -685,7 +685,7 @@ def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: ni # :stopdoc: def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, open_timeout: nil) if local_host || local_port - local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, timeout: resolv_timeout) + local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, timeout: open_timeout || resolv_timeout) resolving_family_names = local_addrinfos.map { |lai| ADDRESS_FAMILIES.key(lai.afamily) }.uniq else local_addrinfos = [] @@ -698,6 +698,7 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, is_windows_environment ||= (RUBY_PLATFORM =~ /mswin|mingw|cygwin/) now = current_clock_time + starts_at = now resolution_delay_expires_at = nil connection_attempt_delay_expires_at = nil user_specified_connect_timeout_at = nil @@ -707,7 +708,7 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, if resolving_family_names.size == 1 family_name = resolving_family_names.first - addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[:family_name], :STREAM, timeout: resolv_timeout) + addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[:family_name], :STREAM, timeout: open_timeout || resolv_timeout) resolution_store.add_resolved(family_name, addrinfos) hostname_resolution_result = nil hostname_resolution_notifier = nil @@ -724,7 +725,6 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, thread } ) - user_specified_resolv_timeout_at = resolv_timeout ? now + resolv_timeout : Float::INFINITY end @@ -758,9 +758,16 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, socket.bind(local_addrinfo) if local_addrinfo result = socket.connect_nonblock(addrinfo, exception: false) else + timeout = + if open_timeout + t = open_timeout - (current_clock_time - starts_at) + t.negative? ? 0 : t + else + connect_timeout + end result = socket = local_addrinfo ? - addrinfo.connect_from(local_addrinfo, timeout: connect_timeout) : - addrinfo.connect(timeout: connect_timeout) + addrinfo.connect_from(local_addrinfo, timeout:) : + addrinfo.connect(timeout:) end if result == :wait_writable @@ -934,7 +941,7 @@ def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_t local_addr_list = nil if local_host != nil || local_port != nil - local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil) + local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil, timeout: open_timeout || resolv_timeout) end timeout = open_timeout ? open_timeout : resolv_timeout From 2117e612cafbd1a5ce4ce1d72cc13f8d9b715aa9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 10:38:28 +0900 Subject: [PATCH 05/23] Disabled gem sync for Ruby 4.0 release --- .github/workflows/sync_default_gems.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index 907dc0b4f025fe..a108bf420e16cc 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -1,4 +1,8 @@ name: Sync default gems + +env: + DEFAULT_GEM_SYNC_ENABLED: false + on: workflow_dispatch: inputs: @@ -58,7 +62,7 @@ jobs: run: | git pull --rebase origin ${GITHUB_REF#refs/heads/} git push origin ${GITHUB_REF#refs/heads/} - if: ${{ steps.sync.outputs.update }} + if: ${{ steps.sync.outputs.update && env.DEFAULT_GEM_SYNC_ENABLED == 'true' }} env: EMAIL: svn-admin@ruby-lang.org GIT_AUTHOR_NAME: git From 3b66efda523fc33070aee6097898dbc5b1af6f4b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 11:29:29 +0900 Subject: [PATCH 06/23] Bundle RubyGems 4.0.2 and Bundler 4.0.2 --- lib/bundler/shared_helpers.rb | 3 +- lib/bundler/version.rb | 2 +- lib/rubygems.rb | 13 +- lib/rubygems/util/atomic_file_writer.rb | 67 --------- .../realworld/fixtures/tapioca/Gemfile.lock | 2 +- .../realworld/fixtures/warbler/Gemfile.lock | 2 +- test/rubygems/test_gem_installer.rb | 34 ++--- test/rubygems/test_gem_remote_fetcher.rb | 134 +++++++++--------- tool/bundler/dev_gems.rb.lock | 2 +- tool/bundler/rubocop_gems.rb.lock | 2 +- tool/bundler/standard_gems.rb.lock | 2 +- tool/bundler/test_gems.rb.lock | 2 +- 12 files changed, 99 insertions(+), 166 deletions(-) delete mode 100644 lib/rubygems/util/atomic_file_writer.rb diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 2aa8abe0a078b0..6419e4299760b7 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -105,8 +105,7 @@ def set_bundle_environment def filesystem_access(path, action = :write, &block) yield(path.dup) rescue Errno::EACCES => e - path_basename = File.basename(path.to_s) - raise unless e.message.include?(path_basename) || action == :create + raise unless e.message.include?(path.to_s) || action == :create raise PermissionError.new(path, action) rescue Errno::EAGAIN diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index 411829b28a3d35..ccb6ecab387818 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false module Bundler - VERSION = "4.0.1".freeze + VERSION = "4.0.2".freeze def self.bundler_major_version @bundler_major_version ||= gem_version.segments.first diff --git a/lib/rubygems.rb b/lib/rubygems.rb index a66e5378436600..a5d6c7fb66a52c 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -9,7 +9,7 @@ require "rbconfig" module Gem - VERSION = "4.0.1" + VERSION = "4.0.2" end require_relative "rubygems/defaults" @@ -17,7 +17,6 @@ module Gem require_relative "rubygems/errors" require_relative "rubygems/target_rbconfig" require_relative "rubygems/win_platform" -require_relative "rubygems/util/atomic_file_writer" ## # RubyGems is the Ruby standard for publishing and managing third party @@ -834,12 +833,14 @@ def self.read_binary(path) end ## - # Atomically write a file in binary mode on all platforms. + # Safely write a file in binary mode on all platforms. def self.write_binary(path, data) - Gem::AtomicFileWriter.open(path) do |file| - file.write(data) - end + File.binwrite(path, data) + rescue Errno::ENOSPC + # If we ran out of space but the file exists, it's *guaranteed* to be corrupted. + File.delete(path) if File.exist?(path) + raise end ## diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb deleted file mode 100644 index 7d1d6a74168f95..00000000000000 --- a/lib/rubygems/util/atomic_file_writer.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -# Based on ActiveSupport's AtomicFile implementation -# Copyright (c) David Heinemeier Hansson -# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb -# Licensed under the MIT License - -module Gem - class AtomicFileWriter - ## - # Write to a file atomically. Useful for situations where you don't - # want other processes or threads to see half-written files. - - def self.open(file_name) - temp_dir = File.dirname(file_name) - require "tempfile" unless defined?(Tempfile) - - Tempfile.create(".#{File.basename(file_name)}", temp_dir) do |temp_file| - temp_file.binmode - return_value = yield temp_file - temp_file.close - - original_permissions = if File.exist?(file_name) - File.stat(file_name) - else - # If not possible, probe which are the default permissions in the - # destination directory. - probe_permissions_in(File.dirname(file_name)) - end - - # Set correct permissions on new file - if original_permissions - begin - File.chown(original_permissions.uid, original_permissions.gid, temp_file.path) - File.chmod(original_permissions.mode, temp_file.path) - rescue Errno::EPERM, Errno::EACCES - # Changing file ownership failed, moving on. - end - end - - # Overwrite original file with temp file - File.rename(temp_file.path, file_name) - return_value - end - end - - def self.probe_permissions_in(dir) # :nodoc: - basename = [ - ".permissions_check", - Thread.current.object_id, - Process.pid, - rand(1_000_000), - ].join(".") - - file_name = File.join(dir, basename) - File.open(file_name, "w") {} - File.stat(file_name) - rescue Errno::ENOENT - nil - ensure - begin - File.unlink(file_name) if File.exist?(file_name) - rescue SystemCallError - end - end - end -end diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock index 866a77125bcaed..d6734a6569491d 100644 --- a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock +++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock @@ -46,4 +46,4 @@ DEPENDENCIES tapioca BUNDLED WITH - 4.0.1 + 4.0.2 diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock index 67c887c84c2554..012f3fee972811 100644 --- a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock +++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock @@ -36,4 +36,4 @@ DEPENDENCIES warbler! BUNDLED WITH - 4.0.1 + 4.0.2 diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 0220a41f88a4f2..293fe1e823dd1a 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2385,31 +2385,25 @@ def test_leaves_no_empty_cached_spec_when_no_more_disk_space installer = Gem::Installer.for_spec @spec installer.gem_home = @gemhome - assert_raise(Errno::ENOSPC) do - Gem::AtomicFileWriter.open(@spec.spec_file) do + File.singleton_class.class_eval do + alias_method :original_binwrite, :binwrite + + def binwrite(path, data) raise Errno::ENOSPC end end - assert_path_not_exist @spec.spec_file - end - - def test_write_default_spec - @spec = setup_base_spec - @spec.files = %w[a.rb b.rb c.rb] - - installer = Gem::Installer.for_spec @spec - installer.gem_home = @gemhome - - installer.write_default_spec - - assert_path_exist installer.default_spec_file - - loaded = Gem::Specification.load installer.default_spec_file + assert_raise Errno::ENOSPC do + installer.write_spec + end - assert_equal @spec.files, loaded.files - assert_equal @spec.name, loaded.name - assert_equal @spec.version, loaded.version + assert_path_not_exist @spec.spec_file + ensure + File.singleton_class.class_eval do + remove_method :binwrite + alias_method :binwrite, :original_binwrite + remove_method :original_binwrite + end end def test_dir diff --git a/test/rubygems/test_gem_remote_fetcher.rb b/test/rubygems/test_gem_remote_fetcher.rb index 9badd75b427169..5c1d89fad6934a 100644 --- a/test/rubygems/test_gem_remote_fetcher.rb +++ b/test/rubygems/test_gem_remote_fetcher.rb @@ -60,7 +60,7 @@ def test_cache_update_path uri = Gem::URI "http://example/file" path = File.join @tempdir, "file" - fetcher = fake_fetcher(uri.to_s, "hello") + fetcher = util_fuck_with_fetcher "hello" data = fetcher.cache_update_path uri, path @@ -75,7 +75,7 @@ def test_cache_update_path_with_utf8_internal_encoding path = File.join @tempdir, "file" data = String.new("\xC8").force_encoding(Encoding::BINARY) - fetcher = fake_fetcher(uri.to_s, data) + fetcher = util_fuck_with_fetcher data written_data = fetcher.cache_update_path uri, path @@ -88,7 +88,7 @@ def test_cache_update_path_no_update uri = Gem::URI "http://example/file" path = File.join @tempdir, "file" - fetcher = fake_fetcher(uri.to_s, "hello") + fetcher = util_fuck_with_fetcher "hello" data = fetcher.cache_update_path uri, path, false @@ -97,79 +97,103 @@ def test_cache_update_path_no_update assert_path_not_exist path end - def test_cache_update_path_overwrites_existing_file - uri = Gem::URI "http://example/file" - path = File.join @tempdir, "file" - - # Create existing file with old content - File.write(path, "old content") - assert_equal "old content", File.read(path) - - fetcher = fake_fetcher(uri.to_s, "new content") + def util_fuck_with_fetcher(data, blow = false) + fetcher = Gem::RemoteFetcher.fetcher + fetcher.instance_variable_set :@test_data, data + + if blow + def fetcher.fetch_path(arg, *rest) + # OMG I'm such an ass + class << self; remove_method :fetch_path; end + def self.fetch_path(arg, *rest) + @test_arg = arg + @test_data + end - data = fetcher.cache_update_path uri, path + raise Gem::RemoteFetcher::FetchError.new("haha!", "") + end + else + def fetcher.fetch_path(arg, *rest) + @test_arg = arg + @test_data + end + end - assert_equal "new content", data - assert_equal "new content", File.read(path) + fetcher end def test_download - a1_data = File.open @a1_gem, "rb", &:read - a1_url = "http://gems.example.com/gems/a-1.gem" + a1_data = nil + File.open @a1_gem, "rb" do |fp| + a1_data = fp.read + end - fetcher = fake_fetcher(a1_url, a1_data) + fetcher = util_fuck_with_fetcher a1_data a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://gems.example.com") - assert_equal a1_url, fetcher.paths.last + assert_equal("http://gems.example.com/gems/a-1.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(a1_cache_gem) end def test_download_with_auth - a1_data = File.open @a1_gem, "rb", &:read - a1_url = "http://user:password@gems.example.com/gems/a-1.gem" + a1_data = nil + File.open @a1_gem, "rb" do |fp| + a1_data = fp.read + end - fetcher = fake_fetcher(a1_url, a1_data) + fetcher = util_fuck_with_fetcher a1_data a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://user:password@gems.example.com") - assert_equal a1_url, fetcher.paths.last + assert_equal("http://user:password@gems.example.com/gems/a-1.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(a1_cache_gem) end def test_download_with_token - a1_data = File.open @a1_gem, "rb", &:read - a1_url = "http://token@gems.example.com/gems/a-1.gem" + a1_data = nil + File.open @a1_gem, "rb" do |fp| + a1_data = fp.read + end - fetcher = fake_fetcher(a1_url, a1_data) + fetcher = util_fuck_with_fetcher a1_data a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://token@gems.example.com") - assert_equal a1_url, fetcher.paths.last + assert_equal("http://token@gems.example.com/gems/a-1.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(a1_cache_gem) end def test_download_with_x_oauth_basic - a1_data = File.open @a1_gem, "rb", &:read - a1_url = "http://token:x-oauth-basic@gems.example.com/gems/a-1.gem" + a1_data = nil + File.open @a1_gem, "rb" do |fp| + a1_data = fp.read + end - fetcher = fake_fetcher(a1_url, a1_data) + fetcher = util_fuck_with_fetcher a1_data a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://token:x-oauth-basic@gems.example.com") - assert_equal a1_url, fetcher.paths.last + assert_equal("http://token:x-oauth-basic@gems.example.com/gems/a-1.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(a1_cache_gem) end def test_download_with_encoded_auth - a1_data = File.open @a1_gem, "rb", &:read - a1_url = "http://user:%25pas%25sword@gems.example.com/gems/a-1.gem" + a1_data = nil + File.open @a1_gem, "rb" do |fp| + a1_data = fp.read + end - fetcher = fake_fetcher(a1_url, a1_data) + fetcher = util_fuck_with_fetcher a1_data a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://user:%25pas%25sword@gems.example.com") - assert_equal a1_url, fetcher.paths.last + assert_equal("http://user:%25pas%25sword@gems.example.com/gems/a-1.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(a1_cache_gem) end @@ -211,9 +235,8 @@ def test_download_local_space def test_download_install_dir a1_data = File.open @a1_gem, "rb", &:read - a1_url = "http://gems.example.com/gems/a-1.gem" - fetcher = fake_fetcher(a1_url, a1_data) + fetcher = util_fuck_with_fetcher a1_data install_dir = File.join @tempdir, "more_gems" @@ -222,7 +245,8 @@ def test_download_install_dir actual = fetcher.download(@a1, "http://gems.example.com", install_dir) assert_equal a1_cache_gem, actual - assert_equal a1_url, fetcher.paths.last + assert_equal("http://gems.example.com/gems/a-1.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(a1_cache_gem) end @@ -258,12 +282,7 @@ def test_download_read_only FileUtils.chmod 0o555, @a1.cache_dir FileUtils.chmod 0o555, @gemhome - fetcher = Gem::RemoteFetcher.fetcher - def fetcher.fetch_path(uri, *rest) - File.read File.join(@test_gem_dir, "a-1.gem") - end - fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem)) - + fetcher = util_fuck_with_fetcher File.read(@a1_gem) fetcher.download(@a1, "http://gems.example.com") a1_cache_gem = File.join Gem.user_dir, "cache", @a1.file_name assert File.exist? a1_cache_gem @@ -282,21 +301,19 @@ def test_download_platform_legacy end e1.loaded_from = File.join(@gemhome, "specifications", e1.full_name) - e1_data = File.open e1_gem, "rb", &:read - - fetcher = Gem::RemoteFetcher.fetcher - def fetcher.fetch_path(uri, *rest) - @call_count ||= 0 - @call_count += 1 - raise Gem::RemoteFetcher::FetchError.new("error", uri) if @call_count == 1 - @test_data + e1_data = nil + File.open e1_gem, "rb" do |fp| + e1_data = fp.read end - fetcher.instance_variable_set(:@test_data, e1_data) + + fetcher = util_fuck_with_fetcher e1_data, :blow_chunks e1_cache_gem = e1.cache_file assert_equal e1_cache_gem, fetcher.download(e1, "http://gems.example.com") + assert_equal("http://gems.example.com/gems/#{e1.original_name}.gem", + fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(e1_cache_gem) end @@ -575,8 +592,6 @@ def test_yaml_error_on_size end end - private - def assert_error(exception_class = Exception) got_exception = false @@ -588,13 +603,4 @@ def assert_error(exception_class = Exception) assert got_exception, "Expected exception conforming to #{exception_class}" end - - def fake_fetcher(url, data) - original_fetcher = Gem::RemoteFetcher.fetcher - fetcher = Gem::FakeFetcher.new - fetcher.data[url] = data - Gem::RemoteFetcher.fetcher = fetcher - ensure - Gem::RemoteFetcher.fetcher = original_fetcher - end end diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock index 45062e42253876..30e29782705f8e 100644 --- a/tool/bundler/dev_gems.rb.lock +++ b/tool/bundler/dev_gems.rb.lock @@ -129,4 +129,4 @@ CHECKSUMS turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f BUNDLED WITH - 4.0.1 + 4.0.2 diff --git a/tool/bundler/rubocop_gems.rb.lock b/tool/bundler/rubocop_gems.rb.lock index b45717d4e3eaca..ca3f816b7f8a4c 100644 --- a/tool/bundler/rubocop_gems.rb.lock +++ b/tool/bundler/rubocop_gems.rb.lock @@ -156,4 +156,4 @@ CHECKSUMS unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5 BUNDLED WITH - 4.0.1 + 4.0.2 diff --git a/tool/bundler/standard_gems.rb.lock b/tool/bundler/standard_gems.rb.lock index 9fff0eb0fcdc89..1e31691b7f734a 100644 --- a/tool/bundler/standard_gems.rb.lock +++ b/tool/bundler/standard_gems.rb.lock @@ -176,4 +176,4 @@ CHECKSUMS unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5 BUNDLED WITH - 4.0.1 + 4.0.2 diff --git a/tool/bundler/test_gems.rb.lock b/tool/bundler/test_gems.rb.lock index ac581de02e7fd5..a257f8f8bc5b40 100644 --- a/tool/bundler/test_gems.rb.lock +++ b/tool/bundler/test_gems.rb.lock @@ -103,4 +103,4 @@ CHECKSUMS tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770 BUNDLED WITH - 4.0.1 + 4.0.2 From 87274c7203bbf5c1c834b4ecd1c20d46ec88d7ac Mon Sep 17 00:00:00 2001 From: git Date: Wed, 17 Dec 2025 03:13:25 +0000 Subject: [PATCH 07/23] Update default gems list at 3b66efda523fc33070aee6097898db [ci skip] --- NEWS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 465f8ec4b204a8..324c47909f4e20 100644 --- a/NEWS.md +++ b/NEWS.md @@ -264,8 +264,8 @@ The following default gem is added. The following default gems are updated. -* RubyGems 4.0.1 -* bundler 4.0.1 +* RubyGems 4.0.2 +* bundler 4.0.2 * date 3.5.1 * digest 3.2.1 * english 0.8.1 From f430fbbfacea5690d790dd9060ca4118431fc2fb Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 22 Nov 2025 23:40:35 +0900 Subject: [PATCH 08/23] IO::Buffer: Fill the test for `IO::Buffer#clear` --- test/ruby/test_io_buffer.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index cc7998842313e8..62766130ced3fb 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -489,7 +489,21 @@ def test_zero_length_each_byte def test_clear buffer = IO::Buffer.new(16) - buffer.set_string("Hello World!") + assert_equal "\0" * 16, buffer.get_string + buffer.clear(1) + assert_equal "\1" * 16, buffer.get_string + buffer.clear(2, 1, 2) + assert_equal "\1" + "\2"*2 + "\1"*13, buffer.get_string + buffer.clear(2, 1) + assert_equal "\1" + "\2"*15, buffer.get_string + buffer.clear(260) + assert_equal "\4" * 16, buffer.get_string + assert_raise(TypeError) {buffer.clear("x")} + + assert_raise(ArgumentError) {buffer.clear(0, 20)} + assert_raise(ArgumentError) {buffer.clear(0, 0, 20)} + assert_raise(ArgumentError) {buffer.clear(0, 10, 10)} + assert_raise(ArgumentError) {buffer.clear(0, (1<<64)-8, 10)} end def test_invalidation From 9519d16381c8a8ddf7e1128a08fd80dfac8ed327 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 22 Nov 2025 23:41:23 +0900 Subject: [PATCH 09/23] IO::Buffer: Guard arguments from GC At least, `string` in `io_buffer_set_string` can be different from `argv[0]` after `rb_str_to_str` call. The other cases may not be necessary. --- io_buffer.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 55f1933194ef7b..c2e1c0ca5fe7fe 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -2569,7 +2569,9 @@ rb_io_buffer_initialize_copy(VALUE self, VALUE source) io_buffer_initialize(self, buffer, NULL, source_size, io_flags_for_size(source_size), Qnil); - return io_buffer_copy_from(buffer, source_base, source_size, 0, NULL); + VALUE result = io_buffer_copy_from(buffer, source_base, source_size, 0, NULL); + RB_GC_GUARD(source); + return result; } /* @@ -2654,7 +2656,9 @@ io_buffer_copy(int argc, VALUE *argv, VALUE self) rb_io_buffer_get_bytes_for_reading(source, &source_base, &source_size); - return io_buffer_copy_from(buffer, source_base, source_size, argc-1, argv+1); + VALUE result = io_buffer_copy_from(buffer, source_base, source_size, argc-1, argv+1); + RB_GC_GUARD(source); + return result; } /* @@ -2732,7 +2736,9 @@ io_buffer_set_string(int argc, VALUE *argv, VALUE self) const void *source_base = RSTRING_PTR(string); size_t source_size = RSTRING_LEN(string); - return io_buffer_copy_from(buffer, source_base, source_size, argc-1, argv+1); + VALUE result = io_buffer_copy_from(buffer, source_base, source_size, argc-1, argv+1); + RB_GC_GUARD(string); + return result; } void From c353b625297162024b5a80480664e599dd49a294 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 22 Nov 2025 23:43:19 +0900 Subject: [PATCH 10/23] [Bug #21787] IO::Buffer: Check addition overflows https://hackerone.com/reports/3437743 --- io_buffer.c | 20 +++++++++++++------- test/ruby/test_io_buffer.rb | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index c2e1c0ca5fe7fe..989d2905e4fb93 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1526,13 +1526,19 @@ VALUE rb_io_buffer_free_locked(VALUE self) return self; } +static bool +size_sum_is_bigger_than(size_t a, size_t b, size_t x) +{ + struct rbimpl_size_mul_overflow_tag size = rbimpl_size_add_overflow(a, b); + return size.left || size.right > x; +} + // Validate that access to the buffer is within bounds, assuming you want to // access length bytes from the specified offset. static inline void io_buffer_validate_range(struct rb_io_buffer *buffer, size_t offset, size_t length) { - // We assume here that offset + length won't overflow: - if (offset + length > buffer->size) { + if (size_sum_is_bigger_than(offset, length, buffer->size)) { rb_raise(rb_eArgError, "Specified offset+length is bigger than the buffer size!"); } } @@ -1842,9 +1848,9 @@ rb_io_buffer_compare(VALUE self, VALUE other) } static void -io_buffer_validate_type(size_t size, size_t offset) +io_buffer_validate_type(size_t size, size_t offset, size_t extend) { - if (offset > size) { + if (size_sum_is_bigger_than(offset, extend, size)) { rb_raise(rb_eArgError, "Type extends beyond end of buffer! (offset=%"PRIdSIZE" > size=%"PRIdSIZE")", offset, size); } } @@ -1944,7 +1950,7 @@ static ID RB_IO_BUFFER_DATA_TYPE_##name; \ static VALUE \ io_buffer_read_##name(const void* base, size_t size, size_t *offset) \ { \ - io_buffer_validate_type(size, *offset + sizeof(type)); \ + io_buffer_validate_type(size, *offset, sizeof(type)); \ type value; \ memcpy(&value, (char*)base + *offset, sizeof(type)); \ if (endian != RB_IO_BUFFER_HOST_ENDIAN) value = swap(value); \ @@ -1955,7 +1961,7 @@ io_buffer_read_##name(const void* base, size_t size, size_t *offset) \ static void \ io_buffer_write_##name(const void* base, size_t size, size_t *offset, VALUE _value) \ { \ - io_buffer_validate_type(size, *offset + sizeof(type)); \ + io_buffer_validate_type(size, *offset, sizeof(type)); \ type value = unwrap(_value); \ if (endian != RB_IO_BUFFER_HOST_ENDIAN) value = swap(value); \ memcpy((char*)base + *offset, &value, sizeof(type)); \ @@ -2483,7 +2489,7 @@ io_buffer_memmove(struct rb_io_buffer *buffer, size_t offset, const void *source io_buffer_validate_range(buffer, offset, length); - if (source_offset + length > source_size) { + if (size_sum_is_bigger_than(source_offset, length, source_size)) { rb_raise(rb_eArgError, "The computed source range exceeds the size of the source buffer!"); } diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 62766130ced3fb..e996fc39b88eb2 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -1,6 +1,7 @@ # frozen_string_literal: false require 'tempfile' +require 'rbconfig/sizeof' class TestIOBuffer < Test::Unit::TestCase experimental = Warning[:experimental] @@ -414,6 +415,8 @@ def test_zero_length_get_string :F64 => [-1.0, 0.0, 0.5, 1.0, 128.0], } + SIZE_MAX = RbConfig::LIMITS["SIZE_MAX"] + def test_get_set_value buffer = IO::Buffer.new(128) @@ -422,6 +425,16 @@ def test_get_set_value buffer.set_value(data_type, 0, value) assert_equal value, buffer.get_value(data_type, 0), "Converting #{value} as #{data_type}." end + assert_raise(ArgumentError) {buffer.get_value(data_type, 128)} + assert_raise(ArgumentError) {buffer.set_value(data_type, 128, 0)} + case data_type + when :U8, :S8 + else + assert_raise(ArgumentError) {buffer.get_value(data_type, 127)} + assert_raise(ArgumentError) {buffer.set_value(data_type, 127, 0)} + assert_raise(ArgumentError) {buffer.get_value(data_type, SIZE_MAX)} + assert_raise(ArgumentError) {buffer.set_value(data_type, SIZE_MAX, 0)} + end end end @@ -503,7 +516,7 @@ def test_clear assert_raise(ArgumentError) {buffer.clear(0, 20)} assert_raise(ArgumentError) {buffer.clear(0, 0, 20)} assert_raise(ArgumentError) {buffer.clear(0, 10, 10)} - assert_raise(ArgumentError) {buffer.clear(0, (1<<64)-8, 10)} + assert_raise(ArgumentError) {buffer.clear(0, SIZE_MAX-7, 10)} end def test_invalidation From 7c402d2c2757b38df8b9406b372d2e1f406296ae Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 11 Dec 2025 15:27:05 +0900 Subject: [PATCH 11/23] IO::Buffer: Warn as experimental at allocation Previously, warned only in `new` and `map`, but not `for` and `string`. --- io_buffer.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 989d2905e4fb93..1ee2bc9324c6d2 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -479,6 +479,8 @@ io_buffer_extract_offset_length(VALUE self, int argc, VALUE argv[], size_t *offs VALUE rb_io_buffer_type_allocate(VALUE self) { + io_buffer_experimental(); + struct rb_io_buffer *buffer = NULL; VALUE instance = TypedData_Make_Struct(self, struct rb_io_buffer, &rb_io_buffer_type, buffer); @@ -649,8 +651,6 @@ rb_io_buffer_new(void *base, size_t size, enum rb_io_buffer_flags flags) VALUE rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags flags) { - io_buffer_experimental(); - VALUE instance = rb_io_buffer_type_allocate(rb_cIOBuffer); struct rb_io_buffer *buffer = NULL; @@ -805,8 +805,6 @@ io_flags_for_size(size_t size) VALUE rb_io_buffer_initialize(int argc, VALUE *argv, VALUE self) { - io_buffer_experimental(); - rb_check_arity(argc, 0, 2); struct rb_io_buffer *buffer = NULL; From e354e9ba1007c10df3ee843bb9daabd89b47a708 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 17 Dec 2025 11:57:57 +0900 Subject: [PATCH 12/23] refactor: utilize a predefined macro --- iseq.c | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/iseq.c b/iseq.c index 90facfad78616c..7457118e07ce96 100644 --- a/iseq.c +++ b/iseq.c @@ -3711,11 +3711,7 @@ rb_iseq_parameters(const rb_iseq_t *iseq, int is_proc) if (is_proc) { for (i = 0; i < body->param.lead_num; i++) { - PARAM_TYPE(opt); - if (PARAM_ID(i) != idItImplicit && rb_id2str(PARAM_ID(i))) { - rb_ary_push(a, ID2SYM(PARAM_ID(i))); - } - rb_ary_push(args, a); + rb_ary_push(args, PARAM(i, opt)); } } else { @@ -3725,11 +3721,7 @@ rb_iseq_parameters(const rb_iseq_t *iseq, int is_proc) } r = body->param.lead_num + body->param.opt_num; for (; i < r; i++) { - PARAM_TYPE(opt); - if (rb_id2str(PARAM_ID(i))) { - rb_ary_push(a, ID2SYM(PARAM_ID(i))); - } - rb_ary_push(args, a); + rb_ary_push(args, PARAM(i, opt)); } if (body->param.flags.has_rest) { CONST_ID(rest, "rest"); @@ -3738,11 +3730,7 @@ rb_iseq_parameters(const rb_iseq_t *iseq, int is_proc) r = body->param.post_start + body->param.post_num; if (is_proc) { for (i = body->param.post_start; i < r; i++) { - PARAM_TYPE(opt); - if (rb_id2str(PARAM_ID(i))) { - rb_ary_push(a, ID2SYM(PARAM_ID(i))); - } - rb_ary_push(args, a); + rb_ary_push(args, PARAM(i, opt)); } } else { From 0e2962f917db1b20a6d34b6105b3768af8e692b8 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 17 Dec 2025 14:14:36 +0900 Subject: [PATCH 13/23] [ruby/io-wait] bump up to 0.4.0 https://github.com/ruby/io-wait/commit/ae676c9d6d --- ext/io/wait/io-wait.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/io/wait/io-wait.gemspec b/ext/io/wait/io-wait.gemspec index ef46fbca0f6930..c1c6172589efc5 100644 --- a/ext/io/wait/io-wait.gemspec +++ b/ext/io/wait/io-wait.gemspec @@ -1,4 +1,4 @@ -_VERSION = "0.4.0.dev" +_VERSION = "0.4.0" Gem::Specification.new do |spec| spec.name = "io-wait" From 4c38419eb81bc388eab0d50f8b2fe6e5edd1e4c7 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 17 Dec 2025 05:20:00 +0000 Subject: [PATCH 14/23] Update default gems list at 0e2962f917db1b20a6d34b6105b376 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 324c47909f4e20..479a49145ccfe0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -276,7 +276,7 @@ The following default gems are updated. * forwardable 1.4.0 * io-console 0.8.2 * io-nonblock 0.3.2 -* io-wait 0.4.0.dev +* io-wait 0.4.0 * ipaddr 1.2.8 * json 2.18.0 * net-http 0.8.0 From 7b5691c3b000c763faa282e1f73db96afa2ecae1 Mon Sep 17 00:00:00 2001 From: Misaki Shioi <31817032+shioimm@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:02:26 +0900 Subject: [PATCH 15/23] `Socket.tcp` and `TCPSocket.new` raises `IO::TiemoutError` with user specified timeout (#15602) * `Socket.tcp` and `TCPSocket.new` raises `IO::TiemoutError` with user specified timeout In https://github.com/ruby/ruby/pull/11880, `rsock_connect()` was changed to raise `IO::TimeoutError` when a user-specified timeout occurs. However, when `TCPSocket.new` attempts to connect to multiple destinations, it does not use `rsock_connect()`, and instead raises `Errno::ETIMEDOUT` on timeout. As a result, the exception class raised on timeout could differ depending on whether there were multiple destinations or not. To align this behavior with the implementation of `rsock_connect()`, this change makes `TCPSocket.new` raise `IO::TimeoutError` when a user-specified timeout occurs. Similarly, `Socket.tcp` is updated to raise `IO::TimeoutError` when a timeout occurs within the method. (Note that the existing behavior of `Addrinfo#connect_internal`, which Socket.tcp depends on internally and which raises `Errno::ETIMEDOUT` on timeout, is not changed.) * [ruby/net-http] Raise `Net::OpenTimeout` when `TCPSocket.open` raises `IO::TimeoutError`. With the changes in https://github.com/ruby/ruby/pull/15602, `TCPSocket.open` now raises `IO::TimeoutError` when a user-specified timeout occurs. This change updates #connect to handle this case accordingly. https://github.com/ruby/net-http/commit/f64109e1cf --- ext/socket/ipsocket.c | 4 +--- ext/socket/lib/socket.rb | 4 ++-- lib/net/http.rb | 4 +++- test/socket/test_socket.rb | 6 +++--- test/socket/test_tcp.rb | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ext/socket/ipsocket.c b/ext/socket/ipsocket.c index ba1b81b3ad864d..20d9101c11510d 100644 --- a/ext/socket/ipsocket.c +++ b/ext/socket/ipsocket.c @@ -29,9 +29,7 @@ struct inetsock_arg void rsock_raise_user_specified_timeout(void) { - VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno")); - VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT")); - rb_raise(etimedout_error, "user specified timeout"); + rb_raise(rb_eIOTimeoutError, "user specified timeout"); } static VALUE diff --git a/ext/socket/lib/socket.rb b/ext/socket/lib/socket.rb index 49fb3fcc6d9834..6d04ca2483a385 100644 --- a/ext/socket/lib/socket.rb +++ b/ext/socket/lib/socket.rb @@ -905,7 +905,7 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, end end - raise(Errno::ETIMEDOUT, 'user specified timeout') if expired?(now, user_specified_open_timeout_at) + raise(IO::TimeoutError, 'user specified timeout') if expired?(now, user_specified_open_timeout_at) if resolution_store.empty_addrinfos? if connecting_sockets.empty? && resolution_store.resolved_all_families? @@ -918,7 +918,7 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, if (expired?(now, user_specified_resolv_timeout_at) || resolution_store.resolved_all_families?) && (expired?(now, user_specified_connect_timeout_at) || connecting_sockets.empty?) - raise Errno::ETIMEDOUT, 'user specified timeout' + raise IO::TimeoutError, 'user specified timeout' end end end diff --git a/lib/net/http.rb b/lib/net/http.rb index c63bdddcad621e..994ddbc0693820 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1676,7 +1676,9 @@ def connect begin s = timeouted_connect(conn_addr, conn_port) rescue => e - e = Net::OpenTimeout.new(e) if e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions + if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions + e = Net::OpenTimeout.new(e) + end raise e, "Failed to open TCP connection to " + "#{conn_addr}:#{conn_port} (#{e.message})" end diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb index e746aca101f808..686114f05c1418 100644 --- a/test/socket/test_socket.rb +++ b/test/socket/test_socket.rb @@ -909,7 +909,7 @@ def test_tcp_socket_resolv_timeout Addrinfo.define_singleton_method(:getaddrinfo) { |*_| sleep } - assert_raise(Errno::ETIMEDOUT) do + assert_raise(IO::TimeoutError) do Socket.tcp("localhost", port, resolv_timeout: 0.01) end ensure @@ -934,7 +934,7 @@ def test_tcp_socket_resolv_timeout_with_connection_failure server.close - assert_raise(Errno::ETIMEDOUT) do + assert_raise(IO::TimeoutError) do Socket.tcp("localhost", port, resolv_timeout: 0.01) end RUBY @@ -951,7 +951,7 @@ def test_tcp_socket_open_timeout end end - assert_raise(Errno::ETIMEDOUT) do + assert_raise(IO::TimeoutError) do Socket.tcp("localhost", 12345, open_timeout: 0.01) end RUBY diff --git a/test/socket/test_tcp.rb b/test/socket/test_tcp.rb index 58fe44a279bcb4..d689ab23765c81 100644 --- a/test/socket/test_tcp.rb +++ b/test/socket/test_tcp.rb @@ -80,7 +80,7 @@ def test_tcp_initialize_open_timeout port = server.connect_address.ip_port server.close - assert_raise(Errno::ETIMEDOUT) do + assert_raise(IO::TimeoutError) do TCPSocket.new( "localhost", port, @@ -321,7 +321,7 @@ def test_initialize_resolv_timeout_with_connection_failure port = server.connect_address.ip_port server.close - assert_raise(Errno::ETIMEDOUT) do + assert_raise(IO::TimeoutError) do TCPSocket.new( "localhost", port, From 8850807eb1d8e6376a4f0dd99cb2f5e3e2988595 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 13:34:12 +0900 Subject: [PATCH 16/23] [ruby/psych] v5.3.1 https://github.com/ruby/psych/commit/8345af9ffb --- ext/psych/lib/psych/versions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/psych/lib/psych/versions.rb b/ext/psych/lib/psych/versions.rb index 942495db9e894e..4c7a80d5c84274 100644 --- a/ext/psych/lib/psych/versions.rb +++ b/ext/psych/lib/psych/versions.rb @@ -2,7 +2,7 @@ module Psych # The version of Psych you are using - VERSION = '5.3.0' + VERSION = '5.3.1' if RUBY_ENGINE == 'jruby' DEFAULT_SNAKEYAML_VERSION = '2.10'.freeze From 01624492ecb14d85e26023169bf8b36c6015c62d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 13:32:45 +0900 Subject: [PATCH 17/23] [ruby/time] v0.4.2 https://github.com/ruby/time/commit/387292f5d2 --- lib/time.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/time.rb b/lib/time.rb index 14bfe4c8fbc43b..e6aab3fa5d444b 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -27,7 +27,7 @@ # # class Time - VERSION = "0.4.1" # :nodoc: + VERSION = "0.4.2" # :nodoc: class << Time From 8e258121942e7c879bc11d221425ac39f73afe8c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 13:23:04 +0900 Subject: [PATCH 18/23] [ruby/timeout] v0.6.0 https://github.com/ruby/timeout/commit/ab79dfff47 --- lib/timeout.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/timeout.rb b/lib/timeout.rb index 9969fa2e57b5d7..5d1f61d8af2421 100644 --- a/lib/timeout.rb +++ b/lib/timeout.rb @@ -20,7 +20,7 @@ module Timeout # The version - VERSION = "0.5.0" + VERSION = "0.6.0" # Internal exception raised to when a timeout is triggered. class ExitException < Exception From d5257bea4811e54c006c4f2f56344d0d155638c9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 14:38:05 +0900 Subject: [PATCH 19/23] Bundle stringio-3.2.0 --- ext/stringio/stringio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 56fb044a3a299f..05bae94529b9db 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -13,7 +13,7 @@ **********************************************************************/ static const char *const -STRINGIO_VERSION = "3.1.9.dev"; +STRINGIO_VERSION = "3.2.0"; #include From df18f3baa658174ef89c8c2c11be4fea9bda4fc7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 14:38:55 +0900 Subject: [PATCH 20/23] Bundle strscan-3.1.6 --- ext/strscan/strscan.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index d63897dc61f35d..11e3d309a81757 100644 --- a/ext/strscan/strscan.c +++ b/ext/strscan/strscan.c @@ -22,7 +22,7 @@ extern size_t onig_region_memsize(const struct re_registers *regs); #include -#define STRSCAN_VERSION "3.1.6.dev" +#define STRSCAN_VERSION "3.1.6" #ifdef HAVE_RB_DEPRECATE_CONSTANT From fedafec78b2e6aa17a4246192a40192d7a0cf69c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 13:28:33 +0900 Subject: [PATCH 21/23] [ruby/net-http] v0.9.0 https://github.com/ruby/net-http/commit/3ccf0c8e6a --- lib/net/http.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/http.rb b/lib/net/http.rb index 994ddbc0693820..4ce8a2c874fad8 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -724,7 +724,7 @@ class HTTPHeaderSyntaxError < StandardError; end class HTTP < Protocol # :stopdoc: - VERSION = "0.8.0" + VERSION = "0.9.0" HTTPVersion = '1.1' begin require 'zlib' From b80fc8bd84d194fdab60d0aee14ce0850a366500 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 13 Dec 2025 17:30:21 +0900 Subject: [PATCH 22/23] [ruby/net-http] Freeze more constants for Ractor compatibility Freeze Net::HTTP::SSL_ATTRIBUTES and IDEMPOTENT_METHODS_. Both constants have been marked as :nodoc:. Together with https://github.com/ruby/openssl/issues/521, this enables HTTPS clients in non-main Ractors on Ruby 4.0. https://github.com/ruby/net-http/commit/f24b3b358b --- lib/net/http.rb | 4 ++-- test/net/http/test_http.rb | 25 +++++++++++++++++++++++++ test/net/http/test_https.rb | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/net/http.rb b/lib/net/http.rb index 4ce8a2c874fad8..3bed9dc3a994e5 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1531,7 +1531,7 @@ def use_ssl=(flag) :verify_depth, :verify_mode, :verify_hostname, - ] # :nodoc: + ].freeze # :nodoc: SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc: @@ -2430,7 +2430,7 @@ def send_entity(path, data, initheader, dest, type, &block) # :stopdoc: - IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc: + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: def transport_request(req) count = 0 diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 366b4cd12c8de5..4e7fa22756cd31 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -1400,3 +1400,28 @@ def test_partial_response assert_raise(EOFError) {http.get('/')} end end + +class TestNetHTTPInRactor < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def test_get + assert_ractor(<<~RUBY, require: 'net/http') + expected = #{$test_net_http_data.dump}.b + ret = Ractor.new { + host = #{config('host').dump} + port = #{config('port')} + Net::HTTP.start(host, port) { |http| + res = http.get('/') + res.body + } + }.value + assert_equal expected, ret + RUBY + end +end if defined?(Ractor) && Ractor.method_defined?(:value) diff --git a/test/net/http/test_https.rb b/test/net/http/test_https.rb index 55b9eb317016aa..f5b21b901ff1af 100644 --- a/test/net/http/test_https.rb +++ b/test/net/http/test_https.rb @@ -266,6 +266,24 @@ def test_max_version assert_match(re_msg, ex.message) end + def test_ractor + assert_ractor(<<~RUBY, require: 'net/https') + expected = #{$test_net_http_data.dump}.b + ret = Ractor.new { + host = #{HOST.dump} + port = #{config('port')} + ca_cert_pem = #{CA_CERT.to_pem.dump} + cert_store = OpenSSL::X509::Store.new.tap { |s| + s.add_cert(OpenSSL::X509::Certificate.new(ca_cert_pem)) + } + Net::HTTP.start(host, port, use_ssl: true, cert_store: cert_store) { |http| + res = http.get('/') + res.body + } + }.value + assert_equal expected, ret + RUBY + end if defined?(Ractor) && Ractor.method_defined?(:value) end class TestNetHTTPSIdentityVerifyFailure < Test::Unit::TestCase From 26447b3597ab95af7cc220c641a1bd58b235fec9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 17 Dec 2025 15:03:06 +0900 Subject: [PATCH 23/23] [ruby/net-http] v0.9.1 https://github.com/ruby/net-http/commit/8cee86e939 --- lib/net/http.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/http.rb b/lib/net/http.rb index 3bed9dc3a994e5..98d6793aee033d 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -724,7 +724,7 @@ class HTTPHeaderSyntaxError < StandardError; end class HTTP < Protocol # :stopdoc: - VERSION = "0.9.0" + VERSION = "0.9.1" HTTPVersion = '1.1' begin require 'zlib'