Skip to content

gc: js_native_call_method reuses one copy of its rooted receiver across ~1200 lines of allocating probes #7528

Description

@proggeramlug

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.

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