Project

General

Profile

Actions

Bug #22453

open

GET_EC() may break when the native thread switches under M:N threads

Bug #22453: GET_EC() may break when the native thread switches under M:N threads

Added by mame (Yusuke Endoh) about 5 hours ago. Updated about 2 hours ago.

Status:
Open
Target version:
-
[ruby-core:127168]

Description

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.

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/thread_pthread.h#L24-L26). 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

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

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

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

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

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

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

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

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)

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join)
#=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21)

32. ruby_float_step

8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join)
#=> [BUG] Segmentation fault at 0x0000000000000030 (Clang 21)

Related issues 4 (1 open — 3 closed)

Related to Ruby - Bug #20243: M:N threading VM_ASSERT failure in rb_current_execution_context with clang 17 (on Linux)OpenActions
Related to Ruby - Bug #21046: Backport: TLS fix for ARM64ClosedActions
Related to Ruby - Bug #21534: ppc64le bootstraptest/test_ractor.rb aborted (core dumped) on configure cppflags=-DRUBY_DEBUG and -O3Closedjaruga (Jun Aruga)Actions
Related to Ruby - Bug #22440: Referencing an autoload constant from multiple Ractors dumps coreClosedko1 (Koichi Sasada)Actions
Actions

Also available in: PDF Atom