mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Ractor: wipe the body of an object hollowed out by move
Ractor#send(move: true) turns the source object into a frozen
Ractor::MovedObject in place: it rewrites the header and gives the slot a
field-less shape, but leaves the old body untouched. Ruby-level access is
blocked by method_missing, yet C code that still holds the object from before
the move keeps reading it with its old type.
The visible case is $~ holding a MatchData that is then moved away:
r = Ractor.new { Ractor.receive }
"abc123xyz".match(/([a-z]+)(\\d+)/)
r.send($~, move: true)
"qqq777".match(/([a-z]+)(\\d+)/).frozen? #=> true
match_alloc_or_reuse() decides on MATCH_BUSY and RMATCH(existing)->capa, and
the husk satisfies both: rewriting the header cleared MATCH_BUSY, and capa
still reads the old capacity. RMatch fields are then written into a frozen
T_OBJECT whose shape says it has no fields, and once the GC walks it as a
ROBJECT the heap is corrupted: [BUG] try to mark T_NONE, out-of-heap,
use-after-poison in RSTRING_LEN, or silently wrong captures.
That the reuse is the cause is easy to confirm: if the following match needs
more registers than the husk's stale capa, no reuse happens and the same
program is clean.
Check the type in match_alloc_or_reuse() before trusting the body, and zero
the body when hollowing an object out, so that any other stale typed read
sees an empty object instead of the old internals (an Enumerator whose
Array#each is in progress, for instance, reads a length of 0 and stops
instead of walking off the end).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
036159508e
commit
556296cbe0
3 files changed
+27
Notes:
git
2026-08-04 16:49:20 +00:00
No files matched your search
@@ -2125,10 +2125,17 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data)
|
||||
shape_id_t shape_id = (RBASIC_SHAPE_ID(obj) & SHAPE_ID_CAPACITY_MASK) | ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_ROBJECT | SHAPE_ID_FL_FROZEN;
|
||||
|
||||
// Avoid mutations using bind_call, etc.
|
||||
size_t slot_size = rb_gc_obj_slot_size(obj);
|
||||
MEMZERO((char *)obj, char, sizeof(struct RBasic));
|
||||
RBASIC(obj)->flags = flags;
|
||||
RBASIC_SET_CLASS_RAW(obj, rb_cRactorMovedObject);
|
||||
|
||||
// Wipe the old body too. The husk has no fields, so nothing reads it as ivars,
|
||||
// but C code that held the object from before the move still reads it with its
|
||||
// old type (an Array iteration in progress, the RMatch capa behind $~): a zeroed
|
||||
// body makes those reads see an empty object instead of stale internals.
|
||||
MEMZERO((char *)obj + sizeof(struct RBasic), char, slot_size - sizeof(struct RBasic));
|
||||
|
||||
// The husk keeps its original (larger) slot, so give it a field-less shape
|
||||
// sized to that slot; otherwise compaction's slot_size == shape_slot_size
|
||||
// invariant is violated.
|
||||
|
||||
@@ -1582,7 +1582,12 @@ rb_match_count(VALUE match)
|
||||
static VALUE
|
||||
match_alloc_or_reuse(VALUE existing, int num_regs)
|
||||
{
|
||||
/* $~ can hold a Ractor::MovedObject: Ractor#send(move: true) hollows the
|
||||
* MatchData out in place and the husk keeps the old RMatch body, so its capa
|
||||
* still reads as reusable. Reusing it would write RMatch fields into a frozen
|
||||
* T_OBJECT, so check the type before trusting the body. */
|
||||
if (!NIL_P(existing) &&
|
||||
RB_TYPE_P(existing, T_MATCH) &&
|
||||
!FL_TEST(existing, MATCH_BUSY) &&
|
||||
RMATCH(existing)->capa >= num_regs * 2) {
|
||||
return existing;
|
||||
|
||||
@@ -476,4 +476,19 @@ class TestRactor < Test::Unit::TestCase
|
||||
end
|
||||
refute Ractor.shareable?(obj), "despite raising, object became shareable"
|
||||
end
|
||||
# $~ can hold the MatchData that a move hollows out in place. The husk keeps
|
||||
# the old RMatch body, so a later match must allocate instead of reusing it
|
||||
# (a reused husk stays frozen and keeps its Ractor::MovedObject shape).
|
||||
def test_move_matchdata_kept_in_backref
|
||||
assert_ractor(<<~'RUBY', timeout: 60)
|
||||
r = Ractor.new { Ractor.receive }
|
||||
"abc123xyz".match(/([a-z]+)(\d+)/) # $~ holds the MatchData
|
||||
r.send($~, move: true) # husked in place; $~ still points at it
|
||||
m = "qqq777".match(/([a-z]+)(\d+)/)
|
||||
assert_instance_of MatchData, m
|
||||
refute_predicate m, :frozen?
|
||||
assert_equal ["qqq777", "qqq", "777"], [m[0], m[1], m[2]]
|
||||
r.value
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user