Repository navigation
gc: array_from_spread_value's Symbol.iterator lookup derefs a from-space object (inlined [...obj.arr]) #7498
Description
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.
Activity
docs: point the two residual #7475 defects at their own issues
proggeramlug commentedon Aug 6, 2026
Simpler repro than the one in the issue body — no shrinking needed. On #7495's branch (iterator drain already fixed), the shipped witness reproduces it directly:
Two corrections to the issue body above:
GC_TYPE_STRING, not the 72-byteGC_TYPE_OBJECTin the first trace, and the retiring minor is Using the fetch API #3, not #0. Both traces are the same call chain, so the site is one thing reached with different values live — but "deterministic at minor #0 ⇒ a table" is therefore NOT established. Treat the table-vs-register question as open.deepClone.array_from_spread_valueends by callingjs_iterator_to_array(array_values_iter(value)), so the two[...obj.arr]lowerings converge; the spread reaches this prototype walk first either way.It does not corrupt the witness's result — that file's output is byte-exact with the oracle after #7495 — so this is a live stale-read with no observed consequence yet, which is the shape that turns into a
TypeError: value is not a functionin some other program once allocation timing moves.test-parity/gc_repsel_corpus.txtrecords that a protected run oftest_gap_gc_iterator_drain_rootingshould go silent when this lands, and that a remaining fault would mean a third site.docs: point the two residual #7475 defects at their own issues
fix(gc): root the iterator drain's live values across .next() (#7475) (…
fix(gc): root the inlined spread's `[Symbol.iterator]` walk (#7498)
fix(gc): root the inlined spread's `[Symbol.iterator]` walk (#7498) (#…