[ruby-core:127168] [Ruby Bug#22453] GET_EC() may break when the native thread switches under M:N threads
Issue #22453 has been reported by mame (Yusuke Endoh). ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by byroot (Jean Boussier).
I think option 1 (Avoid GET_EC() as much as possible and pass ec explicitly.) is not realistic because it requires rewriting a large amount of existing code.
I agree that it's not a suitable solution short term, especially since in many case the caller is the public C API, so passing the `ec` isn't just an internal refactoring, it also require C API and C extensions changes. However, from my experience `GET_EC()` is quite a hotspot, and I merged several performance improvement patches that basically passed `ec` around instead of calling `GET_EC()`, as such I think we should keep that in mind when designing new APIs, and any refactoring that eliminate some `GET_EC()` would be welcome. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119586 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by mame (Yusuke Endoh). @byroot I agree. This proposal is to make everything safe for now, because there are too many places that would need to be fixed. I'm not against reducing `GET_EC()` calls, not as fixes but as improvements. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119587 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by luke-gru (Luke Gruber). I agree something needs to be done about this. I wonder if something like this would work (it appears to): ```patch +// With -fPIC, the default TLS model (general-dynamic) gets the address of a +// TLS variable from __tls_get_addr(), and the compiler may reuse that address +// for the rest of the function. Under M:N threads the native thread can change +// mid-function, so the reused address points into another thread's TLS. +// initial-exec on x86-64 reads through %fs on each access instead. +#if defined(__x86_64__) && defined(__ELF__) && RBIMPL_HAS_ATTRIBUTE(tls_model) +# define RB_THREAD_LOCAL_TLS_MODEL __attribute__((tls_model("initial-exec"))) +#else +# define RB_THREAD_LOCAL_TLS_MODEL +#endif - RUBY_EXTERN RB_THREAD_LOCAL_SPECIFIER struct rb_execution_context_struct *ruby_current_ec; + RUBY_EXTERN RB_THREAD_LOCAL_SPECIFIER RB_THREAD_LOCAL_TLS_MODEL struct rb_execution_context_struct *ruby_current_ec; ``` Every TLS read becomes `mov %fs:(%reg)`, like in a static build. The register holds only the variable's offset into the TLS block, which should be the same for each thread, and `%fs` base is per-thread, it applies the correct thread on each access. Nothing guarantees that this will work... the compiler could still generate a pointer to fs+offset and call it after a thread switch, but no compiler I tested does this. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119596 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by luke-gru (Luke Gruber). Something like this should definitely work too, and it's arguably safer as it doesn't rely the compiler's optimizations. ```patch + // Under M:N threads the native thread can change in the middle of a C + // function. The compiler assumes it doesn't, so it may reuse a TLS address + // (e.g. the result of __tls_get_addr() with -fPIC) after a switch, which + // then points into another thread's TLS. Reading the variable entirely in + // asm volatile hides the TLS address from the compiler, so each read uses + // the current thread's %fs. + # if defined(__x86_64__) && defined(__ELF__) + static inline struct rb_execution_context_struct * + rb_current_ec_tls(void) + { + struct rb_execution_context_struct *ec; + __asm__ volatile("movq ruby_current_ec@gottpoff(%%rip), %0\n\t" + "movq %%fs:(%0), %0" + : "=r"(ec)); + return ec; + } + # else + # define rb_current_ec_tls() ruby_current_ec + # endif + - rb_execution_context_t * volatile ec = ruby_current_ec; + rb_execution_context_t * volatile ec = rb_current_ec_tls(); ``` ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119597 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by mame (Yusuke Endoh). @luke-gru Before discussing x86-64 specific improvements, I'd like to first agree on making NOINLINE the default on all platforms, including non-x86-64. I ran ruby-bench (headline benchmarks, interpreter, GCC 16), and the NOINLINE build appears to be about 1-4% faster than the current build. | bench | base (ms) | NOINLINE (ms) | base/NOINLINE | |----------------|----------:|--------------:|--------------:| | chunky-png | 626.5 | 610.4 | 1.026 (***) | | erubi-rails | 1161.4 | 1119.8 | 1.037 (***) | | hexapdf | 1907.9 | 1874.1 | 1.018 (**) | | liquid-c | 51.2 | 50.6 | 1.011 | | liquid-compile | 40.1 | 41.1 | 0.977 (***) | | liquid-il | 287.1 | 277.4 | 1.035 (***) | | liquid-render | 127.2 | 125.8 | 1.011 (**) | | mail | 101.9 | 101.2 | 1.008 | | psych-load | 1581.7 | 1558.5 | 1.015 (*) | | rubocop | 141.4 | 144.6 | 0.978 | | ruby-lsp | 126.7 | 125.4 | 1.010 (*) | (Higher is better for NOINLINE. ***: p < 0.001, **: p < 0.01, *: p < 0.05) It fixes many problems at once and is even slightly faster, so since Ruby 4.1 is coming soon, how about going with this for now? I'm not against adding initial-exec as an x86-64 specific improvement, but I'm not familiar enough with it to judge. I asked Claude to look into it, and it seems it can cause problems for applications that dlopen libruby.so (e.g., vim's Ruby interface): * libruby currently requires 136 bytes of TLS (mostly from the Rust code of YJIT/ZJIT). * If even one TLS variable uses initial-exec, all 136 bytes are placed in the static TLS area. * glibc reserves 512 bytes of surplus static TLS for dlopen'ed libraries, so it is fine (unless other dlopen'ed libraries use it up). * musl has no surplus, so dlopen always fails (confirmed). * FreeBSD has 128 bytes, which is probably not enough, so it would fail (not confirmed). So it would probably be best to enable it only on x86-64 with glibc. There may be other things to consider as well. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119602 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by luke-gru (Luke Gruber). I agree to make noinline the default for 4.1. Maybe we can use one of the alternatives for static x86 builds at a later time. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119603 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by mame (Yusuke Endoh). @luke-gru Thank you. I've also got approval from @ko1, so I'll merge it for now. Could you create a separate ticket or PR for the initial-exec approach? I had Claude evaluate your change, and it seems to be about 1-2% faster than NOINLINE. I think it would be better for you to create it rather than me, since I don't understand it very well. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119605 * Author: mame (Yusuke Endoh) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
Issue #22453 has been updated by mame (Yusuke Endoh). Merged. Thanks. Just for the record, I looked into why NOINLINE can be faster. As somewhat expected, L1 icache misses decrease. GET_EC() has about 900 call sites in libruby, and inlining all of them bloats the code. With NOINLINE, each of them becomes basically a single call instruction, so the code gets smaller (.text: -22KB) and icache misses decrease. I confirmed this with perf stat on the ruby-bench headline benchmarks (GCC 16, x86-64, interpreter, 3 runs each): | bench | L1-icache-load-misses (base → NOINLINE) | |----------------|----------------------------------------:| | chunky-png | 24M → 15M (-36%) | | erubi-rails | 397M → 332M (-16%) | | hexapdf | 448M → 326M (-27%) | | liquid-c | 209M → 162M (-23%) | | liquid-compile | 147M → 128M (-13%) | | liquid-il | 75M → 58M (-22%) | | liquid-render | 153M → 118M (-23%) | | mail | 131M → 127M (-3%) | | psych-load | 640M → 516M (-19%) | | rubocop | 114M → 82M (-28%) | | ruby-lsp | 90M → 67M (-26%) | (activerecord, lobsters, railsbench, sequel and shipit are excluded because they failed to set up in my environment.) That said, whether this leads to a measurable speedup may depend on the CPU, e.g., its icache size. ---------------------------------------- Bug #22453: GET_EC() may break when the native thread switches under M:N threads https://bugs.ruby-lang.org/issues/22453#change-119611 * Author: mame (Yusuke Endoh) * Status: Closed * Assignee: ko1 (Koichi Sasada) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- There are many bugs caused by the bad interaction between the M:N thread model and `GET_EC()`. A simple example is code that switches the native thread inside a block of `String#split`. ```ruby 8.times.map do Ractor.new do 50.times do ",,".split(/(,)/) { sleep 0.001 } end :ok end end.map(&:value) ``` When Ruby is built with Clang 21, this causes a segmentation fault: ``` $ ./build-clang21/miniruby 31_string_split.rb 31_string_split.rb:2: warning: Ractor API is experimental and may change in future versions of Ruby. 31_string_split.rb:4: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.1.0dev (2026-10-14T18:01:17Z master 90a3551c8a) +PRISM [x86_64-linux] ``` I had Claude Code analyze the problem, and it found 32 similar bugs (listed at the end). I believe there are many more. ## Cause The cause is a bad interaction between C compiler optimizations and the M:N thread model. A C compiler may optimize code assuming that the native thread does not change during the execution of a function. For an access to a variable stored in Thread-local Storage (TLS), it may cache the pointer to the variable in the function and generate code that dereferences the cached pointer in later accesses. On the other hand, under the M:N thread model, the native thread may change in the middle of the execution of a C function. So, if the native thread switches after the pointer is cached, the pointer still points to the old TLS, and dereferencing it may cause a SEGV or other failures. In the `String#split` example above, `str_alloc_embed` is inlined into `rb_str_split_m`, and the TLS address used by `GET_EC()` in it is cached. Then, when `sleep 0.001` in the block of `split` switches the native thread, the invalid address is dereferenced. This problem has already been reported in #20243, #21046, #21534, #22440 and others, but it has been addressed by fixing individual places or by workarounds for specific platforms. However, with newer compilers such as GCC 16 and Clang 21, I have confirmed that it happens in many places on x86-64. ## How to fix There are two options. 1. Avoid `GET_EC()` as much as possible and pass `ec` explicitly. 2. Make `rb_current_ec()` NOINLINE so that the compiler cannot cache the TLS address. I think option 1 is not realistic because it requires rewriting a large amount of existing code. Option 2 is already applied on arm64 and ppc64le (https://github.com/ruby/ruby/blob/61d31c4d2b956e1bf7fc09cced93ad20d9c5c81c/t...). I propose to apply it unconditionally. PR: https://github.com/ruby/ruby/pull/19421 With this change, none of the 32 reproducers reproduce anymore. ## Performance The concern with NOINLINE is performance, so I measured it. Some cases get slower and others get faster. Overall, I think the impact is small. Change in execution time from before to after the patch (median of 20 runs; negative means faster): | Benchmark | GCC 16 | Clang 21 | |---|---|---| | alloc (`Object.new`) | -0.4% | +0.0% | | method (method call) | -0.9% | +0.4% | | block (`yield`) | -1.1% | -1.4% | | cfunc (`Array#size` etc.) | -0.8% | +1.5% | Environment: AMD Ryzen 9 6900HX, Ubuntu, source at 90a3551c8a, built with --enable-shared. ## Reproducers ### 1. `rb_f_raise` ```ruby class E < StandardError def exception(*) = (sleep 0.001; self) end 8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16) ``` ### 2. `fiber_initialize` ```ruby class H < Hash def initialize_copy(*) = (sleep 0.001; super) end 8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 3. `nurat_convert` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 4. `f_complex_polar_real` ```ruby class N < Numeric def real? = true def coerce(o) = [self, self] def to_f = 1.0 def *(o) = (sleep 0.001; 2) def +(o) = 3 end 8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 5. `nurat_rationalize` ```ruby class Eps < Numeric def real? = true def abs = self def zero? = false def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000]) end 8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 6. `rb_complex_uminus` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 7. `rb_complex_conjugate` ```ruby class A < Numeric def real? = true def -@ = self def -(o) = (sleep 0.001; 5) end class B < Numeric def real? = true def -@ = Complex(1, 2) end 8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 8. `rb_complex_pow` ```ruby class Z < Numeric def ==(o) = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 9. `rb_complex_pow (complex_pow_for_special_angle)` ```ruby class N < Numeric def real? = true def <(o) = (sleep 0.001; true) def -@ = 3 end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 10. `f_muldiv` ```ruby 4.times.map do |i| Ractor.new(i) do |i| q = Rational(7**3000 + i, 3**1500 + 1) t = Time.now 2.times.map { Thread.new { q.round(5) while Time.now - t < 0.5 } }.each(&:join) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 11. `exec_recursive` ```ruby class Slow def inspect = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 12. `exec_recursive (outermost)` ```ruby class SlowKey def hash = (sleep 0.001; raise "boom") end 8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 13. `rb_thread_sleep_deadly` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { Thread.stop } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } sleeper.wakeup rescue nil sleep 0.002 end finished = true end.join sleeper.wakeup rescue nil until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 14. `mutex_lock_uninterruptible` ```ruby class Ping < StandardError; end 8.times.map do Ractor.new do m = Mutex.new cv = ConditionVariable.new finished = false sleeper = Thread.new do until finished begin Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } } rescue Ping end end end Thread.new do t_end = Time.now + 0.05 while Time.now < t_end m.synchronize do cv.signal 5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 } end sleep 0.002 end finished = true end.join m.synchronize { cv.broadcast } until sleeper.join(0.001) end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 15. `gc_finalize_deferred` ```ruby 8.times.map do Ractor.new do 10.times do 4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) } GC.start sleep 0.0001 end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000024 (GCC 16, Clang 21) ``` ### 16. `rb_str_times` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 17. `rb_const_search_from` ```ruby 50.times do |i| path = File.expand_path("nt_switch_autoload_#{i}.rb") File.write(path, "sleep 0.001\n") # does not define the constant autoload :"C#{i}", path end 8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000098 (GCC 16) ``` ### 18. `rb_to_int` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 19. `rb_check_integer_type` ```ruby class N def to_int = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 20. `rb_f_integer1` ```ruby class N def to_i = (sleep 0.001; 3) end 8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (GCC 16, Clang 21) ``` ### 21. `nucomp_s_new` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 22. `nucomp_s_polar` ```ruby class N < Numeric def real? = (sleep 0.001; true) end 8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (GCC 16) ``` ### 23. `detach_process_watcher` ```ruby 8.times.map do Ractor.new do 10.times do ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) } ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value } end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 24. `proc_waitall` ```ruby 8.times.map do Ractor.new do 12.times do 4.times { Process.spawn("sleep", "0.001") } 2.times.map { Thread.new { Process.waitall } }.each(&:join) end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 25. `rb_thread_io_close_wait` ```ruby 12.times.map do Ractor.new do 100.times do r, w = IO.pipe ths = 8.times.map { Thread.new { r.read rescue nil } } Thread.pass until ths.all? { it.status == "sleep" } r.close ths.each(&:join) w.close end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 26. `rb_error_frozen_object` ```ruby class F def self.to_s = (sleep 0.001; "F") def self.inspect = (sleep 0.001; "F") end 8.times.map do Ractor.new do 50.times do F.new.freeze.instance_variable_set(:@a, 1) rescue FrozenError end end end.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000010 (Clang 21) ``` ### 27. `rb_ary_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 28. `rb_ary_repeated_combination` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 29. `rb_ary_repeated_permutation` ```ruby 8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 30. `rb_ary_transpose` ```ruby class A def initialize(slow) = @slow = slow def to_ary = (sleep 0.001 if @slow; [1, 2, 3]) end 8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 31. `rb_str_split_m` ```ruby 8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` ### 32. `ruby_float_step` ```ruby 8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join) #=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21) ``` -- https://bugs.ruby-lang.org/
participants (3)
-
byroot (Jean Boussier) -
luke-gru (Luke Gruber) -
mame (Yusuke Endoh)