mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 08:25:20 +08:00
Always use the no-inline rb_current_ec()
Under the M:N thread scheduler, a Ruby thread in a non-main Ractor may be
resumed on a different native thread after it blocks. The C compiler
assumes that the native thread never changes within a function, so it may
compute the address of ruby_current_ec once (`__tls_get_addr` in
libruby.so) and reuse it after the switch. The address then refers to the
TLS of the previous native thread, which leads to a SEGV or to using
another thread's ec.
8.times.map do
Ractor.new do
50.times do
",,".split(/(,)/) { sleep 0.001 }
end
:ok
end
end.map(&:value)
#=> [BUG] Segmentation fault at 0x0000000000000030 (with Clang 21)
arm64 and ppc64le already use the out-of-line rb_current_ec()
(RB_THREAD_CURRENT_EC_NOINLINE), which prevents the compiler from caching
the address. Use it on all platforms, and remove the macro.
[Bug #22453]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
574524d570
commit
053f8da910
7 files changed
+374
-20
Notes:
git
2026-10-21 15:28:05 +00:00
No files matched your search
@@ -0,0 +1,368 @@
|
||||
# Under M:N threads, a thread of a non-main Ractor may be resumed on another
|
||||
# native thread after it blocks. If a C function caches the TLS address of
|
||||
# ruby_current_ec across such a switch, it reads the ec of another native
|
||||
# thread and crashes. Each test below blocks in Ruby code called back from
|
||||
# such a C function.
|
||||
|
||||
# rb_f_raise
|
||||
assert_equal 'ok', %q{
|
||||
class E < StandardError
|
||||
def exception(*) = (sleep 0.001; self)
|
||||
end
|
||||
8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# fiber_initialize
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# nurat_convert
|
||||
assert_equal 'ok', %q{
|
||||
class N
|
||||
def to_int = (sleep 0.001; 3)
|
||||
end
|
||||
8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# f_complex_polar_real
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# nurat_rationalize
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_complex_uminus
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_complex_conjugate
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_complex_pow
|
||||
assert_equal 'ok', %q{
|
||||
class Z < Numeric
|
||||
def ==(o) = (sleep 0.001; true)
|
||||
end
|
||||
8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_complex_pow (complex_pow_for_special_angle)
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# exec_recursive
|
||||
assert_equal 'ok', %q{
|
||||
class Slow
|
||||
def inspect = (sleep 0.001; raise "boom")
|
||||
end
|
||||
8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# exec_recursive (outermost)
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_thread_sleep_deadly
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# mutex_lock_uninterruptible
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# gc_finalize_deferred
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_str_times
|
||||
assert_equal 'ok', %q{
|
||||
class N
|
||||
def to_int = (sleep 0.001; 3)
|
||||
end
|
||||
8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_const_search_from
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_to_int
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_check_integer_type
|
||||
assert_equal 'ok', %q{
|
||||
class N
|
||||
def to_int = (sleep 0.001; 3)
|
||||
end
|
||||
8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_f_integer1
|
||||
assert_equal 'ok', %q{
|
||||
class N
|
||||
def to_i = (sleep 0.001; 3)
|
||||
end
|
||||
8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# nucomp_s_new
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# nucomp_s_polar
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
if /wasi|mswin|mingw/ !~ target_platform
|
||||
# detach_process_watcher
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# proc_waitall
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_thread_io_close_wait
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
end
|
||||
|
||||
# rb_error_frozen_object
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_ary_permutation
|
||||
assert_equal 'ok', %q{
|
||||
8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_ary_repeated_combination
|
||||
assert_equal 'ok', %q{
|
||||
8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_ary_repeated_permutation
|
||||
assert_equal 'ok', %q{
|
||||
8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_ary_transpose
|
||||
assert_equal 'ok', %q{
|
||||
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)
|
||||
:ok
|
||||
}
|
||||
|
||||
# rb_str_split_m
|
||||
assert_equal 'ok', %q{
|
||||
8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
|
||||
# ruby_float_step
|
||||
assert_equal 'ok', %q{
|
||||
8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join)
|
||||
:ok
|
||||
}
|
||||
@@ -2936,7 +2936,7 @@ gc_mark_func_data_slotp_of(rb_ractor_t *const cr)
|
||||
#define GC_MARK_FUNC_DATA_SLOTP() gc_mark_func_data_slotp_of(rb_current_ractor_raw(false))
|
||||
|
||||
/* Resolve the current Ractor once for a whole marking walk. rb_current_ractor_raw() is an
|
||||
* out-of-line rb_current_ec() call wherever RB_THREAD_CURRENT_EC_NOINLINE is set (arm64), so
|
||||
* out-of-line rb_current_ec() call, so
|
||||
* the walk threads this ctx down rather than repeating the lookup per reference. */
|
||||
static inline struct rb_gc_mark_ctx
|
||||
gc_current_mark_ctx(void)
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ void rb_gc_mark_and_move(VALUE *ptr);
|
||||
|
||||
/* The objspace a marking walk marks into, plus the per-Ractor traversal redirect slot
|
||||
* (vm_core.h's gc_mark_func_data_struct) and the shareable-check flag cached from it.
|
||||
* Resolving the current Ractor costs an out-of-line rb_current_ec() call on arm64, so a
|
||||
* Resolving the current Ractor costs an out-of-line rb_current_ec() call, so a
|
||||
* walk resolves it once into a ctx and threads that down instead of repeating it per
|
||||
* reference. Pass the pointer along wherever one is already in hand; build a fresh one
|
||||
* only at an entry point that has none (rb_gc_current_mark_ctx below). */
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
|
||||
RUBY_EXTERN struct rb_execution_context_struct *ruby_current_ec;
|
||||
NOINLINE(struct rb_execution_context_struct *rb_current_ec_noinline(void)); // for assertions
|
||||
struct rb_execution_context_struct *rb_current_ec(void);
|
||||
|
||||
#endif /* RUBY_THREAD_NONE_H */
|
||||
+3
-10
@@ -20,21 +20,14 @@
|
||||
#define RB_NATIVETHREAD_LOCK_INIT PTHREAD_MUTEX_INITIALIZER
|
||||
#define RB_NATIVETHREAD_COND_INIT PTHREAD_COND_INITIALIZER
|
||||
|
||||
// TLS can not be accessed across .so on arm64 and perhaps ppc64le too.
|
||||
#if defined(__arm64__) || defined(__aarch64__) || defined(__powerpc64__)
|
||||
# define RB_THREAD_CURRENT_EC_NOINLINE
|
||||
#endif
|
||||
|
||||
#include "thread_sched.h"
|
||||
|
||||
#ifdef RB_THREAD_LOCAL_SPECIFIER
|
||||
NOINLINE(void rb_current_ec_set(struct rb_execution_context_struct *));
|
||||
|
||||
# ifdef RB_THREAD_CURRENT_EC_NOINLINE
|
||||
NOINLINE(struct rb_execution_context_struct *rb_current_ec(void));
|
||||
# else
|
||||
RUBY_EXTERN RB_THREAD_LOCAL_SPECIFIER struct rb_execution_context_struct *ruby_current_ec;
|
||||
# endif
|
||||
// Under M:N threads, a thread may move to another native thread in the middle of
|
||||
// a C function, so the compiler must not cache the TLS address of ruby_current_ec.
|
||||
NOINLINE(struct rb_execution_context_struct *rb_current_ec(void));
|
||||
|
||||
// for RUBY_DEBUG_LOG()
|
||||
RUBY_EXTERN RB_THREAD_LOCAL_SPECIFIER rb_atomic_t ruby_nt_serial;
|
||||
|
||||
@@ -745,15 +745,11 @@ rb_current_ec_set(rb_execution_context_t *ec)
|
||||
ruby_current_ec = ec;
|
||||
}
|
||||
|
||||
|
||||
#ifdef RB_THREAD_CURRENT_EC_NOINLINE
|
||||
rb_execution_context_t *
|
||||
rb_current_ec(void)
|
||||
{
|
||||
return ruby_current_ec;
|
||||
}
|
||||
|
||||
#endif
|
||||
#else
|
||||
native_tls_key_t ruby_current_ec_key;
|
||||
|
||||
|
||||
@@ -2216,11 +2216,7 @@ static inline rb_execution_context_t *
|
||||
rb_current_execution_context(bool expect_ec)
|
||||
{
|
||||
#ifdef RB_THREAD_LOCAL_SPECIFIER
|
||||
#ifdef RB_THREAD_CURRENT_EC_NOINLINE
|
||||
rb_execution_context_t * volatile ec = rb_current_ec();
|
||||
#else
|
||||
rb_execution_context_t * volatile ec = ruby_current_ec;
|
||||
#endif
|
||||
|
||||
/* On the shared objects, `__tls_get_addr()` is used to access the TLS
|
||||
* and the address of the `ruby_current_ec` can be stored on a function
|
||||
|
||||
Reference in new issue
Block a user