Skip to content

fix(gc): decode lsl #12 frame adjustments in the aarch64 prologue walker - #7398

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7394-aarch64-prologue-shift12
Aug 4, 2026
Merged

fix(gc): decode lsl #12 frame adjustments in the aarch64 prologue walker#7398
proggeramlug merged 2 commits into
mainfrom
fix/7394-aarch64-prologue-shift12

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #7394.

What it is

fp_to_sp_offset (crates/perry-runtime/src/gc/roots/stack_maps.rs) decodes a
generated function's prologue to recover its body stack pointer, which the fast
x29-chain walker uses as the base for every SP-relative root slot in that frame.

Its add x29, sp, #imm and sub sp, sp, #imm patterns masked in bit 22
the ADD/SUB (immediate) sh field, which selects lsl #12 on the immediate:

const ADD_FP_SP_MASK: u32 = 0xFFC0_03FF;   // bit 22 required to be 0
const SUB_SP_SP_MASK: u32 = 0xFFC0_03FF;

So an instruction using the shifted form did not match the opcode comparison at
all. LLVM switches to lsl #12 the moment a frame needs 4 KiB or more, and a
generated function crosses that line routinely — each string-concat chain spills
its own [32 x double] buffer. 80 functions in one gap-test binary carry a
shifted frame adjustment.

The measured case

perry_fn_test_gap_gc_call_argument_rooting_ts__run, read out of the binary at
+0x20:

9101c3fd   add x29, sp, #0x70          ; fp established
d14007ff   sub sp, sp, #0x1, lsl #12   ; sh=1 -- no match, accumulation run ENDS
d12103ff   sub sp, sp, #0x840          ; never reached

The dropped term is not the whole cost. Because the shifted sub failed to
match, it also terminated #7328's contiguous-sub accumulation run, so the
sub sp, sp, #0x840 behind it was dropped too. The decoder reported 0x70 for
a frame whose body SP is 0x18B0 below the frame pointer, and the walker handed
the collector slot addresses 6208 bytes off.

Evacuation writes through the slots it is given, so this both missed live
roots and rewrote unrelated stack words.

It is reachable in the shipping configuration

