Skip to content

fix(gc): root the receiver across the .size arm's three allocating helpers - #7385

Merged
proggeramlug merged 1 commit into
mainfrom
fix/7341-size-arm-rooting
Aug 4, 2026
Merged

fix(gc): root the receiver across the .size arm's three allocating helpers#7385
proggeramlug merged 1 commit into
mainfrom
fix/7341-size-arm-rooting

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Three real stale-receiver sites in js_object_get_field_by_name's Map/Set subclass .size arm. It does not close its two tests — see below.

The bug

The arm calls three helpers that allocate, in sequence, with the receiver held in a bare local across all of them:

helper why it allocates
own_key_present inside the original if condition
class_instance_has_member builds a String for its cache probe
subclass_backing_of js_string_from_bytes rebuilds its constant BACKING_KEY on every call

Any can drive an evacuating minor, and on the None fall-through every later arm dereferences the receiver again. The faults are the GC-type probes at +560 and +664 — reached after the plausibility checks pass, because a retired from-space address still looks like a plausible heap pointer.

Not the fast lane

The arm is gated on the key being exactly "size", and the scope opens only after that test. Opening it ahead would put a RuntimeHandleScope on every property read reaching this block — the one cost this arm must not have. I had it wrong in the first draft and caught it before this PR.

This also retires the standing question of whether the property-read fast lane must root: it never arose. This is a .size special case, not the general path.

What it does not do

test_gap_field_lane_semantics and test_gap_put_value_plan_cache are still red. Each fix moves the fault to the next site in the same function — +664 → +560 → +820 — so at least a fourth remains.

The three closed here are real: a no-op leaves the faulting offset byte-identical, and these moved twice. But the function is a chain, not a single defect, and I'd rather land three verified sites with that stated than imply a closed catch.

Regression

58 pass / 2 fail on the object/assign/class/field/shape gap set — byte-identical to pristine main, both failures pre-existing (one already in known_failures.json).

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when reading the .size property on Map and Set subclasses during memory management operations.
    • Preserved correct behavior for own properties, inherited properties, and backing collection sizes.
    • Added coverage notes for remaining related edge cases under investigation.

…lpers

js_object_get_field_by_name's Map/Set subclass .size arm calls three
helpers that allocate, in sequence, with the receiver in a bare local
across all of them: own_key_present (inside the if condition),
class_instance_has_member (builds a String for its cache probe), and
subclass_backing_of (calls js_string_from_bytes to rebuild its constant
BACKING_KEY on every call).

Any of the three can drive an evacuating minor, and on the None
fall-through every later arm dereferences the receiver again. The faults
are the GC-type probes at +560 and +664, reached after the plausibility
checks pass because a retired from-space address still looks like a
plausible heap pointer.

The scope opens only after the key is confirmed to be "size". Opening it
ahead of the key test would put a RuntimeHandleScope on every property
read reaching this block, which is the one cost this arm must not have.

This does NOT close test_gap_field_lane_semantics or
test_gap_put_value_plan_cache: each fix moves the fault to the next site
in the same function (+664 -> +560 -> +820), so at least a fourth
remains. The three closed here are real -- a no-op leaves the offset
byte-identical -- but the function is a chain.

58 pass / 2 fail on object/assign/class/field/shape, byte-identical to
pristine main (both failures pre-existing).
@proggeramlug
proggeramlug merged commit 05a9658 into main Aug 4, 2026
18 of 44 checks passed
@proggeramlug
proggeramlug deleted the fix/7341-size-arm-rooting branch August 4, 2026 16:17
@coderabbitai

coderabbitai Bot commented Aug 4, 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: f13fe41b-bef5-4614-a669-a4ee79b1dbf4

📥 Commits

Reviewing files that changed from the base of the PR and between 0f3182d and f4a864a.

📒 Files selected for processing (2)
  • changelog.d/7384-size-arm-rooting.md
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs

📝 Walkthrough

Walkthrough

The .size path in js_object_get_field_by_name now roots Map/Set subclass receivers across GC-capable helpers and refreshes relocated pointers. Existing own-property, inherited-size, and backing-collection behavior remains unchanged. The changelog documents remaining stale-receiver failures.

Changes

Map and Set size receiver rooting

Layer / File(s) Summary
Root and refresh the size receiver
crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs, changelog.d/7384-size-arm-rooting.md
The receiver uses mutable state so the runtime can store its relocated address. The .size path roots the receiver across allocation-capable checks, refreshes it after GC, and preserves property precedence and Map/Set size returns. The changelog records the remaining failing tests.

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

Possibly related PRs

  • PerryTS/perry#6941: Both changes fix GC-induced stale receiver pointers with handle rooting.
  • PerryTS/perry#6983: Both changes root and refresh object references across allocation-capable operations.
  • PerryTS/perry#7342: Both changes fix stale GC-moved receivers in runtime property paths.

Suggested reviewers: 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/7341-size-arm-rooting

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
I wrote both fragments before opening their PRs and guessed the next
number, so each was off by one: the mirror-remaining-sites work is #7383
(not 7382) and the size-arm rooting is #7385 (not 7384).

7384 is worse than cosmetic -- it is an open PR by another author, so the
fragment claimed a number already in use and would have collided when
that PR adds its own.

Fragment names are PR-keyed precisely so in-flight PRs never collide;
guessing the number defeats that. Open the PR first, then name the
fragment.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request Aug 4, 2026
* feat(gc): RuntimeHandle::across_* and a raw-handle debt ratchet

The layer-3 half of the rooting-by-construction RFC. A RuntimeHandleScope
gives an object liveness -- the collector marks it and rewrites the slot
-- and does nothing for a raw pointer already read out of that slot. That
copy is invisible to the collector, and if the object moves it names
from-space.

Every rooting bug fixed in the #7341 sweep had rooting ALREADY. What was
missing each time was ordering the re-read against the collection point.
across_* runs the allocating call and returns the post-collection address
in one step, so the pre-call pointer is never bound. The .size arm of
js_object_get_field_by_name is converted as the worked example.

This is a debt counter, NOT a soundness proof, and the script says so.
Rust has no effect system to mark "may allocate", so no signature can
reject holding a stale copy across such a call, and a &mut Heap token
cannot cross extern "C". The ratchet instead makes the unconverted count
visible and monotonically decreasing: 1006 sites across 110 files, wired
into test.yml beside the address-classification audit.

Checked against negative controls, per the four-ways-a-gate-cannot-fail
rule: sabotaging across_mut to return the pre-call pointer fails the test
with the intended message; the ratchet fails on a rise and refuses to
raise its own baseline; and --self-test asserts the matcher still fires
on the shapes it counts and still ignores across_*, so a broken matcher
cannot report zero and pass forever.

Also drops a redundant republish in the .size arm that rustc flagged as
assigned-never-read (introduced by #7385, superseded by the arm-level one).

* docs: name the fragment for its real PR (#7389)

---------

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