Skip to content

fix(gc): a Symbol's description was stored into the header already stale - #7376

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7341-symbol-description-stale
Aug 4, 2026
Merged

fix(gc): a Symbol's description was stored into the header already stale#7376
proggeramlug merged 2 commits into
mainfrom
fix/7341-symbol-description-stale

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes 6 of 31 catches in #7341 — two apparent clusters, one root cause.

The bug

alloc_symbol calls gc_malloc — a collection point — and then writes the description pointer its caller computed before that call:

let raw = gc_malloc(size_of::<SymbolHeader>(), GC_TYPE_STRING);  // CAN COLLECT
(*ptr).description = description;                                 // pre-collection pointer

An evacuating minor moves the description string, so a live SymbolHeader holds a retired from-space address. Same shape and same fix as RegExpHeader::flags_ptr (#7374): root across the allocation, re-read after.

Two readers, two apparent clusters

js_symbol_to_string reaches the stale field directly. infer_symbol_function_name reaches it through js_object_literal_infer_computed_function_name, which had been triaged as a separate cluster because its backtrace names a different frame — and which this fix closes too (3/3 verified).

Worth stating as method: grouping catches by frame #0 is the right first cut, but it over-counts clusters whenever one bad field has several readers.

A related gap left open deliberately

The header is allocated GC_TYPE_STRING, whose payload the collector treats as opaque — so a fresh (non-registered) symbol's description is never marked or rewritten after construction. The existing comment says so:

"kept alive through the SYMBOL_REGISTRY (for registered symbols) or not at all (for fresh symbols — in practice they live for the duration of the program, which is fine for test workloads)"

This change makes the stored value correct. It does not make the description live. That is a latent use-after-free and it is called out in the changelog and commit rather than left for the green tests to imply otherwise. Tracked in #7341.

Verification

6/6 affected tests clean, byte-identical to Node.

The 2 symbol-filtered unit-test failures seen on this branch are pre-existing flake (#7365), confirmed by running the same filter 4× on each side: clean main gives 1/3/2/2 failures, this branch gives 1/0/0/1.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed stale symbol descriptions after garbage collection during allocation.
    • Improved symbol stringification and inferred symbol function names by ensuring descriptions remain valid when memory is relocated.

Ralph Küpper added 2 commits August 4, 2026 15:16
alloc_symbol calls gc_malloc -- a collection point -- and then writes the
description pointer its caller computed BEFORE that call. An evacuating
minor moves the description string, so a live SymbolHeader holds a retired
from-space address and js_symbol_to_string faults reading it through
str_from_header.

Same shape and same fix as RegExpHeader::flags_ptr (#7374): root across
the allocation, re-read after. 3/3 cluster tests clean, byte-identical to
Node.

LEFT OPEN DELIBERATELY: the header is allocated GC_TYPE_STRING, whose
payload the collector treats as opaque, so a fresh symbol's description is
never marked or rewritten after construction. The comment at the site
already says this -- 'kept alive through the SYMBOL_REGISTRY (for
registered symbols) or not at all (for fresh symbols ... which is fine for
test workloads)'. This makes the STORED value correct; keeping it alive
for the symbol's lifetime is a separate fix, noted in the changelog and
tracked in #7341.
js_object_literal_infer_computed_function_name was triaged as a separate
cluster because its backtrace names a different frame. It reaches the same
stale (*sym_ptr).description through infer_symbol_function_name, so the
#7376 fix closes it too -- verified 3/3 clean, byte-identical to Node.

Two distinct faulting frames, one root cause. Worth recording: grouping
catches by frame #0 is the right first cut, but it over-counts clusters
whenever one bad field has several readers.
@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: eea4a4fa-ea2c-4cde-a73f-3db1d1fb728f

📥 Commits

Reviewing files that changed from the base of the PR and between 148f97b and af99970.

📒 Files selected for processing (2)
  • changelog.d/7376-symbol-description-stale.md
  • crates/perry-runtime/src/symbol.rs

📝 Walkthrough

Walkthrough

alloc_symbol now roots the description pointer across garbage collection, reloads its relocated address, and stores it in the new symbol header. A changelog entry documents the fix and an unresolved lifetime issue for fresh, unregistered symbols.

Changes

Symbol description GC safety

Layer / File(s) Summary
Root description during symbol allocation
crates/perry-runtime/src/symbol.rs, changelog.d/7376-symbol-description-stale.md
alloc_symbol roots and reloads the description pointer across gc_malloc. The changelog documents the affected paths and the remaining lifetime issue.

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

Possibly related issues

  • PerryTS/perry issue 7246 — Directly addresses the alloc_symbol description-pointer relocation defect.

Possibly related PRs

Suggested reviewers: andrewtdiz, thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary fix for stale Symbol description pointers during garbage collection.
Description check ✅ Passed The description explains the bug, fix, affected readers, related issue, known limitation, and verification results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/7341-symbol-description-stale

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 87b5dcf into main Aug 4, 2026
22 of 45 checks passed
@proggeramlug
proggeramlug deleted the fix/7341-symbol-description-stale branch August 4, 2026 14:25
proggeramlug added a commit that referenced this pull request Aug 4, 2026
…r it was written (#7378)

#7341's quarantine produced 31 real stale-pointer bugs; four were fixed
(#7373-#7376). None existed when the RFC's 'would it have caught the real
bugs' table was written, so they are the strongest available calibration.

It would have caught ONE of the four. #7375 (await polling a moved
promise) is squarely in scope and would have been caught completely --
notably it had survived a comment explicitly reasoning about the
surrounding hazard, which is the RFC's own central argument. The other
three lived in perry-runtime, which this RFC does not govern.

Recording that honestly rather than only the win, because the ratio is
the useful part: it says where the remaining risk lives. Three of four
were layer 3, where RuntimeHandleScope exists (675 uses) but is optional.

The sharper finding is that all four were the SAME defect shape --
ordering a root relative to a collection point, never a missing root.
That is exactly what a Raw that dies at the next &mut emit enforces, so
the design generalises; the open question is whether layer 3 needs the
same discipline rather than whether codegen does.

Also notes the sample is favourable: the eight catches left open in #7341
are caller-side, and Raw<'e> does not cross a function boundary either.

No code change -- the RFC says step 1 wants a quiet tree, and expr/ is
under edit by #7375.

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