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
Found while validating #7963 (PR #7978). This is a latent defect on main, not a defect of that PR: it reproduces identically on a pristine origin/main build and on the branch.
Symptom
A comparison whose operands are two calls — the shape every gap probe in test-files/ uses for its verdict line —
leaves observed()'s result as an SSA temporary that is live across expected(). When expected() allocates through a loop back-edge it collects, the temporary is never rewritten, and the run faults on retired from-space memory:
[gc-fromspace-protect] FAULT: signal 10 at 0x38af4b20e5c
This address is RETIRED FROM-SPACE. ...
block=0x38af4b20000 +3676 retired_bytes=4104 retired_by_minor=#134
last-known object: user_ptr=0x38af4b20e58 obj_type=3 size=216
The faulting instruction IS the stale use. Backtrace:
0 … perry_runtime::arena::quarantine::fromspace_fault_handler
1 … perry_runtime::gc::schedule::schedule_fault_handler
2 libsystem_platform.dylib _sigtramp
3 … js_jsvalue_equals + 328
4 … js_eq + 20
5 … main + 688
obj_type=3 is GC_TYPE_STRING, and the faulting address is user_ptr + 4, which is exactly StringHeader::byte_len (crates/perry-runtime/src/string/mod.rs:308 — offset 0 is utf16_len). So the faulting instruction is js_jsvalue_equals reading the length of a string operand that has moved.
The backtrace was obtained by compiling against an unstripped runtime:
— exits 0 with copying_minors=301 moved_objects=110912, i.e. the collector was extremely busy and the rooted form survives it. That difference is the whole bug: the value is identical, only its binding changed.
Why it matters
It is the plain docs/src/internals/gc-rooting-invariant.md rule — a GC-managed value's root store must dominate every subsequent site that can collect — applied to a call-result temporary feeding a binary operator. Any f() === g(), f() + g(), f().concat(g()) … where the left operand is heap-allocated and the right operand can collect has the same shape.
scripts/gc_root_dominance_check.py is the right instrument for this class, but it reported 0 root store(s) in the corpus on this program's --trace llvm output, both with and without PERRY_INLINE_SHADOW_SLOT=0 — so either the module genuinely emits no root stores (which would itself be the bug) or the checker's corpus-liveness guard is telling us the IR is not the IR it expects. Worth resolving first: a checker that cannot see this program cannot gate it.
docs/src/internals/gc-rooting-invariant.md names three shapes of this defect already (#7184 out-of-frame slot index, #7192 store after an allocating call, #7207 plain alloca_entry). This looks like a fourth: no store at all, because the value never became a named local.
Found while validating #7963 (PR #7978). This is a latent defect on
main, not a defect of that PR: it reproduces identically on a pristineorigin/mainbuild and on the branch.Symptom
A comparison whose operands are two calls — the shape every gap probe in
test-files/uses for its verdict line —leaves
observed()'s result as an SSA temporary that is live acrossexpected(). Whenexpected()allocates through a loop back-edge it collects, the temporary is never rewritten, and the run faults on retired from-space memory:obj_type=3isGC_TYPE_STRING, and the faulting address isuser_ptr + 4, which is exactlyStringHeader::byte_len(crates/perry-runtime/src/string/mod.rs:308— offset 0 isutf16_len). So the faulting instruction isjs_jsvalue_equalsreading the length of a string operand that has moved.The backtrace was obtained by compiling against an unstripped runtime:
(
PERRY_DEBUG_SYMBOLS=1is what suppressesstrip_final_binary; without it the release binary keeps 159 symbols and nothing resolves.)Reproduce
Take
test-files/test_gap_gc_define_property_descriptor_rooting.tsfrom PR #7978 and put its three verdict lines back into the inline form:then
exits 138. Binding both sides first —
— exits 0 with
copying_minors=301 moved_objects=110912, i.e. the collector was extremely busy and the rooted form survives it. That difference is the whole bug: the value is identical, only its binding changed.Why it matters
docs/src/internals/gc-rooting-invariant.mdrule — a GC-managed value's root store must dominate every subsequent site that can collect — applied to a call-result temporary feeding a binary operator. Anyf() === g(),f() + g(),f().concat(g())… where the left operand is heap-allocated and the right operand can collect has the same shape.obj_type=3, a string) invites attributing it to whatever runtime helper the probe exercises. gc: from-space fault in the Object.defineProperty / descriptor-getter family (the window #6949's scope note defers) #7963's own report may be this defect rather than thedefinePropertywindow; PR fix(gc): root Object.defineProperty's receiver, key and descriptor fields across its own allocating calls #7978 fixes a genuine, independently-provendefinePropertyrooting hole, but its first-draft probe was faulting here, not there.Where to look
scripts/gc_root_dominance_check.pyis the right instrument for this class, but it reported0 root store(s) in the corpuson this program's--trace llvmoutput, both with and withoutPERRY_INLINE_SHADOW_SLOT=0— so either the module genuinely emits no root stores (which would itself be the bug) or the checker's corpus-liveness guard is telling us the IR is not the IR it expects. Worth resolving first: a checker that cannot see this program cannot gate it.docs/src/internals/gc-rooting-invariant.mdnames three shapes of this defect already (#7184 out-of-frame slot index, #7192 store after an allocating call, #7207 plainalloca_entry). This looks like a fourth: no store at all, because the value never became a named local.