Skip to content

runtime: js_regexp_exec holds the subject payload borrow across the lastIndex Get/ToLength user side effects #8428

Description

@proggeramlug

Problem

js_regexp_exec borrows the subject string's payload, then observes lastIndex — a Get → ToLength that, per the function's own spec comment, deliberately surfaces user valueOf/toString side effects — while holding the borrow (crates/perry-runtime/src/regex/exec.rs:36-75):

let str_data = string_as_str(s);                 // :36 — borrow of s's payload
...
// "the read — and any valueOf/toString side effect of a coercible
//  lastIndex — is observed exactly once" (test262 …lastindex-access)
let last_index_read = regex_last_index_offset(re);  // :51 — can run user JS
...
super::exec_array::utf16_index_to_byte(str_data, last_index)  // :61 — reads borrow
...
let search_str = &str_data[search_start_byte..];              // :75 — reads borrow

re.lastIndex = { valueOf() { /* arbitrary JS */ return 0; } }; re.exec(subject) is the trigger (the comment cites the exact test262 family proving this path runs user code). User JS in the window can run a moving collection today (back-edge safepoint polls in the callback, default-on since #7721, make the copying minor eligible); if the subject string is evacuated, str_data dangles into from-space for the entire match.

Also audit set_last_index_throwing (:68) in the same pass: if a user-defined lastIndex accessor can run there, the same hazard applies after that call. And check the sibling entry points that share this shape — regex/match_string.rs:81, regex/match_all.rs:92, regex/replace_* — for the same borrow-before-user-code ordering.

Background (borrowed-heap-slice class)

Perry strings: STRING_TAG NaN-box → 20-byte StringHeader, WTF-8 payload inline (crates/perry-runtime/src/string/mod.rs:307), young-generation, relocated by the copying minor. Rooting rewrites slots, never an already-materialized &str (HeapKeyBytes doc, crates/perry-runtime/src/object/field_get_set.rs:11-27). string_as_str<'a> (string/mod.rs:791) has a caller-chosen lifetime.

Suggested fix

Reorder is again the minimal fix: perform the lastIndex read (:51) — and, if it can run user code, the reset at :68before taking str_data. The spec ordering (lastIndex Get happens before the match) is exactly what the reorder produces; the current code already reads lastIndex up front, it just takes the borrow one step too early. If any later call in the function can allocate through the GC before the last str_data use (the match itself is pure Rust over the borrow; the result-array allocation at :95+ happens after — verify the borrow is dead by then, or re-derive), re-derive str_data from a rooted handle after each such point instead (RuntimeHandleScope::root_string_ptr + re-read; string/split.rs:479-520 shows the ranges-not-borrows pattern for match loops).

Repro / validation

  • Fixture test-files/test_issue_<this>_exec_lastindex_reentrant.ts: global regex, dynamically-built young subject (>5 bytes, heap), re.lastIndex = {valueOf(){ let junk=""; for(let i=0;i<5000;i++) junk = junk + "x"; return 0; }}, exec + print groups; byte-compare vs Node 26.5.1. Cover both the std-regex arm and the fancy-regex arm (a lookbehind pattern) — the borrow feeds both.
  • Fault demonstration BEFORE the fix: PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=0 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=64 PERRY_GC_FORCE_EVACUATE=1 — expect a from-space fault naming obj_type 3; PERRY_GC_DIAG=1 must show [gc-fromspace-protect] retired_set= (non-vacuous). Clean after.
  • Build note: cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static (stale-.a trap); perry-runtime tests RUST_TEST_THREADS=1.

Siblings, same class: #8423 (latent allocation-point windows), js_string_normalize (user toString), js_string_repeat (user valueOf) — filed separately.

Workflow

PR = code + tests + changelog.d/<PR>-<slug>.md; no version bump (maintainer bumps at merge).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions