Skip to content

fix(gc/codegen): epoch-gate read-PIC pointer tokens against GC address recycling (#6080) - #7434

Merged
proggeramlug merged 2 commits into
mainfrom
fix/6080-read-pic-gc-epoch
Aug 5, 2026
Merged

fix(gc/codegen): epoch-gate read-PIC pointer tokens against GC address recycling (#6080)#7434
proggeramlug merged 2 commits into
mainfrom
fix/6080-read-pic-gc-epoch

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Closes the remaining half of #6080(a) ABA staleness of the read-PIC keys-pointer token. ((b) was fixed by #6254.)

What was still broken

Since #6803/#6807, plain stamped objects prime never-reused ShapeId tokens (ABA-immune), and raw keys pointers are only primed for GC_FLAG_SHAPE_SHARED arrays. But shape-shared means rooted, not address-stable: the copying minor moves shape-shared arrays like anything else (move_young merely preserves the flag), every rooted reference is rewritten — except the @perry_ic_N globals, which no GC scanner knows about. After the from-space flip the vacated address is recycled; a different-shape keys array landing there makes a primed site falsely pointer-match, and the inline hit path loads the wrong slot. Silent wrong value, exactly the class #6006 fixed in the sibling transition cache. Class instances (class_id != 0) are the main exposed population.

The fix (the epoch design from the issue thread, narrowed to pointer tokens)

  • runtime: process-global PERRY_IC_EPOCH (AtomicU64, starts at 1 so a zeroinitializer cache can never match). Bumped in GcStats::record_collection — the documented single per-collection funnel, covering the copying fast path, the minor fallback, full mark-sweep, and budgeted cycles — and a second time at budgeted-sweep entry, because budgeted sweep slices interleave with the mutator before the end-of-cycle funnel runs (primes taken after that bump reference marked arrays, which later slices of the same sweep never free). Double-bump is harmless: one extra re-prime.
  • js_object_get_field_ic_miss snapshots the live epoch into cache[2] at prime time ([i64;2][i64;3]; the emitted global was already [8 x i64]).
  • codegen (lower_generic_property_get): the inline hit predicate ANDs in is_stamp || cache[2] == @PERRY_IC_EPOCH. Pointer tokens re-prime once per collection; ShapeId tokens skip the check entirely (ids are never reused), so the hot stamped population pays two loads + icmp + or that fold into the existing hit cond_br and never re-primes after GC. Any epoch change happens inside a call (a collection), so LLVM hoisting of the epoch load stays sound.

The full-outline path (js_object_get_field_ic) already re-validates every read through the miss handler; it inherits the epoch stamping unchanged.

Why not the alternatives the thread ruled out

Pinning shape-shared arrays trades the silent wrong value for un-traced key strings collected under a pinned array (pinned objects are not traced) and would need a new root scanner + cap. The epoch is one atomic add per collection and two loads per pointer-token hit.

Coverage (each gate can fail)

  • IR contract test (generic_property_get_hit_path_is_epoch_gated): emitted IR must carry the cache[2] gep and the @PERRY_IC_EPOCH load — deleting the guard turns it red.
  • test_record_collection_bumps_read_pic_epoch: the funnel must advance the epoch.
  • pointer_token_prime_stamps_epoch_and_goes_stale_on_bump: a class-instance prime stores the raw keys pointer + live epoch, a bump strands it (the guard's exact inputs), and re-priming heals it.

Validation

  • cargo test -p perry-runtime --lib c3c_pic_tests / telemetry — green (Windows, --test-threads=1).
  • cargo test -p perry-codegen --lib property_get — green.
  • Windows e2e probe: 200k class-instance reads through one monomorphic any-typed site with dynamic-shape churn; PERRY_GC_DIAG=1 confirms copying minors actually ran (copied_objects=6912, 102 diag lines) — sum exact, defineProperty-after-prime still returns the getter value (the fix(codegen): read-PIC honors descriptors installed after priming (#6080) #6254 half), mixed stamped/class population exact. Same results under PERRY_GEN_GC=0.

Out of scope, noted for follow-up

The #6812 3-way dynamic-key write IC compares the same discriminated shape token and packs 4 (token, slot) ways into its 8-slot global (no free epoch slot), so its pointer-token ways carry the same exposure; needs its own treatment (per-way epoch packing or a keys-content check like the #6006 transition validation). Commenting on #6080 with this.

No version bump per external-contribution flow — maintainer bumps at merge.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed stale inline-cache entries after garbage collection, preventing incorrect property reads caused by reused object addresses.
    • Ensured cached property lookups are refreshed safely after collection and incremental sweeping.
    • Preserved fast-path behavior for stable shape-based property tokens.
  • Tests

    • Added regression coverage for cache invalidation and garbage-collection epoch updates.

Ralph Kuepper added 2 commits August 5, 2026 12:32
…s recycling (#6080)

#6080(a) residual: class instances (and any receiver without a #6804
ShapeId stamp) still prime the RAW keys-array pointer into their
per-site @perry_ic_N cache. Those globals are invisible to every GC
scanner, and GC_FLAG_SHAPE_SHARED keeps a keys array LIVE but not
address-STABLE - the copying minor moves it like anything else and the
vacated from-space address is recycled, so a stale prime can
pointer-match a different-shape keys array and the inline hit path
loads the wrong slot, silently.

Fix (the epoch design from the #6080 thread, narrowed to pointer
tokens):

- runtime: process-global PERRY_IC_EPOCH (starts at 1 so a
  zeroinitializer cache can never match), bumped in
  GcStats::record_collection - the single per-collection funnel - and
  again at budgeted-sweep ENTRY, because budgeted sweep slices
  interleave with the mutator before the end-of-cycle funnel runs.
- js_object_get_field_ic_miss snapshots the live epoch into cache[2]
  at prime time (cache widens [i64;2] -> [i64;3]; the emitted global
  was already [8 x i64], so no layout change).
- codegen: the inline monomorphic hit predicate requires
  cache[2] == @PERRY_IC_EPOCH before trusting a pointer token.
  Shape-ID tokens (bit 62) skip the check - ids are never reused - so
  the hot stamped-plain-object population never re-primes after GC.

Coverage: IR contract test asserts the emitted guard (epoch-slot gep +
@PERRY_IC_EPOCH load); runtime tests assert the funnel bumps the epoch
and that a pointer-token prime goes stale on bump (the guard's exact
inputs, so the instrument provably CAN fail). Probe run on Windows:
200k class-instance reads through one monomorphic site under copying
minors (copied_objects>0 confirmed via PERRY_GC_DIAG) and under
PERRY_GEN_GC=0, all values correct; defineProperty-after-prime (#6254
half) still honored.

Sibling not covered here: the #6812 3-way dynamic-key WRITE IC compares
the same discriminated shape token and packs 4 ways into its 8-slot
global (no free epoch slot); noted on #6080.
@coderabbitai

coderabbitai Bot commented Aug 5, 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: 77267e7c-05b0-4295-9953-4f48b9865d52

📥 Commits

Reviewing files that changed from the base of the PR and between 3a26fda and ecd93e8.

📒 Files selected for processing (12)
  • changelog.d/7434-read-pic-gc-epoch.md
  • crates/perry-codegen/src/expr/property_get/generic_dispatch.rs
  • crates/perry-codegen/src/expr/property_get/tests.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-runtime/src/gc/cycle.rs
  • crates/perry-runtime/src/gc/telemetry.rs
  • crates/perry-runtime/src/gc/tests/telemetry_verifier.rs
  • crates/perry-runtime/src/node_submodules/tests.rs
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/field_get_set/ic_miss.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/value/dynamic_object.rs

📝 Walkthrough

Walkthrough

The change adds a process-wide GC epoch to property-read PICs. Pointer-token hits require a matching cached epoch, while shape-ID tokens bypass validation. GC collection and sweep paths advance the epoch, and regression tests cover cache refresh and generated validation.

Changes

Read PIC epoch invalidation

Layer / File(s) Summary
Runtime cache epoch storage
crates/perry-runtime/src/object/field_get_set/ic_miss.rs, crates/perry-runtime/src/object/field_get_set.rs, crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/node_submodules/tests.rs, crates/perry-runtime/src/value/dynamic_object.rs
PIC entries now use three slots for the token, field index, and priming epoch. The miss path records the current epoch and tests refresh stale pointer-token entries.
Codegen epoch validation
crates/perry-codegen/src/runtime_decls/objects.rs, crates/perry-codegen/src/expr/property_get/generic_dispatch.rs, crates/perry-codegen/src/expr/property_get/tests.rs
Generated property-get hit paths load PERRY_IC_EPOCH and reject pointer tokens with stale cached epochs. Shape-ID tokens bypass the check.
GC epoch invalidation
crates/perry-runtime/src/gc/cycle.rs, crates/perry-runtime/src/gc/telemetry.rs, crates/perry-runtime/src/gc/tests/telemetry_verifier.rs, changelog.d/7434-read-pic-gc-epoch.md
GC sweep initialization and collection recording advance the PIC epoch. The changelog and telemetry test document and verify the behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GC
  participant PERRY_IC_EPOCH
  participant GenericPropertyGet
  participant js_object_get_field_ic_miss
  GC->>PERRY_IC_EPOCH: bump epoch during sweep or collection
  GenericPropertyGet->>PERRY_IC_EPOCH: load live epoch
  GenericPropertyGet->>GenericPropertyGet: compare pointer-token cache[2]
  GenericPropertyGet->>js_object_get_field_ic_miss: route stale token to miss path
  js_object_get_field_ic_miss->>PERRY_IC_EPOCH: snapshot current epoch
  js_object_get_field_ic_miss-->>GenericPropertyGet: re-prime cache[2]
Loading

Possibly related issues

  • PerryTS/perry issue 6080 — Addresses raw keys-array ABA staleness through PERRY_IC_EPOCH.
  • PerryTS/perry issue 7231 — Covers stale unrooted GC-pointer caches and epoch-based invalidation.

Possibly related PRs

  • PerryTS/perry#6807 — Overlaps in PIC shape-token handling and token priming.
  • PerryTS/perry#6808 — Modifies the same property-get PIC and cache-token priming paths.
  • PerryTS/perry#6810 — Modifies the same IC miss implementation with related GC validation behavior.

Suggested labels: performance

Suggested reviewers: thehypnoo, andrewtdiz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the GC epoch validation added for read-PIC pointer tokens.
Description check ✅ Passed The description explains the issue, fix, scope, related issue, tests, and validation, but does not use all template headings or include the checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/6080-read-pic-gc-epoch

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 145fa02 into main Aug 5, 2026
8 of 11 checks passed
@proggeramlug
proggeramlug deleted the fix/6080-read-pic-gc-epoch branch August 5, 2026 11:21
proggeramlug pushed a commit that referenced this pull request Aug 5, 2026
cargo fmt --all -- --check failed on main. lint is a REQUIRED context,
so while it is red every merge bypasses a required gate.

Two hunks, six lines: an assert! in gc/tests/telemetry_verifier.rs over
rustfmt's default fn_call_width of 60, and a pub(crate) use in
object/field_get_set.rs added below the pub use block where
reorder_imports wants it above.

Not a toolchain artifact: CI uses dtolnay/rust-toolchain@stable and the
repo has no rust-toolchain.toml or rustfmt.toml.
proggeramlug added a commit that referenced this pull request Aug 5, 2026
cargo fmt --all -- --check failed on main. lint is a REQUIRED context,
so while it is red every merge bypasses a required gate.

Two hunks, six lines: an assert! in gc/tests/telemetry_verifier.rs over
rustfmt's default fn_call_width of 60, and a pub(crate) use in
object/field_get_set.rs added below the pub use block where
reorder_imports wants it above.

Not a toolchain artifact: CI uses dtolnay/rust-toolchain@stable and the
repo has no rust-toolchain.toml or rustfmt.toml.

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.

1 participant