Skip to content

Commit 053f8da

Browse files
mameclaude
andcommitted
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>
1 parent 574524d commit 053f8da

7 files changed

Lines changed: 374 additions & 20 deletions

File tree

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
# Under M:N threads, a thread of a non-main Ractor may be resumed on another
2+
# native thread after it blocks. If a C function caches the TLS address of
3+
# ruby_current_ec across such a switch, it reads the ec of another native
4+
# thread and crashes. Each test below blocks in Ruby code called back from
5+
# such a C function.
6+
7+
# rb_f_raise
8+
assert_equal 'ok', %q{
9+
class E < StandardError
10+
def exception(*) = (sleep 0.001; self)
11+
end
12+
8.times.map { Ractor.new { 50.times { raise E.new rescue nil } } }.each(&:join)
13+
:ok
14+
}
15+
16+
# fiber_initialize
17+
assert_equal 'ok', %q{
18+
class H < Hash
19+
def initialize_copy(*) = (sleep 0.001; super)
20+
end
21+
8.times.map { Ractor.new { 50.times { Fiber.new(storage: H.new) {}.resume } } }.each(&:join)
22+
:ok
23+
}
24+
25+
# nurat_convert
26+
assert_equal 'ok', %q{
27+
class N
28+
def to_int = (sleep 0.001; 3)
29+
end
30+
8.times.map { Ractor.new { 25.times { Rational(N.new, N.new) } } }.each(&:join)
31+
:ok
32+
}
33+
34+
# f_complex_polar_real
35+
assert_equal 'ok', %q{
36+
class N < Numeric
37+
def real? = true
38+
def coerce(o) = [self, self]
39+
def to_f = 1.0
40+
def *(o) = (sleep 0.001; 2)
41+
def +(o) = 3
42+
end
43+
8.times.map { Ractor.new { 25.times { Complex.polar(N.new, 2) } } }.each(&:join)
44+
:ok
45+
}
46+
47+
# nurat_rationalize
48+
assert_equal 'ok', %q{
49+
class Eps < Numeric
50+
def real? = true
51+
def abs = self
52+
def zero? = false
53+
def coerce(o) = (sleep 0.001; [o, 1r / 1_000_000_000])
54+
end
55+
8.times.map { Ractor.new { 25.times { (3r / 7).rationalize(Eps.new) } } }.each(&:join)
56+
:ok
57+
}
58+
59+
# rb_complex_uminus
60+
assert_equal 'ok', %q{
61+
class A < Numeric
62+
def real? = true
63+
def -@ = self
64+
def -(o) = (sleep 0.001; 5)
65+
end
66+
class B < Numeric
67+
def real? = true
68+
def -@ = Complex(1, 2)
69+
end
70+
8.times.map { Ractor.new { 50.times { -Complex.rect(A.new, B.new) } } }.each(&:join)
71+
:ok
72+
}
73+
74+
# rb_complex_conjugate
75+
assert_equal 'ok', %q{
76+
class A < Numeric
77+
def real? = true
78+
def -@ = self
79+
def -(o) = (sleep 0.001; 5)
80+
end
81+
class B < Numeric
82+
def real? = true
83+
def -@ = Complex(1, 2)
84+
end
85+
8.times.map { Ractor.new { 50.times { Complex.rect(A.new, B.new).conjugate } } }.each(&:join)
86+
:ok
87+
}
88+
89+
# rb_complex_pow
90+
assert_equal 'ok', %q{
91+
class Z < Numeric
92+
def ==(o) = (sleep 0.001; true)
93+
end
94+
8.times.map { Ractor.new { 50.times { Complex(1, 2) ** Z.new } } }.each(&:join)
95+
:ok
96+
}
97+
98+
# rb_complex_pow (complex_pow_for_special_angle)
99+
assert_equal 'ok', %q{
100+
class N < Numeric
101+
def real? = true
102+
def <(o) = (sleep 0.001; true)
103+
def -@ = 3
104+
end
105+
8.times.map { Ractor.new { 50.times { Complex.rect(N.new, 0) ** 2 } } }.each(&:join)
106+
:ok
107+
}
108+
109+
# exec_recursive
110+
assert_equal 'ok', %q{
111+
class Slow
112+
def inspect = (sleep 0.001; raise "boom")
113+
end
114+
8.times.map { Ractor.new { 50.times { [Slow.new].inspect rescue nil } } }.each(&:join)
115+
:ok
116+
}
117+
118+
# exec_recursive (outermost)
119+
assert_equal 'ok', %q{
120+
class SlowKey
121+
def hash = (sleep 0.001; raise "boom")
122+
end
123+
8.times.map { Ractor.new { h = {}; 50.times { (h[SlowKey.new] = 1) rescue nil } } }.each(&:join)
124+
:ok
125+
}
126+
127+
# rb_thread_sleep_deadly
128+
assert_equal 'ok', %q{
129+
class Ping < StandardError; end
130+
8.times.map do
131+
Ractor.new do
132+
finished = false
133+
sleeper = Thread.new do
134+
until finished
135+
begin
136+
Thread.handle_interrupt(Object => :never) { Thread.stop }
137+
rescue Ping
138+
end
139+
end
140+
end
141+
Thread.new do
142+
t_end = Time.now + 0.05
143+
while Time.now < t_end
144+
5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 }
145+
sleeper.wakeup rescue nil
146+
sleep 0.002
147+
end
148+
finished = true
149+
end.join
150+
sleeper.wakeup rescue nil until sleeper.join(0.001)
151+
end
152+
end.each(&:join)
153+
:ok
154+
}
155+
156+
# mutex_lock_uninterruptible
157+
assert_equal 'ok', %q{
158+
class Ping < StandardError; end
159+
8.times.map do
160+
Ractor.new do
161+
m = Mutex.new
162+
cv = ConditionVariable.new
163+
finished = false
164+
sleeper = Thread.new do
165+
until finished
166+
begin
167+
Thread.handle_interrupt(Object => :never) { m.synchronize { cv.wait(m) } }
168+
rescue Ping
169+
end
170+
end
171+
end
172+
Thread.new do
173+
t_end = Time.now + 0.05
174+
while Time.now < t_end
175+
m.synchronize do
176+
cv.signal
177+
5.times { sleeper.raise(Ping) rescue nil; sleep 0.0005 }
178+
end
179+
sleep 0.002
180+
end
181+
finished = true
182+
end.join
183+
m.synchronize { cv.broadcast } until sleeper.join(0.001)
184+
end
185+
end.each(&:join)
186+
:ok
187+
}
188+
189+
# gc_finalize_deferred
190+
assert_equal 'ok', %q{
191+
8.times.map do
192+
Ractor.new do
193+
10.times do
194+
4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) }
195+
GC.start
196+
sleep 0.0001
197+
end
198+
end
199+
end.each(&:join)
200+
:ok
201+
}
202+
203+
# rb_str_times
204+
assert_equal 'ok', %q{
205+
class N
206+
def to_int = (sleep 0.001; 3)
207+
end
208+
8.times.map { Ractor.new { 50.times { "ab" * N.new } } }.each(&:join)
209+
:ok
210+
}
211+
212+
# rb_const_search_from
213+
assert_equal 'ok', %q{
214+
50.times do |i|
215+
path = File.expand_path("nt_switch_autoload_#{i}.rb")
216+
File.write(path, "sleep 0.001\n") # does not define the constant
217+
autoload :"C#{i}", path
218+
end
219+
8.times.map { Ractor.new { 50.times { |i| Object.const_get(:"C#{i}") rescue nil } } }.each(&:join)
220+
:ok
221+
}
222+
223+
# rb_to_int
224+
assert_equal 'ok', %q{
225+
class N
226+
def to_int = (sleep 0.001; 3)
227+
end
228+
8.times.map { Ractor.new { 50.times { [1, 2, 3, 4].first(N.new) } } }.each(&:join)
229+
:ok
230+
}
231+
232+
# rb_check_integer_type
233+
assert_equal 'ok', %q{
234+
class N
235+
def to_int = (sleep 0.001; 3)
236+
end
237+
8.times.map { Ractor.new { 50.times { "abc".byteslice(N.new) } } }.each(&:join)
238+
:ok
239+
}
240+
241+
# rb_f_integer1
242+
assert_equal 'ok', %q{
243+
class N
244+
def to_i = (sleep 0.001; 3)
245+
end
246+
8.times.map { Ractor.new { 50.times { Integer(N.new) } } }.each(&:join)
247+
:ok
248+
}
249+
250+
# nucomp_s_new
251+
assert_equal 'ok', %q{
252+
class N < Numeric
253+
def real? = (sleep 0.001; true)
254+
end
255+
8.times.map { Ractor.new { 50.times { Complex.rect(N.new) rescue nil } } }.each(&:join)
256+
:ok
257+
}
258+
259+
# nucomp_s_polar
260+
assert_equal 'ok', %q{
261+
class N < Numeric
262+
def real? = (sleep 0.001; true)
263+
end
264+
8.times.map { Ractor.new { 50.times { Complex.polar(N.new) rescue nil } } }.each(&:join)
265+
:ok
266+
}
267+
268+
if /wasi|mswin|mingw/ !~ target_platform
269+
# detach_process_watcher
270+
assert_equal 'ok', %q{
271+
8.times.map do
272+
Ractor.new do
273+
10.times do
274+
ths = 2.times.map { Process.detach(Process.spawn("sleep", "0.001")) }
275+
ths.each { raise "bad #{it.value.inspect}" unless Process::Status === it.value }
276+
end
277+
end
278+
end.each(&:join)
279+
:ok
280+
}
281+
282+
# proc_waitall
283+
assert_equal 'ok', %q{
284+
8.times.map do
285+
Ractor.new do
286+
12.times do
287+
4.times { Process.spawn("sleep", "0.001") }
288+
2.times.map { Thread.new { Process.waitall } }.each(&:join)
289+
end
290+
end
291+
end.each(&:join)
292+
:ok
293+
}
294+
295+
# rb_thread_io_close_wait
296+
assert_equal 'ok', %q{
297+
12.times.map do
298+
Ractor.new do
299+
100.times do
300+
r, w = IO.pipe
301+
ths = 8.times.map { Thread.new { r.read rescue nil } }
302+
Thread.pass until ths.all? { it.status == "sleep" }
303+
r.close
304+
ths.each(&:join)
305+
w.close
306+
end
307+
end
308+
end.each(&:join)
309+
:ok
310+
}
311+
end
312+
313+
# rb_error_frozen_object
314+
assert_equal 'ok', %q{
315+
class F
316+
def self.to_s = (sleep 0.001; "F")
317+
def self.inspect = (sleep 0.001; "F")
318+
end
319+
8.times.map do
320+
Ractor.new do
321+
50.times do
322+
F.new.freeze.instance_variable_set(:@a, 1)
323+
rescue FrozenError
324+
end
325+
end
326+
end.each(&:join)
327+
:ok
328+
}
329+
330+
# rb_ary_permutation
331+
assert_equal 'ok', %q{
332+
8.times.map { Ractor.new { 12.times { n = 0; [*0...200].permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join)
333+
:ok
334+
}
335+
336+
# rb_ary_repeated_combination
337+
assert_equal 'ok', %q{
338+
8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_combination(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join)
339+
:ok
340+
}
341+
342+
# rb_ary_repeated_permutation
343+
assert_equal 'ok', %q{
344+
8.times.map { Ractor.new { 12.times { n = 0; [1, 2].repeated_permutation(200) { sleep 0.001; break if (n += 1) > 5 } } } }.each(&:join)
345+
:ok
346+
}
347+
348+
# rb_ary_transpose
349+
assert_equal 'ok', %q{
350+
class A
351+
def initialize(slow) = @slow = slow
352+
def to_ary = (sleep 0.001 if @slow; [1, 2, 3])
353+
end
354+
8.times.map { Ractor.new { 2.times { Array.new(200) { A.new(it == 0) }.transpose } } }.each(&:join)
355+
:ok
356+
}
357+
358+
# rb_str_split_m
359+
assert_equal 'ok', %q{
360+
8.times.map { Ractor.new { 12.times { ",,".split(/(,)/) { sleep 0.001 } } } }.each(&:join)
361+
:ok
362+
}
363+
364+
# ruby_float_step
365+
assert_equal 'ok', %q{
366+
8.times.map { Ractor.new { 12.times { 1e300.step(2e300, 2e299) { sleep 0.001 } } } }.each(&:join)
367+
:ok
368+
}

‎gc.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2936,7 +2936,7 @@ gc_mark_func_data_slotp_of(rb_ractor_t *const cr)
29362936
#define GC_MARK_FUNC_DATA_SLOTP() gc_mark_func_data_slotp_of(rb_current_ractor_raw(false))
29372937

29382938
/* Resolve the current Ractor once for a whole marking walk. rb_current_ractor_raw() is an
2939-
* out-of-line rb_current_ec() call wherever RB_THREAD_CURRENT_EC_NOINLINE is set (arm64), so
2939+
* out-of-line rb_current_ec() call, so
29402940
* the walk threads this ctx down rather than repeating the lookup per reference. */
29412941
static inline struct rb_gc_mark_ctx
29422942
gc_current_mark_ctx(void)

‎internal/gc.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ void rb_gc_mark_and_move(VALUE *ptr);
221221

222222
/* The objspace a marking walk marks into, plus the per-Ractor traversal redirect slot
223223
* (vm_core.h's gc_mark_func_data_struct) and the shareable-check flag cached from it.
224-
* Resolving the current Ractor costs an out-of-line rb_current_ec() call on arm64, so a
224+
* Resolving the current Ractor costs an out-of-line rb_current_ec() call, so a
225225
* walk resolves it once into a ctx and threads that down instead of repeating it per
226226
* reference. Pass the pointer along wherever one is already in hand; build a fresh one
227227
* only at an entry point that has none (rb_gc_current_mark_ctx below). */

‎thread_none.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@
2121

2222
RUBY_EXTERN struct rb_execution_context_struct *ruby_current_ec;
2323
NOINLINE(struct rb_execution_context_struct *rb_current_ec_noinline(void)); // for assertions
24+
struct rb_execution_context_struct *rb_current_ec(void);
2425

2526
#endif /* RUBY_THREAD_NONE_H */

0 commit comments

Comments
 (0)