Found while building the regression witness for #7475 / #7495. Distinct from the js_iterator_to_array bug #7495 fixes, and present on both link modes, so it is not an auto-optimize artifact.
What
[...obj.arr] has two lowerings. When the spread's enclosing function stays out of line it calls js_iterator_to_array directly — that path is the one #7495 rooted. When the function is small enough to inline, the spread routes through array_from_spread_value instead, which first resolves [Symbol.iterator] via js_object_get_symbol_property. A stale from-space object is dereferenced inside THAT prototype walk.
Repro
Take #7495's witness test-files/test_gap_gc_iterator_drain_rooting.ts and shrink deepClone until it inlines — e.g. drop name / meta.created / meta.updated so the body is just { id, meta: { tags: [...o.meta.tags] } }. On #7495's branch (i.e. with the iterator drain already fixed):
PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=200 ./out
[gc-fromspace-protect] FAULT: signal 10 at 0x313baa409a8
block=0x313baa10000 +199080 retired_bytes=1048552 retired_by_minor=#0
last-known object: user_ptr=0x313baa409b0 obj_type=2 size=72
2 js_object_get_field_by_name + 132
3 perry_runtime::object::field_get_set::fetch_subclass_handle_id + 112
4 perry_runtime::object::field_get_set::get_field_by_name_tail::get_field_by_name_object_tail + 13956
5 perry_runtime::object::field_get_set::accessors::default_object_prototype_property_value + 276
6 perry_runtime::object::field_get_set::accessors::array_prototype_property_value + 592
7 perry_runtime::object::field_get_set::get_field_by_name_tail::get_field_by_name_object_tail + 14464
8 js_object_get_field_by_name_f64 + 244
9 js_object_get_symbol_property + 2496
10 perry_runtime::array::iterator::array_from_spread_value + 1200
11 perry_fn_..._run + 1148
Without the instrument it surfaces as TypeError: next is not a function / TypeError: value is not a function, the #7341-family signature.
First thing to check
Per CLAUDE.md: "an unrooted register goes bad only when a collection lands in its window, so it is intermittent; an unrooted cache goes bad at collection #0 and stays bad, so a perfectly reproducible GC bug means a table, not a register." This one is perfectly reproducible and the retiring minor is #0, so start at the side tables the prototype walk reads, not at a register in array_from_spread_value. crates/perry-runtime/src/gc/tests/runtime_roots/prototype_addr_cache.rs already exists for one such cache; fetch_subclass_handle_id is the frame that actually faults.
Second candidate, if the tables come back clean: array_from_spread_value reads raw_ptr from its value argument near the top and then carries it through ~a dozen classification probes (entries_array_for_small_handle_id, is_registered_buffer, subclass_backing_for_default_iteration, array_subclass_dense_snapshot, try_read_as_search_params, the symbol lookup, …), several of which allocate. Neither value nor raw_ptr is rooted.
Scope note
Both links fail identically, so unlike #7475's headline symptom this is not sensitive to the auto-optimize feature set — every user is exposed.
Found while building the regression witness for #7475 / #7495. Distinct from the
js_iterator_to_arraybug #7495 fixes, and present on both link modes, so it is not an auto-optimize artifact.What
[...obj.arr]has two lowerings. When the spread's enclosing function stays out of line it callsjs_iterator_to_arraydirectly — that path is the one #7495 rooted. When the function is small enough to inline, the spread routes througharray_from_spread_valueinstead, which first resolves[Symbol.iterator]viajs_object_get_symbol_property. A stale from-space object is dereferenced inside THAT prototype walk.Repro
Take #7495's witness
test-files/test_gap_gc_iterator_drain_rooting.tsand shrinkdeepCloneuntil it inlines — e.g. dropname/meta.created/meta.updatedso the body is just{ id, meta: { tags: [...o.meta.tags] } }. On #7495's branch (i.e. with the iterator drain already fixed):Without the instrument it surfaces as
TypeError: next is not a function/TypeError: value is not a function, the #7341-family signature.First thing to check
Per CLAUDE.md: "an unrooted register goes bad only when a collection lands in its window, so it is intermittent; an unrooted cache goes bad at collection #0 and stays bad, so a perfectly reproducible GC bug means a table, not a register." This one is perfectly reproducible and the retiring minor is
#0, so start at the side tables the prototype walk reads, not at a register inarray_from_spread_value.crates/perry-runtime/src/gc/tests/runtime_roots/prototype_addr_cache.rsalready exists for one such cache;fetch_subclass_handle_idis the frame that actually faults.Second candidate, if the tables come back clean:
array_from_spread_valuereadsraw_ptrfrom itsvalueargument near the top and then carries it through ~a dozen classification probes (entries_array_for_small_handle_id,is_registered_buffer,subclass_backing_for_default_iteration,array_subclass_dense_snapshot,try_read_as_search_params, the symbol lookup, …), several of which allocate. Neithervaluenorraw_ptris rooted.Scope note
Both links fail identically, so unlike #7475's headline symptom this is not sensitive to the auto-optimize feature set — every user is exposed.