fix(gc): fold the post-fp-setup stack allocation into the walker's frame base (#7328) - #7329
Merged
Merged
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe AArch64 stack-map walker now includes contiguous ChangesAArch64 stack-map offset fix
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
proggeramlug
pushed a commit
that referenced
this pull request
Aug 3, 2026
…echanism
Two corrections and one measurement.
The gap suite re-run against RS4GC in-process, two arms per test
(shadow-stack control + RS4GC), 479/479: 447 pass->pass, 19 pre-existing
diffs unchanged, 13 node_fail, ZERO new regressions, ZERO refusals, ZERO
compile failures. Zero refusals is the load-bearing number -- 128 of the
479 tests contain `try {}` and the bridge cannot compile any of them.
The earlier soak's "13 regressions, do not flip" was measured against
the bridge, before #7329/#7330, on a backend that structurally cannot
compile a quarter of the suite. It should not be carried forward.
And the x86-64 mechanism was wrong. The workflow comment claimed
_Unwind_GetGR(ctx, 7) "does not reliably return the stack pointer".
Measured on x86-64 Linux (glibc 2.39, gcc 13.3.0): it SEGFAULTS. RBX,
RBP and RIP return correctly; RAX and RSP both SIGSEGV, because libgcc
tracks only the columns CFI restores and RSP is derived from the CFA
rather than tracked. The fault is in the call itself, so no address
validation after it can help -- the previous wording pointed at the
wrong fix. Details and a reproducer in #7333.
proggeramlug
added a commit
that referenced
this pull request
Aug 4, 2026
* fix(gc): split the precise-root analysis from its lowering One knob answered two questions. "Which locals hold GC pointers, and where must each stay live" is the analysis and is backend-independent. "Is that answer represented as a heap-backed shadow frame or a native stack map" is the lowering, and LlFunction already chose it independently -- enable_shadow_frame_inner and reserve_shadow_slot both take the native path first. But the eight sites that build the slot map all gated on shadow_stack_enabled(), so PERRY_SHADOW_STACK=0 switched the ANALYSIS off and left the statepoint lowering with nothing to lower. The result was a binary with no precise frame roots at all: no __perry_gcmap section, same size as a plain shadow-off build, correct output. Nothing distinguished it from a good build until a collection freed a live object. #7332 made the pair a hard error as a stopgap. Route those eight sites through precise_root_analysis_enabled() instead and the pair becomes expressible, which is what the stopgap was standing in for. Measured on 01_nursery_churn: PERRY_STATEPOINTS=1 with and without PERRY_SHADOW_STACK=0 now emit an identical 885-byte root map and an identical __text. The knob keeps its own meaning on its own -- no gcmap, and still observable against the default build. A mode nobody can select is a mode nobody can measure, so this is the prerequisite for the shadow-stack lowering ever being removed rather than merely being switched off in one configuration nobody tests. * docs: changelog fragment for #7340 * docs(gc): record the full-suite RS4GC result and correct the x86-64 mechanism Two corrections and one measurement. The gap suite re-run against RS4GC in-process, two arms per test (shadow-stack control + RS4GC), 479/479: 447 pass->pass, 19 pre-existing diffs unchanged, 13 node_fail, ZERO new regressions, ZERO refusals, ZERO compile failures. Zero refusals is the load-bearing number -- 128 of the 479 tests contain `try {}` and the bridge cannot compile any of them. The earlier soak's "13 regressions, do not flip" was measured against the bridge, before #7329/#7330, on a backend that structurally cannot compile a quarter of the suite. It should not be carried forward. And the x86-64 mechanism was wrong. The workflow comment claimed _Unwind_GetGR(ctx, 7) "does not reliably return the stack pointer". Measured on x86-64 Linux (glibc 2.39, gcc 13.3.0): it SEGFAULTS. RBX, RBP and RIP return correctly; RAX and RSP both SIGSEGV, because libgcc tracks only the columns CFI restores and RSP is derived from the CFA rather than tracked. The fault is in the call itself, so no address validation after it can help -- the previous wording pointed at the wrong fix. Details and a reproducer in #7333. * docs(gc): measure the statepoint binary-size axis — it is root density, not metadata The plan asserted 'closing that axis needs fewer roots, not a tighter encoding' on the strength of one app measurement. Measured directly with two 2000-function programs: root-free functions +0 bytes (no map emitted, text identical) root-dense functions +4,330,592 B (97% __text, 21% gcmap) So statepoints carry NO fixed cost -- a function with nothing live across a safepoint pays nothing -- and the growth is the per-root relocation sequence, not the map. #7314's compact map fully answered the metadata objection, but metadata was never the dominant term at scale. Runtime on the same probes, quiet host, median of 5: statepoints 1-2% faster, every probe neutral or faster. --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7328.
Root cause
The fast x29-chain stack-map walker derived a frame's base from
add x29, sp, #immalone, on the premise — stated in the function's own doc comment — that establishing the frame pointer is a function's last stack adjustment.It is not. LLVM emits a further allocation after the
addwhen a function has a large or separately laid-out local area:So every slot in such a frame was read 368 bytes high.
Reproduced, then fixed
PERRY_STACKMAP_WALKER=verifyonnotry_controlwith collections forced:The first slot agrees — that frame has no trailing
sub— and the other five are each exactly +368, matchingsub sp, sp, #0x170found in the disassembly.After the fix, the same command is clean, with movement asserted:
eligible=true,copied_objects=28per cycle. That distinction matters here — my first attempt reported 5/5 clean whilePERRY_GC_DIAGshowedeligible=false fallback=conservative_stack, i.e. no copying minor ran at all.PERRY_CONSERVATIVE_STACK_SCAN=offwas needed to make copying eligible (#7255).Why this is worse than a crash
It is a silent wrong answer.
verifyis not the default, so an ordinary run has nothing to disagree with the fast walker — it enumerates wrong addresses, the collector treats them as the root set, and live objects are missed. ForcingPERRY_STACKMAP_WALKER=unwindon the same probe raised objects copied from 23 to 110.It affects both statepoint backends, since they share the walker.
The fix
Accumulate the contiguous run of
sub sp, sp, #immimmediately following theadd. Asub spseparated from that run is a body operation — a dynamic alloca, a call-argument area — whose effect the stack map's own slot offsets already carry, and is deliberately not folded in.Verification
Four unit tests: the trailing-
subcase, the unchanged common shape, asubafter the prologue run (must not be counted), and a leaf with no fp setup (must still fail closed so the caller falls back to the unwinder).Sabotage-checked — neutralising the accumulation fails
a_sub_after_the_fp_setup_is_includedwith "the trailingsub sp, sp, #0x170must be added to the fp offset".cargo fmtclean; file-size, GC store-site and addr-class gates green. (ci_public_baseline_checkis expected-red — the artifact is deliberately stale during the current optimization work.)Not verified
aarch64 only — the decoder is
#[cfg(target_arch = "aarch64")]and x86-64 returnsNone(falls back to the unwinder), so there is no x86-64 path to regress. Whether this clears any of the 13 gap regressions the statepoint soak found is not measured here; #7327 (the bridge emitting no statepoint oninvoke) is the more likely cause of the exception-shaped ones.Summary by CodeRabbit
Bug Fixes
Tests