fix(runtime): root repeat's receiver across the count coercion - #8443
Conversation
`js_string_repeat` borrowed the receiver's inline WTF-8 payload and only then coerced `count`. ToNumber on an object count runs user JS (`valueOf` / `Symbol.toPrimitive`), whose loop back-edge polls are moving-GC safepoints, so an evacuating minor inside the callback left the borrow naming retired from-space and the result was copied out of garbage. Coerce first, then borrow. Deferring the borrow alone is not enough: `s` is a raw pointer in a native Rust frame, which the collector does not scan by default, so it is parked in a `RuntimeHandleScope` and the post-collection address is taken back out via `across_const`. padStart/padEnd were audited for the same shape and are unaffected — codegen emits both of their coercions (`js_number_coerce` for maxLength, `js_string_pad_fill` for the fill) before the receiver handle is re-read, so no user code runs inside those helpers while the payload is borrowed. The new fixture covers them too, so a future move of either coercion into the helper lands on a test that is already watching.
|
Warning Review limit reached
Next review available in: 34 minutes Limit details: You’ve used all 8 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
✨ 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 |
|
Validated as part of an 11-PR batch (#8439, #8440, #8441, #8442, #8443, #8444, #8446,
One thing stated plainly: these are hardening, not demonstrated repairsI could not make the underlying bugs reproduce. My probe passes on unmodified That is consistent with the string audit having found these windows by reading rather than by |
Closes #8427.
Problem
js_string_repeatborrowed the receiver's inline WTF-8 payload and thencoerced
count:ToNumber on an object count runs user JS (
valueOf/Symbol.toPrimitive).A loop in that callback hits back-edge safepoint polls (default-on, #7721)
where the copying minor is eligible; when it evacuates a young receiver,
str_datanames retired from-space andrepeatcopies garbage.Fix
Coerce first, borrow second — but that alone is not sufficient.
sis a rawpointer in a native Rust frame, which the collector does not scan by
default, so the later
string_as_str(s)would still deref a stale address.It is parked in a
RuntimeHandleScopeand the post-collection address istaken back out via
across_const, the sanctioned combinator for this shape(no new bare raw-handle reads —
scripts/raw_handle_debt.pystays at its978 baseline).
The
count == 0early return moves above the borrow, so the payload is readonly on the path that actually needs it. Observable ordering per ECMA-262
§22.1.3.17 is unchanged: ToIntegerOrInfinity still runs for an empty
receiver, and a negative count still throws before the empty-string return —
both are asserted in the fixture and byte-compared against Node 26.5.1.
padStart / padEnd: audited, unaffected
The issue asked to check them. They are not affected, and the PR carries
evidence rather than a reading: codegen emits both of their coercions —
js_number_coerceformaxLengthandjs_string_pad_fill(ToString) forthe fill — before
reread_recvproduces the receiver handle(
lower_string_method.rs:855-870). No user code runs insidejs_string_pad_start/_pad_endafterstring_as_str; everything past it(
to_length,decode_wtf8_units,build_pad_chunk,finish_pad_result) ispure Rust, and
finish_pad_resultcopies the payload into an ownedVecbefore the destination allocation. The fixture exercises both with reentrant
allocating
maxLengthand fill callbacks anyway, so a future move ofeither coercion into the helper lands on a test already watching.
Validation
test-files/test_gap_gc_string_repeat_reentrant_count.ts— a gap fixture, soit runs in the per-PR tier, not only the nightly sweep. Built with
cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static(.amtimes verified to move after each edit), rununder the instruments in its
parity-envline.The gate was proved able to go red. Same fixture, same instruments, the
only difference being the runtime
.a:falsefalsetruetruetruetruetruetrueBoth arms report
copying_minors=530 moved_objects=16930— the subjectcollector ran, so the green verdict is not vacuous. The pad rows staying
green in both arms is the empirical half of the audit above (that code is
byte-identical across the two builds).
Without the instruments the fixture passes on both runtimes, which is the
point of the
parity-envline: this class is invisible to an unpaced run.Also green:
cargo fmt --all -- --check,scripts/check_file_size.sh,scripts/addr_class_inventory.py(the twohandle-floornotes it prints arepre-existing on
main),scripts/raw_handle_debt.py(978, at baseline),scripts/gc_runtime_root_holders.py,scripts/check_gc_env_knobs.py,scripts/check_node_version_consistency.py.No version bump (maintainer bumps at merge).
Siblings in the same class, filed separately: #8426 (
js_string_normalize),#8428 (
js_regexp_execlastIndex).