From 3680bcd506649bd6b7ef753742309c243826dd8f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 10:13:26 +0900 Subject: [PATCH 1/9] Suppress the Ruby::Box experimental warning in test subprocesses Under RUBY_BOX=1 every spawned ruby prints an experimental warning to stderr, which breaks tests asserting on subprocess output. Prepend -W:no-experimental to RUBYOPT for child processes and add a ruby_box_enabled? helper for box-specific guards. --- test/rubygems/helper.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 1fe8577b4fc9..44690f68555b 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -397,6 +397,10 @@ def setup ENV["BUNDLE_USER_HOME"] = nil ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = "true" + # Child ruby processes inherit RUBY_BOX and print an experimental + # warning on startup, breaking assertions on subprocess stderr. + ENV["RUBYOPT"] = [ENV["RUBYOPT"], "-W:no-experimental"].compact.join(" ") if ruby_box_enabled? + @current_dir = Dir.pwd @fetcher = nil @@ -1367,6 +1371,13 @@ def ruby_repo? !ENV["GEM_COMMAND"].nil? end + ## + # Is this test running under Ruby::Box (RUBY_BOX=1)? + + def ruby_box_enabled? + defined?(Ruby::Box) && Ruby::Box.enabled? + end + ## # Returns the make command for the current platform. For versions of Ruby # built on MS Windows with VC++ or Borland it will return 'nmake'. On all From 64481df749ef1d5a3cfd614d0236f0dba7314c91 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 10:13:36 +0900 Subject: [PATCH 2/9] Use Process.last_status instead of $? Ruby::Box leaves $? uninitialized, so exit status checks read exit 0 regardless of the real status. Under RUBY_BOX=1 this made Gem::Source::Git#rev_parse swallow git rev-parse failures, and made the cargo availability pend in the test suite misjudge a broken toolchain as usable. Process.last_status reports the real status in both modes. --- lib/bundler/gem_helper.rb | 2 +- lib/rubygems/source/git.rb | 3 ++- test/rubygems/test_exit.rb | 3 ++- test/rubygems/test_gem_ext_builder.rb | 2 +- test/rubygems/test_gem_ext_cargo_builder.rb | 2 +- test/rubygems/test_require.rb | 4 ++-- test/rubygems/test_rubygems.rb | 4 ++-- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index e3af1b957f35..ea02e7e7712b 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -216,7 +216,7 @@ def sh_with_status(cmd, &block) Bundler.ui.debug(cmd) SharedHelpers.chdir(base) do outbuf = IO.popen(cmd, err: [:child, :out], &:read) - status = $? + status = Process.last_status block&.call(outbuf) if status.success? [outbuf, status] end diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index baf2f9dd4c09..1e1b99b95fe5 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -188,9 +188,10 @@ def rev_parse # :nodoc: hash = Gem::Util.popen(git_command, "rev-parse", @reference).strip end + # Process.last_status instead of $?, which Ruby::Box leaves uninitialized raise Gem::Exception, "unable to find reference #{@reference} in #{@repository}" unless - $?.success? + Process.last_status.success? hash end diff --git a/test/rubygems/test_exit.rb b/test/rubygems/test_exit.rb index 396837edadfa..c339c39a67af 100644 --- a/test/rubygems/test_exit.rb +++ b/test/rubygems/test_exit.rb @@ -6,7 +6,8 @@ class TestGemExit < Gem::TestCase def test_exit system(*ruby_with_rubygems_in_load_path, "-e", "raise Gem::SystemExitException.new(2)") - assert_equal 2, $?.exitstatus + # Process.last_status instead of $?, which Ruby::Box leaves uninitialized + assert_equal 2, Process.last_status.exitstatus end def test_status diff --git a/test/rubygems/test_gem_ext_builder.rb b/test/rubygems/test_gem_ext_builder.rb index 8f90687ede30..3c773fe0f3f7 100644 --- a/test/rubygems/test_gem_ext_builder.rb +++ b/test/rubygems/test_gem_ext_builder.rb @@ -494,7 +494,7 @@ def self.expand(val, config = CONFIG); val; end system(Gem.ruby, "-rmkmf", "-e", "exit MakeMakefile::RbConfig::CONFIG['host_os'] == 'fake_os'", "--", "--target-rbconfig=#{fake_rbconfig}") end - unless $?.success? + unless Process.last_status.success? assert_include(stderr, "uninitialized constant MakeMakefile::RbConfig") pend "This version of mkmf does not support --target-rbconfig" end diff --git a/test/rubygems/test_gem_ext_cargo_builder.rb b/test/rubygems/test_gem_ext_cargo_builder.rb index b970e442c250..94673a478c4e 100644 --- a/test/rubygems/test_gem_ext_cargo_builder.rb +++ b/test/rubygems/test_gem_ext_cargo_builder.rb @@ -199,7 +199,7 @@ def skip_unsupported_platforms! pend "jruby not supported" if Gem.java_platform? pend "truffleruby not supported (yet)" if RUBY_ENGINE == "truffleruby" system(@rust_envs, "cargo", "-V", out: IO::NULL, err: [:child, :out]) - pend "cargo not present" unless $?.success? + pend "cargo not present" unless Process.last_status.success? pend "ruby.h is not provided by ruby repo" if ruby_repo? pend "rust toolchain of mingw is broken" if mingw_windows? end diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index db86a3090565..6816e42bfe13 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -484,7 +484,7 @@ def test_realworld_default_gem puts Gem.loaded_specs["json"] RUBY output = Gem::Util.popen(*ruby_with_rubygems_in_load_path, "-e", cmd).strip - assert $?.success? + assert Process.last_status.success? refute_empty output end @@ -508,7 +508,7 @@ def test_realworld_upgraded_default_gem assert_equal "999.99.9", output.lines[0].chomp # Make sure only files from the newer json gem are loaded, and no files from the default json gem assert_equal ["#{@gemhome}/gems/json-999.99.9/lib/json.rb"], output.lines.grep(%r{/gems/json-}).map(&:chomp) - assert $?.success? + assert Process.last_status.success? end def test_default_gem_and_normal_gem diff --git a/test/rubygems/test_rubygems.rb b/test/rubygems/test_rubygems.rb index 6566b5981e69..02393c57fcae 100644 --- a/test/rubygems/test_rubygems.rb +++ b/test/rubygems/test_rubygems.rb @@ -5,7 +5,7 @@ class GemTest < Gem::TestCase def test_rubygems_normal_behaviour _ = Gem::Util.popen(*ruby_with_rubygems_in_load_path, "-e", "'require \"rubygems\"'", { err: [:child, :out] }).strip - assert $?.success? + assert Process.last_status.success? end def test_operating_system_other_exceptions @@ -17,7 +17,7 @@ def test_operating_system_other_exceptions RUBY output = Gem::Util.popen(*ruby_with_rubygems_and_fake_operating_system_in_load_path(path), "-e", "'require \"rubygems\"'", { err: [:child, :out] }).strip - assert !$?.success? + assert !Process.last_status.success? assert_match(/undefined local variable or method [`']intentionally_not_implemented_method'/, output) assert_includes output, "Loading the #{operating_system_rb_at(path)} file caused an error. " \ "This file is owned by your OS, not by rubygems upstream. " \ From 3692d9012e0abdaf9eca58d1c36be5fa39bca0a8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 10:13:55 +0900 Subject: [PATCH 3/9] Pend tests that capture stdio under Ruby::Box Ruby::Box gives each box detached copies of the stdio globals, so reassigning or reopening $stdout/$stderr cannot capture output written by Kernel#warn, Kernel#puts or subprocesses, and $VERBOSE = nil cannot silence category warnings in assert_ractor children. Pend the affected tests until https://bugs.ruby-lang.org/issues/21867 is resolved. --- test/rubygems/helper.rb | 11 +++++++++++ test/rubygems/test_deprecate.rb | 3 +++ test/rubygems/test_gem.rb | 2 ++ test/rubygems/test_gem_commands_build_command.rb | 1 + test/rubygems/test_gem_commands_open_command.rb | 1 + test/rubygems/test_gem_config_file.rb | 1 + test/rubygems/test_gem_package.rb | 2 ++ test/rubygems/test_gem_package_tar_header_ractor.rb | 5 +++++ test/rubygems/test_gem_request_set.rb | 2 ++ .../test_gem_request_set_gem_dependency_api.rb | 3 +++ test/rubygems/test_gem_specification.rb | 5 +++++ test/rubygems/test_gem_stub_specification.rb | 1 + test/rubygems/test_require.rb | 4 ++++ 13 files changed, 41 insertions(+) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 44690f68555b..80df3b7d49b1 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1378,6 +1378,17 @@ def ruby_box_enabled? defined?(Ruby::Box) && Ruby::Box.enabled? end + ## + # Ruby::Box gives each box detached copies of the stdio globals, so + # reassigning $stdout/$stderr cannot capture output written by Kernel#warn, + # Kernel#puts or subprocesses. Pends until the ruby-core fix for + # https://bugs.ruby-lang.org/issues/21867 lands. + + def pend_for_ruby_box_stdio_capture + pend "Ruby::Box breaks $stdout/$stderr capture (https://bugs.ruby-lang.org/issues/21867)" if ruby_box_enabled? + end + + ## # Returns the make command for the current platform. For versions of Ruby # built on MS Windows with VC++ or Borland it will return 'nmake'. On all diff --git a/test/rubygems/test_deprecate.rb b/test/rubygems/test_deprecate.rb index bb6a0b5ceaaf..5700d356e756 100644 --- a/test/rubygems/test_deprecate.rb +++ b/test/rubygems/test_deprecate.rb @@ -132,6 +132,7 @@ def test_deprecated_method_calls_the_old_method end def test_deprecated_method_outputs_a_warning + pend_for_ruby_box_stdio_capture out, err = capture_output do thing = Thing.new thing.foo @@ -165,6 +166,7 @@ def execute end def test_deprecated_method_outputs_a_warning_old_way + pend_for_ruby_box_stdio_capture out, err = capture_output do thing = OtherThing.new thing.foo @@ -180,6 +182,7 @@ def test_deprecated_method_outputs_a_warning_old_way end def test_deprecated_method_when_class_overrides_format + pend_for_ruby_box_stdio_capture out, err = capture_output do thing = ThingWithFormat.new thing.foo diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index c81b0b0547ae..7862ef5f4d2f 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -1291,6 +1291,7 @@ def test_self_try_activate_missing_prerelease end def test_self_try_activate_missing_extensions + pend_for_ruby_box_stdio_capture spec = util_spec "ext", "1" do |s| s.extensions = %w[ext/extconf.rb] s.installed_by_version = v("2.2") @@ -1346,6 +1347,7 @@ def test_setting_paths_does_not_mutate_parameter_object end def test_deprecated_paths= + pend_for_ruby_box_stdio_capture stdout, stderr = capture_output do Gem.paths = { "GEM_HOME" => Gem.paths.home, "GEM_PATH" => [Gem.paths.home, "foo"] } diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index cd88421c0754..a46a6447ee34 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -195,6 +195,7 @@ def test_execute_strict_with_warnings end def test_execute_bad_spec + pend_for_ruby_box_stdio_capture @gem.date = "2010-11-08" gemspec_file = File.join(@tempdir, @gem.spec_name) diff --git a/test/rubygems/test_gem_commands_open_command.rb b/test/rubygems/test_gem_commands_open_command.rb index 3a774a9343c0..c30117a58ecc 100644 --- a/test/rubygems/test_gem_commands_open_command.rb +++ b/test/rubygems/test_gem_commands_open_command.rb @@ -21,6 +21,7 @@ def gem(name, version = "1.0") end def test_execute + pend_for_ruby_box_stdio_capture omit "JRuby on Windows spawns the editor with a different cwd" if Gem.win_platform? && Gem.java_platform? @cmd.options[:args] = %w[foo] diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index 2c33192a4b3a..7120c49e327a 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -314,6 +314,7 @@ def test_handle_arguments_backtrace end def test_handle_arguments_debug + pend_for_ruby_box_stdio_capture assert_equal false, $DEBUG args = %w[--debug] diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index 7b8ac4736d2f..d70e8f60a819 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -1077,6 +1077,7 @@ def test_verify_corrupt end def test_verify_corrupt_tar_metadata_entry + pend_for_ruby_box_stdio_capture gem = tar_file_header("metadata.gz", "", 0, 999, Time.now) File.open "corrupt.gem", "wb" do |io| @@ -1113,6 +1114,7 @@ def test_verify_corrupt_tar_checksums_entry end def test_verify_corrupt_tar_data_entry + pend_for_ruby_box_stdio_capture gem = tar_file_header("data.tar.gz", "", 0, 100, Time.now) File.open "corrupt.gem", "wb" do |io| diff --git a/test/rubygems/test_gem_package_tar_header_ractor.rb b/test/rubygems/test_gem_package_tar_header_ractor.rb index 57140648052e..676822f79bc3 100644 --- a/test/rubygems/test_gem_package_tar_header_ractor.rb +++ b/test/rubygems/test_gem_package_tar_header_ractor.rb @@ -8,6 +8,11 @@ end class TestGemPackageTarHeaderRactor < Gem::Package::TarTestCase + def setup + super + pend "Ruby::Box ignores $VERBOSE=, so assert_ractor cannot keep the Ractor experimental warning out of the child stderr (ruby-core bug, same family as https://bugs.ruby-lang.org/issues/21867)" if ruby_box_enabled? + end + SETUP = <<~RUBY header = { name: "x", diff --git a/test/rubygems/test_gem_request_set.rb b/test/rubygems/test_gem_request_set.rb index 8c8be04fb9f9..60ff8724aef4 100644 --- a/test/rubygems/test_gem_request_set.rb +++ b/test/rubygems/test_gem_request_set.rb @@ -71,6 +71,7 @@ def test_install_from_gemdeps end def test_install_from_gemdeps_explain + pend_for_ruby_box_stdio_capture spec_fetcher do |fetcher| fetcher.gem "a", 2 end @@ -94,6 +95,7 @@ def test_install_from_gemdeps_explain end def test_install_from_gemdeps_explain_verbose + pend_for_ruby_box_stdio_capture spec_fetcher do |fetcher| fetcher.gem "a", 2 end diff --git a/test/rubygems/test_gem_request_set_gem_dependency_api.rb b/test/rubygems/test_gem_request_set_gem_dependency_api.rb index 4b5eaa38eda8..d8f4e7f6e92b 100644 --- a/test/rubygems/test_gem_request_set_gem_dependency_api.rb +++ b/test/rubygems/test_gem_request_set_gem_dependency_api.rb @@ -78,6 +78,7 @@ def test_gem end def test_gem_duplicate + pend_for_ruby_box_stdio_capture @gda.gem "a" _, err = capture_output do @@ -128,6 +129,7 @@ def test_gem_bitbucket_expand_path end def test_gem_git_branch + pend_for_ruby_box_stdio_capture _, err = capture_output do @gda.gem "a", git: "git/a", branch: "other", tag: "v1" end @@ -149,6 +151,7 @@ def test_gem_git_gist end def test_gem_git_ref + pend_for_ruby_box_stdio_capture _, err = capture_output do @gda.gem "a", git: "git/a", ref: "abcd123", branch: "other" end diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index c63e68be47dd..fba0bc36e334 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -1575,6 +1575,7 @@ def test_contains_requirable_file_eh end def test_contains_requirable_file_eh_extension + pend_for_ruby_box_stdio_capture ext_spec _, err = capture_output do @@ -3180,6 +3181,7 @@ def test_validate_files end def test_unresolved_specs + pend_for_ruby_box_stdio_capture specification = Gem::Specification.clone set_orig specification @@ -3206,6 +3208,7 @@ def test_unresolved_specs end def test_unresolved_specs_with_versions + pend_for_ruby_box_stdio_capture specification = Gem::Specification.clone set_orig specification @@ -3238,6 +3241,7 @@ def test_unresolved_specs_with_versions end def test_unresolved_specs_with_duplicated_versions + pend_for_ruby_box_stdio_capture specification = Gem::Specification.clone set_orig specification @@ -3291,6 +3295,7 @@ def test_unresolved_specs_with_unrestricted_deps_on_default_gems end def test_duplicate_runtime_dependency + pend_for_ruby_box_stdio_capture expected = "WARNING: duplicated b dependency [\"~> 3.0\", \"~> 3.0\"]\n" out, err = capture_output do @a1.add_dependency "b", "~> 3.0", "~> 3.0" diff --git a/test/rubygems/test_gem_stub_specification.rb b/test/rubygems/test_gem_stub_specification.rb index 744ffa7d059e..bbdd96c6cb63 100644 --- a/test/rubygems/test_gem_stub_specification.rb +++ b/test/rubygems/test_gem_stub_specification.rb @@ -66,6 +66,7 @@ def test_contains_requirable_file_eh end def test_contains_requirable_file_eh_extension + pend_for_ruby_box_stdio_capture stub_with_extension do |stub| _, err = capture_output do if RUBY_ENGINE == "jruby" diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index 6816e42bfe13..ef1bb2e465a2 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -718,6 +718,7 @@ def test_require_bundler ["", "Kernel."].each do |prefix| define_method "test_no_kernel_require_in_#{prefix.tr(".", "_")}warn_with_uplevel" do + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/sub.rb", "#{prefix}warn 'uplevel', 'test', uplevel: 1\n") File.write(dir + "/main.rb", "require 'sub'\n") @@ -733,6 +734,7 @@ def test_require_bundler end define_method "test_no_other_behavioral_changes_with_#{prefix.tr(".", "_")}warn" do + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/main.rb", "#{prefix}warn({x:1}, {y:2}, [])\n") _, err = capture_subprocess_io do @@ -748,6 +750,7 @@ def test_require_bundler end def test_no_crash_when_overriding_warn_with_warning_module + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/main.rb", "module Warning; def warn(str); super; end; end; warn 'Foo Bar'") _, err = capture_subprocess_io do @@ -762,6 +765,7 @@ def test_no_crash_when_overriding_warn_with_warning_module end def test_expected_backtrace_location_when_inheriting_from_basic_object_and_including_kernel + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/main.rb", "\nrequire 'sub'\n") File.write(dir + "/sub.rb", <<-'RUBY') From 47e1282a00f16d61d23f044ba9d5afe727183172 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 10:14:03 +0900 Subject: [PATCH 4/9] Pend Marshal round-trip tests under Ruby::Box Marshal in the main box cannot resolve Gem:: and other boxed constants, so loading dumps of Gem::Specification, Gem::Version or Date raises ArgumentError under RUBY_BOX=1. Pend the affected tests until https://bugs.ruby-lang.org/issues/22090 is resolved. --- test/rubygems/helper.rb | 8 ++++++++ .../test_gem_commands_specification_command.rb | 1 + test/rubygems/test_gem_dependency_installer.rb | 2 ++ test/rubygems/test_gem_requirement.rb | 1 + test/rubygems/test_gem_safe_marshal.rb | 10 +++++++++- test/rubygems/test_gem_specification.rb | 5 +++++ 6 files changed, 26 insertions(+), 1 deletion(-) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 80df3b7d49b1..83f5b111fa4b 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1388,6 +1388,14 @@ def pend_for_ruby_box_stdio_capture pend "Ruby::Box breaks $stdout/$stderr capture (https://bugs.ruby-lang.org/issues/21867)" if ruby_box_enabled? end + ## + # Under Ruby::Box, Marshal in the main box cannot resolve Gem:: (and other + # boxed) constants. Pends until the ruby-core fix for + # https://bugs.ruby-lang.org/issues/22090 lands. + + def pend_for_ruby_box_marshal + pend "Marshal cannot resolve boxed constants under Ruby::Box (https://bugs.ruby-lang.org/issues/22090)" if ruby_box_enabled? + end ## # Returns the make command for the current platform. For versions of Ruby diff --git a/test/rubygems/test_gem_commands_specification_command.rb b/test/rubygems/test_gem_commands_specification_command.rb index 454d6ea1c6f7..ee96eaa892a5 100644 --- a/test/rubygems/test_gem_commands_specification_command.rb +++ b/test/rubygems/test_gem_commands_specification_command.rb @@ -137,6 +137,7 @@ def test_execute_file end def test_execute_marshal + pend_for_ruby_box_marshal foo = util_spec "foo", "2" install_specs foo diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index 27fad9513536..1c189911b4c7 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -1103,6 +1103,7 @@ def test_install_version_default end def test_install_legacy_spec_with_nil_required_ruby_version + pend_for_ruby_box_marshal path = File.expand_path "data/null-required-ruby-version.gemspec.rz", __dir__ spec = Marshal.load Gem.read_binary(path) def spec.validate(*args); end @@ -1128,6 +1129,7 @@ def spec.validate(*args); end end def test_install_legacy_spec_with_nil_required_rubygems_version + pend_for_ruby_box_marshal path = File.expand_path "data/null-required-rubygems-version.gemspec.rz", __dir__ spec = Marshal.load Gem.read_binary(path) def spec.validate(*args); end diff --git a/test/rubygems/test_gem_requirement.rb b/test/rubygems/test_gem_requirement.rb index 00634dc7f43f..bd3988b32300 100644 --- a/test/rubygems/test_gem_requirement.rb +++ b/test/rubygems/test_gem_requirement.rb @@ -431,6 +431,7 @@ def self.exploit(arg) end def test_marshal_load_attack + pend_for_ruby_box_marshal wa = Gem::Net::WriteAdapter.allocate wa.instance_variable_set(:@socket, self.class) wa.instance_variable_set(:@method_id, :exploit) diff --git a/test/rubygems/test_gem_safe_marshal.rb b/test/rubygems/test_gem_safe_marshal.rb index 1937c02be0cb..806fb3149f2b 100644 --- a/test/rubygems/test_gem_safe_marshal.rb +++ b/test/rubygems/test_gem_safe_marshal.rb @@ -317,10 +317,12 @@ def test_array_subclass end def test_frozen_object + pend_for_ruby_box_marshal assert_safe_load_as Gem::Version.new("1.abc").freeze end def test_date + pend_for_ruby_box_marshal assert_safe_load_as Date.new(1994, 12, 9) end @@ -369,6 +371,7 @@ def test_gem_spec_unmarshall_license end def test_gem_spec_unmarshall_required_ruby_rubygems_version + pend_for_ruby_box_marshal spec = Gem::Specification.new do |s| s.name = "hi" s.version = "1.2.3" @@ -500,7 +503,12 @@ def test_date_user_defined_rejected def assert_safe_load_marshal(dumped, additional_methods: [], permitted_ivars: nil, equality: true, marshal_dump_equality: true, inspect: true, to_s: true) - loaded = Marshal.load(dumped) + loaded = begin + Marshal.load(dumped) + rescue ArgumentError => e + pend_for_ruby_box_marshal if e.message.include?("undefined class/module") + raise + end safe_loaded = assert_nothing_raised("dumped: #{dumped.b.inspect} loaded: #{loaded.inspect}") do if permitted_ivars diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index fba0bc36e334..f01b5534331b 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -710,6 +710,7 @@ def test_self_attribute_names end def test_self__load_future + pend_for_ruby_box_marshal spec = Gem::Specification.new spec.name = "a" spec.version = "1" @@ -1064,6 +1065,7 @@ def test_self_stubs_returns_only_specified_named_specs end def test_handles_private_null_type + pend_for_ruby_box_marshal yaml_defined = Object.const_defined?("YAML") path = File.expand_path "data/pry-0.4.7.gemspec.rz", __dir__ @@ -1076,6 +1078,7 @@ def test_handles_private_null_type end def test_handles_dependencies_with_syck_requirements_bug + pend_for_ruby_box_marshal yaml_defined = Object.const_defined?("YAML") path = File.expand_path "data/excon-0.7.7.gemspec.rz", __dir__ @@ -1281,6 +1284,7 @@ def test_set_version_to_nil_after_setting_version end def test__dump + pend_for_ruby_box_marshal @a2.platform = Gem::Platform.local @a2.instance_variable_set :@original_platform, "old_platform" @@ -3794,6 +3798,7 @@ def test_version_change_reset_cache_file end def test__load_fixes_Date_objects + pend_for_ruby_box_marshal spec = util_spec "a", 1 spec.instance_variable_set :@date, Date.today From 3b2655a97861757b1e2b4515d7a0ab60729c9f33 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 11:42:52 +0900 Subject: [PATCH 5/9] Add a RUBY_BOX=1 CI lane on ruby-head The suite is green under RUBY_BOX=1 now, so run it on ruby-head as a canary to catch regressions on either side early. --- .github/workflows/rubygems.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rubygems.yml b/.github/workflows/rubygems.yml index efa2a7ffeeb6..2e83ad6f2f09 100644 --- a/.github/workflows/rubygems.yml +++ b/.github/workflows/rubygems.yml @@ -60,6 +60,10 @@ jobs: - ruby: { name: no symlinks, value: 4.0.5 } os: { name: Windows, value: windows-2025 } symlink: off + + - ruby: { name: "head (RUBY_BOX=1)", value: head } + os: { name: Ubuntu, value: ubuntu-24.04 } + ruby_box: true env: RUBYGEMS_USE_PSYCH: ${{ matrix.use_psych || 'false' }} @@ -79,7 +83,10 @@ jobs: run: bin/rake setup - name: Run Test run: bin/rake test - if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby' && matrix.symlink != 'off' + if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby' && matrix.symlink != 'off' && !matrix.ruby_box + - name: Run Test (RUBY_BOX=1) + run: RUBY_BOX=1 bin/rake test + if: matrix.ruby_box - name: Run Test isolatedly run: bin/rake test:isolated if: matrix.ruby.name == '3.4' && matrix.os.name != 'Windows' From eb392aabd8a7d3935e5e6c4ddc2580f4a674c862 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 13:19:06 +0900 Subject: [PATCH 6/9] Fix the RUBY_BOX=1 lane failures yamllint rejects the redundantly quoted matrix name. The cargo tests require the installed gem with -r, which bypasses gem activation under RUBY_BOX=1 because the command line require does not go through the RubyGems Kernel#require override, so require inside -e instead. --- .github/workflows/rubygems.yml | 2 +- test/rubygems/test_gem_ext_cargo_builder.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rubygems.yml b/.github/workflows/rubygems.yml index 2e83ad6f2f09..b1f8f4e28217 100644 --- a/.github/workflows/rubygems.yml +++ b/.github/workflows/rubygems.yml @@ -61,7 +61,7 @@ jobs: os: { name: Windows, value: windows-2025 } symlink: off - - ruby: { name: "head (RUBY_BOX=1)", value: head } + - ruby: { name: head (RUBY_BOX=1), value: head } os: { name: Ubuntu, value: ubuntu-24.04 } ruby_box: true env: diff --git a/test/rubygems/test_gem_ext_cargo_builder.rb b/test/rubygems/test_gem_ext_cargo_builder.rb index 94673a478c4e..ae94475f65ae 100644 --- a/test/rubygems/test_gem_ext_cargo_builder.rb +++ b/test/rubygems/test_gem_ext_cargo_builder.rb @@ -111,7 +111,8 @@ def test_full_integration Open3.capture2e(*gem, "build", "rust_ruby_example.gemspec", "--output", built_gem) Open3.capture2e(*gem, "install", "--verbose", "--local", built_gem, *ARGV) - stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-rrust_ruby_example", "-e", "puts 'Result: ' + RustRubyExample.reverse('hello world')") + # Require inside -e because -r bypasses gem activation under RUBY_BOX=1 + stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-e", "require 'rust_ruby_example'; puts 'Result: ' + RustRubyExample.reverse('hello world')") assert status.success?, stdout_and_stderr_str assert_match "Result: #{"hello world".reverse}", stdout_and_stderr_str end @@ -134,7 +135,7 @@ def test_custom_name Open3.capture2e(*gem, "install", "--verbose", "--local", built_gem, *ARGV) end - stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-rcustom_name", "-e", "puts 'Result: ' + CustomName.say_hello") + stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-e", "require 'custom_name'; puts 'Result: ' + CustomName.say_hello") assert status.success?, stdout_and_stderr_str assert_match "Result: Hello world!", stdout_and_stderr_str From 861044c050cc0bd763b9939e4ec8280f3f0f68ea Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 14:14:04 +0900 Subject: [PATCH 7/9] Filter Ruby::Box experimental warnings from captured spec stderr Under RUBY_BOX every ruby subprocess spawned by the specs prints a two-line experimental warning to stderr, breaking specs that assert clean stderr. Strip those lines when the host ruby has boxes enabled, instead of injecting -W:no-experimental into RUBYOPT, because several specs assert the exact RUBYOPT propagated to subprocesses. --- spec/support/command_execution.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/support/command_execution.rb b/spec/support/command_execution.rb index e2915b996d9d..14da54dca4da 100644 --- a/spec/support/command_execution.rb +++ b/spec/support/command_execution.rb @@ -2,6 +2,13 @@ module Spec class CommandExecution + # Under RUBY_BOX, every spawned ruby prints an experimental warning to + # stderr, breaking specs that assert clean stderr. + RUBY_BOX_WARNING = Regexp.union( + /^[^\n]*: warning: Ruby::Box is experimental, and the behavior may change in the future!\n?/, + %r{^See https://docs\.ruby-lang\.org/\S+ for known issues, etc\.\n?} + ) + def initialize(command, timeout:) @command = command @timeout = timeout @@ -72,7 +79,13 @@ def failure? attr_reader :failure_reason def normalize(string) - string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n") + string = string.dup.force_encoding(Encoding::UTF_8).scrub.gsub("\r\n", "\n") + string = string.gsub(RUBY_BOX_WARNING, "") if ruby_box_enabled? + string.strip + end + + def ruby_box_enabled? + defined?(Ruby::Box) && Ruby::Box.enabled? end end end From 2c7725c78abc847e52ca37178362e4a25638caad Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 14:47:00 +0900 Subject: [PATCH 8/9] Scope the File.expand_path stub in shared_helpers_spec The special-characters and whitespace contexts stubbed File.expand_path for every argument. Under RUBY_BOX=1 with the turbo_tests workers, Bundler::Settings is still unloaded when these examples run, and reading the repository .bundle/config triggers a lazy require of rubygems/yaml_serializer. The default gem activation path in Kernel#require then resolves paths through the stub and crashes with Errno::ENOENT on a fake gemspec path. Stub only the expansion of bundler/setup, which is all these examples need faked. --- spec/bundler/shared_helpers_spec.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/bundler/shared_helpers_spec.rb b/spec/bundler/shared_helpers_spec.rb index 1619b0a14a59..232da4143a24 100644 --- a/spec/bundler/shared_helpers_spec.rb +++ b/spec/bundler/shared_helpers_spec.rb @@ -387,7 +387,11 @@ before do ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup" - allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup") + # Only fake the resolution of bundler/setup itself. A blanket stub + # breaks unrelated RubyGems path lookups triggered lazily inside the + # example, see #set_rubyopt. + allow(File).to receive(:expand_path).and_call_original + allow(File).to receive(:expand_path).with("setup", anything).and_return("#{install_path}/bundler/setup") allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup") end @@ -403,7 +407,8 @@ let(:install_path) { "/opt/ruby with space/lib" } before do - allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup") + allow(File).to receive(:expand_path).and_call_original + allow(File).to receive(:expand_path).with("setup", anything).and_return("#{install_path}/bundler/setup") allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup") end From 5f2d4815dc283d05265f481f00463753d1ace990 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 15:33:20 +0900 Subject: [PATCH 9/9] Skip the RUBYOPT -r native extension example under Ruby::Box The example passes RUBYOPT=-rnative_child to the bundle install subprocess, but under RUBY_BOX=1 require_libraries_in_main_box calls rb_require_string directly and bypasses the RubyGems Kernel#require override, so the gem is never activated and the require raises LoadError. This is an unreported ruby-core bug related to [Bug #21760] item 3, and the scenario itself is legitimate, so skip it until the Ruby::Box require semantics are settled upstream. --- spec/commands/install_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/commands/install_spec.rb b/spec/commands/install_spec.rb index a0b56a3d2970..dc9efb11e57f 100644 --- a/spec/commands/install_spec.rb +++ b/spec/commands/install_spec.rb @@ -2063,6 +2063,10 @@ def run end it "preserves bundled native extensions when BUNDLE_CLEAN removes another gem" do + # The command-line/RUBYOPT -r bypasses gem activation under RUBY_BOX=1, a + # ruby-core bug (unreported, related to [Bug #21760] item 3) + skip "-r cannot activate gems under Ruby::Box" if defined?(Ruby::Box) && Ruby::Box.enabled? + build_repo4 do build_gem "native_child", "1.0", &:add_c_extension build_gem "native_parent", "1.0" do |s|