Autoload encodings on the main ractor

None of the datastructures involved in the require process are
safe to call on a secondary ractor, however when autoloading
encodings, we do so from the current ractor.

So all sorts of corruption can happen when using an autoloaded
encoding for the first time from a secondary ractor.
This commit is contained in:
Jean Boussier 2025-07-04 09:39:12 +02:00
parent 002d746418
commit 482f4cad82
5 changed files with 81 additions and 19 deletions

View file

@ -763,36 +763,47 @@ load_encoding(const char *name)
} }
static int static int
enc_autoload_body(struct enc_table *enc_table, rb_encoding *enc) enc_autoload_body(rb_encoding *enc)
{ {
rb_encoding *base = enc_table->list[ENC_TO_ENCINDEX(enc)].base; rb_encoding *base;
int i = 0;
GLOBAL_ENC_TABLE_LOCKING(enc_table) {
base = enc_table->list[ENC_TO_ENCINDEX(enc)].base;
if (base) {
do {
if (i >= enc_table->count) {
i = -1;
break;
}
} while (enc_table->list[i].enc != base && (++i, 1));
}
}
if (i == -1) return -1;
if (base) { if (base) {
int i = 0;
do {
if (i >= enc_table->count) return -1;
} while (enc_table->list[i].enc != base && (++i, 1));
if (rb_enc_autoload_p(base)) { if (rb_enc_autoload_p(base)) {
if (rb_enc_autoload(base) < 0) return -1; if (rb_enc_autoload(base) < 0) return -1;
} }
i = enc->ruby_encoding_index; i = enc->ruby_encoding_index;
enc_register_at(enc_table, i & ENC_INDEX_MASK, rb_enc_name(enc), base);
GLOBAL_ENC_TABLE_LOCKING(enc_table) {
enc_register_at(enc_table, i & ENC_INDEX_MASK, rb_enc_name(enc), base);
}
((rb_raw_encoding *)enc)->ruby_encoding_index = i; ((rb_raw_encoding *)enc)->ruby_encoding_index = i;
i &= ENC_INDEX_MASK; i &= ENC_INDEX_MASK;
return i; return i;
} }
else {
return -2; return -2;
}
} }
int int
rb_enc_autoload(rb_encoding *enc) rb_enc_autoload(rb_encoding *enc)
{ {
int i; int i = enc_autoload_body(enc);
GLOBAL_ENC_TABLE_LOCKING(enc_table) {
i = enc_autoload_body(enc_table, enc);
}
if (i == -2) { if (i == -2) {
i = load_encoding(rb_enc_name(enc)); i = load_encoding(rb_enc_name(enc));
} }

8
load.c
View file

@ -372,6 +372,8 @@ features_index_add_single(vm_ns_t *vm_ns, const char* str, size_t len, VALUE off
static void static void
features_index_add(vm_ns_t *vm_ns, VALUE feature, VALUE offset) features_index_add(vm_ns_t *vm_ns, VALUE feature, VALUE offset)
{ {
RUBY_ASSERT(rb_ractor_main_p());
const char *feature_str, *feature_end, *ext, *p; const char *feature_str, *feature_end, *ext, *p;
bool rb = false; bool rb = false;
@ -1523,6 +1525,10 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa
int int
rb_require_internal_silent(VALUE fname) rb_require_internal_silent(VALUE fname)
{ {
if (!rb_ractor_main_p()) {
return NUM2INT(rb_ractor_require(fname, true));
}
rb_execution_context_t *ec = GET_EC(); rb_execution_context_t *ec = GET_EC();
return require_internal(ec, fname, 1, false); return require_internal(ec, fname, 1, false);
} }
@ -1559,7 +1565,7 @@ rb_require_string_internal(VALUE fname, bool resurrect)
// main ractor check // main ractor check
if (!rb_ractor_main_p()) { if (!rb_ractor_main_p()) {
if (resurrect) fname = rb_str_resurrect(fname); if (resurrect) fname = rb_str_resurrect(fname);
return rb_ractor_require(fname); return rb_ractor_require(fname, false);
} }
else { else {
int result = require_internal(ec, fname, 1, RTEST(ruby_verbose)); int result = require_internal(ec, fname, 1, RTEST(ruby_verbose));

View file

@ -2263,6 +2263,8 @@ struct cross_ractor_require {
// autoload // autoload
VALUE module; VALUE module;
ID name; ID name;
bool silent;
}; };
static void static void
@ -2294,7 +2296,14 @@ require_body(VALUE data)
ID require; ID require;
CONST_ID(require, "require"); CONST_ID(require, "require");
crr->result = rb_funcallv(Qnil, require, 1, &crr->feature);
if (crr->silent) {
int rb_require_internal_silent(VALUE fname);
crr->result = INT2NUM(rb_require_internal_silent(crr->feature));
}
else {
crr->result = rb_funcallv(Qnil, require, 1, &crr->feature);
}
return Qnil; return Qnil;
} }
@ -2338,10 +2347,21 @@ ractor_require_protect(VALUE crr_obj, VALUE (*func)(VALUE))
struct cross_ractor_require *crr; struct cross_ractor_require *crr;
TypedData_Get_Struct(crr_obj, struct cross_ractor_require, &cross_ractor_require_data_type, crr); TypedData_Get_Struct(crr_obj, struct cross_ractor_require, &cross_ractor_require_data_type, crr);
VALUE debug, errinfo;
if (crr->silent) {
debug = ruby_debug;
errinfo = rb_errinfo();
}
// catch any error // catch any error
rb_rescue2(func, (VALUE)crr, rb_rescue2(func, (VALUE)crr,
require_rescue, (VALUE)crr, rb_eException, 0); require_rescue, (VALUE)crr, rb_eException, 0);
if (crr->silent) {
ruby_debug = debug;
rb_set_errinfo(errinfo);
}
rb_rescue2(require_result_copy_body, (VALUE)crr, rb_rescue2(require_result_copy_body, (VALUE)crr,
require_result_copy_resuce, (VALUE)crr, rb_eException, 0); require_result_copy_resuce, (VALUE)crr, rb_eException, 0);
@ -2357,8 +2377,11 @@ ractor_require_func(void *crr_obj)
} }
VALUE VALUE
rb_ractor_require(VALUE feature) rb_ractor_require(VALUE feature, bool silent)
{ {
// We're about to block on the main ractor, so if we're holding the global lock we'll deadlock.
ASSERT_vm_unlocking();
struct cross_ractor_require *crr; struct cross_ractor_require *crr;
VALUE crr_obj = TypedData_Make_Struct(0, struct cross_ractor_require, &cross_ractor_require_data_type, crr); VALUE crr_obj = TypedData_Make_Struct(0, struct cross_ractor_require, &cross_ractor_require_data_type, crr);
FL_SET_RAW(crr_obj, RUBY_FL_SHAREABLE); FL_SET_RAW(crr_obj, RUBY_FL_SHAREABLE);
@ -2368,6 +2391,7 @@ rb_ractor_require(VALUE feature)
crr->port = ractor_port_new(GET_RACTOR()); crr->port = ractor_port_new(GET_RACTOR());
crr->result = Qundef; crr->result = Qundef;
crr->exception = Qundef; crr->exception = Qundef;
crr->silent = silent;
rb_execution_context_t *ec = GET_EC(); rb_execution_context_t *ec = GET_EC();
rb_ractor_t *main_r = GET_VM()->ractor.main_ractor; rb_ractor_t *main_r = GET_VM()->ractor.main_ractor;
@ -2395,7 +2419,7 @@ rb_ractor_require(VALUE feature)
static VALUE static VALUE
ractor_require(rb_execution_context_t *ec, VALUE self, VALUE feature) ractor_require(rb_execution_context_t *ec, VALUE self, VALUE feature)
{ {
return rb_ractor_require(feature); return rb_ractor_require(feature, false);
} }
static VALUE static VALUE

View file

@ -134,7 +134,7 @@ void rb_ractor_terminate_all(void);
bool rb_ractor_main_p_(void); bool rb_ractor_main_p_(void);
void rb_ractor_atfork(rb_vm_t *vm, rb_thread_t *th); void rb_ractor_atfork(rb_vm_t *vm, rb_thread_t *th);
void rb_ractor_terminate_atfork(rb_vm_t *vm, rb_ractor_t *th); void rb_ractor_terminate_atfork(rb_vm_t *vm, rb_ractor_t *th);
VALUE rb_ractor_require(VALUE feature); VALUE rb_ractor_require(VALUE feature, bool silent);
VALUE rb_ractor_autoload_load(VALUE space, ID id); VALUE rb_ractor_autoload_load(VALUE space, ID id);
VALUE rb_ractor_ensure_shareable(VALUE obj, VALUE name); VALUE rb_ractor_ensure_shareable(VALUE obj, VALUE name);

View file

@ -136,4 +136,25 @@ class TestEncoding < Test::Unit::TestCase
assert "[Bug #19562]" assert "[Bug #19562]"
end; end;
end end
def test_ractor_lazy_load_encoding_concurrently
assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}")
begin;
rs = []
autoload_encodings = Encoding.list.select { |e| e.inspect.include?("(autoload)") }.freeze
7.times do
rs << Ractor.new(autoload_encodings) do |encodings|
str = "abc".dup
encodings.each do |enc|
str.force_encoding(enc)
end
end
end
while rs.any?
r, _obj = Ractor.select(*rs)
rs.delete(r)
end
assert rs.empty?
end;
end
end end