Skip to content

GC (aarch64): the platform-unwinder stack-map walker lands one frame size below the correct SP #7399

Description

@proggeramlug

Split out of #7394, which fixed the other half. The fast x29-chain walker was
decoding lsl #12 frame adjustments wrong; that is fixed and the default
configuration is correct. The platform-unwinder path is independently wrong,
by exactly one frame size.

Measured

test_gap_gc_call_argument_rooting, aarch64 macOS, RS4GC default backend, at
the commit that fixes the fast walker:

PERRY_STACKMAP_WALKER PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0
fast (default) bad 0
unwind bad 1
verify panics — slot sets differ

verify prints both sets:

fast  : [0x16f625130, 0x16f625138, 0x16f625210]
unwind: [0x16f623950, 0x16f625060, 0x16f625068]

Pairing by frame rather than by sort order:

frame fast unwind unwind − fast
A (2 slots) …5130, …5138 …5060, …5068 −0xD0
B (1 slot) …5210 …3950 −0x18C0

0x18C0 is exactly the total frame of
perry_fn_test_gap_gc_call_argument_rooting_ts__run
(0x80 pre-index + 0x1000 + 0x840). The unwinder lands one whole frame
below the correct body SP, and 0xD0 is consistent with the same relationship on
the smaller frame.

Where

crates/perry-runtime/src/gc/roots/stack_maps.rs, the Itanium unwind::walk_frame
SP-relative base:

let cfa = _Unwind_GetCFA(context);
cfa.checked_sub(CFA_RETURN_ADDRESS_BYTES)      // 0 on aarch64
   .and_then(|v| v.checked_sub(record.stack_size as usize))

The delta being exactly record.stack_size on both frames points at a double
subtraction — i.e. _Unwind_GetCFA here already yields the value the formula is
trying to derive. That is a hypothesis, not a measurement; it needs confirming
before the formula is changed, and any change has to be checked on x86-64/Linux,
where CFA_RETURN_ADDRESS_BYTES = 8 and the current formula is what the Linux
gate runs against.

Why it is not merely a bisection knob

WalkerMode::Fast falls back to this path whenever chain_walkable is false
or fp_chain::visit returns None on an anomaly:

WalkerMode::Fast => {
    if index.chain_walkable {
        if let Some(stats) = fp_chain::visit(index, visit) { return stats; }
    }
    let mut stats = unwind::visit(index, visit);   // <- here

So a frame the fast walker declines is handed to a walker that computes the
wrong base — and evacuation writes through the slots it is given.

It also means PERRY_STACKMAP_WALKER=verify cannot be promoted to a CI gate
until this is fixed: verify performs its real visitation through the unwinder,
so it currently both trips on the difference and does the wrong visitation.
That gate is the only check that can catch a fast walk silently skipping frames
— exactly the class of bug #7394 turned out to be — so it is worth having.

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