Fix sending backtraces across ractors

[Bug #21818]

Currently exceptions can be sent across ractors,
but because of a limitation in the TypedData API,
the exception backtrace is duped as an empty backtrace.

The problem is that backtraces are embedded objects,
hence the classic `rb_class_alloc(klass)` API is insufficient
because we need to know the size of the Backtrace object we're
duping to instantiate the copy.

This is worked around by changing Ractors to call `#clone` on objects
rather than use `rb_obj_clone`, and to implement `Thread::Backtrace#clone`
to properly clone the variable size object.
This commit is contained in:
Jean Boussier committed 2026-04-01 11:43:55 +01:00
1 parent 3f1acedb62
commit 9a2ffd8e54
7 files changed
+140 -17
Notes: git 2026-04-01 10:44:23 +00:00

No files matched your search

+1 -1
View File
@@ -507,7 +507,7 @@ assert_equal 'false', %q{
}
# To copy the object, now Marshal#dump is used
assert_match /can not copy unshareable object/, %q{
assert_match /can't clone unshareable instance of Thread/, %q{
obj = Thread.new{}
begin
r = Ractor.new obj do |msg|
+1
View File
@@ -28,6 +28,7 @@ firstline, predefined = __LINE__+1, %[\
send
__send__
__recursive_key__
clone
initialize
initialize_copy
initialize_clone
+1
View File
@@ -19980,6 +19980,7 @@ vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/compilers.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/error.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/gc.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/imemo.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/object.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/serial.h
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/set_table.h
+26 -4
View File
@@ -2066,6 +2066,31 @@ ractor_move(VALUE obj)
}
}
static VALUE
ractor_call_clone_try(VALUE obj)
{
return rb_funcall(obj, idClone, 0);
}
static VALUE
ractor_call_clone_rescue(VALUE obj, VALUE exc)
{
rb_raise(rb_eRactorError, "can't clone unshareable instance of %"PRIsVALUE, rb_class_of(obj));
UNREACHABLE_RETURN(Qnil);
}
static VALUE
ractor_obj_clone(VALUE obj)
{
VALUE clone = rb_rescue(ractor_call_clone_try, obj, ractor_call_clone_rescue, obj);
if (obj == clone) {
rb_raise(rb_eRactorError, "#clone returned self");
}
return clone;
}
static enum obj_traverse_iterator_result
copy_enter(VALUE obj, struct obj_traverse_replace_data *data)
{
@@ -2074,10 +2099,7 @@ copy_enter(VALUE obj, struct obj_traverse_replace_data *data)
return traverse_skip;
}
else {
if (!rb_get_alloc_func(rb_obj_class(obj))) {
rb_raise(rb_eRactorError, "can not copy unshareable object %+"PRIsVALUE, obj);
}
data->replacement = rb_obj_clone(obj);
data->replacement = ractor_obj_clone(obj);
return traverse_cont;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
%# -*- c -*-
/* DO NOT EDIT THIS FILE DIRECTLY */
/* DO NOT EDIT THIS FILE DIRECTLY: source is at template/id.c.tmpl */
/**********************************************************************
id.c -
+52 -1
View File
@@ -50,6 +50,57 @@ class TestRactor < Test::Unit::TestCase
assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()> because it refers unshareable objects", exception: Ractor::Error)
end
def test_sending_exception_with_backtrace
assert_ractor(<<~'RUBY')
def build_error
raise "Test"
rescue => error
error
end
error = build_error
refute_empty error.backtrace
refute_empty error.backtrace_locations
backtrace, backtrace_locations = Ractor.new(error) do |error2|
[error2.backtrace, error2.backtrace_locations]
end.value
assert_equal error.backtrace, backtrace
refute_empty backtrace_locations
RUBY
end
def test_sending_exception_with_array_backtrace
assert_ractor(<<~'RUBY')
error = StandardError.new
error.set_backtrace(["foo", "bar"])
refute_empty error.backtrace
assert_nil error.backtrace_locations
backtrace, backtrace_locations = Ractor.new(error) do |error2|
[error2.backtrace, error2.backtrace_locations]
end.value
assert_equal error.backtrace, backtrace
assert_nil backtrace_locations
RUBY
end
def test_sending_object_with_broken_clone
assert_ractor(<<~'RUBY')
o = Object.new
def o.clone
self
end
ractor = Ractor.new { Ractor.receive }
error = assert_raise Ractor::Error do
ractor.send(o)
end
assert_match "#clone returned self", error.message
RUBY
end
def test_default_thread_group
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
@@ -219,7 +270,7 @@ class TestRactor < Test::Unit::TestCase
err = assert_raise(Ractor::Error) do
Ractor.new(pr) {}.join
end
assert_match(/can not copy unshareable object/, err.message)
assert_match(/can not copy Proc object/, err.message)
RUBY
end
+58 -10
View File
@@ -13,6 +13,7 @@
#include "internal.h"
#include "internal/class.h"
#include "internal/error.h"
#include "internal/object.h"
#include "internal/vm.h"
#include "iseq.h"
#include "ruby/debug.h"
@@ -576,14 +577,6 @@ rb_backtrace_p(VALUE obj)
return rb_typeddata_is_kind_of(obj, &backtrace_data_type);
}
static VALUE
backtrace_alloc(VALUE klass)
{
rb_backtrace_t *bt;
VALUE obj = TypedData_Make_Struct(klass, rb_backtrace_t, &backtrace_data_type, bt);
return obj;
}
static VALUE
backtrace_alloc_capa(long num_frames, rb_backtrace_t **backtrace)
{
@@ -962,6 +955,46 @@ backtrace_limit(VALUE self)
return LONG2NUM(rb_backtrace_length_limit);
}
/* :nodoc: */
static VALUE
backtrace_clone(VALUE self)
{
rb_backtrace_t *bt;
TypedData_Get_Struct(self, rb_backtrace_t, &backtrace_data_type, bt);
rb_backtrace_t *other_bt;
VALUE clone = backtrace_alloc_capa(bt->backtrace_size, &other_bt);
rb_obj_clone_setup(self, clone, Qfalse);
return clone;
}
/* :nodoc: */
static VALUE
backtrace_dup(VALUE self)
{
rb_notimplement();
UNREACHABLE_RETURN(Qnil);
}
/* :nodoc: */
static VALUE
backtrace_initialize_copy(VALUE self, VALUE original)
{
rb_backtrace_t *bt;
TypedData_Get_Struct(self, rb_backtrace_t, &backtrace_data_type, bt);
rb_backtrace_t *original_bt;
TypedData_Get_Struct(original, rb_backtrace_t, &backtrace_data_type, original_bt);
bt->backtrace_size = original_bt->backtrace_size;
MEMCPY(bt->backtrace, original_bt->backtrace, rb_backtrace_location_t, original_bt->backtrace_size);
return Qnil;
}
VALUE
rb_ec_backtrace_str_ary(const rb_execution_context_t *ec, long lev, long n)
{
@@ -1406,6 +1439,14 @@ each_caller_location(int argc, VALUE *argv, VALUE _)
return Qnil;
}
static VALUE
backtrace_no_allocator(VALUE klass)
{
rb_notimplement();
UNREACHABLE_RETURN(Qnil);
}
/* called from Init_vm() in vm.c */
void
Init_vm_backtrace(void)
@@ -1416,11 +1457,18 @@ Init_vm_backtrace(void)
* settings of the current session.
*/
rb_cBacktrace = rb_define_class_under(rb_cThread, "Backtrace", rb_cObject);
rb_define_alloc_func(rb_cBacktrace, backtrace_alloc);
rb_undef_method(CLASS_OF(rb_cBacktrace), "new");
// Can't undefine the allocator, as it's needed as a key by Marshal
rb_define_alloc_func(rb_cBacktrace, backtrace_no_allocator);
rb_marshal_define_compat(rb_cBacktrace, rb_cArray, backtrace_dump_data, backtrace_load_data);
rb_undef_method(CLASS_OF(rb_cBacktrace), "new");
rb_define_singleton_method(rb_cBacktrace, "limit", backtrace_limit, 0);
rb_define_method(rb_cBacktrace, "clone", backtrace_clone, 0);
rb_define_method(rb_cBacktrace, "dup", backtrace_dup, 0);
rb_define_method(rb_cBacktrace, "initialize_copy", backtrace_initialize_copy, 1);
/*
* An object representation of a stack frame, initialized by
* Kernel#caller_locations.