RS4GC/statepoints is the default root backend wherever the runtime can walk
frames (rs4gc_enabled(): "Default: on wherever the runtime can actually walk
the frames"), and the x29 chain is the default walker. This is not a
quarantine-only artifact:

build PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0, conservative scan ON
default (RS4GC) bad 1 — a wrong answer
PERRY_RS4GC=0 (shadow stack) bad 0, while evacuating 6344 objects

PERRY_GC_HEAP_LIMIT is a heap-size knob, not a correctness knob; it only makes
the collector run sooner. The shadow-stack arm is not vacuously clean — it ran a
real copying minor (copied_objects=6344 copied_bytes=427648) in the same run.

The fix

immediate_of decodes sh for both the add and the sub forms, and bit 22
comes out of both opcode masks (0xFFC0_03FF0xFF80_03FF).

Verification

test before after
test_gap_gc_call_argument_rooting quarantine fault 138 / bad 1 clean, bad 0
test_gap_gc_same_module_call_argument_rooting quarantine fault 138 clean, bad 0
test_gap_gc_process_env_cache_rooting (RS4GC) quarantine fault 138 clean, bad 0

Confirmed non-vacuous: the passing runs still report
[gc-copy-minor] ran copied_objects=6344.

Four new decoder unit tests cover the shifted sub, the shifted add, and the
measured two-sub prologue; the three #7328 tests are unchanged and still pass
(7/7). Full gap suite run for regressions is reported in a comment below.

Deliberately not in scope

Two things the same investigation turned up that this PR does not claim to
fix, so they are not silently folded in:

  • PERRY_STACKMAP_WALKER=unwind still returns bad 1. Its SP-relative base
    (_Unwind_GetCFA minus the recorded stack size) disagrees with the now-correct
    fast walk by exactly one frame size on both frames measured. That path is the
    Fast walker's fallback, so it matters; filed as GC (aarch64): the platform-unwinder stack-map walker lands one frame size below the correct SP #7399.
  • test_gap_gc_process_env_cache_rooting still faults on the shadow-stack
    backend, which this fix does not touch — a separate defect.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed AArch64 garbage collection stack scanning for stack frames of 4 KiB or larger.
    • Improved handling of shifted stack adjustments to prevent missed references and stack-slot corruption.
    • Added coverage for shifted frame setup and allocation scenarios.

…alker (#7394)

`fp_to_sp_offset` masked bit 22 — the ADD/SUB (immediate) `sh` field — into
its opcode comparison, so `sub sp, sp, #imm, lsl #12` did not match. LLVM
emits that form for every frame ≥ 4 KiB, which generated functions cross
routinely (80 in one gap-test binary).

The dropped term was not the whole cost: a non-matching word also ends
#7328's contiguous-`sub` accumulation run, so any further `sub sp` in the
same prologue was dropped with it. `..._gc_call_argument_rooting_ts__run`
resolved to fp-0x70 instead of fp-0x18B0, and the fast walker handed the
collector slot addresses 6208 bytes off — which evacuation then wrote
through.

Reachable in the shipping configuration: RS4GC is the default root backend
here and the x29 chain the default walker. The test printed `bad 1` under
`PERRY_GC_HEAP_LIMIT=8` alone, conservative scan ON, where the `PERRY_RS4GC=0`
build printed `bad 0` after evacuating 6344 objects.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a77a4c3-e8f2-4677-815d-c73c56a835ff

📥 Commits

Reviewing files that changed from the base of the PR and between 530df40 and ded8e44.

📒 Files selected for processing (2)
  • changelog.d/7398-aarch64-prologue-shift12.md
  • crates/perry-runtime/src/gc/roots/stack_maps.rs

📝 Walkthrough

Walkthrough

The AArch64 stack-map walker now decodes lsl #12`` ADD and SUB prologue instructions. It applies scaled immediates to frame-size calculations and adds fixtures and tests for shifted stack adjustments.

Changes

AArch64 prologue decoding

Layer / File(s) Summary
Shifted immediate decoding
crates/perry-runtime/src/gc/roots/stack_maps.rs
The decoder matches shifted ADD/SUB instructions, decodes scaled immediates, and applies them to frame-pointer and stack adjustments.
Decoder fixtures and validation
crates/perry-runtime/src/gc/roots/stack_maps.rs, changelog.d/7398-aarch64-prologue-shift12.md
Fixtures and tests cover shifted frame setup, shifted trailing allocations, and continued SUB accumulation. The changelog documents the fix and the separate unwind issue.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR fixes stack-map frame-offset decoding, but issue #7394 requires a structural fix for protecting pointer-bearing arguments and receivers across allocation. Address the argument and receiver lowering defect described in #7394, or link this PR to an issue whose coding requirements cover the AArch64 prologue walker fix.
Out of Scope Changes check ⚠️ Warning The AArch64 prologue walker changes are related to the reported symptom but are outside the linked issue's stated argument and receiver lowering scope. Move the prologue walker fix to a matching issue or update the linked issue to include this root-slot decoding requirement.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the AArch64 GC prologue decoding fix and matches the main changes.
Description check ✅ Passed The description explains the problem, implementation, tests, measured impact, and deliberate exclusions, although it does not use the repository template headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7394-aarch64-prologue-shift12

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit f0cbe2a into main Aug 4, 2026
7 of 11 checks passed
@proggeramlug
proggeramlug deleted the fix/7394-aarch64-prologue-shift12 branch August 4, 2026 21:13
proggeramlug added a commit that referenced this pull request Aug 5, 2026
* ci(gc): the macOS in-process RS4GC arm could never pass

`RS4GC works on a stock toolchain via the in-process backend` asserts
that a copying minor actually moved objects, by counting
`[gc-copy-minor] ran copied_objects=` lines in the probe's stderr.

Both of those prints are gated on PERRY_GC_DIAG (gc/copying.rs:993 and
:1246), and this step never set it -- the sibling walker step does. So
the trace held nothing but the probe's own #gcmetric lines, the assert
read 0 copying minors / 0 objects copied off an effectively empty file,
and the step failed regardless of how the collector behaved.

The inverse of the usual hazard: not a gate that cannot fail, but one
that cannot PASS. It shipped with the step in #7339 and had never
executed, because three of the four arms in this matrix were permanently
queued until #7393 added a concurrency group.

Also makes the assert say what actually happened. A trace with no
[gc-*] diagnostics at all is indistinguishable, by counts alone, from a
collector that moved nothing, and the old message asserted the latter.
That misdiagnosis is what made this cost a build to identify.

Reproduced on macOS aarch64 against current main (cd29706, which
contains #7398 and #7400, so it was not already fixed): the step as
written reproduces the CI error byte-for-byte with a 3-line stderr; the
same binary with PERRY_GC_DIAG=1 reports 2 copying minors / 10892 objects
copied and the full step exits 0, stdout unchanged so the control diff
still holds.

The new branch is capable of failing: it fires on the pre-fix trace,
passes on the post-fix one, and two negative controls (diagnostics
present but zero copies; a synthetic manual_collect trace) still fail
with the original message.

This does not make the workflow green -- the other three arms fail
earlier in "Probe matrix" for unrelated reasons.

* docs: changelog fragment for #7414

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#7341: the aarch64 stack-map fast walker reads the wrong stack for frames >= 4 KiB (lsl #12 prologue adjustments)

1 participant