Skip to content

GC: js_regexp_new holds &str borrows into movable StringHeaders across its whole body (residual #7217 allocation-point witness) #7247

Description

@proggeramlug

Summary

test_gap_gc_regexp_receiver_rooting still exits 139 on every allocation-point arm, 10/10 deterministic, after #7217's fix landed. #7217 turned out to be the lazy globalThis bootstrap — a different site — and closing it fixed test_gap_gc_spread_accessor_rooting and test_gap_gc_static_block_this_rooting but left this one untouched (base and fixed both exit=139, 10/10 each). So the ten regexp_receiver entries in test-parity/gc_repsel_triage.txt are retargeted here, not deleted.

Reproduction

export PERRY_RUNTIME_DIR=<build>/perry-dev PERRY_NO_AUTO_OPTIMIZE=1
perry test-files/test_gap_gc_regexp_receiver_rooting.ts -o /tmp/rx   # no compile-time env
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off /tmp/rx
# exit=139, no output, 10/10

Clean on the safepoint route (loop_polls) and on the shipped default, exactly as #7227 claimed.

The site, named by #7196's quarantine

PERRY_GC_PROTECT_FROMSPACE=1 faults at the stale dereference (build with PERRY_DEBUG_SYMBOLS=1 for the frames):

[gc-fromspace-protect] FAULT: signal 10 at 0x27609a90064
  block=0x27609a90000 +100 retired_bytes=640 retired_by_minor=#0
  last-known object: user_ptr=0x27609a90050 obj_type=3 size=40
2  std::thread::local::LocalKey<RefCell<HashMap<(String,String), Arc<fancy_regex::Regex>>>>::with::{closure}
     (perry_runtime::regex::js_regexp_new)
3  js_regexp_new + 1396
4  main + 228

obj_type=3 is a string, and the frame is the REGEX_CACHE.with(...) closure inside js_regexp_new.

Why

crates/perry-runtime/src/regex.rs:616 opens with

let pattern_str = if is_valid_ptr(pattern) { string_as_str(pattern) } else { "" };
let raw_flags_str = if is_valid_ptr(flags) { string_as_str(flags) } else { "" };

Both are &str borrows into the heap StringHeader's payload region, and they are held for the whole function — through validate_and_canonicalize_flags, the REGEX_CACHE probe (pattern_str.to_string(), flags_str.to_string()), the validation block, get_or_compile_regex, and the RegExpHeader allocation. Every one of those is an allocation point on this route, and an evacuating minor moves the pattern string out from under the borrow.

This is the #7215 borrow shape, and the same one #7216 fixed for object_assign_string_source by taking one owned copy up front (let owned: String = s.to_string();). The &str-into-a-movable-StringHeader pattern is the family, not the individual site: string_as_str / str_bytes_from_jsvalue / js_string_key_bytes all hand out a pointer into a relocatable payload, and their own safety notes say so.

Suggested shape

Take owned copies of pattern_str / raw_flags_str before the first allocation (js_regexp_new is already O(pattern length) — it does pattern.chars().collect::<Vec<char>>() on a cache miss), or root the two StringHeaders in a RuntimeHandleScope and re-derive the slices after each allocating step. The owned copy is the one #7216 chose for the sibling and it removes the dependence on where the collection lands.

Refs #7217, #7227, #7216, #7215, #7196.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions