Skip to content

Commit faa3cc8

Browse files
ko1byrootclaude
committed
Fix Ractor move source husk breaking compaction slot size invariant
When an object is moved to another Ractor with `Ractor.send(obj, move: true)`, the moved-out source is neutralized into a RactorMovedObject husk by move_leave. The husk stays in the object's original, possibly larger, slot, but its shape was reset to 0, losing the capacity. Shape 0 implies the smallest slot, so the husk ended up with rb_obj_shape_slot_size(husk) != rb_gc_obj_slot_size(husk). Since re-embeddable T_OBJECT fields landed, compaction requires every live object to satisfy rb_gc_obj_slot_size == rb_obj_shape_slot_size (asserted in gc_ref_update_object under RGENGC_CHECK_MODE). A GC.compact in the receiving Ractor walks the husk and trips the assertion; in a release build it updates references with a mismatched slot width and can corrupt the slot. Give the husk an empty, frozen, RObject-layout shape while preserving its SHAPE_ID_CAPACITY_MASK, so the slot size still matches and no field is exposed. Co-Authored-By: Jean Boussier <jean.boussier@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5167ab8 commit faa3cc8

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

bootstraptest/test_ractor.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,3 +2668,27 @@ def foo(a:, b:, c:) = super(a: a, b: b, c: c)
26682668
:ok # platform without fork
26692669
end
26702670
}
2671+
2672+
# A moved object's source is neutralized into a RactorMovedObject husk that
2673+
# stays in its original (possibly larger) slot. It must be given a shape whose
2674+
# slot size matches that slot, or a later compaction in the receiver trips the
2675+
# slot_size == shape_slot_size invariant (RGENGC_CHECK_MODE) / corrupts the slot.
2676+
assert_equal 'ok', %q{
2677+
r = Ractor.new do
2678+
while (o = Ractor.receive)
2679+
begin
2680+
GC.compact
2681+
rescue NotImplementedError
2682+
# no-op on platforms without GC.compact (e.g. MMTk)
2683+
end
2684+
end
2685+
:ok
2686+
end
2687+
500.times do
2688+
o = Object.new
2689+
12.times { |i| o.instance_variable_set("@i#{i}", i) } # overflow to a larger slot
2690+
r.send(o, move: true)
2691+
end
2692+
r.send(nil)
2693+
r.value
2694+
}

ractor.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2109,11 +2109,17 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data)
21092109
}
21102110

21112111
VALUE flags = T_OBJECT | FL_FREEZE | (RBASIC(obj)->flags & FL_PROMOTED);
2112+
shape_id_t shape_id = (RBASIC_SHAPE_ID(obj) & SHAPE_ID_CAPACITY_MASK) | ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_ROBJECT | SHAPE_ID_FL_FROZEN;
21122113

21132114
// Avoid mutations using bind_call, etc.
21142115
MEMZERO((char *)obj, char, sizeof(struct RBasic));
21152116
RBASIC(obj)->flags = flags;
21162117
RBASIC_SET_CLASS_RAW(obj, rb_cRactorMovedObject);
2118+
2119+
// The husk keeps its original (larger) slot, so give it a field-less shape
2120+
// sized to that slot; otherwise compaction's slot_size == shape_slot_size
2121+
// invariant is violated.
2122+
RBASIC_SET_FULL_SHAPE_ID(obj, shape_id);
21172123
return traverse_cont;
21182124
}
21192125

0 commit comments

Comments
 (0)