From 90a9b64238eca659df9d59d35f6cf59fa3851119 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 4 Dec 2025 16:13:59 -0800 Subject: [PATCH 01/10] Use rb_current_ec_noinline in ractor_{lock,unlock} We're seeing an occasional crash on CI because this ends up inlined all the way into ractor_wait_receive. On llvm (possibly other compilers) the thread local address of ec ends up cached (not the value of ec, the address ec is read from). So if we are migrated to another native thread, that may be invalid. Using rb_current_ec_noinline avoids this problems. It would be good to adjust this code so that ec (or current ractor) is calculated once and then passed through to both lock and unlock. --- ractor.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ractor.c b/ractor.c index 9fec70a1dfab71..f65a4f79c1f8e8 100644 --- a/ractor.c +++ b/ractor.c @@ -72,15 +72,17 @@ ractor_lock(rb_ractor_t *r, const char *file, int line) ASSERT_ractor_unlocking(r); rb_native_mutex_lock(&r->sync.lock); - if (rb_current_execution_context(false)) { - VM_ASSERT(!GET_RACTOR()->malloc_gc_disabled); - GET_RACTOR()->malloc_gc_disabled = true; + const rb_execution_context_t *ec = rb_current_ec_noinline(); + if (ec) { + rb_ractor_t *cr = rb_ec_ractor_ptr(ec); + VM_ASSERT(!cr->malloc_gc_disabled); + cr->malloc_gc_disabled = true; } #if RACTOR_CHECK_MODE > 0 - if (rb_current_execution_context(false) != NULL) { - rb_ractor_t *cr = rb_current_ractor_raw(false); - r->sync.locked_by = cr ? rb_ractor_self(cr) : Qundef; + if (ec != NULL) { + rb_ractor_t *cr = rb_ec_ractor_ptr(ec); + r->sync.locked_by = rb_ractor_self(cr); } #endif @@ -105,9 +107,11 @@ ractor_unlock(rb_ractor_t *r, const char *file, int line) r->sync.locked_by = Qnil; #endif - if (rb_current_execution_context(false)) { - VM_ASSERT(GET_RACTOR()->malloc_gc_disabled); - GET_RACTOR()->malloc_gc_disabled = false; + const rb_execution_context_t *ec = rb_current_ec_noinline(); + if (ec) { + rb_ractor_t *cr = rb_ec_ractor_ptr(ec); + VM_ASSERT(cr->malloc_gc_disabled); + cr->malloc_gc_disabled = false; } rb_native_mutex_unlock(&r->sync.lock); From 66bda731901d4588c9df1a671022231ea0d03216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Mon, 8 Dec 2025 22:58:35 +0100 Subject: [PATCH 02/10] Remove unused local variables in test/ruby/test_io_buffer.rb (#15451) --- test/ruby/test_io_buffer.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 9ff22b4bb356eb..cc7998842313e8 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -852,9 +852,6 @@ def test_integer_endianness_swapping assert_equal value, result_be, "#{be_type}: round-trip failed" # Verify byte patterns are different when endianness differs from host - le_bytes = buffer.get_string(0, buffer_size) - be_bytes = buffer.get_string(buffer_size, buffer_size) - if host_is_le # On little-endian host: le_type should match host, be_type should be swapped # So the byte patterns should be different (unless value is symmetric) From 55ea3ec00f5166423cd7dcd67e220cd264a766f6 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 7 Dec 2025 12:43:44 -0500 Subject: [PATCH 03/10] Fix strict aliasing warning in rb_int128_to_numeric If we don't have uint128, then rb_int128_to_numeric emits a strict aliasing warning: numeric.c:3641:39: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 3641 | return rb_uint128_to_numeric(*(rb_uint128_t*)&n); | ^~~~~~~~~~~~~~~~~ --- internal/numeric.h | 5 +++++ io_buffer.c | 5 ----- numeric.c | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/numeric.h b/internal/numeric.h index 75181a7f168620..d3905f048c2ab5 100644 --- a/internal/numeric.h +++ b/internal/numeric.h @@ -164,6 +164,11 @@ union rb_int128 { }; typedef union rb_int128 rb_int128_t; +union uint128_int128_conversion { + rb_uint128_t uint128; + rb_int128_t int128; +}; + // Conversion functions for 128-bit integers: rb_uint128_t rb_numeric_to_uint128(VALUE x); rb_int128_t rb_numeric_to_int128(VALUE x); diff --git a/io_buffer.c b/io_buffer.c index b81527ff71147a..55f1933194ef7b 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1928,11 +1928,6 @@ ruby_swap128_uint(rb_uint128_t x) return result; } -union uint128_int128_conversion { - rb_uint128_t uint128; - rb_int128_t int128; -}; - static inline rb_int128_t ruby_swap128_int(rb_int128_t x) { diff --git a/numeric.c b/numeric.c index d9e837644ffa05..1942a6164f616a 100644 --- a/numeric.c +++ b/numeric.c @@ -3638,7 +3638,10 @@ rb_int128_to_numeric(rb_int128_t n) } else { // Positive value - return rb_uint128_to_numeric(*(rb_uint128_t*)&n); + union uint128_int128_conversion conversion = { + .int128 = n + }; + return rb_uint128_to_numeric(conversion.uint128); } #endif } From ca8630b6363a706a43bfdb5a3359addcfb616d19 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Dec 2025 15:33:54 +0900 Subject: [PATCH 04/10] [ruby/rubygems] Extract and generate only bundler bin files instead of full installation. https://github.com/ruby/rubygems/commit/a70e573973 --- lib/rubygems/commands/setup_command.rb | 6 ++++-- test/rubygems/test_gem_commands_setup_command.rb | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 8fcece5d5ce252..175599967cf62d 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -402,8 +402,10 @@ def install_default_bundler_gem(bin_dir) install_dir: default_dir, wrappers: true ) - installer.install - File.delete installer.spec_file + # We need to install only executable and default spec files. + # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory. + installer.extract_bin + installer.generate_bin installer.write_default_spec ensure FileUtils.rm_f built_gem diff --git a/test/rubygems/test_gem_commands_setup_command.rb b/test/rubygems/test_gem_commands_setup_command.rb index dfd951268dc962..b33e05ab28dc55 100644 --- a/test/rubygems/test_gem_commands_setup_command.rb +++ b/test/rubygems/test_gem_commands_setup_command.rb @@ -31,6 +31,7 @@ def setup gemspec = util_spec "bundler", "9.9.9" do |s| s.bindir = "exe" s.executables = ["bundle", "bundler"] + s.files = ["lib/bundler.rb"] end File.open "bundler/bundler.gemspec", "w" do |io| @@ -222,6 +223,9 @@ def test_install_default_bundler_gem assert_path_exist "#{Gem.dir}/gems/bundler-#{bundler_version}" assert_path_exist "#{Gem.dir}/gems/bundler-audit-1.0.0" + + assert_path_exist "#{Gem.dir}/gems/bundler-#{bundler_version}/exe/bundle" + assert_path_not_exist "#{Gem.dir}/gems/bundler-#{bundler_version}/lib/bundler.rb" end def test_install_default_bundler_gem_with_default_gems_not_installed_at_default_dir From 09f8b8e3ad46683f1166b5dc1d5ef01cacfae3e4 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 8 Dec 2025 11:40:32 +0100 Subject: [PATCH 05/10] Test that Ractor.shareable_proc keeps the original Proc intact --- bootstraptest/test_ractor.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 9042f945f7f4f4..98fa7c276741b8 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -167,6 +167,25 @@ [pr1.call == self, pr2.call == nil] } +# Ractor.shareable_proc keeps the original Proc intact +assert_equal '[SyntaxError, [Object, 43, 43], Binding]', %q{ + a = 42 + pr1 = Proc.new do + [self.class, eval("a"), binding.local_variable_get(:a)] + end + a += 1 + pr2 = Ractor.shareable_proc(&pr1) + + r = [] + begin + pr2.call + rescue SyntaxError + r << SyntaxError + end + + r << pr1.call << pr1.binding.class +} + # Ractor::IsolationError cases assert_equal '3', %q{ ok = 0 From 39a3b88659a04729a7eec30bbc5ea804a565b9eb Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 8 Dec 2025 11:42:12 +0100 Subject: [PATCH 06/10] Fix some descriptions in bootstraptest/test_ractor.rb --- bootstraptest/test_ractor.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 98fa7c276741b8..75286e2d0c941e 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -150,14 +150,14 @@ Ractor.shareable_lambda{ a }.call } -# Ractor.make_shareable issue for locals in proc [Bug #18023] +# Ractor.shareable_proc issue for locals in proc [Bug #18023] assert_equal '[:a, :b, :c, :d, :e]', %q{ v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e closure = Proc.new { [v1, v2, v3, v4, v5] } Ractor.shareable_proc(&closure).call } -# Ractor.make_shareable makes a copy of given Proc +# Ractor.shareable_proc makes a copy of given Proc assert_equal '[true, true]', %q{ pr1 = Proc.new do self From 4cb3a61b5ec7714f57a7f43409ccc74746e7df6e Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 8 Dec 2025 11:47:03 +0100 Subject: [PATCH 07/10] Fix Ractor test to not depend on the previous test --- bootstraptest/test_ractor.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 75286e2d0c941e..17b1c05173c7bb 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -200,9 +200,9 @@ begin cond = false - a = 1 - a = 2 if cond - Ractor.shareable_proc{a} + b = 1 + b = 2 if cond + Ractor.shareable_proc{b} rescue Ractor::IsolationError => e ok += 1 end From 007a70a15c2911845f83872b83d39eeca7f0f607 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 8 Dec 2025 12:03:44 +0100 Subject: [PATCH 08/10] Test that Ractor.make_shareable mutates the original Proc --- bootstraptest/test_ractor.rb | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 17b1c05173c7bb..81dc2d6b8d04be 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -186,6 +186,37 @@ r << pr1.call << pr1.binding.class } +# Ractor.make_shareable mutates the original Proc +# This is the current behavior, it's currently considered safe enough +# because in most cases it would raise anyway due to not-shared self or not-shared captured variable value +assert_equal '[[42, 42], Binding, true, SyntaxError, "Can\'t create Binding from isolated Proc"]', %q{ + a = 42 + pr1 = nil.instance_exec do + Proc.new do + [eval("a"), binding.local_variable_get(:a)] + end + end + + r = [pr1.call, pr1.binding.class] + + pr2 = Ractor.make_shareable(pr1) + r << pr1.equal?(pr2) + + begin + pr1.call + rescue SyntaxError + r << SyntaxError + end + + begin + r << pr1.binding + rescue ArgumentError + r << $!.message + end + + r +} + # Ractor::IsolationError cases assert_equal '3', %q{ ok = 0 From 55668d748457e7d9db4db93c050e80afe2e9bb47 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Fri, 5 Dec 2025 16:13:23 -0800 Subject: [PATCH 09/10] Only globally clear the flag being cleared This solution is not quite correct because it doesn't solve multiple Ractors subscribing to the same event, but this will avoid unrelated events clobbering the flags for other events. This however will work corretly for subscribing to global ObjectSpace GC events. --- vm_trace.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/vm_trace.c b/vm_trace.c index 58424008fd4f0e..3a445892761521 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -127,9 +127,15 @@ update_global_event_hook(rb_event_flag_t prev_events, rb_event_flag_t new_events rb_clear_bf_ccs(); } - ruby_vm_event_flags = new_events; - ruby_vm_event_enabled_global_flags |= new_events; - rb_objspace_set_event_hook(new_events); + // FIXME: Which flags are enabled globally comes from multiple lists, one + // per-ractor and a global list. + // This incorrectly assumes the lists have mutually exclusive flags set. + // This is true for the global (objspace) events, but not for ex. multiple + // Ractors listening for the same iseq events. + rb_event_flag_t new_events_global = (ruby_vm_event_flags & ~prev_events) | new_events; + ruby_vm_event_flags = new_events_global; + ruby_vm_event_enabled_global_flags |= new_events_global; + rb_objspace_set_event_hook(new_events_global); // Invalidate JIT code as needed if (first_time_iseq_events_p || enable_c_call || enable_c_return) { From de94f88c6820c94b69df3343c6050cdb8cc0610e Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 3 Dec 2025 00:06:03 -0800 Subject: [PATCH 10/10] Register internal tracepoints globally Internal tracepoints only make sense to run globally, and they already took completely different paths. --- test/objspace/test_objspace.rb | 27 +++++++++++++++++++++++++ test/objspace/test_ractor.rb | 28 ++++++++++++++++++++++++++ vm.c | 2 ++ vm_core.h | 27 +++++++++++++++++++++++-- vm_trace.c | 36 +++++++++++++++++++++++++++------- 5 files changed, 111 insertions(+), 9 deletions(-) diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index e35fc0c14ecfe5..ea3c6aa7192146 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -288,6 +288,33 @@ def test_trace_object_allocations_gc_stress assert true # success end + def test_trace_object_allocations_with_other_tracepoint + # Test that ObjectSpace.trace_object_allocations isn't changed by changes + # to another tracepoint + line_tp = TracePoint.new(:line) { } + + ObjectSpace.trace_object_allocations_start + + obj1 = Object.new; line1 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj1) + assert_equal line1, ObjectSpace.allocation_sourceline(obj1) + + line_tp.enable + + obj2 = Object.new; line2 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj2) + assert_equal line2, ObjectSpace.allocation_sourceline(obj2) + + line_tp.disable + + obj3 = Object.new; line3 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj3) + assert_equal line3, ObjectSpace.allocation_sourceline(obj3) + ensure + ObjectSpace.trace_object_allocations_stop + ObjectSpace.trace_object_allocations_clear + end + def test_trace_object_allocations_compaction omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) diff --git a/test/objspace/test_ractor.rb b/test/objspace/test_ractor.rb index eb3044cda3c3c3..996d83fbd214dd 100644 --- a/test/objspace/test_ractor.rb +++ b/test/objspace/test_ractor.rb @@ -52,4 +52,32 @@ def fin ractors.each(&:join) RUBY end + + def test_trace_object_allocations_with_ractor_tracepoint + # Test that ObjectSpace.trace_object_allocations works globally across all Ractors + assert_ractor(<<~'RUBY', require: 'objspace') + ObjectSpace.trace_object_allocations do + obj1 = Object.new; line1 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj1) + assert_equal line1, ObjectSpace.allocation_sourceline(obj1) + + r = Ractor.new { + obj = Object.new; line = __LINE__ + [line, obj] + } + + obj2 = Object.new; line2 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj2) + assert_equal line2, ObjectSpace.allocation_sourceline(obj2) + + expected_line, ractor_obj = r.value + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(ractor_obj) + assert_equal expected_line, ObjectSpace.allocation_sourceline(ractor_obj) + + obj3 = Object.new; line3 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj3) + assert_equal line3, ObjectSpace.allocation_sourceline(obj3) + end + RUBY + end end diff --git a/vm.c b/vm.c index b15ee0ba236b57..f832fe401f124b 100644 --- a/vm.c +++ b/vm.c @@ -3314,6 +3314,8 @@ rb_vm_mark(void *ptr) rb_gc_mark_values(RUBY_NSIG, vm->trap_list.cmd); + rb_hook_list_mark(&vm->global_hooks); + rb_id_table_foreach_values(vm->negative_cme_table, vm_mark_negative_cme, NULL); rb_mark_tbl_no_pin(vm->overloaded_cme_table); for (i=0; ihooks; } +static inline rb_hook_list_t * +rb_vm_global_hooks(const rb_execution_context_t *ec) +{ + return &rb_ec_vm_ptr(ec)->global_hooks; +} + +static inline rb_hook_list_t * +rb_ec_hooks(const rb_execution_context_t *ec, rb_event_flag_t event) +{ + // Should be a single bit set + VM_ASSERT(event != 0 && ((event - 1) & event) == 0); + + if (event & RUBY_INTERNAL_EVENT_OBJSPACE_MASK) { + return rb_vm_global_hooks(ec); + } + else { + return rb_ec_ractor_hooks(ec); + } +} + #define EXEC_EVENT_HOOK(ec_, flag_, self_, id_, called_id_, klass_, data_) \ - EXEC_EVENT_HOOK_ORIG(ec_, rb_ec_ractor_hooks(ec_), flag_, self_, id_, called_id_, klass_, data_, 0) + EXEC_EVENT_HOOK_ORIG(ec_, rb_ec_hooks(ec_, flag_), flag_, self_, id_, called_id_, klass_, data_, 0) #define EXEC_EVENT_HOOK_AND_POP_FRAME(ec_, flag_, self_, id_, called_id_, klass_, data_) \ - EXEC_EVENT_HOOK_ORIG(ec_, rb_ec_ractor_hooks(ec_), flag_, self_, id_, called_id_, klass_, data_, 1) + EXEC_EVENT_HOOK_ORIG(ec_, rb_ec_hooks(ec_, flag_), flag_, self_, id_, called_id_, klass_, data_, 1) static inline void rb_exec_event_hook_script_compiled(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE eval_script) diff --git a/vm_trace.c b/vm_trace.c index 3a445892761521..b2fc436a96b210 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -194,7 +194,17 @@ hook_list_connect(VALUE list_owner, rb_hook_list_t *list, rb_event_hook_t *hook, static void connect_event_hook(const rb_execution_context_t *ec, rb_event_hook_t *hook) { - rb_hook_list_t *list = rb_ec_ractor_hooks(ec); + rb_hook_list_t *list; + + /* internal events are VM-global, non-internal are ractor-local */ + VM_ASSERT(!(hook->events & RUBY_INTERNAL_EVENT_OBJSPACE_MASK) || !(hook->events & ~RUBY_INTERNAL_EVENT_OBJSPACE_MASK)); + + if (hook->events & RUBY_INTERNAL_EVENT_OBJSPACE_MASK) { + list = rb_vm_global_hooks(ec); + } + else { + list = rb_ec_ractor_hooks(ec); + } hook_list_connect(Qundef, list, hook, TRUE); } @@ -278,11 +288,9 @@ clean_hooks_check(rb_hook_list_t *list) #define MATCH_ANY_FILTER_TH ((rb_thread_t *)1) -/* if func is 0, then clear all funcs */ static int -remove_event_hook(const rb_execution_context_t *ec, const rb_thread_t *filter_th, rb_event_hook_func_t func, VALUE data) +remove_event_hook_from_list(rb_hook_list_t *list, const rb_thread_t *filter_th, rb_event_hook_func_t func, VALUE data) { - rb_hook_list_t *list = rb_ec_ractor_hooks(ec); int ret = 0; rb_event_hook_t *hook = list->hooks; @@ -303,6 +311,18 @@ remove_event_hook(const rb_execution_context_t *ec, const rb_thread_t *filter_th return ret; } +/* if func is 0, then clear all funcs */ +static int +remove_event_hook(const rb_execution_context_t *ec, const rb_thread_t *filter_th, rb_event_hook_func_t func, VALUE data) +{ + int ret = 0; + + ret += remove_event_hook_from_list(rb_ec_ractor_hooks(ec), filter_th, func, data); + ret += remove_event_hook_from_list(rb_vm_global_hooks(ec), filter_th, func, data); + + return ret; +} + static int rb_threadptr_remove_event_hook(const rb_execution_context_t *ec, const rb_thread_t *filter_th, rb_event_hook_func_t func, VALUE data) { @@ -427,8 +447,10 @@ rb_exec_event_hooks(rb_trace_arg_t *trace_arg, rb_hook_list_t *hooks, int pop_p) { rb_execution_context_t *ec = trace_arg->ec; - if (UNLIKELY(trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) { - if (ec->trace_arg && (ec->trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) { + if (UNLIKELY(trace_arg->event & RUBY_INTERNAL_EVENT_OBJSPACE_MASK)) { + VM_ASSERT(hooks == rb_vm_global_hooks(ec)); + + if (ec->trace_arg && (ec->trace_arg->event & RUBY_INTERNAL_EVENT_OBJSPACE_MASK)) { /* skip hooks because this thread doing INTERNAL_EVENT */ } else { @@ -436,7 +458,7 @@ rb_exec_event_hooks(rb_trace_arg_t *trace_arg, rb_hook_list_t *hooks, int pop_p) ec->trace_arg = trace_arg; /* only global hooks */ - exec_hooks_unprotected(ec, rb_ec_ractor_hooks(ec), trace_arg); + exec_hooks_unprotected(ec, hooks, trace_arg); ec->trace_arg = prev_trace_arg; } }