From b2e032dce51a86abbf2fa0730cf07f0a72fa9b73 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 13:33:50 -0700 Subject: [PATCH 01/13] auto_request_review.yml: Un-fork auto-request-review The PR has been upstreamed. --- .github/workflows/auto_request_review.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml index e5389bcf22b7bf..1be2b11b8634e4 100644 --- a/.github/workflows/auto_request_review.yml +++ b/.github/workflows/auto_request_review.yml @@ -14,8 +14,7 @@ jobs: if: ${{ github.repository == 'ruby/ruby' && github.base_ref == 'master' }} steps: - name: Request review based on files changes and/or groups the author belongs to - # Using a fork until https://github.com/necojackarc/auto-request-review/pull/135 is merged - uses: k0kubun/auto-request-review@0df295a0ff5c5d302770f589497280132131c63d # master + uses: necojackarc/auto-request-review@5d3060495e58e9cb41f51de50e808d3135d5374e # master with: # scope: public_repo token: ${{ secrets.MATZBOT_AUTO_REQUEST_REVIEW_TOKEN }} From 409b88d9d8a0017cf62109f10ce7f1faf66076b0 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 13:57:58 -0700 Subject: [PATCH 02/13] Fix race condition between linking ruby and update-default-gemspecs `yes-update-default-gemspecs` uses `$(XRUBY)` which execs the ruby binary, but only depended on `main` (extensions and encodings). When `make runirb` runs with parallel jobs, the linker may still be writing the ruby binary while `update-default-gemspecs` tries to exec it, causing `Errno::EACCES` (Permission denied). Add `$(PROGRAM)` as a dependency so the binary is fully linked before attempting to execute it. This is to prevent CI from randomly failing on the runirb step, e.g. https://github.com/ruby/ruby/actions/runs/23921136182/job/69767485157?pr=16648 --- common.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.mk b/common.mk index 3105c0977cc64e..a43b9304f773c5 100644 --- a/common.mk +++ b/common.mk @@ -1578,7 +1578,7 @@ no-test-bundled-gems-precheck: update-default-gemspecs: $(TEST_RUNNABLE)-update-default-gemspecs no-update-default-gemspecs: -yes-update-default-gemspecs: $(PRECHECK_BUNDLED_GEMS:yes=main) +yes-update-default-gemspecs: $(PRECHECK_BUNDLED_GEMS:yes=main) $(PROGRAM) @$(MAKEDIRS) $(srcdir)/.bundle/specifications @$(XRUBY) -W0 -C "$(srcdir)" -rrubygems \ -e "destdir = ARGV.shift" \ From bcd1479670cdc3432f5b13b43b0d59244c734d77 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 14:28:26 -0700 Subject: [PATCH 03/13] Fix random Broken pipe error in TestOpenURI#test_read_timeout The server thread writes the HTTP response while the client has a 0.1s read_timeout. On slow CI (especially Windows), the client can time out and close the socket before the server finishes writing headers or the response body, causing Errno::EPIPE in the server thread. Since the broken pipe is expected when the client times out, rescue it along with ECONNRESET and ECONNABORTED. This attempts to mitigate random CI failures like: https://github.com/ruby/ruby/actions/runs/23921136139/job/69767484567?pr=16648 1) Error: TestOpenURI#test_read_timeout: Test::Unit::ProxyError: Broken pipe D:/a/ruby/ruby/src/test/open-uri/test_open-uri.rb:80:in 'IO#write' D:/a/ruby/ruby/src/test/open-uri/test_open-uri.rb:80:in 'IO#print' D:/a/ruby/ruby/src/test/open-uri/test_open-uri.rb:80:in 'block (2 levels) in TestOpenURI#test_read_timeout' --- test/open-uri/test_open-uri.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/open-uri/test_open-uri.rb b/test/open-uri/test_open-uri.rb index 0679180ce9fbfb..6f08b4089cf19a 100644 --- a/test/open-uri/test_open-uri.rb +++ b/test/open-uri/test_open-uri.rb @@ -80,6 +80,8 @@ def test_read_timeout sock.print "Content-Length: 4\r\n\r\n" sleep 1 sock.print "ab\r\n" + rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED + # expected when client times out and closes the connection ensure sock.close end From e957b3a9d0aa57fec5a62f4b7f3a2f73627a0e66 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 14:34:07 -0700 Subject: [PATCH 04/13] YJIT: Stop checking rb_zjit_enabled_p on YJIT callbacks (#16648) --- cont.c | 2 +- vm.c | 4 ++-- yjit.c | 2 +- zjit.h | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cont.c b/cont.c index e5239635081629..c44d233a9fcdb8 100644 --- a/cont.c +++ b/cont.c @@ -1480,7 +1480,7 @@ rb_yjit_cancel_jit_return(void *leave_exit, void *leave_exception) const rb_control_frame_t *cfp = cont->ec->cfp; while (!RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(cont->ec, cfp)) { - if (CFP_JIT_RETURN(cfp) && cfp->jit_return != leave_exception) { + if (cfp->jit_return && cfp->jit_return != leave_exception) { ((rb_control_frame_t *)cfp)->jit_return = leave_exit; } cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); diff --git a/vm.c b/vm.c index 0398b9f74c9683..ad8a22ef4b2fc8 100644 --- a/vm.c +++ b/vm.c @@ -2852,7 +2852,7 @@ zjit_materialize_frames(rb_control_frame_t *cfp) if (!rb_zjit_enabled_p) return; while (true) { - if (CFP_JIT_RETURN(cfp)) { + if (CFP_ZJIT_FRAME(cfp)) { const zjit_jit_frame_t *jit_frame = (const zjit_jit_frame_t *)cfp->jit_return; cfp->pc = jit_frame->pc; cfp->_iseq = (rb_iseq_t *)jit_frame->iseq; @@ -3671,7 +3671,7 @@ rb_execution_context_update(rb_execution_context_t *ec) while (cfp != limit_cfp) { const VALUE *ep = cfp->ep; cfp->self = rb_gc_location(cfp->self); - if (CFP_JIT_RETURN(cfp)) { + if (CFP_ZJIT_FRAME(cfp)) { rb_zjit_jit_frame_update_references((zjit_jit_frame_t *)cfp->jit_return); // block_code must always be relocated. For ISEQ frames, the JIT caller // may have written it (gen_block_handler_specval) for passing blocks. diff --git a/yjit.c b/yjit.c index 46565cb6c0d035..2e7216a1915406 100644 --- a/yjit.c +++ b/yjit.c @@ -480,7 +480,7 @@ rb_yjit_set_exception_return(rb_control_frame_t *cfp, void *leave_exit, void *le // If it's a FINISH frame, just normally exit with a non-Qundef value. cfp->jit_return = leave_exit; } - else if (CFP_JIT_RETURN(cfp)) { + else if (cfp->jit_return) { while (!VM_FRAME_FINISHED_P(cfp)) { if (cfp->jit_return == leave_exit) { // Unlike jit_exec(), leave_exit is not safe on a non-FINISH frame on diff --git a/zjit.h b/zjit.h index d67c8b82f21efe..065f08e32f9994 100644 --- a/zjit.h +++ b/zjit.h @@ -75,7 +75,7 @@ enum zjit_poison_values { // YJIT also uses jit_return (as a return address), so this must only return // non-NULL when ZJIT is enabled and has set jit_return to a JITFrame pointer. static inline void * -CFP_JIT_RETURN(const rb_control_frame_t *cfp) +CFP_ZJIT_FRAME(const rb_control_frame_t *cfp) { if (!rb_zjit_enabled_p) return NULL; #if USE_ZJIT @@ -87,7 +87,7 @@ CFP_JIT_RETURN(const rb_control_frame_t *cfp) static inline const VALUE* CFP_PC(const rb_control_frame_t *cfp) { - if (CFP_JIT_RETURN(cfp)) { + if (CFP_ZJIT_FRAME(cfp)) { return ((const zjit_jit_frame_t *)cfp->jit_return)->pc; } return cfp->pc; @@ -96,7 +96,7 @@ CFP_PC(const rb_control_frame_t *cfp) static inline const rb_iseq_t* CFP_ISEQ(const rb_control_frame_t *cfp) { - if (CFP_JIT_RETURN(cfp)) { + if (CFP_ZJIT_FRAME(cfp)) { return ((const zjit_jit_frame_t *)cfp->jit_return)->iseq; } return cfp->_iseq; From 85146a5f47e23cab44eee97255c64eca8bb2d2e9 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 15:50:11 -0700 Subject: [PATCH 05/13] ZJIT: Use CFP_PC in warn_unused_block for outlined PC (#16649) warn_unused_block uses the caller's PC to build a dedup key for the unused block warning table. When called from ZJIT-compiled code via rb_vm_send, cfp->pc may be stale (poisoned or uninitialized) because ZJIT stores the real PC in cfp->jit_return instead. Use CFP_PC() which reads from jit_return when available, falling back to cfp->pc for interpreter frames. This prevents the dedup key from being based on a garbage PC value that could randomly collide with prior entries, suppressing warnings that should be emitted. --- vm_insnhelper.c | 2 +- zjit/src/codegen_tests.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 705199ea1fd4c3..32244ac1334bdb 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -3219,7 +3219,7 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling, if (UNLIKELY(!ISEQ_BODY(iseq)->param.flags.use_block && calling->block_handler != VM_BLOCK_HANDLER_NONE && !(vm_ci_flag(calling->cd->ci) & (VM_CALL_OPT_SEND | VM_CALL_SUPER)))) { - warn_unused_block(vm_cc_cme(cc), iseq, (void *)ec->cfp->pc); + warn_unused_block(vm_cc_cme(cc), iseq, (void *)CFP_PC(ec->cfp)); } if (LIKELY(!(vm_ci_flag(ci) & VM_CALL_KW_SPLAT))) { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 7c8a3c758b1fbd..f92ed92eb8f253 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -5544,3 +5544,30 @@ fn test_send_block_to_method_not_using_block() { test "), @"42"); } + +#[test] +fn test_send_block_unused_warning_emitted_from_jit() { + // When ZJIT compiles a send with a block as a dynamic dispatch fallback + // (gen_send -> rb_vm_send), warn_unused_block uses cfp->pc for the dedup + // key. We save cfp->pc before calling rb_vm_send so the key is stable + // and won't spuriously collide with prior entries in the dedup table. + assert_snapshot!(inspect(r#" + $warnings = [] + module Warning + def warn(message, category: nil) + $warnings << message + end + end + + def m_unused_block_warn_test = 42 + + def test + $VERBOSE = true + m_unused_block_warn_test {} + $warnings.any? { |w| w.include?("may be ignored") } + end + + test + test + "#), @"true"); +} From 1d98daa11abfc635fc9ffb280d8c015122a2c20f Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 16:04:10 -0700 Subject: [PATCH 06/13] Relax RSS limit in test_named_structs_are_not_rooted The default limit of 2.0 is too tight for RSS measurements on macOS, causing random CI failures when the ratio lands just above 2.0 (e.g. 2.05). Increase to 2.2 to account for normal RSS measurement noise. https://github.com/ruby/ruby/actions/runs/23923072484/job/69773771596 1) Failure: TestStruct::TopStruct#test_named_structs_are_not_rooted [/Users/runner/work/ruby/ruby/src/test/ruby/test_struct.rb:541]: rss: 8781824 => 18006016. Expected 2.050373134328358 to be < 2.0. ruby -v: ruby 4.1.0dev (2026-04-02T21:34:07Z master e957b3a9d0) +PRISM [arm64-darwin23] --- test/ruby/test_struct.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ruby/test_struct.rb b/test/ruby/test_struct.rb index 01e5cc68f6eadc..294498c30f25f8 100644 --- a/test/ruby/test_struct.rb +++ b/test/ruby/test_struct.rb @@ -538,7 +538,7 @@ def test_named_structs_are_not_rooted omit 'skip on riscv64-linux CI machine. See https://github.com/ruby/ruby/pull/13422' if ENV['RUBY_DEBUG'] == 'ci' && /riscv64-linux/ =~ RUBY_DESCRIPTION # [Bug #20311] - assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true) + assert_no_memory_leak([], <<~PREP, <<~CODE, rss: true, limit: 2.2) code = proc do Struct.new("A") Struct.send(:remove_const, :A) From a6c6e563f1d3f43074e157b4f46eb08bc586deed Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 2 Apr 2026 16:27:24 -0700 Subject: [PATCH 07/13] Fix random timeout in test_thread_join_during_finalizers on Windows (#16650) Reduce the number of spawned processes from 50 to 20 and increase the timeout from 30s to 60s. Process creation is expensive on Windows, and the combination caused the test to exceed the 30s limit on Windows CI. --- test/ruby/test_thread.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 2dea3359c185bb..fae7bb2c2c269a 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -1667,7 +1667,7 @@ def test_mn_threads_sub_millisecond_sleep # [Bug #21926] def test_thread_join_during_finalizers - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) begin; require 'open3' @@ -1690,7 +1690,7 @@ def self.make_finalizer(stdin, stdout, stderr, wait_thread) end end - 50.times { ProcessWrapper.new } + 20.times { ProcessWrapper.new } GC.stress = true 1000.times { Object.new } end; From b8c3508248df73a13ce14617f44023d086aeb1fa Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 2 Apr 2026 17:01:06 -0400 Subject: [PATCH 08/13] ZJIT: Avoid transmute dodging rustc/LLVM int-to-ptr miscompilation The transmute triggered a miscompilation on Rust 1.85.0, where the callback was "optimized" to unconditional go to a `SIGTRAP`ping instruction. While later Rust versions don't have the same miscompilation, it seems that this is also partly our fault for being funky with the pointer provenance in this code. It's possible rust will make some changes that break the transmute. Use the Exposed Provenance API here and steer clear of transmute. See: https://github.com/rust-lang/rust/pull/121282 See: https://github.com/rust-lang/rust/issues/147265 See: https://github.com/rust-lang/rust/issues/128409#issuecomment-2259134985 --- zjit/src/cruby.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 3c00275478523c..38c8044f8f6f48 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1228,13 +1228,13 @@ pub mod test_utils { // "Fun" double pointer dance to get a thin function pointer to pass through C unsafe extern "C" fn callback_wrapper(data: VALUE) -> VALUE { // SAFETY: shorter lifetime than the data local in the caller frame - let callback: &mut &mut dyn FnMut() = unsafe { std::mem::transmute(data) }; - callback(); + let callback: *mut &mut dyn FnMut() = std::ptr::with_exposed_provenance_mut(data.0); + unsafe { (*callback)() }; Qnil } let mut state: c_int = 0; - unsafe { super::rb_protect(Some(callback_wrapper), VALUE((&mut data) as *mut _ as usize), &mut state) }; + unsafe { super::rb_protect(Some(callback_wrapper), VALUE((&raw mut data).expose_provenance()), &mut state) }; if state != 0 { unsafe { rb_zjit_print_exception(); } assert_eq!(0, state, "Exceptional unwind in callback. Ruby exception?"); From 61bef957bc2f3a0806ce4a60cb2f9498008eaf06 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 2 Apr 2026 15:02:49 -0400 Subject: [PATCH 09/13] YJIT: ZJIT: Use opt-level=1 on dev builds to avoid SystemStackError Despite adding rb_ec_stack_check() to Rust code entry points, we've seen SystemStackError causing test failures due to debug builds consume too much native stack space. Let's use opt-level=1 so rustc/LLVM are more efficient with stack space usage, hopefully enough to fit in the margin in rb_ec_stack_check(). Continue to use opt-level=0 in the test profile. --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 521129d92d2863..33010e65fb63ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,11 +36,15 @@ yjit = [ "dep:yjit" ] zjit = [ "dep:zjit" ] [profile.dev] -opt-level = 0 +opt-level = 1 # On 0, functions use so much stack space that we get stray `SystemStackError`s debug = true debug-assertions = true overflow-checks = true +[profile.test] +inherits = "dev" +opt-level = 0 + [profile.dev_nodebug] inherits = "dev" From 6ebe6f976fbd34091ec69850f49cc5bed04d9f9f Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 2 Apr 2026 15:41:58 -0400 Subject: [PATCH 10/13] Revert "ZJIT: Skip TestOpenURIProxy on Linux (#16606)" This reverts commit d368d42ec6b50cece947556a2185ce73fa493bfe. --- test/.excludes-zjit/TestOpenURIProxy.rb | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 test/.excludes-zjit/TestOpenURIProxy.rb diff --git a/test/.excludes-zjit/TestOpenURIProxy.rb b/test/.excludes-zjit/TestOpenURIProxy.rb deleted file mode 100644 index f08a2d7e89b201..00000000000000 --- a/test/.excludes-zjit/TestOpenURIProxy.rb +++ /dev/null @@ -1,3 +0,0 @@ -if RUBY_PLATFORM =~ /linux/ - exclude(/test_/, 'randomly fails with SystemStackError (Shopify/ruby#964)') -end From 23175641853cfe7e1a98655ae706f86fbf093535 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 2 Apr 2026 15:42:28 -0400 Subject: [PATCH 11/13] Revert "ZJIT: Skip TestOpenURI and TestOpenURISSL (#16603)" This reverts commit 33b9d2ab9f45a0c66665fb8dc40ddebd07c7b7b7. --- test/.excludes-zjit/TestOpenURI.rb | 3 --- test/.excludes-zjit/TestOpenURISSL.rb | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 test/.excludes-zjit/TestOpenURI.rb delete mode 100644 test/.excludes-zjit/TestOpenURISSL.rb diff --git a/test/.excludes-zjit/TestOpenURI.rb b/test/.excludes-zjit/TestOpenURI.rb deleted file mode 100644 index f08a2d7e89b201..00000000000000 --- a/test/.excludes-zjit/TestOpenURI.rb +++ /dev/null @@ -1,3 +0,0 @@ -if RUBY_PLATFORM =~ /linux/ - exclude(/test_/, 'randomly fails with SystemStackError (Shopify/ruby#964)') -end diff --git a/test/.excludes-zjit/TestOpenURISSL.rb b/test/.excludes-zjit/TestOpenURISSL.rb deleted file mode 100644 index f08a2d7e89b201..00000000000000 --- a/test/.excludes-zjit/TestOpenURISSL.rb +++ /dev/null @@ -1,3 +0,0 @@ -if RUBY_PLATFORM =~ /linux/ - exclude(/test_/, 'randomly fails with SystemStackError (Shopify/ruby#964)') -end From bbe94d9bccdffea016bf3fbeb6a3f3c8bb5a56ff Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Apr 2026 09:24:03 +0900 Subject: [PATCH 12/13] Fix typo "rescured" to "rescued" --- test/ruby/test_thread.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index fae7bb2c2c269a..fb5a4aa528a7ce 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -798,7 +798,7 @@ def test_handle_interrupt_invalid_argument def for_test_handle_interrupt_with_return Thread.handle_interrupt(Object => :never){ - Thread.current.raise RuntimeError.new("have to be rescured") + Thread.current.raise RuntimeError.new("have to be rescued") return } rescue @@ -815,7 +815,7 @@ def test_handle_interrupt_with_break assert_nothing_raised do begin Thread.handle_interrupt(Object => :never){ - Thread.current.raise RuntimeError.new("have to be rescured") + Thread.current.raise RuntimeError.new("have to be rescued") break } rescue From 6200574d4af133fa1e2a000bcf21e1133e93a556 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Apr 2026 09:26:20 +0900 Subject: [PATCH 13/13] Fix bug reference number and tracker ID to match Bug #21342 --- test/ruby/test_thread.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index fb5a4aa528a7ce..c3d9dcf56dabae 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -1592,10 +1592,9 @@ def frame_for_deadlock_test_2 INPUT end - # [Bug #21342] def test_unlock_locked_mutex_with_collected_fiber - bug21127 = '[ruby-core:120930] [Bug #21127]' - assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}", bug21127) + bug21342 = '[ruby-core:122121] [Bug #21342]' + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}", bug21342) begin; 5.times do m = Mutex.new