Add a new `Primitive.attr :always_inline`. Force inline ISEQs with this
attribute. This is helpful for e.g. `BasicObject#!=`, which is otherwise
megamorphic, and very small.
Other JITs also have a "always inline small methods" policy but I didn't
want to wade into that yet.
Matches Thread#alive? and Fiber#alive?. Returns false once the ractor's
result has been recorded. Reads sync.legacy under the ractor lock, as
Ractor#monitor does, so it cannot race with termination.
[Feature #22441]
Defining a method in a refinement makes rb_method_entry_make (VM lock held)
add an entry to the refined class by calling rb_add_method. rb_add_method must
be called without the VM lock: it asserts that, and emits warnings, which can
run Ruby code, after its critical section. With a second Ractor around that
tripped ASSERT_vm_unlocking and called rb_warn with the lock held (e.g.
refine(Object) { def respond_to? }).
Add the entry with rb_method_entry_make directly and let it store its warnings
in the caller's struct, so the outermost rb_add_method emits them after
unlocking. Every warning is still emitted; the 'object_id' and '__send__'
ones, which used to be emitted twice, now once.
rb_const_remove deleted the entry from the constant table without the VM
lock and then freed it, while a Ractor that does not own the class looks
the table up under the VM lock but reads the entry after releasing it.
Reading a constant while its owner removes it could crash in the table
lookup or return garbage (an Integer, Float or false that was never
assigned).
Delete under the VM lock, and have the readers that may not own the class
copy the entry while holding it (rb_const_lookup_copy). Class#dup and
the singleton class clone walk the original's table under the lock for
the same reason; the Class#dup check collects the candidates under the
lock and runs rb_ractor_shareable_p after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Don't try and guess from profiles; nil check is cheap. Helps Rails activerecord/relation/delegation.rb:
def create(model, ...)
relation_class_for(model).new(model, ...)
end
Depending on what keywords are passed (none, some), the hash in splatkw
can either be nil or a Hash. Just check for both cases. The `to_hash`
case is probably very rare.
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>
ruby_nt_serial and RUBY_NT_SERIAL were declared only when
RB_THREAD_CURRENT_EC_NOINLINE is not defined, so RUBY_DEBUG_LOG() did not
show the native thread serial on arm64 and ppc64le. ruby_nt_serial is not
related to the way ruby_current_ec is accessed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some example results in the docs no longer match what Ruby prints:
* Array#flatten and Array#flatten!: Set#inspect now shows Set[6, 7]
* Module: Mod.constants also includes Math::DomainError
* Module#const_get and Module#const_set: Floats are shown with full
precision
* Complex#imag: sin(PI/4) is 0.7071067811865475
* String#<<: missing # in #<Encoding:UTF-8>
* Enumerable#inject: typo #=>>
A Ractor's zombies with finalizers are poisoned and moved to the main
objspace when the Ractor terminates. If the process exits before they
are finalized, rb_gc_impl_shutdown_call_finalizer_i touches them and
ASAN reports use-after-poison.
```
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)
#=> ERROR: AddressSanitizer: use-after-poison (sometimes)
```
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They index the match registers, whose count is an int, rather than a String, so rb_len_t does not apply. Only the length of a group name given as a String stays in rb_len_t.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thread_sched_wait_events() returned thread_sched_wait_event when an
interrupt was already pending at entry, without looking at the fd, and
thread_io_wait() reported that as readiness: under RUBY_MN_THREADS=2,
IO#wait_readable on a pipe nobody wrote to returned truthy when a trap
arrived just before the wait.
A wakeup for a masked interrupt (a trap arriving inside a trap handler)
was reported as a timeout, so wait_readable(0.3) in a trap handler
returned nil at once.
Add thread_sched_wait_interrupted for both "stopped for an interrupt"
cases. thread_io_wait() now runs the whole wait under one tag, handles
the interrupt and waits again on the M:N scheduler until its deadline,
and treats a wakeup without an event before the deadline the same way.
At the deadline it probes the fd once. rb_thread_io_blocking_call()
retries func for every M:N outcome but unhandled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the previous two commits: wait_readable(0) and
IO.select([io], nil, nil, 0) on an io at EOF must always report it,
also while another thread keeps asking for the GVL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same as the previous commit, for the select(2) path used by IO.select
(and by thread_io_wait without poll). When an interrupt was pending,
select was skipped with result 0, which wait_retryable takes as a
timeout, so IO.select([io], nil, nil, 0) could return nil for an io at
EOF. With another thread looping on Thread.pass, 137-153 of about 10^6
such calls returned nil; 0 with this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wait_for_single_fd_blocking_region skips ppoll when an interrupt is
already pending, but left result 0, which wait_retryable reads as a
timeout. With a zero timeout the deadline has passed, so it does not
retry: IO#wait_readable(0) returns nil even for a socket at EOF. This
happens about once per 10^4 probes whenever another thread is around
(timer interrupt).
Net::HTTP#begin_transport probes a kept-alive connection with
wait_readable(0) && eof?, so it reused a connection the server had
already closed and the next non-idempotent request raised EOFError
(spec/ruby/library/net-http/http/request_spec.rb:71 on trunk_asan).
Report the skip as EINTR so the probe polls again after the interrupt
is handled. The same pattern remains in do_select.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The C++ interface is intentionally minimal: it provides only a
couple of niceties in order to largely stay out of the way. It is
mostly used to provide:
* RAII for opaque resources
* Safe up and down casting for nodes
* string_view for various resources
* Iteration over resource lists
Note that if consumers want to use it, they must be on C++17 at
minimum.
https://github.com/ruby/prism/commit/57b8dbe0e9
`__MINGW_PRINTF_FORMAT` is defined in stdio.h.
```
prism/buffer.c: In function 'pm_buffer_append_format':
prism/buffer.c:114:5: warning: function 'pm_buffer_append_format' might be a candidate for 'gnu_printf' format attribute [-Wsuggest-attribute=format]
114 | int result = vsnprintf(NULL, 0, format, arguments);
| ^~~
prism/buffer.c:123:9: warning: function 'pm_buffer_append_format' might be a candidate for 'gnu_printf' format attribute [-Wsuggest-attribute=format]
123 | vsnprintf(buffer->value + cursor, length, format, arguments);
| ^~~~~~~~~
```
https://github.com/ruby/prism/commit/72da47973e
We do not want strings to have the ability or the contract of
having to be freed. This makes the API simpler and keeps us away
from dynamic allocation.
https://github.com/ruby/prism/commit/c9b06230ec
So that consumers don't unintentionally encode the fact that a
constant id is actually 1 + its index in the constants table.
Ideally this makes it so that we can change the underlying
implementation if we want to.
https://github.com/ruby/prism/commit/3a7f3cb6d5
Returns the binary representation of the address value as a string of
"0" and "1" characters, right adjusted to the address size (32
characters for IPv4, 128 for IPv6).
https://github.com/ruby/ipaddr/commit/63beaa749e
errno was cleared only once before the retry loop. Under M:N threads, the
thread may move to another native thread while it waits for the fd, and
errno is per native thread. If func then succeeded without touching errno,
a stale ETIMEDOUT left by another thread raised IO::TimeoutError on an IO
without a timeout, and the data that func had read was lost.
8.times.map { Ractor.new {
Thread.new { r, = IO.pipe; r.timeout = 0.0001; 30000.times { r.read(1) rescue nil } }
r, w = IO.pipe
Thread.new { 10000.times { w << "x"; sleep 0.0001 } }
10000.times { r.readpartial(1) }
} }.each(&:join)
#=> Blocking operation timed out! (IO::TimeoutError)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prefork() flushed rb_stdout and rb_stderr, which are the main Ractor's IOs.
From another Ractor that takes the write_lock of an IO the Ractor does not
own. When a main Ractor thread holds it (any thread logging to $stdout will
do), the spawning Ractor's only thread sleeps on the mutex and
rb_check_deadlock, which only looks at the Ractor's own threads, raises
"No live threads left. Deadlock?" (fatal) from Kernel#spawn/system/popen.
Use rb_ractor_stdout()/rb_ractor_stderr() so each Ractor flushes its own.
[Bug #22434]