Skip to content

fix(runtime): write-barrier RegExp header pattern/flags string fields#6288

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/regexp-header-young-string-write-barrier
Jul 11, 2026
Merged

fix(runtime): write-barrier RegExp header pattern/flags string fields#6288
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/regexp-header-young-string-write-barrier

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

js_regexp_new allocates the RegExpHeader via gc_malloc (the only
GC_TYPE_OBJECT malloc site) and then stores its pattern_ptr and
flags_ptr — both GC-managed StringHeaders — with plain raw field writes:

(*ptr).pattern_ptr = pattern;
(*ptr).flags_ptr = canonical_flags_ptr;   // js_string_from_str -> YOUNG string

The header is malloc-backed, so the GC treats it as old-generation, and
canonical_flags_ptr is a freshly-allocated nursery string — an old→young
edge. The raw writes fire no write barrier, so that edge is never recorded
in the remembered set. A copying minor GC (the common collection under the
default generational policy) doesn't re-trace old objects, so the young
flags/pattern string is swept while the retained RegExp still points at it.
The next minor's scan of the header — or any read of the field — then
dereferences freed memory.

PERRY_GC_VERIFY_EVACUATION=1 reports it precisely: ~2000 uncovered
object → string edges at the flags_ptr slot. Without the verifier it's an
intermittent SIGSEGV/SIGBUS whose faulting pointer is a reused nursery
slot. A regex-heavy workload (e.g. ANSI/emoji scanning in a terminal UI)
reproduces it within seconds. The compiled-regex program is cached, so the leak
is per RegExp object, not per distinct pattern.

The GC already scans these two slots correctly for a full mark (the magic-
tagged RegExp layout in gc/layout.rs), so this is purely the missing
remembered-set edge on the copied-minor path.

Fix

After the stores, remember both edges with runtime_write_barrier_gc_slot,
mirroring every other native-header pointer store (closure captures, object
prototype slots, array headers). It detects the malloc parent and only
remembers genuinely-young children, so an already-old or interned string is a
harmless no-op.

Validation

  • On a large esbuild-bundled CLI app that reproduced the crash,
    PERRY_GC_VERIFY_EVACUATION goes from ~2000 missing edges + abort to
    zero missing edges + no abort.
  • perry-runtime regex tests: 37 passed / 0 failed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved regular expression memory management to ensure pattern and flag values remain available during garbage collection.
    • Prevented potential issues where regular expressions could lose associated string data after memory cleanup.

`js_regexp_new` allocates the `RegExpHeader` via `gc_malloc` (the only
`GC_TYPE_OBJECT` malloc site) and then stores its `pattern_ptr` and
`flags_ptr` — both GC-managed `StringHeader`s — with plain raw field writes:

    (*ptr).pattern_ptr = pattern;
    (*ptr).flags_ptr = canonical_flags_ptr;   // js_string_from_str -> YOUNG

The header is malloc-backed, so the GC treats it as old-generation, and
`canonical_flags_ptr` is a freshly-allocated nursery string — an old->young
edge. But the raw writes fire no write barrier, so that edge is never
recorded in the remembered set. A copying minor GC (the common collection
under the default generational policy) does not re-trace old objects, so the
young flags/pattern string is swept while the retained RegExp still points at
it. The next minor's scan of the header — or any read of the field — then
dereferences freed memory. `PERRY_GC_VERIFY_EVACUATION=1` reports it exactly:
~2000 uncovered `object -> string` edges at the `flags_ptr` slot; without the
verifier it is an intermittent SIGSEGV/SIGBUS whose faulting pointer is a
reused nursery slot. A regex-heavy workload (e.g. ANSI/emoji scanning in a
terminal UI) reproduces it within seconds; the compiled RegExp program is
cached, so the leak is per RegExp *object*, not per distinct pattern.

The GC already scans these two slots correctly for a full mark (the magic-
tagged RegExp layout in `gc/layout.rs`), so this is purely the missing
remembered-set edge on the copied-minor path.

Fix: after the stores, remember both edges with
`runtime_write_barrier_gc_slot`, mirroring every other native-header pointer
store (closure captures, object prototype slots, array headers). It detects
the malloc parent and only remembers genuinely-young children, so an
already-old or interned string is a harmless no-op.

Validation: on a large esbuild-bundled CLI app that reproduced the crash,
`PERRY_GC_VERIFY_EVACUATION` goes from ~2000 missing edges + abort to zero
missing edges + no abort; `perry-runtime` regex tests: 37 passed / 0 failed.
@coderabbitai

coderabbitai Bot commented Jul 11, 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: 0d8f7b47-2acc-44a2-bc6f-15f469e41c28

📥 Commits

Reviewing files that changed from the base of the PR and between bf8912e and 9181dc9.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/regex.rs

📝 Walkthrough

Walkthrough

js_regexp_new now records GC write barriers when storing the pattern and flags string pointers into a newly allocated RegExpHeader, with null checks for each pointer.

Changes

RegExp GC barrier integration

Layer / File(s) Summary
Barrier RegExp string slots
crates/perry-runtime/src/regex.rs
js_regexp_new computes the relevant header and slot addresses, then records write-barrier entries for non-null pattern and flags pointers.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main runtime GC write-barrier fix.
Description check ✅ Passed It covers the problem, fix, and validation, and is mostly aligned with the template despite missing some optional sections.
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 unit tests (beta)
  • Create PR with unit tests

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 45c3280 into PerryTS:main Jul 11, 2026
25 checks passed
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