Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,25 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det

[[Feature #21861]]

### Ractor-scoped GC address registration

* `rb_gc_register_address` and `rb_global_variable` now register the address
with the calling Ractor, and only that Ractor's GC marks the stored object.

Any Ractor may register an address, but the value stored through it must be a
special constant, a shareable object, or an unshareable object owned by the
registering Ractor. Storing another Ractor's unshareable object can result in
the object being freed while the address still refers to it (use-after-free).

If the address has process lifetime (a static VALUE), register it from the
main Ractor or keep the stored values shareable.

When the registering Ractor is joined with `Ractor#value`, remaining
registrations move to the joining Ractor; otherwise they move to the main
Ractor once the dead Ractor is collected.

[[Feature #22277]]

### Removed APIs

The following APIs, which have been deprecated for many years, are removed.
Expand Down
14 changes: 14 additions & 0 deletions bootstraptest/test_ractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3303,3 +3303,17 @@ def st.m; :strct end

[untimed_result, th2.value]
}

# last_cwd is set on init in the main Ractor, but Dir.pwd attempts to store into
# it. When this is done on a child Ractor we violate the invariant that the
# owning and registering Ractor must be the same. On a normal build the
# use-after-free is invisible, because we only zero out the flags, so the cache
# check compares the slot bytes as usual and just thinks it's a cache miss.
#
# This test exists because it will hit a use-after-poison on ASAN builds
assert_equal 'ok', %q{
Dir.chdir("..")
Ractor.new { Dir.pwd; 500_000.times { "y" * 300 } }.join
Dir.pwd
'ok'
}
24 changes: 16 additions & 8 deletions dir.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ char *strchr(char*,char);
#include "internal/object.h"
#include "internal/imemo.h"
#include "internal/vm.h"
#include "vm_core.h"
#include "ruby/encoding.h"
#include "ruby/ractor.h"

#include "ruby/ruby.h"
#include "ruby/thread.h"
#include "ruby/util.h"
Expand Down Expand Up @@ -1270,24 +1273,29 @@ dir_chdir0(VALUE path)
}

static struct {
VALUE thread;
rb_thread_t *thread; /* only ever compared, never dereferenced */
VALUE path;
int line;
int blocking;
} chdir_lock = {
.blocking = 0, .thread = Qnil,
.blocking = 0, .thread = NULL,
.path = Qnil, .line = 0,
};

static void
chdir_enter(void)
{
if (chdir_lock.blocking == 0) {
chdir_lock.path = rb_source_location(&chdir_lock.line);
VALUE path = rb_source_location(&chdir_lock.line);
/* chdir_lock.path is registered on the main Ractor, but the source
* location string belongs to the calling Ractor. To avoid the dangling
* reference on local GC, this needs to be shareable
*/
chdir_lock.path = NIL_P(path) ? Qnil : RB_OBJ_SET_FROZEN_SHAREABLE(rb_str_dup(path));
}
chdir_lock.blocking++;
if (NIL_P(chdir_lock.thread)) {
chdir_lock.thread = rb_thread_current();
if (chdir_lock.thread == NULL) {
chdir_lock.thread = rb_thread_ptr(rb_thread_current());
}
}

Expand All @@ -1296,7 +1304,7 @@ chdir_leave(void)
{
chdir_lock.blocking--;
if (chdir_lock.blocking == 0) {
chdir_lock.thread = Qnil;
chdir_lock.thread = NULL;
chdir_lock.path = Qnil;
chdir_lock.line = 0;
}
Expand All @@ -1307,7 +1315,7 @@ chdir_alone_block_p(void)
{
int block_given = rb_block_given_p();
if (chdir_lock.blocking > 0) {
if (rb_thread_current() != chdir_lock.thread)
if (rb_thread_ptr(rb_thread_current()) != chdir_lock.thread)
rb_raise(rb_eRuntimeError, "conflicting chdir during another chdir block");
if (!block_given) {
if (!NIL_P(chdir_lock.path)) {
Expand Down Expand Up @@ -1658,6 +1666,7 @@ rb_dir_getwd_ospath(void)
cached_cwd = rb_str_new(path, (long)len);
#endif
rb_str_freeze(cached_cwd);
RB_OBJ_SET_SHAREABLE(cached_cwd);
RUBY_ATOMIC_VALUE_SET(last_cwd, cached_cwd);
}
return cached_cwd;
Expand Down Expand Up @@ -4146,7 +4155,6 @@ Init_Dir(void)
#endif

rb_gc_register_address(&chdir_lock.path);
rb_gc_register_address(&chdir_lock.thread);
rb_gc_register_address(&last_cwd);

rb_cDir = rb_define_class("Dir", rb_cObject);
Expand Down
19 changes: 19 additions & 0 deletions doc/extension.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,22 @@ or the objects themselves by

void rb_gc_register_mark_object(VALUE object)

Registration is scoped to the calling Ractor. Any Ractor may register an
address. The stored value must be a special constant, a shareable object,
or an unshareable object owned by the registering Ractor. If the address
holds another Ractor's unshareable object, the owning Ractor's GC cannot
see the registration and can free the stored object while the registered
address still refers to it. This can cause a use-after-free if the
registering Ractor attempts to use the Object stored.

If the address has process lifetime (a +static VALUE+), either register it
from the main Ractor or keep the stored values shareable.

Calling +Ractor#value+ on a Ractor moves its registrations to the calling
Ractor; otherwise they move to the main Ractor once the dead Ractor is
collected. If you rely on a Ractor inheriting another Ractor's registered
globals, call +Ractor#value+.

=== Prepare extconf.rb

If the file named extconf.rb exists, it will be executed to generate
Expand Down Expand Up @@ -1552,6 +1568,9 @@ golf_prelude.rb :: goruby specific libraries.
<tt>void rb_global_variable(VALUE *var)</tt> ::

Tells GC to protect C global variable, which holds Ruby value to be marked.
The registration belongs to the calling Ractor: the stored value must be
a special constant, a shareable object, or an unshareable object owned by
the registering Ractor.

<tt>void rb_gc_register_mark_object(VALUE object)</tt> ::

Expand Down
68 changes: 68 additions & 0 deletions ext/-test-/gc/register/register.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ruby.h"
#include "ruby/internal/has/feature.h"

/*
* Regression test for a heap-use-after-free in rb_gc_unregister_address().
Expand Down Expand Up @@ -52,11 +53,78 @@ gc_unregister_address_keeps_siblings(VALUE self)
return result;
}

static VALUE static_slot;

static VALUE
gc_register_static(VALUE self, VALUE v)
{
rb_gc_register_address(&static_slot);
static_slot = v;
return Qtrue;
}

static VALUE
gc_unregister_static(VALUE self)
{
rb_gc_unregister_address(&static_slot);
return Qnil;
}

static VALUE
gc_static_slot_value(VALUE self)
{
return static_slot;
}

static VALUE
gc_static_slot_eq(VALUE self, VALUE v)
{
if (!RB_TYPE_P(static_slot, T_STRING) || !RB_TYPE_P(v, T_STRING)) return Qfalse;
return rb_str_equal(static_slot, v);
}

static VALUE
gc_assign_static(VALUE self, VALUE v)
{
static_slot = v;
return v;
}

static VALUE
gc_register_current_static(VALUE self)
{
rb_gc_register_address(&static_slot);
return Qnil;
}

/* Mirrors the VM-side RB_GC_REGISTERED_ADDR_CHECK definition without including a
* private GC header: debug and ASAN builds record the registration-time value. */
static VALUE
gc_registered_address_check_enabled_p(VALUE self)
{
#if RUBY_DEBUG || defined(__SANITIZE_ADDRESS__) || RBIMPL_HAS_FEATURE(address_sanitizer)
return Qtrue;
#else
return Qfalse;
#endif
}

void
Init_register(void)
{
rb_ext_ractor_safe(true);

VALUE mBug = rb_define_module("Bug");
VALUE mGC = rb_define_module_under(mBug, "GC");
rb_define_singleton_method(mGC, "unregister_address_keeps_siblings?",
gc_unregister_address_keeps_siblings, 0);
rb_define_singleton_method(mGC, "register_static", gc_register_static, 1);
rb_define_singleton_method(mGC, "unregister_static", gc_unregister_static, 0);
rb_define_singleton_method(mGC, "static_slot_value", gc_static_slot_value, 0);
rb_define_singleton_method(mGC, "assign_static", gc_assign_static, 1);
rb_define_singleton_method(mGC, "static_slot_eq?", gc_static_slot_eq, 1);
rb_define_singleton_method(mGC, "register_current_static",
gc_register_current_static, 0);
rb_define_singleton_method(mGC, "registered_address_check_enabled?",
gc_registered_address_check_enabled_p, 0);
}
9 changes: 9 additions & 0 deletions ext/-test-/string/capacity.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ bug_str_new_shared(VALUE klass, VALUE str)
return rb_str_new_shared(str);
}

static VALUE
bug_str_tmp_frozen_acquire_release(VALUE klass, VALUE str)
{
VALUE tmp = rb_str_tmp_frozen_acquire(str);
rb_str_tmp_frozen_release(str, tmp);
return str;
}

void
Init_string_capacity(VALUE klass)
{
rb_define_singleton_method(klass, "capacity", bug_str_capacity, 1);
rb_define_singleton_method(klass, "rb_str_new_shared", bug_str_new_shared, 1);
rb_define_singleton_method(klass, "tmp_frozen_acquire_release", bug_str_tmp_frozen_acquire_release, 1);
}
18 changes: 18 additions & 0 deletions ext/json/lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ def parser=(parser) # :nodoc:
def generator=(generator) # :nodoc:
old, $VERBOSE = $VERBOSE, nil

unless generator::State.respond_to?(:default_sort_keys_proc_unchecked=, true)
generator::State.singleton_class.class_eval do
alias_method :default_sort_keys_proc_unchecked=, :default_sort_keys_proc=
private :default_sort_keys_proc_unchecked=

def default_sort_keys_proc=(proc)
unless ::Proc === proc
raise ::TypeError, "sort_key_proc must be a Proc"
end
if defined?(::Ractor) && !::Ractor.shareable?(proc) && !::Ractor.current.equal?(::Ractor.main)
raise ::Ractor::IsolationError,
"can not set a non-shareable Proc as the default sort_keys proc from a non-main Ractor"
end
self.default_sort_keys_proc_unchecked = proc
end
end
end

# The default proc used when the +sort_keys+ generation option is +true+.
# It returns a new hash with the entries sorted by their keys.
sort_keys_proc = ->(hash) { hash.sort.to_h }
Expand Down
Loading