Found while closing #7498. Distinct from it: #7498's two faults are gone from
this reproducer's protected run, and this one remains. Present on both link
modes.
What
js_native_call_method does root its receiver — object_handle on the
first lines of the function. It then reads it out once:
let object_handle = root_scope.root_nanbox_f64(object);
...
let object = object_handle.get_nanbox_f64();
let jsval = JSValue::from_bits(object.to_bits());
and that local object (99 uses) and the derived jsval (32 uses) are reused
for the next ~1200 lines, across a dozen probes that allocate. A value read
out of a root and held in a register across a call is not rooted
(docs/src/internals/gc-rooting-invariant.md) — the root keeps the object
alive and the collector rewrites the slot, not the copy.
The measured deref is the closure-magic probe at native_call_method.rs:1041:
let raw_addr = if jsval.is_pointer() {
crate::value::js_nanbox_get_pointer(object) as usize // stale copy
} ...
if crate::value::addr_class::is_above_handle_band(raw_addr)
&& crate::closure::is_closure_ptr(raw_addr) // dereferences it
Repro
On main (or on #7527's branch — unchanged by it), with the shipped witness:
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
array_from_spread_value + 2148 (js_closure_call0 — the bound @@iterator thunk)
js_native_call_method + 5604
main
and under lldb the faulting instruction is the magic read itself:
stop reason = EXC_BAD_ACCESS (code=2, address=0x25773a102fc)
frame #0: js_native_call_method + 6060
-> ldr w8, [x28, #0xc] ; CLOSURE_MAGIC, 0x434C4F53
5/5 under the quarantine. Symbolicate with PERRY_LINK_MAP=<path> — the perry
link passes -Wl,-no_exported_symbols, so atos and nm return nothing and
the handler's own backtrace prints bare offsets.
Observable consequence
This one is not latent. On macOS/arm64, test_gap_gc_iterator_drain_rooting
prints badLen 1 instead of badLen 0, 5/5, on PERRY_NO_AUTO_OPTIMIZE=1 —
i.e. one of 50 000 clones loses its tags array. (On main today it is worse:
TypeError: next is not a function, 5/5; #7527 removes that and leaves the
badLen 1.)
Knob bisect says the copying minor, not policy evacuation:
| knob |
result |
PERRY_GEN_GC=0 |
correct |
PERRY_GC_SCAVENGE=0 |
correct |
PERRY_WRITE_BARRIERS=0 |
correct |
PERRY_GEN_GC_EVACUATE=0 |
still badLen 1 |
First thing to check — and what NOT to do
Do not patch the faulting line. That was tried on #7527's branch: re-reading
object_handle at the top of the raw_addr block made the fault move 800 bytes
further into the same function (+6060 → +6868, ldurb w8, [x19, #-0x8], the
GcHeader obj_type read). One arm of a 99-use function is half a fix, and the
half that is missing is the half that still crashes.
The shape that closes it is the one #7495 used for
dispatch_array_iterator_method: shadow the parameter with a reader so the
pre-collection address is not nameable at all —
let object = || object_handle.get_nanbox_f64();
— and rewrite the 99 object / 32 jsval uses through it. Mechanical, but it
is the single hottest dispatcher in the runtime and it deserves its own PR and
its own review rather than riding along on a spread fix.
Note the file is 1969 lines against the 2000-line cap
(scripts/check_file_size.sh), so the rewrite has ~30 lines of headroom; the
closure form is net-neutral, a comment block is not.
Acceptance
test_gap_gc_iterator_drain_rooting (already registered in
test-parity/gc_repsel_corpus.txt) is the witness: it must print badLen 0
and go clean under PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=200 on both links. Check
PERRY_GC_DIAG=1 prints [gc-fromspace-protect] retired_set=#N before
believing a clean run — a run with zero copying minors protects nothing.
Found while closing #7498. Distinct from it: #7498's two faults are gone from
this reproducer's protected run, and this one remains. Present on both link
modes.
What
js_native_call_methoddoes root its receiver —object_handleon thefirst lines of the function. It then reads it out once:
and that local
object(99 uses) and the derivedjsval(32 uses) are reusedfor the next ~1200 lines, across a dozen probes that allocate. A value read
out of a root and held in a register across a call is not rooted
(
docs/src/internals/gc-rooting-invariant.md) — the root keeps the objectalive and the collector rewrites the slot, not the copy.
The measured deref is the closure-magic probe at
native_call_method.rs:1041:Repro
On
main(or on #7527's branch — unchanged by it), with the shipped witness:and under lldb the faulting instruction is the magic read itself:
5/5 under the quarantine. Symbolicate with
PERRY_LINK_MAP=<path>— the perrylink passes
-Wl,-no_exported_symbols, soatosandnmreturn nothing andthe handler's own backtrace prints bare offsets.
Observable consequence
This one is not latent. On macOS/arm64,
test_gap_gc_iterator_drain_rootingprints
badLen 1instead ofbadLen 0, 5/5, onPERRY_NO_AUTO_OPTIMIZE=1—i.e. one of 50 000 clones loses its
tagsarray. (Onmaintoday it is worse:TypeError: next is not a function, 5/5; #7527 removes that and leaves thebadLen 1.)Knob bisect says the copying minor, not policy evacuation:
PERRY_GEN_GC=0PERRY_GC_SCAVENGE=0PERRY_WRITE_BARRIERS=0PERRY_GEN_GC_EVACUATE=0badLen 1First thing to check — and what NOT to do
Do not patch the faulting line. That was tried on #7527's branch: re-reading
object_handleat the top of theraw_addrblock made the fault move 800 bytesfurther into the same function (
+6060→+6868,ldurb w8, [x19, #-0x8], theGcHeader
obj_typeread). One arm of a 99-use function is half a fix, and thehalf that is missing is the half that still crashes.
The shape that closes it is the one #7495 used for
dispatch_array_iterator_method: shadow the parameter with a reader so thepre-collection address is not nameable at all —
— and rewrite the 99
object/ 32jsvaluses through it. Mechanical, but itis the single hottest dispatcher in the runtime and it deserves its own PR and
its own review rather than riding along on a spread fix.
Note the file is 1969 lines against the 2000-line cap
(
scripts/check_file_size.sh), so the rewrite has ~30 lines of headroom; theclosure form is net-neutral, a comment block is not.
Acceptance
test_gap_gc_iterator_drain_rooting(already registered intest-parity/gc_repsel_corpus.txt) is the witness: it must printbadLen 0and go clean under
PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=200on both links. CheckPERRY_GC_DIAG=1prints[gc-fromspace-protect] retired_set=#Nbeforebelieving a clean run — a run with zero copying minors protects nothing.