Skip to content

gc: array_from_spread_value's Symbol.iterator lookup derefs a from-space object (inlined [...obj.arr]) #7498

Closed

Description

@proggeramlug

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

  1. added a commit that references this issue on Aug 6, 2026
  2. proggeramlug commented on Aug 6, 2026

    @proggeramlug
    ContributorAuthor

    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:

    PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=200 \
      ./out-of test-files/test_gap_gc_iterator_drain_rooting.ts
    
    [gc-fromspace-protect] FAULT: signal 10 at 0x5d40cd2ffb9
      block=0x5d40cc30000 +1048505 retired_bytes=1048560 retired_by_minor=#3
      last-known object: user_ptr=0x5d40cd2ffc0 obj_type=3 size=56
    2  js_object_get_field_by_name + 132
    3  perry_runtime::object::field_get_set::accessors::default_object_prototype_property_value + 276
    4  perry_runtime::object::field_get_set::accessors::array_prototype_property_value + 592
    5  perry_runtime::object::field_get_set::get_field_by_name_tail::get_field_by_name_object_tail + 14464
    6  perry_runtime::object::field_get_set::accessors::array_prototype_property_value + 548
    7  perry_runtime::object::field_get_set::get_field_by_name_tail::get_field_by_name_object_tail + 14464
    8  perry_runtime::url::search_params::url_search_params_backing_of + 204
    9  js_native_call_method + 2872
    10 perry_runtime::array::iterator::array_from_spread_value + 1360
    11 main + 2496
    

    Two corrections to the issue body above:

    • the stale object here is a 56-byte GC_TYPE_STRING, not the 72-byte GC_TYPE_OBJECT in 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.
    • it does not need an inlined deepClone. array_from_spread_value ends by calling js_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 function in some other program once allocation timing moves. test-parity/gc_repsel_corpus.txt records that a protected run of test_gap_gc_iterator_drain_rooting should go silent when this lands, and that a remaining fault would mean a third site.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions