Skip to content

fix(gc): refuse the knob combination that emits a rootless binary (#7326) - #7332

Merged
proggeramlug merged 1 commit into
mainfrom
fix/7326-rootless-combination
Aug 3, 2026
Merged

fix(gc): refuse the knob combination that emits a rootless binary (#7326)#7332
proggeramlug merged 1 commit into
mainfrom
fix/7326-rootless-combination

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #7326.

PERRY_SHADOW_STACK=0 + PERRY_STATEPOINTS=1 produced a binary with no precise frame roots at all — no __perry_gcmap section, same size as a plain shadow-off build, and it ran and printed the correct answer. Nothing distinguished it from a correct build until a collection moved something live.

The cause is structural, not a missing check

The statepoint backends are an alternative lowering of the shadow stack's root-set analysis, not an independent mechanism. reserve_shadow_slot() is the single entry point that, under native_stack_roots_enabled(), allocates a stack-map slot instead — and the caller returns empty maps outright when the shadow stack is off. Switching one off switches the other off with it.

The consequence for the adoption plan

Because they share this analysis, "delete the shadow stack and keep statepoints" is not currently expressible. Any plan treating them as two interchangeable mechanisms — including the one in docs/engine-plan.md — needs that premise corrected before the swap can happen. That is a decoupling job, not a flag flip.

Verified

Summary by CodeRabbit

  • Bug Fixes

    • Added validation to prevent incompatible runtime configuration combinations.
    • The application now reports a clear error when shadow stacks are disabled alongside statepoint or RS4GC modes, avoiding unsupported builds.
  • Documentation

    • Added changelog documentation describing the configuration restriction and its impact.

@proggeramlug
proggeramlug merged commit 0ef6010 into main Aug 3, 2026
26 of 44 checks passed
@proggeramlug
proggeramlug deleted the fix/7326-rootless-combination branch August 3, 2026 18:47
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fd9ae83-6bf0-4eae-8817-6bf83e9f48d3

📥 Commits

Reviewing files that changed from the base of the PR and between 564cbec and b7c7198.

📒 Files selected for processing (2)
  • changelog.d/7331-rootless-combination-refused.md
  • crates/perry-codegen/src/codegen/helpers.rs

📝 Walkthrough

Walkthrough

The codegen now rejects PERRY_SHADOW_STACK=0 when statepoints or RS4GC are enabled. The changelog documents that these backends depend on shadow-stack root-set analysis.

Changes

Rootless backend validation

Layer / File(s) Summary
Shadow-stack backend compatibility guard
crates/perry-codegen/src/codegen/helpers.rs, changelog.d/7331-rootless-combination-refused.md
shadow_stack_enabled now raises a descriptive panic for incompatible statepoint or RS4GC settings. The changelog documents the shared root-set analysis and hard-error policy.

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

Possibly related PRs

  • PerryTS/perry#7292: Both changes address GC correctness involving statepoints and shadow-stack root tracking.
  • PerryTS/perry#7314: This change extends related statepoint and RS4GC configuration checks in perry-codegen.
  • PerryTS/perry#7322: Both changes cover native-root backend compatibility with shadow-stack settings.

Suggested reviewers: andrewtdiz, thehypnoo

✨ 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/7326-rootless-combination

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 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>
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.

PERRY_SHADOW_STACK=0 + PERRY_STATEPOINTS=1 silently emits a binary with no precise roots

1 participant