From 6774d2d2e8ce2571f6f5154df3fdbc539c86d3da Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 11 Sep 2026 08:49:48 +0900 Subject: [PATCH 01/11] Fix the SYNTAX_SUGGEST_DEBUG spec on Windows cmd.exe does not understand the POSIX `VAR=value command` form and tried to run SYNTAX_SUGGEST_DEBUG as a command. Co-Authored-By: Claude Opus 5 --- spec/syntax_suggest/integration/ruby_command_line_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/syntax_suggest/integration/ruby_command_line_spec.rb b/spec/syntax_suggest/integration/ruby_command_line_spec.rb index 761bd760b5d56a..1cd0615507e0ce 100644 --- a/spec/syntax_suggest/integration/ruby_command_line_spec.rb +++ b/spec/syntax_suggest/integration/ruby_command_line_spec.rb @@ -208,7 +208,7 @@ def lol puts "haha" EOM - out = `SYNTAX_SUGGEST_DEBUG=1 #{ruby} -I#{lib_dir} -rsyntax_suggest -r#{monkeypatch} #{script} 2>&1` + out = IO.popen({"SYNTAX_SUGGEST_DEBUG" => "1"}, "#{ruby} -I#{lib_dir} -rsyntax_suggest -r#{monkeypatch} #{script} 2>&1", &:read) expect($?.success?).to be_falsey expect(out).to include("boom from monkeypatch") From f21fbbd02922e11346d4d4f97389571a3806f47b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 10 Sep 2026 19:53:31 +0900 Subject: [PATCH 02/11] Remove obsolete GC compaction skip guards for s390x Compaction has been suppressed on s390x since 2021 because the tests randomly stalled past the 1800s output timeout. That no longer happens: on s390x.rubyci.org these all pass individually, inside a full serial test-all, and across a 100-run soak under CPU saturation, with GC.compact and GC.verify_compaction_references both behaving correctly. https://github.com/ruby/ruby/pull/5077 Co-Authored-By: Claude Opus 5 --- test/ruby/test_array.rb | 1 - test/ruby/test_enumerator.rb | 2 -- test/ruby/test_eval.rb | 1 - test/ruby/test_exception.rb | 2 -- test/ruby/test_gc_compact.rb | 5 ----- test/ruby/test_method.rb | 1 - test/ruby/test_regexp.rb | 3 --- test/ruby/test_string.rb | 5 ----- test/ruby/test_symbol.rb | 2 -- test/ruby/test_variable.rb | 1 - test/ruby/test_weakkeymap.rb | 1 - test/ruby/test_weakmap.rb | 1 - tool/lib/envutil.rb | 2 -- 13 files changed, 27 deletions(-) diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 59ddc8c9bf468b..8f05c83ebc2d18 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1821,7 +1821,6 @@ def test_slice_out_of_range end def test_slice_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress { assert_equal([1, 2, 3, 4, 5], (0..10).to_a[1, 5]) } EnvUtil.under_gc_compact_stress do a = [0, 1, 2, 3, 4, 5] diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb index 9b972d7b22e1aa..7a461ff2982d0c 100644 --- a/test/ruby/test_enumerator.rb +++ b/test/ruby/test_enumerator.rb @@ -128,7 +128,6 @@ def test_with_index end def test_with_index_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do assert_equal([[1, 0], [2, 1], [3, 2]], @obj.to_enum(:foo, 1, 2, 3).with_index.to_a) assert_equal([[1, 5], [2, 6], [3, 7]], @obj.to_enum(:foo, 1, 2, 3).with_index(5).to_a) @@ -864,7 +863,6 @@ def test_lazy_chain end def test_lazy_chain_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do ea = (10..).lazy.select(&:even?).take(10) ed = (20..).lazy.select(&:odd?) diff --git a/test/ruby/test_eval.rb b/test/ruby/test_eval.rb index d2145bec5d9f13..b880b03e08f5d8 100644 --- a/test/ruby/test_eval.rb +++ b/test/ruby/test_eval.rb @@ -639,7 +639,6 @@ def test_syntax_error_no_memory_leak def test_outer_local_variable_under_gc_compact_stress omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) - omit "compaction is not supported on s390x" if /s390x/ =~ RUBY_PLATFORM assert_separately([], <<~RUBY) o = Object.new diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 9790f9ec9c46fd..0562897afe47b7 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -1478,8 +1478,6 @@ def test_detailed_message end def test_detailed_message_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 - # The first error display lazily requires did_you_mean and friends; inside the # block that library load costs one full mark+compact per allocation, enough to # trip the parallel runner's no-response timeout. Load it here instead. diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index c4b49b9cd653fa..3e5c6f2aaabf30 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true require 'test/unit' -if RUBY_PLATFORM =~ /s390x/ - warn "Currently, it is known that the compaction does not work well on s390x; contribution is welcome https://github.com/ruby/ruby/pull/5077" - return -end - class TestGCCompact < Test::Unit::TestCase module CompactionSupportInspector def supports_compact? diff --git a/test/ruby/test_method.rb b/test/ruby/test_method.rb index cf21d9942835b4..6285c9e1a410bf 100644 --- a/test/ruby/test_method.rb +++ b/test/ruby/test_method.rb @@ -492,7 +492,6 @@ def m.bar; :bar; end end def test_clone_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do o = Object.new def o.foo; :foo; end diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb index 0518f5828e6c4e..364c1fd8059ba4 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -73,7 +73,6 @@ def test_to_s end def test_to_s_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do str = "abcd\u3042" [:UTF_16BE, :UTF_16LE, :UTF_32BE, :UTF_32LE].each do |es| @@ -471,7 +470,6 @@ def test_inspect end def test_inspect_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do assert_equal('/(?-mix:\\/)|/', Regexp.union(/\//, "").inspect) end @@ -904,7 +902,6 @@ def test_match end def test_match_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do m = /(?.)(?[^aeiou])?(?.+)/.match("hoge\u3042") assert_equal("h", m.match(:foo)) diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index cc7e07fb363f34..2367d6bfd48a1b 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -926,7 +926,6 @@ def test_undump end def test_undump_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 a = S("Test") << 1 << 2 << 3 << 9 << 13 << 10 EnvUtil.under_gc_compact_stress do assert_equal(a, S('"Test\\x01\\x02\\x03\\t\\r\\n"').undump) @@ -1451,7 +1450,6 @@ def test_gsub end def test_gsub_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress { assert_equal(S("hll"), S("hello").gsub(/([aeiou])/, S('<\1>'))) } end @@ -1499,7 +1497,6 @@ def test_gsub! end def test_gsub_bang_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do a = S("hello") a.gsub!(/([aeiou])/, S('<\1>')) @@ -1870,7 +1867,6 @@ def test_scan end def test_scan_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress { assert_equal([["1a"], ["2b"], ["3c"]], S("1a2b3c").scan(/(\d.)/)) } end @@ -2418,7 +2414,6 @@ def o.to_s; self; end end def test_sub_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress do m = /&(?.*?);/.match(S("aaa & yyy")) assert_equal("amp", m["foo"]) diff --git a/test/ruby/test_symbol.rb b/test/ruby/test_symbol.rb index fa65dca22508b5..15f1eab78ef2d4 100644 --- a/test/ruby/test_symbol.rb +++ b/test/ruby/test_symbol.rb @@ -122,8 +122,6 @@ def test_inspect end def test_inspect_under_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 - EnvUtil.under_gc_compact_stress do assert_inspect_evaled(':testing') end diff --git a/test/ruby/test_variable.rb b/test/ruby/test_variable.rb index e138633536dcd9..c7addcf77c162a 100644 --- a/test/ruby/test_variable.rb +++ b/test/ruby/test_variable.rb @@ -521,7 +521,6 @@ def test_external_ivars end def test_exivar_resize_with_compaction_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 objs = 10_000.times.map do ExIvar.new end diff --git a/test/ruby/test_weakkeymap.rb b/test/ruby/test_weakkeymap.rb index 91c1538076f9f4..850949825f1ba5 100644 --- a/test/ruby/test_weakkeymap.rb +++ b/test/ruby/test_weakkeymap.rb @@ -138,7 +138,6 @@ def test_compaction end def test_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress { ObjectSpace::WeakKeyMap.new } end diff --git a/test/ruby/test_weakmap.rb b/test/ruby/test_weakmap.rb index d240e35fd0b707..789a54789ac9ea 100644 --- a/test/ruby/test_weakmap.rb +++ b/test/ruby/test_weakmap.rb @@ -257,7 +257,6 @@ def test_compaction end def test_gc_compact_stress - omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 EnvUtil.under_gc_compact_stress { ObjectSpace::WeakMap.new } end diff --git a/tool/lib/envutil.rb b/tool/lib/envutil.rb index f1de2c8977ea83..176a8a4bc837f7 100644 --- a/tool/lib/envutil.rb +++ b/tool/lib/envutil.rb @@ -365,8 +365,6 @@ def under_gc_stress(stress = true) module_function :under_gc_stress def under_gc_compact_stress(val = :empty, &block) - raise "compaction doesn't work well on s390x. Omit the test in the caller." if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 - if GC.respond_to?(:auto_compact) auto_compact = GC.auto_compact GC.auto_compact = val From 2dfc58b14fa1d82fe445788ee8a4f1c4e47fd4da Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 10 Sep 2026 19:54:00 +0900 Subject: [PATCH 03/11] Remove the obsolete s390x skip from test_timeout_longer_than_global It passed 70 out of 70 runs on s390x.rubyci.org across an idle machine, a saturated one, and 2x CPU oversubscription. Its pair, test_timeout_shorter_than_global, keeps the guard: that one still reproduces the original failure, but only under oversubscription, so the sensitivity is scheduling delay rather than anything about s390x. Co-Authored-By: Claude Opus 5 --- test/ruby/test_regexp.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb index 364c1fd8059ba4..a4575ebff9451e 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -2139,7 +2139,6 @@ def test_timeout_shorter_than_global end def test_timeout_longer_than_global - omit "timeout test is too unstable on s390x" if RUBY_PLATFORM =~ /s390x/ per_instance_redos_test(0.01, 0.5, 0.5) end From 910741083c65d346ac64a871713c88a8b7de6318 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 11 Sep 2026 07:23:52 +0900 Subject: [PATCH 04/11] Remove the s390x skip from test_ractor_lazy_load_encoding_random It passed 40 out of 40 runs on s390x.rubyci.org, 30 of them under CPU saturation. The guard came from GitHub Actions failures in 2025, and the test has since gained a 30 second timeout for the new pool layout. Co-Authored-By: Claude Opus 5 --- test/ruby/test_transcode.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/ruby/test_transcode.rb b/test/ruby/test_transcode.rb index 29aff2cb2bbab0..6c3ae3a109f7eb 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -2398,7 +2398,6 @@ def test_ractor_lazy_load_encoding end def test_ractor_lazy_load_encoding_random - omit 'unstable on s390x' if RUBY_PLATFORM =~ /s390x/ assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) begin; rs = [] From 5cd116303d2a158e3f1f724f12a50f180bfcf7dc Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 10 Sep 2026 18:45:40 +0000 Subject: [PATCH 05/11] Remove functions nothing calls None of these has a caller, a declaration in any header, or a place in libruby's dynamic symbol table, so an extension cannot reach them either. * rb_vm_opt_newarray_pack: left over from cb9510f539, which moved the instruction to the _buffer form. * rb_autoload: a wrapper around rb_autoload_str, deprecated and dropped from intern.h in Ruby 2.3 [Feature #11664]. * rb_big_fdiv: a wrapper around rb_big_fdiv_double, unused since Bignum stopped defining #fdiv. * rb_io_unbuffered: a wrapper around rb_io_synchronized, which is what it was renamed to before 1.8.0. * rb_ractor_hooks: an accessor for cr->pub.hooks; only its sibling rb_ractor_targeted_hooks is used. * rb_assert_sig: a leftover debugging aid. Co-Authored-By: Claude Opus 5 (1M context) --- bignum.c | 5 ----- io.c | 5 ----- ractor.c | 5 ----- thread_pthread.c | 15 --------------- variable.c | 9 --------- vm_insnhelper.c | 5 ----- 6 files changed, 44 deletions(-) diff --git a/bignum.c b/bignum.c index d95444a56378e3..1c4fc094c2f06c 100644 --- a/bignum.c +++ b/bignum.c @@ -6371,11 +6371,6 @@ rb_big_fdiv_double(VALUE x, VALUE y) return NUM2DBL(v); } -VALUE -rb_big_fdiv(VALUE x, VALUE y) -{ - return DBL2NUM(rb_big_fdiv_double(x, y)); -} VALUE rb_big_pow(VALUE x, VALUE y) diff --git a/io.c b/io.c index 522c8da0484f66..d446991c281577 100644 --- a/io.c +++ b/io.c @@ -7428,11 +7428,6 @@ rb_io_synchronized(rb_io_t *fptr) fptr->mode |= FMODE_SYNC; } -void -rb_io_unbuffered(rb_io_t *fptr) -{ - rb_io_synchronized(fptr); -} int rb_pipe(int *pipes) diff --git a/ractor.c b/ractor.c index 4714ad94a2f16a..3c2b54425cebb8 100644 --- a/ractor.c +++ b/ractor.c @@ -1452,11 +1452,6 @@ rb_ractor_stderr_set(VALUE err) } } -rb_hook_list_t * -rb_ractor_hooks(rb_ractor_t *cr) -{ - return &cr->pub.hooks; -} st_table * rb_ractor_targeted_hooks(rb_ractor_t *cr) diff --git a/thread_pthread.c b/thread_pthread.c index 0aaaf5de6f852f..a10570317919c0 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1226,21 +1226,6 @@ static int timer_thread_set_timeout(rb_vm_t *vm); #include "thread_sched_mn.c" - -void -rb_assert_sig(void) -{ - sigset_t oldmask; - pthread_sigmask(0, NULL, &oldmask); - if (sigismember(&oldmask, SIGVTALRM)) { - rb_bug("!!!"); - } - else { - RUBY_DEBUG_LOG("ok"); - } -} - - /* only use signal-safe system calls here */ static void signal_communication_pipe(int fd) diff --git a/variable.c b/variable.c index 8e7c528aec7627..2d4659637e0bc6 100644 --- a/variable.c +++ b/variable.c @@ -2790,15 +2790,6 @@ get_autoload_data(VALUE autoload_const_value, struct autoload_const **autoload_c return autoload_data; } -void -rb_autoload(VALUE module, ID name, const char *feature) -{ - if (!feature || !*feature) { - rb_raise(rb_eArgError, "empty feature name"); - } - - rb_autoload_str(module, name, rb_fstring_cstr(feature)); -} static void const_set(VALUE klass, ID id, VALUE val); static void const_added(VALUE klass, ID const_name); diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 4cf24f13c89e00..1f9d8acc805b09 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6656,11 +6656,6 @@ rb_vm_opt_newarray_pack_buffer(rb_execution_context_t *ec, rb_num_t array_len, c return vm_opt_newarray_pack_buffer(ec, array_len, ptr, fmt, buffer); } -VALUE -rb_vm_opt_newarray_pack(rb_execution_context_t *ec, rb_num_t array_len, const VALUE *ptr, VALUE fmt) -{ - return vm_opt_newarray_pack_buffer(ec, array_len, ptr, fmt, Qundef); -} #undef id_cmp From 9821d51b0b741a666c3464d90e744be57635a9a6 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 10 Sep 2026 21:08:36 +0900 Subject: [PATCH 06/11] Run test_port_receive_dnt_with_port_send on macOS again The omit dates from when the assertion had a 30 second timeout and the [Bug #21398] deadlock had just been fixed. It is 90 seconds now, scaled by ten on the macOS CI jobs, and the job itself takes about a second. Co-Authored-By: Claude Opus 5 --- test/ruby/test_ractor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 5b21ee361dfad2..93af32a3a1741d 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -356,7 +356,7 @@ def test_require_non_string # [Bug #21398] def test_port_receive_dnt_with_port_send - omit 'unstable on windows and macos-14' if RUBY_PLATFORM =~ /mswin|mingw|darwin/ + omit 'unstable on windows' if RUBY_PLATFORM =~ /mswin|mingw/ assert_ractor(<<~'RUBY', timeout: 90) THREADS = 10 JOBS_PER_THREAD = 50 From 1712837b7ff893d073e022d530303cacac5b7ea9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 10 Sep 2026 21:08:36 +0900 Subject: [PATCH 07/11] Give the crash report tests time to symbolize the backtrace Turning the C level backtrace into file:line pairs walks the whole of the binary's debug info, and that is nearly all of what a crash costs: 0.8s of CPU with the dSYM in place against 0.01s without it, on an idle arm64 macOS host. The default subprocess budget of 10 seconds is meant for a child that does none of that work, and on a loaded CI host it ran out, which is what these tests were omitted for. Co-Authored-By: Claude Opus 5 --- test/-ext-/bug_reporter/test_bug_reporter.rb | 4 ++-- test/ruby/test_rubyoptions.rb | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test/-ext-/bug_reporter/test_bug_reporter.rb b/test/-ext-/bug_reporter/test_bug_reporter.rb index 4b654acc251c44..0f262864a726c0 100644 --- a/test/-ext-/bug_reporter/test_bug_reporter.rb +++ b/test/-ext-/bug_reporter/test_bug_reporter.rb @@ -6,7 +6,6 @@ class TestBugReporter < Test::Unit::TestCase def test_bug_reporter_add - omit if macos? && ENV["CI"] # we're getting timeouts even after 100s in CI description = RUBY_DESCRIPTION description = description.sub(/\+PRISM /, '') unless ParserSupport.prism_enabled_in_subprocess? expected_stderr = [ @@ -26,7 +25,8 @@ def test_bug_reporter_add args.push("--zjit") if JITSupport.zjit_enabled? args.unshift({"RUBY_ON_BUG" => nil, "RUBY_CRASH_REPORT" => nil}) stdin = "#{no_core}register_sample_bug_reporter(12345); Bug.segv" - assert_in_out_err(args, stdin, [], expected_stderr, encoding: "ASCII-8BIT") + # Writing the report is slow, see TestRubyOptions#assert_segv. + assert_in_out_err(args, stdin, [], expected_stderr, encoding: "ASCII-8BIT", timeout: 60) ensure FileUtils.rm_rf(tmpdir) if tmpdir end diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index b50926591ebd76..71ccf0ed90f159 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -844,8 +844,12 @@ module SEGVTest KILL_SELF = "Bug.segv" end - def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, **opt, &block) - omit if macos? && ENV["CI"] # we're getting timeouts even after 100s in CI, not sure why. + # Turning the C level backtrace into file:line pairs walks the whole of the + # binary's debug info, and that is nearly all of what a crash costs: 0.8s of + # CPU with the dSYM in place against 0.01s without it, on an idle arm64 macOS + # host. The default subprocess budget of 10 seconds is meant for a child + # that does none of that work. + def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, timeout: 60, **opt, &block) # We want YJIT to be enabled in the subprocess if it's enabled for us # so that the Ruby description matches. env = Hash === args.first ? args.shift : {} @@ -867,7 +871,7 @@ def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, **opt, &b end assert_in_out_err(args, test_stdin, *tests, encoding: "ASCII-8BIT", - **SEGVTest::ExecOptions, **opt, &block) + timeout: timeout, **SEGVTest::ExecOptions, **opt, &block) end def test_segv_test From b6e284dcdfd8a805ef3b13679eaebefe3482f809 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 10 Sep 2026 21:08:36 +0900 Subject: [PATCH 08/11] Keep the message of a Timeout::Error raised in a test A subprocess that expires its timeout reports which assertion it was and the output it did manage to produce. Dropping that left nothing to go on for the crash report timeouts on macOS CI. Co-Authored-By: Claude Opus 5 --- tool/lib/test/unit.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb index 71a21de62817c9..04e48621293670 100644 --- a/tool/lib/test/unit.rb +++ b/tool/lib/test/unit.rb @@ -1840,7 +1840,12 @@ def puke klass, meth, e "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" when Timeout::Error @errors += 1 - "Timeout:\n#{klass}##{meth}\n" + # A worker that stopped responding is reported with a bare + # Timeout::Error and has nothing to say. One raised inside the + # test says what expired and carries the output of the + # subprocess that hung, which is all there is to go on. + detail = e.message == e.class.name ? "" : "#{e.message}\n" + "Timeout:\n#{klass}##{meth}\n#{detail}" else @errors += 1 bt = Test::filter_backtrace(e.backtrace).join "\n " From 67d83762c6a0f158a4db6b38db7ca661781612b2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 10 Sep 2026 21:08:27 +0900 Subject: [PATCH 09/11] [ruby/resolv] Retry a refused nameserver from a new UDP socket macOS 26.1 and later deliver only every other ICMP port unreachable for the same pair of endpoints, so the second attempt over a socket that has already seen one waits out its whole timeout instead of failing at once. Resolving against a nameserver with nothing listening took 50 seconds there rather than returning immediately. https://github.com/ruby/resolv/commit/9d09be1ad0 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 48 +++++++++++++++++++++++++++++++++-------- test/resolv/test_dns.rb | 2 -- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index ae6d85204571d6..aac5cdd2161af0 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -880,20 +880,37 @@ def lazy_initialize @mutex.synchronize { next if @initialized @initialized = true - is_ipv6 = @host.index(':') - sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) - @socks = [sock] - sock.do_not_reverse_lookup = true - DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") - sock.connect(@host, @port) + connect_socket } self end + # The socket to talk to the nameserver over, opening one if there is + # none yet. #recv_reply may have replaced it since a sender was + # created, so senders ask for it per request instead of holding on to + # one. + def sock + lazy_initialize + @socks[0] + end + def recv_reply(readable_socks, timelimit = nil) lazy_initialize reply = readable_socks[0].recv(UDPSize) return reply, nil + rescue Errno::ECONNREFUSED, Errno::ECONNRESET + # The kernel reports these from an ICMP message, and a second one for + # the same pair of endpoints is not always passed on: macOS 26.1 and + # later deliver every other one. A retry over this socket would then + # wait out its whole timeout rather than fail at once, so start over + # from a new source port. + @mutex.synchronize { + if @initialized + @socks&.each(&:close) + connect_socket + end + } + raise end def sender(msg, data, host=@host, port=@port) @@ -904,7 +921,7 @@ def sender(msg, data, host=@host, port=@port) id = DNS.allocate_request_id(@host, @port) request = msg.encode request[0,2] = [id].pack('n') - return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) + return @senders[[nil,id]] = Sender.new(request, data, self) end def close @@ -919,10 +936,23 @@ def close end end + private def connect_socket + is_ipv6 = @host.index(':') + sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) + @socks = [sock] + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") + sock.connect(@host, @port) + end + class Sender < Requester::Sender # :nodoc: + def initialize(msg, data, requester) + super(msg, data, nil) + @requester = requester + end + def send - raise "@sock is nil." if @sock.nil? - @sock.send(@msg, 0) + @requester.sock.send(@msg, 0) end attr_reader :data end diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index f2649ac7a75800..5b726054e67c5b 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -525,8 +525,6 @@ def test_no_server if RUBY_PLATFORM.match?(/mingw/) # cannot repo locally omit 'Timeout Error on MinGW CI' - elsif macos?([26,1]..[]) - omit 'Timeout Error on macOS 26.1+' else raise Timeout::Error end From f3ebfe5d36e70ef36d6633a009173175b2b60460 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 10 Sep 2026 17:55:26 +0000 Subject: [PATCH 10/11] Initialize as.extended before the fields object is allocated class_allocate_complex_instance() allocates the object with an extended shape and fills in as.extended only after allocating the fields object. A GC during that allocation sees a shape that says "extended" and marks whatever the reused slot left in as.extended: [BUG] try to mark T_NONE object (obj: out-of-heap:..., parent: ... T_OBJECT/(complex) ...) Before 86951165b8 ("Get rid of ROBJECT_HEAP flag") the mark path keyed off the ROBJECT_HEAP flag, which was set together with as.extended, so the window took the embedded path and read nothing. The shape is now set at allocation, which makes the window live. Reproduces with GC.stress on a class whose instances start with a complex shape, e.g. bootstraptest/test_yjit.rb under `-s`. Add a bootstraptest that enables GC.stress itself so a regular btest run covers it. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Claude Fable 5.1 --- bootstraptest/test_gc.rb | 11 +++++++++++ gc.c | 3 +++ 2 files changed, 14 insertions(+) diff --git a/bootstraptest/test_gc.rb b/bootstraptest/test_gc.rb index eb68c9845e37c9..6e3dee731437b8 100644 --- a/bootstraptest/test_gc.rb +++ b/bootstraptest/test_gc.rb @@ -32,3 +32,14 @@ end end }, '[ruby-dev:39453]' + +assert_normal_exit %q{ + # A class whose instances start with a complex shape: as.extended must be + # initialized before the fields object allocation can trigger a GC. + ivars = 1024.times.map { |i| "@iv_#{i} = #{i}\n" }.join + klass = Class.new + klass.class_eval "def initialize() #{ivars} end" + GC.stress = true + klass.allocate + GC.stress = false +}, 'complex T_OBJECT marked before as.extended is set' diff --git a/gc.c b/gc.c index 5187c748ee8483..fdb23a8e435520 100644 --- a/gc.c +++ b/gc.c @@ -1231,6 +1231,9 @@ static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_transition_extended(ROOT_COMPLEX_SHAPE_ID), sizeof(struct RObject)); + // The shape already says extended, so a GC during the allocation below + // would mark an uninitialized as.extended. + ROBJECT(obj)->as.extended = Qfalse; VALUE fields_obj = rb_imemo_fields_new_complex(obj, ROOT_COMPLEX_SHAPE_ID, capacity, false); ROBJECT_SET_EXTENDED(obj, fields_obj); return obj; From 64b7a5671d37aa069c1cbeb89f472605024f7143 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 10 Sep 2026 18:17:54 +0000 Subject: [PATCH 11/11] Remove the workqueue eb38fb670b added it as a thread-safe counterpart to postponed_job, for MJIT to enqueue work from its own thread [Bug #15316]. MJIT and RJIT are gone and nothing enqueues anything any more: rb_workqueue_register() has no caller, no declaration in any header, and hidden visibility, so an extension cannot reach it either. What is left is rb_postponed_job_flush() taking workqueue_lock on every call to steal a list that is always empty. rb_postponed_job_flush() no longer needs the VM: the preregistered job table it drains is a file-static. Drop the argument. Co-Authored-By: Claude Opus 5 (1M context) --- thread.c | 3 +- vm.c | 3 -- vm_core.h | 6 +--- vm_trace.c | 90 +++++------------------------------------------------- 4 files changed, 9 insertions(+), 93 deletions(-) diff --git a/thread.c b/thread.c index ebaf61d963e075..56e7c2b5c15fd3 100644 --- a/thread.c +++ b/thread.c @@ -2861,7 +2861,7 @@ rb_threadptr_execute_interrupts(rb_thread_t *th, int blocking_timing) } if (postponed_job_interrupt) { - rb_postponed_job_flush(th->vm); + rb_postponed_job_flush(); } if (trap_interrupt) { @@ -5947,7 +5947,6 @@ Init_Thread_Mutex(void) { rb_thread_t *th = GET_THREAD(); - rb_native_mutex_initialize(&th->vm->workqueue_lock); rb_native_mutex_initialize(&th->vm->once_lock); rb_native_cond_initialize(&th->vm->once_cond); rb_native_mutex_initialize(&th->interrupt_lock); diff --git a/vm.c b/vm.c index f93fa106a94b80..0efad702883cec 100644 --- a/vm.c +++ b/vm.c @@ -3596,7 +3596,6 @@ ruby_vm_destruct(rb_vm_t *vm) } rb_objspace_free(objspace); } - rb_native_mutex_destroy(&vm->workqueue_lock); rb_native_mutex_destroy(&vm->once_lock); rb_native_cond_destroy(&vm->once_cond); /* after freeing objspace, you *can't* use ruby_xfree() */ @@ -3614,7 +3613,6 @@ ruby_vm_destruct(rb_vm_t *vm) return 0; } -size_t rb_vm_memsize_workqueue(struct ccan_list_head *workqueue); // vm_trace.c // Used for VM memsize reporting. Returns the size of the at_exit list by // looping through the linked list and adding up the size of the structs. @@ -3669,7 +3667,6 @@ vm_memsize(const void *ptr) return ( sizeof(rb_vm_t) + rb_vm_memsize_postponed_job_queue() + - rb_vm_memsize_workqueue(&vm->workqueue) + vm_memsize_at_exit_list(vm->at_exit) + (rb_st_memsize(&vm->ci_table) - sizeof(struct st_table)) + vm_memsize_builtin_function_table(vm->builtin_function_table) + diff --git a/vm_core.h b/vm_core.h index 97b1bfb694a3f7..8372f7ca4aa799 100644 --- a/vm_core.h +++ b/vm_core.h @@ -778,9 +778,6 @@ typedef struct rb_vm_struct { int src_encoding_index; - /* workqueue (thread-safe, NOT async-signal-safe) */ - struct ccan_list_head workqueue; /* <=> rb_workqueue_job.jnode */ - rb_nativethread_lock_t workqueue_lock; /* `once` completion event (see vm_once_dispatch) */ rb_nativethread_lock_t once_lock; @@ -2093,7 +2090,6 @@ void rb_thread_wakeup_timer_thread(int); static inline void rb_vm_living_threads_init(rb_vm_t *vm) { - ccan_list_head_init(&vm->workqueue); ccan_list_head_init(&vm->ractor.set); ccan_list_head_init(&vm->ractor.terminated_set); } @@ -2495,7 +2491,7 @@ int rb_thread_check_trap_pending(void); #define RUBY_EVENT_COVERAGE_LINE 0x010000 #define RUBY_EVENT_COVERAGE_BRANCH 0x020000 -void rb_postponed_job_flush(rb_vm_t *vm); +void rb_postponed_job_flush(void); void rb_postponed_job_trigger_for_ractor(unsigned int h, VALUE running_ractor); // ractor.c diff --git a/vm_trace.c b/vm_trace.c index 4fe5f0740d0c40..8b99335b95962e 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -1794,62 +1794,12 @@ Init_vm_trace(void) } /* - * Ruby actually has two separate mechanisms for enqueueing work from contexts - * where it is not safe to run Ruby code, to run later on when it is safe. One - * is async-signal-safe but more limited, and accessed through the - * `rb_postponed_job_preregister` and `rb_postponed_job_trigger` functions. The - * other is more flexible but cannot be used in signal handlers, and is accessed - * through the `rb_workqueue_register` function. - * - * The postponed job functions form part of Ruby's extension API, but the - * workqueue functions are for internal use only. + * Work enqueued from a context where it is not safe to run Ruby code, to run + * later on when it is safe. Registering is async-signal-safe, and is part of + * Ruby's extension API: rb_postponed_job_preregister and + * rb_postponed_job_trigger. */ -struct rb_workqueue_job { - struct ccan_list_node jnode; /* <=> vm->workqueue */ - rb_postponed_job_func_t func; - void *data; -}; - -// Used for VM memsize reporting. Returns the size of a list of rb_workqueue_job -// structs. Defined here because the struct definition lives here as well. -size_t -rb_vm_memsize_workqueue(struct ccan_list_head *workqueue) -{ - struct rb_workqueue_job *work = 0; - size_t size = 0; - - ccan_list_for_each(workqueue, work, jnode) { - size += sizeof(struct rb_workqueue_job); - } - - return size; -} - -/* - * thread-safe and called from non-Ruby thread - * returns FALSE on failure (ENOMEM), TRUE otherwise - */ -int -rb_workqueue_register(unsigned flags, rb_postponed_job_func_t func, void *data) -{ - struct rb_workqueue_job *wq_job = malloc(sizeof(*wq_job)); - rb_vm_t *vm = GET_VM(); - - if (!wq_job) return FALSE; - wq_job->func = func; - wq_job->data = data; - - rb_nativethread_lock_lock(&vm->workqueue_lock); - ccan_list_add_tail(&vm->workqueue, &wq_job->jnode); - rb_nativethread_lock_unlock(&vm->workqueue_lock); - - // TODO: current implementation affects only main ractor - RUBY_VM_SET_POSTPONED_JOB_INTERRUPT(rb_vm_main_ractor_ec(vm)); - - return TRUE; -} - #define PJOB_TABLE_SIZE (sizeof(rb_atomic_t) * CHAR_BIT) /* pre-registered jobs table, for async-safe jobs */ typedef struct rb_postponed_job_queue { @@ -1971,20 +1921,13 @@ rb_postponed_job_trigger_for_ractor(unsigned int h, VALUE running_ractor) } void -rb_postponed_job_flush(rb_vm_t *vm) +rb_postponed_job_flush(void) { rb_postponed_job_queues_t *pjq = &postponed_job_queue; rb_execution_context_t *ec = GET_EC(); const rb_atomic_t block_mask = POSTPONED_JOB_INTERRUPT_MASK | TRAP_INTERRUPT_MASK; volatile rb_atomic_t saved_mask = ec->interrupt_mask & block_mask; VALUE volatile saved_errno = ec->errinfo; - struct ccan_list_head tmp; - - ccan_list_head_init(&tmp); - - rb_nativethread_lock_lock(&vm->workqueue_lock); - ccan_list_append_list(&tmp, &vm->workqueue); - rb_nativethread_lock_unlock(&vm->workqueue_lock); volatile rb_atomic_t triggered_bits = RUBY_ATOMIC_EXCHANGE(pjq->triggered_bitset, 0); @@ -2007,16 +1950,6 @@ rb_postponed_job_flush(rb_vm_t *vm) void *data = RUBY_ATOMIC_PTR_LOAD(pjq->table[i].data); (func)(data); } - - /* execute workqueue jobs */ - struct rb_workqueue_job *wq_job; - while ((wq_job = ccan_list_pop(&tmp, struct rb_workqueue_job, jnode))) { - rb_postponed_job_func_t func = wq_job->func; - void *data = wq_job->data; - - free(wq_job); - (func)(data); - } } EC_POP_TAG(); } @@ -2024,17 +1957,8 @@ rb_postponed_job_flush(rb_vm_t *vm) ec->interrupt_mask &= ~(saved_mask ^ block_mask); ec->errinfo = saved_errno; - /* If we threw an exception, there might be leftover workqueue items; carry them over - * to a subsequent execution of flush */ - if (!ccan_list_empty(&tmp)) { - rb_nativethread_lock_lock(&vm->workqueue_lock); - ccan_list_prepend_list(&vm->workqueue, &tmp); - rb_nativethread_lock_unlock(&vm->workqueue_lock); - - RUBY_VM_SET_POSTPONED_JOB_INTERRUPT(GET_EC()); - } - /* likewise with any remaining-to-be-executed bits of the preregistered postponed - * job table. A merged bit can carry a Ractor-directed job that must not run on another + /* If we threw an exception, carry the bits that did not run yet over to a subsequent + * flush. A merged bit can carry a Ractor-directed job that must not run on another * Ractor (rb_postponed_job_trigger_for_ractor), so re-post it to this Ractor's own mask * rather than to the global bitset. */ if (triggered_bits) {