Unshare arrays only when they become shareable

obj_traverse_i called rb_ary_cancel_sharing on every array it visited,
before the traversal knew whether the object graph was shareable.

So Ractor.shareable? modified frozen arrays even when it returned false.

This commit moves rb_ary_cancel_sharing into mark_shareable, which
runs only after the traversal succeeds.

[Bug #22460]
This commit is contained in:
Matt Valentine-House authored and eightbitraptor committed 2026-10-22 13:01:48 +01:00
1 parent 9fa6602683
commit 0d83a93784
3 files changed
+18 -4
Notes: git 2026-10-22 12:02:25 +00:00

No files matched your search

+2 -2
View File
@@ -579,8 +579,8 @@ rb_ary_cancel_sharing(VALUE ary)
else if (ARY_SHARED_ROOT_OCCUPIED(shared_root) && len > ((shared_len = RARRAY_LEN(shared_root))>>1)) {
rb_len_t shift = RARRAY_CONST_PTR(ary) - RARRAY_CONST_PTR(shared_root);
FL_UNSET_SHARED(ary);
ARY_SET_PTR(ary, RARRAY_CONST_PTR(shared_root));
ARY_SET_CAPA(ary, shared_len);
ARY_SET_PTR_FORCE(ary, RARRAY_CONST_PTR(shared_root));
ARY_SET_CAPA_FORCE(ary, shared_len);
RARRAY_PTR_USE(ary, ptr, {
MEMMOVE(ptr, ptr+shift, VALUE, len);
});
+3 -2
View File
@@ -1754,8 +1754,6 @@ obj_traverse_i(VALUE obj, struct obj_traverse_data *data)
case T_ARRAY:
{
rb_ary_cancel_sharing(obj);
for (rb_len_t i = 0; i < RARRAY_LEN(obj); i++) {
VALUE e = RARRAY_AREF(obj, i);
if (obj_traverse_i(e, data)) return 1;
@@ -2002,6 +2000,9 @@ mark_shareable(VALUE obj)
if (RB_BUILTIN_TYPE(obj) == T_STRING) {
rb_str_make_independent(obj);
}
else if (RB_BUILTIN_TYPE(obj) == T_ARRAY) {
rb_ary_cancel_sharing(obj);
}
rb_obj_set_shareable_no_assert(obj);
return traverse_cont;
+13
View File
@@ -955,6 +955,19 @@ class TestRactor < Test::Unit::TestCase
RUBY
end
# [Bug #22460] Ractor.shareable? must not replace the buffer of a frozen shared array.
def test_shareable_p_keeps_frozen_shared_array_buffer
assert_ractor(<<~'RUBY')
o = Object.new
a = Array.new(200) { it }
a[50] = o
b = a.pop(150)
o.define_singleton_method(:to_s) { Ractor.shareable?(b); "x" }
b.freeze
assert_nothing_raised { assert_equal "x 51", "%1$s %2$s" % b }
RUBY
end
# Ractor::Port.new must not deadlock under GC.stress (regression guard for a stress GC
# triggered by malloc while the ractor lock is held).
def test_port_new_under_gc_stress