Skip to content

test_gap_6908_proxy_array_mutators hangs (10s timeout) after 'sort-cmp' — pre-existing, unsnapshotted #7548

Closed

Description

@proggeramlug

Symptom

test-files/test_gap_6908_proxy_array_mutators.ts hangs — the harness kills it after its 10 s budget. Perry prints 5 of node's lines and then stops making progress:

...
reverse-hole: 4,3,,1 true true false true
sort: 1,10,9 true
sort-cmp: 2,4,10,33          <- last line Perry emits

Node continues past that point (dense: 0,3 2, objlike-reverse: b a c, objlike-splice: 5,3 4 1) and exits 0. So the hang is in whatever the test does immediately after sort-cmp — the dense / object-like mutator section.

Reproduced on the pinned quiet mini and on the dev Mac, perry-dev build, deterministic.

Not a regression, and not snapshotted

A/B'd against f06270d06 — the commit before today's 40-PR merge stack — with its own build of the full 5-package set: exit 124, identical last line. So this predates today's work and is not caused by it.

It is absent from both test-parity/gap_snapshot.json and test-parity/known_failures.json, i.e. nothing recorded it as accepted. That makes it the same class as #7518 and #7526: a real defect sitting unnoticed because parity is gated to tag pushes only, so no per-PR run ever reports it.

Per run_gap_tests.sh's own policy — "Crashes (SIGSEGV/SIGABRT/timeout) are hard defects, never cosmetic gaps" — and the guard added in #7530, this cannot be snapshotted. It needs fixing or an open tracking issue; this is the issue.

Where to look

test_gap_6908_proxy_array_mutators.ts was added by #7424 (d255ae604, "trap-route reverse/splice/fill/copyWithin on Proxy receivers; receiver-less mutator thunks throw"). A hang rather than a throw or a wrong value suggests an unbounded loop in the trap-routing path — e.g. a mutator that re-enters its own trap, or a length-driven loop whose bound is re-read through a trap that never converges.

Worth bisecting between #7424 and now: the test presumably passed when that PR landed, which would put a second regression in the same window that produced #7518 (c6ed8175d, 2026-07-30) and #7521.

Acceptance

Byte-identical to node with exit 0, plus a note on whether the hang is bounded-but-slow or genuinely non-terminating (the 10 s budget cannot distinguish them).

Activity

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

    @proggeramlug
    ContributorAuthor

    Fixed in #7551.

    The hang is bounded, not infinite — a loop over ~6·10^8 iterations. The 10 s budget cannot tell the two apart, and the distinction mattered: the guess in the issue ("a mutator re-entering its own trap, or a length-driven loop whose bound is re-read through a trap that never converges") is not what happens. Tracing proxy_set_str_key shows every element write of the splice completing normally; it is the final Set(proxy, "length", 6) that walks.

    Root cause — not in the Proxy path at all

    js_array_grow reallocates an array's header+elements as one allocation and leaves a #233 forwarding stub at the old address. The stub's first 8 bytes are exactly where length and capacity live, so they read back as the two halves of the forwarding pointer. The array branches of Object.* (crates/perry-runtime/src/object/array_object_ops.rs) reinterpreted the caller's pointer with a bare obj as *ArrayHeader cast, so a JS binding still holding the pre-grow address made (*arr).length return a heap address: old_len = 615098568, cap = 1273 (together a ~4.9 TB mimalloc address). is_array_object cannot tell a stub apart — it keeps obj_type == GC_TYPE_ARRAY.

    ArraySetLength then took n(6) < old_len(615098568) and ran its shrink walk, one to_string() + attrs side-table probe per index.

    It reduces to a repro with no splice, no mutator and no Proxy:

    const t = [1, 2, 3, 4, 5];
    Object.defineProperty(t, "5", { value: 5, writable: true, enumerable: true, configurable: true });
    Object.defineProperty(t, "length", { value: 6 });   // walks 6·10^8 times

    A second, worse hang on the same root cause

    mark_all_array_props has the identical shape, so Object.freeze / Object.seal hangs on any array that has ever outgrown its dense capacity — no defineProperty and no Proxy needed:

    const t = [1, 2];
    t.push(3);
    Object.freeze(t);   // never returns

    That is independent of #7424 entirely and has been shipping since #5025 (2026-06-11).

    Which commit — the test never passed

    The issue suggested bisecting between #7424 and now. There is no regression commit to find. I built perry at d255ae604 (#7424, the PR that added test_gap_6908_proxy_array_mutators.ts) from an exported source tree in its own target dir with the same -p set, and ran the test that commit ships: exit 137 after the identical five lines, last line sort-cmp: 2,4,10,33. It was broken on arrival. All four bare casts are present verbatim there; they date to #4709 (2026-06-06, ArraySetLength) and #5025 (2026-06-11, freeze/seal on arrays) and were never touched since.

    So this is a third instance of the class you flagged, but with a different mechanism: not "a regression landed unseen" — "a test landed already red and no per-PR job could say so", because parity is gated to tag pushes.

    Sibling mutators

    The discriminator is not the mutator; it is whether the mutator writes an index at or beyond the receiver's dense capacity and then writes length.

    • Hung: push, unshift, splice with inserts > deletes, splice(len, 0, x).
    • Never affected: pop, shift, reverse, sort, fill, copyWithin — and splice remove-only / equal-count. That includes all their .call forms.

    So of the four mutators #7424 trap-routed, only splice could hang; reverse / fill / copyWithin are clean.

    Adjacent defects found while fixing this (not fixed in #7551)

    1. The array attrs side table is keyed inconsistently. Object.getOwnPropertyDescriptor reads at the caller's (possibly pre-grow) address, while the element-write rejection path resolves through clean_arr_ptr first. After fix(object): resolve the array forwarding chain before reading a header in Object.* (#7548) #7551, Object.freeze on a grown array terminates and reports correct descriptors, but the write is still not rejected — t[0] = 99 lands where node keeps 1. Previously invisible because the freeze hung. I measured re-keying these writers canonically: it regressed getOwnPropertyDescriptor without gaining the write rejection, so the fix belongs on the reader side and is out of scope here.
    2. Array.prototype.push.call(proxy, …) and .unshift.call(proxy, …) silently no-op (node mutates). Verified identical on a pristine build, so pre-existing and unrelated to the mutator work.
    3. getOwnPropertyDescriptor loses an accessor defined at an index past capacity — typeof d.get is undefined, node says function. The mirror of (1), from the accessor branch's existing canonical re-keying. Also pre-existing.

    Happy to file 1–3 separately if you want them tracked.

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