fix(runtime): write-barrier RegExp header pattern/flags string fields#6288
Merged
proggeramlug merged 1 commit intoJul 11, 2026
Conversation
`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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
ChangesRegExp GC barrier integration
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
js_regexp_newallocates theRegExpHeaderviagc_malloc(the onlyGC_TYPE_OBJECTmalloc site) and then stores itspattern_ptrandflags_ptr— both GC-managedStringHeaders — with plain raw field writes:The header is malloc-backed, so the GC treats it as old-generation, and
canonical_flags_ptris a freshly-allocated nursery string — an old→youngedge. 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=1reports it precisely: ~2000 uncoveredobject → stringedges at theflags_ptrslot. Without the verifier it's anintermittent
SIGSEGV/SIGBUSwhose faulting pointer is a reused nurseryslot. 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 missingremembered-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
PERRY_GC_VERIFY_EVACUATIONgoes from ~2000 missing edges + abort tozero missing edges + no abort.
perry-runtimeregex tests: 37 passed / 0 failed.Summary by CodeRabbit