[Bug #22467]
If we perform IO read without the GVL during GC compaction and the string
we are writing to is embedded, then it can get mprotect'd by the read
barrier in the GC, which can cause the IO read operation to fail.
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>
`zjit_during_incremental_marking` was one process-wide global, written
by `gc_set_incremental_marking` on every transition of any objspace.
Since 24be314b45, two objspaces can run incremental marks at the same
time, so when one finished its mark it wrote zero while another was
still marking.
`gen_write_barrier` skips `rb_gc_writebarrier` when the receiver is young,
not shareable, and the word reads zero, so JIT code in the still-marking
objspace skipped the barrier.
A white object stored into a young black receiver was never marked, the
sweep freed it, and the next major crashed with
```
[BUG] try to mark T_NONE object (parent: T_ARRAY/Array).
```
This bug can be reproduced in a few seconds with
```
RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=0.5 ./ruby --zjit test.rb, where test.rb is:
```
```
Ractor.new {
loop {
GC.start(full_mark: true, immediate_mark: false, global: false)
GC.start(global: false)
}
}
loop {
GC.start(full_mark: true, immediate_mark: false, global: false)
Array.new(2_000) { [nil] }.each { |b|
200.times { Object.new }
sleep 0.0002
b[0] = Object.new
}
}
```
Limiting the oldobject limit factor ensures we keep running majors which
makes this bug easier to trigger, and the array is pre-sized because the
inlined ArrayAset side-exits on an empty array.
This commit moves zjit_incremental_marking into rb_objspace_t and makes
it a bool, so each Ractor can signal to zjit when it's incrementally
marking. Because this is now directly addressable per objspace we've
also removed the incremental marking flag.
A getaddrinfo thread calls getaddrinfo(3) inside rb_thread_prevent_fork,
but on glibc its thread exit takes the resolver configuration lock again
after the thread has released the fork lock. A fork that lands then
leaves the child with that lock held, and every later name lookup in the
child blocks forever. getnameinfo(3) attaches the same resolver state,
even for a name in /etc/hosts, and its thread ran outside
rb_thread_prevent_fork altogether.
Make the lookup threads joinable and have fork join any that may still
be exiting. As a lookup thread finishes, still inside
rb_thread_prevent_fork, it swaps itself into a single slot with an
atomic exchange and joins the thread it displaced, which has usually
exited by then, and rb_thread_acquire_fork_lock joins whatever is left
in the slot. Once lookups settle, at most one finished thread is left
unjoined. The slot holds pthread_t as a pointer, with NULL for empty,
which assumes pthread_t is scalar and never 0.
getnameinfo threads now run within rb_thread_prevent_fork and register
as they exit, as getaddrinfo threads do, and where getnameinfo runs
without a helper thread it is called within rb_thread_prevent_fork too.
So fork also waits for a reverse lookup in progress, as it already did
for a forward one. raddrinfo_pthread_create now always creates joinable
threads, which drops its pthread_detach fallback for platforms without
pthread_attr_setdetachstate.
The slot is kept across fork, so every fork path that keeps running
Ruby code has to take the fork lock. On the BSDs, Process.daemon forks
with daemon(3) and did not take it, so the daemon would inherit the
registered thread, and its next fork would abort in pthread_join with
ESRCH. Take and release the fork lock around daemon(3) as rb_fork_ruby
does. daemon(3) can fail in the child as well as in the parent, so
compare pids to tell which process returned.
[Bug #22406]
Where rb_len_t is wider than long, a heredoc opener past column LONG_MAX made the long offset negative, and the identifier and the restored lexer position were read from before the line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was previously prohibited as it wasn't handled correctly. The
reason it wasn't handled correctly is that at the point of the super
call, the refinement module didn't have the class super pointer
set correctly, so it couldn't determine the ancestor to start the
lookup for the super method to call. Fix that by lazily creating a
refinement module iclass with the class super pointer set correctly.
This is cached in a hidden hash so it's only created the first time
it is needed (similar to how singleton classes are created).
Unfortunately, storing this hidden hash in an instance variable in
the iclass failed CI due to shape_id layout mismatch errors. Storing
the hidden hash in an instance variable of the class/module itself is
possible, but that doesn't work if the class/module is frozen. So the
only option is to store this in rb_classext_struct. I didn't want to
increase the size of rb_classext_struct, so this uses the classpath
member to store the hash. That member is currently only used for
classes/modules and not for iclasses, so it shouldn't cause any
problems. Ultimately, I think the classpath entry should be moved
into the as union as that would be easier to understand. However, I
did not do so in this commit in order to avoid unnecessary churn.
By lazily creating the iclass, we don't create unnecessary iclasses.
For example, if Enumerable is refined, this only generates refinement
module iclasses for the classes using Enumerable where the refinement
method is actually called.
While working on specs for this, I ran into an infinite loop because
current_method_entry didn't correctly handle bmethods. So update
current_method_entry to handle that.
Implements [Feature #22309]
Only imemo_count_up and imemo_count_value treat the count as unsigned, so keep cnt as rb_len_t for the other users of MEMO and give them their own member.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These hold String and Array lengths or offsets into them, so they would truncate on mswin once rb_len_t widens. A build with rb_len_t set to long long reported them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file, dir, load and ruby.c changes go together because rb_get_expanded_load_path() returns the maximum path length through a pointer. Lengths passed to the Windows string conversion helpers that take `long *` stay `long`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ARY_MAX_SIZE follows rb_len_t, so the index errors report the value with PRIdLEN. The repeated_permutation exponent stays `long`, because rb_int_positive_pow() takes an `unsigned long` and the value is the user's Integer rather than an array length.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MEMO counter becomes rb_len_t, which is wider than `unsigned long` once rb_len_t widens, so the Enumerable#count overflow builds the next value from RB_ULEN_MAX instead of assuming `unsigned long` words.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RString capa becomes pointer-sized everywhere once rb_len_t widens, so fake strings can keep their precomputed hash on LLP64 as well.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once rb_len_t widens this changes the `long *` output parameters of rb_range_beg_len(), rb_str_subpos(), rb_arithmetic_sequence_beg_len_step(), ruby_enc_find_basename() and ruby_enc_find_extname(), so extensions passing `long *` to them will need to be updated. The extension guides list these signatures verbatim and follow along.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit adds `jit_calls` and `jit_threshold_reached` to iseq dumps which
dumps the number of calls to the iseq and whether the iseq has been JIT
compiled or not, respectively.
For `WriteBarrier a, b`, inline the fast-path of the write barrier that covers the majority of cases and doesn't require any actual barrier:
* `a` isn't old
* `a` isn't shareable
* not incremental marking
Removes the vast majority of barrier C calls.
Lobsters before:
```
Top-20 calls to C functions from JIT code (69.2% of total 50,814,557):
rb_hash_aref: 5,770,278 (11.4%)
rb_vm_opt_send_without_block: 5,428,040 (10.7%)
rb_gc_writebarrier: 3,304,955 ( 6.5%)
...
```
After: completely gone from top-20. Total C calls drop from ~50.8MM to ~47.6MM.
On a microbenchmark such as `setivar_object` or `setivar_young`, it takes per-iteration time from ~41ms to ~36ms (roughly -12%).
```c
void
rb_gc_impl_writebarrier(void *objspace_ptr, VALUE a, VALUE b)
{
rb_objspace_t *objspace = objspace_ptr;
#if RGENGC_CHECK_MODE
if (SPECIAL_CONST_P(a)) rb_bug("rb_gc_writebarrier: a is special const: %"PRIxVALUE, a);
if (SPECIAL_CONST_P(b)) rb_bug("rb_gc_writebarrier: b is special const: %"PRIxVALUE, b);
#else
RBIMPL_ASSERT_OR_ASSUME(!SPECIAL_CONST_P(a));
RBIMPL_ASSERT_OR_ASSUME(!SPECIAL_CONST_P(b));
#endif
GC_ASSERT(!during_gc);
GC_ASSERT(RB_BUILTIN_TYPE(a) != T_NONE);
GC_ASSERT(RB_BUILTIN_TYPE(a) != T_MOVED);
GC_ASSERT(RB_BUILTIN_TYPE(a) != T_ZOMBIE);
/* A shareable object now references an unshareable one: record b as a shref so its
* owner's local GC roots it (the parent may live in another objspace, untraversed
* there). Only b's owner stores this, on its own page: a plain store suffices. */
if (RB_UNLIKELY(RB_FL_TEST_RAW(a, RUBY_FL_SHAREABLE)) &&
!RB_FL_TEST_RAW(b, RUBY_FL_SHAREABLE)) {
struct heap_page *bpage = GET_HEAP_PAGE(b);
if (!_MARKED_IN_BITMAP(bpage->shref_bits, bpage, b)) {
_MARK_IN_BITMAP(bpage->shref_bits, bpage, b);
bpage->flags.has_shref_objects = TRUE;
}
}
retry:
if (!is_incremental_marking(objspace)) {
/* The generational barrier covers old->young edges within one objspace only; a
* foreign a or b has age bits another objspace mutates, unsafe to read, so check
* locality first when multi-Ractor (a foreign a is shareable and the shref above
* already keeps b alive). With a single Ractor nothing is foreign. */
if ((rb_gc_multi_ractor_p() &&
(GET_HEAP_OBJSPACE(a) != objspace || GET_HEAP_OBJSPACE(b) != objspace)) ||
!RVALUE_OLD_P(objspace, a) || RVALUE_OLD_P(objspace, b)) {
// do nothing
}
else {
gc_writebarrier_generational(a, b, objspace);
}
}
else {
/* Slow path, no lock: incremental marking only runs while the process has a single
* objspace, so the owning Ractor's GVL already serializes this barrier against its
* own GC. */
if (is_incremental_marking(objspace)) {
gc_writebarrier_incremental(a, b, objspace);
}
else {
goto retry;
}
}
return;
}
```
* Make `__FILE__` a frozen core method that return the iseq path.
* Make `load_from_binary` accept `file, path` like compile.
[Feature #17593]
[Feature #22395]
Ractor.make_shareable could previously take the VM lock and then call
rb_funcall(id_freeze), which is not correct to do. Create a new
rb_objspace_reachable_objects_from_local() function that works the same as the
non-local version except doesn't lock. The callback given to it must skip foreign
objects (classes/modules, etc.) as running mark_children could traverse data
structures that could be mutated out from under it.
The reachable_objects_from_local callback must skip foreign objects altogether
instead of checking their shareability because foreign objects that aren't
shareable could be collected or have their page unmapped due to RLGC. Of course,
foreign edges should be shareable anyway. This was a bug that existed in the
locking version as well. Unfortunately, that means we can't check that T_DATA
objects don't refer to unshareables across object spaces while making them
shareable.
The alternative is to check and risk a crash, and raise if we see these illegal
edges.
RtlUnwindEx(), which longjmp() and C++ exceptions use, needs unwind info for every frame, so raising through JIT frames skipped __finally blocks and C++ destructors. Exception dispatch also could not walk through JIT frames to reach the unhandled exception filter that the compaction read barrier relies on. Describe the entry frame, which now always has a frame pointer on Windows, for the whole region.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During method aliasing, we check if the methods `a` and `b` are equal
before aliasing. If they are equal, we don't need to alias. That check
was running Ruby code with the VM lock held when the first method was a
bmethod, which is not good and can cause incorrect behavior. Now, we call
`rb_proc_eq` directly. This means an override of `Proc#==` will not be
called to check if 2 methods are equal and will instead use the definition
of Proc equality at VM startup. `Method#==` and `UnboundMethod==` will
also not call into `Proc#==` now for bmethods. This is probably better behavior
even without the VM lock fix because `Method#hash` never called `Proc#hash` when
the method was a bmethod, it used `rb_hash_proc` directly.
Fixes [Bug #22398]
This commit renames rb_str_tmp_frozen_no_embed_acquire to rb_str_no_gvl_safe_acquire
and makes it part of the public C API.
There is currently no way for C extensions that use no GVL operations to
safely read from a string because it has no way of guaranteeing that the
string is not embedded. Embedded strings are not safe for no GVL operations
because GC compaction can run, which will mprotect the memory region and
will cause any read to SEGV.
See https://github.com/ruby/ruby/pull/9817 for when rb_str_tmp_frozen_no_embed_acquire
was introduced.
[Feature #22232]
Guaranteeing that any hash can be converted into an st_table is
limiting the evolution of RHash.
None of the public usages of `RHASH_TBL` were justified, they all
had equivalent `rb_hash_*` public APIs to perform the same thing
more efficiently without disabling write barriers nor coercing the
hash into an st_table.
capa does not include the terminator, so when a buffer moves
between strings with different terminator lengths the capacity has to
be converted or we will try to free the buffer with the wrong size:
[BUG] buffer 0x00005030039ff768 freed with old_size=18, but was allocated with size=17
[BUG] buffer 0x00005631adc48f48 freed with old_size=8268, but was allocated with size=8269
Three related fixes:
* `rb_str_tmp_frozen_release`: convert tmp's capacity to orig's
terminator length when handing the buffer back. This is the inverse
of a calculation added to `rb_str_tmp_frozen_no_embed_acquire` in
91619f0230.
* `heap_str_make_shared`: since bdf3032e35
the root created by `rb_str_tmp_frozen_acquire` does not copy the
encoding of the string it takes the buffer from, so the two can have
different terminator lengths and the root's capacity has to be
adjusted as well, otherwise the root frees fewer bytes than were
allocated once its only child stops sharing the buffer. termlen is
passed in because the caller assigns the encoding of the root after
this function returns.
* `rb_str_tmp_frozen_no_embed_acquire`: unset `STR_BORROWED` on the
root. `STR_SET_SHARED` sets `STR_BORROWED` on roots with
`klass == 0` to mark the ones that may have other children, but this
root only ever has orig as its child, so unset it like
`heap_str_make_shared` does. Any string that starts sharing the
root later will set the flag again. Without this
`rb_str_tmp_frozen_release` never gives the buffer back, so strings
written by `io_fwrite` stay shared and are copied on their next
modification.
The release and make_shared adjustments go together
as they were previously canceling each other out.
The registered_globals list made every Ractor's GC scan every slot under
a shared lock. This commit splits it into per-Ractor lists.
We also maintain a global registry that references every Ractor that has
ever registered so that unregistering from a Ractor other than the one
that registered can look up the owning list.
When a Ractor is joined, the dead Ractor's slots are moved to the
joining Ractor's heap before the objspace merge happens.
This change tightens up the requirements for values stored in a
registered address.
Previously, Every Ractor's local GC scanned the shared list. So if a
slot held an unshareable object living in a different Ractor's heap,
that object was always kept alive regardless.
After this change, only the registering Ractor's GC scans its list. So if a
slot holds another Ractor's unshareable object, the owning Ractor's
local GC cannot see that reference, so it can collect the object while
the registered slot still points at it.
Storing such an object was already a Ractor containment violation. It
was just hidden by the shared scan.
[Feature #22277]
Previously, major GC incremental marking was disabled (even on the main Ractor)
when more than 1 Ractor was running. By changing the WB implementation, we now
have it back.
rb_warn_deprecated_to_remove_at warns only when Warning[:deprecated]
is enabled, so a deprecation scheduled to warn by default in a later
version needed a hand-written rb_warn and a separate commit to switch.
rb_warn_to_remove_at emits the same message with rb_warn and no
category. rb_warn_scheduled_deprecation(X.Y, X.Z, ...) selects
between the two at compile time by RUBY_API_VERSION_CODE: gated by
Warning[:deprecated] before X.Y, shown by default from X.Y, and a
compile error in RUBY_DEBUG builds at X.Z, like the existing removal
check. ruby/version.h is now included unconditionally and RUBY_VERSION_SINCE
and the version-string macros are always defined for that, while the
compile-time checks and RBIMPL_TODO stay RUBY_DEBUG only. Both
functions are exported so that the new -test-/warning extension can
exercise both phases.
Use it for the two alias deprecations in rb_alias, which are scheduled
to warn by default in 4.2 and to be removed in 4.3.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rb_class_duplicate_classext builds the whole copy first and the caller only
then puts it in the class's table, so a GC in the middle collects the tables
it has made and marking the class afterwards walks into a freed object. Put
the classext in the table first instead. Marking it while it is still half
built is fine, since ZALLOC leaves every field zero.
$ RUBY_BOX=1 ruby --disable-gems -e 'GC.stress = true; module Enumerable; def m; end; end'
-e:1: [BUG] try to mark T_NONE object (obj: out-of-heap:0x..., parent: ... T_MODULE/Module Enumerable)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
msvc supports flexible array as it's language extensions.
The /Za msvc compiler option disables this and other extensions, but it is not necessary to consider it.
The Windows SDK headers contain anonymous unions/structs, and using /Za makes the SDK unusable.
Using the option (one of -std:[c11|c17|clatest]) to enable standard C support in msvc causes the following error:
[BUG] vm_call_method: unsupported method type (-7)
Simply disable it.
Keyword tracking only needs membership, so replace identity hashes
with identity sets to avoid storing unused values. Add `rb_set_p`
for the type assertion in `vm_check_keyword`.
Add benchmarks for 31, 32 and 128 keyword parameters with sparse and
dense dynamic defaults, using both direct keywords and keyword splats.
The 31-parameter cases provide a bitmask-only control.
Local arm64 macOS measurements show about 16-26% higher throughput
with many omitted dynamic defaults at 32 and 128 parameters. Smaller
differences remain noisy, including in the bitmask-only controls.
The tracking container is created at call time, leaving IBF and
`ISeq#to_a` unchanged. Verify IBF-loaded calls with the Integer
refinement and colliding keyword indices from [Bug #22335].
Now fibers can be collected by multiple ractors at once when those Ractors
are running local GCs. Fibers are created in the Ractor that owns them, so
if multiple Ractors have unmarked fibers during local GC sweeping, we can
now collect them concurrently instead of deferring their collection to a
postponed job. Each fiber pool is protected by its own native mutex.
In the future, maybe fiber pools will be ractor-local, but we have to
experiment with it first and see if it's enough of a performance win to
justify the change. We also want to minimize resource acquisition for each
Ractor as they're supposed to be lighweight.
If the array object passed into String#% is modified during conversion
(such as via to_s, to_i, to_f, etc.) then it can be a use-after-free if
the buffer of the array is freed.
This commit changes it to detect if the buffer has been changed and raises
an error when that happens.
The following script reproduces the crash:
count = 1_000
obj = Object.new
ary = [obj, *Array.new(count - 1) { "x" }]
obj.define_singleton_method(:to_s) do
ary.replace([])
"X"
end
str = "%s" * count
puts (str % ary).length
`rb_freeze_singleton_class` is an implementation detail of
`RB_OBJ_FREEZE`, so deprecate direct calls from extension libraries
before removing its public declaration in Ruby 4.2.
Previously we had a check that objects which were shareable, but not
frozen on a non-main Ractor would raise an exception if instance
variables were read. This prevented a non-main Ractor reading
non-shareable objects.
This changes the behaviour to forbid all writes to shareable objects
(even when unfrozen). This is done by re-purposing the frozen flag on
the object shape to reflect (FL_FROZEN || FL_SHAREABLE).
With IV writes forbidden on all shareable objects whether or not they
are frozen (with the exception of classes and modules), we can also
remove the check against reading instance variables, since no unsafe
instance variables can be set.
[Bug #22291]
This commit changes Array#& to use a temporary Set instead of Hash for
performance and memory savings. We can see in the following benchmark:
a = (0...10_000).to_a
b = (5_000...15_000).to_a
1_000.times { a & b }
Results:
Benchmark 1: master
Time (mean ± σ): 196.4 ms ± 9.1 ms [User: 182.9 ms, System: 9.5 ms]
Range (min … max): 186.3 ms … 214.7 ms 13 runs
Benchmark 2: branch
Time (mean ± σ): 163.4 ms ± 3.3 ms [User: 152.7 ms, System: 7.8 ms]
Range (min … max): 157.7 ms … 169.9 ms 17 runs
Summary
branch ran
1.20 ± 0.06 times faster than master
This commit changes Array#| to use a temporary Set instead of Hash for
performance and memory savings. We can see in the following benchmark:
a = (0...10_000).to_a
b = (5_000...15_000).to_a
1_000.times { a | b }
Results:
Benchmark 1: master
Time (mean ± σ): 237.5 ms ± 2.0 ms [User: 223.4 ms, System: 10.9 ms]
Range (min … max): 234.6 ms … 240.7 ms 12 runs
Benchmark 2: branch
Time (mean ± σ): 165.2 ms ± 3.8 ms [User: 151.2 ms, System: 10.8 ms]
Range (min … max): 159.8 ms … 174.2 ms 18 runs
Summary
branch ran
1.44 ± 0.04 times faster than master