You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
test_gap_6908_proxy_array_mutators hangs (10s timeout) after 'sort-cmp' — pre-existing, unsnapshotted #7548
test-files/test_gap_6908_proxy_array_mutators.tshangs — 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 bothtest-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).
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:
constt=[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:
constt=[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 addedtest_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)
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.
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.
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.
Activity
changelog: fragment for #7551 (array forwarding stub in Object.* head…
proggeramlug commentedon Aug 6, 2026
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. Tracingproxy_set_str_keyshows every element write of the splice completing normally; it is the finalSet(proxy, "length", 6)that walks.Root cause — not in the Proxy path at all
js_array_growreallocates 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 wherelengthandcapacitylive, so they read back as the two halves of the forwarding pointer. The array branches ofObject.*(crates/perry-runtime/src/object/array_object_ops.rs) reinterpreted the caller's pointer with a bareobj as *ArrayHeadercast, so a JS binding still holding the pre-grow address made(*arr).lengthreturn a heap address:old_len = 615098568,cap = 1273(together a ~4.9 TB mimalloc address).is_array_objectcannot tell a stub apart — it keepsobj_type == GC_TYPE_ARRAY.ArraySetLengththen tookn(6) < old_len(615098568)and ran its shrink walk, oneto_string()+ attrs side-table probe per index.It reduces to a repro with no splice, no mutator and no Proxy:
A second, worse hang on the same root cause
mark_all_array_propshas the identical shape, soObject.freeze/Object.sealhangs on any array that has ever outgrown its dense capacity — nodefinePropertyand no Proxy needed: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
perryatd255ae604(#7424, the PR that addedtest_gap_6908_proxy_array_mutators.ts) from an exported source tree in its own target dir with the same-pset, and ran the test that commit ships: exit 137 after the identical five lines, last linesort-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
parityis 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.push,unshift,splicewith inserts > deletes,splice(len, 0, x).pop,shift,reverse,sort,fill,copyWithin— andspliceremove-only / equal-count. That includes all their.callforms.So of the four mutators #7424 trap-routed, only
splicecould hang;reverse/fill/copyWithinare clean.Adjacent defects found while fixing this (not fixed in #7551)
Object.getOwnPropertyDescriptorreads at the caller's (possibly pre-grow) address, while the element-write rejection path resolves throughclean_arr_ptrfirst. After fix(object): resolve the array forwarding chain before reading a header in Object.* (#7548) #7551,Object.freezeon a grown array terminates and reports correct descriptors, but the write is still not rejected —t[0] = 99lands where node keeps1. Previously invisible because the freeze hung. I measured re-keying these writers canonically: it regressedgetOwnPropertyDescriptorwithout gaining the write rejection, so the fix belongs on the reader side and is out of scope here.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.getOwnPropertyDescriptorloses an accessor defined at an index past capacity —typeof d.getisundefined, node saysfunction. 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.
fix(object): resolve the array forwarding chain before reading a head…
changelog: fragment for #7551 (array forwarding stub in Object.* head…
fix(object): resolve the array forwarding chain before reading a head…