Skip to content

fix(runtime): convert #7811/#7815's new bare raw-handle reads to across_* (#7838) - #7840

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7838-raw-handle-debt-ratchet
Aug 11, 2026
Merged

fix(runtime): convert #7811/#7815's new bare raw-handle reads to across_* (#7838)#7840
proggeramlug merged 2 commits into
mainfrom
fix/7838-raw-handle-debt-ratchet

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Closes #7838.

The raw-handle debt ratchet is red on main: 1,013 bare reads against a baseline of 998, four per-module violations. The +15 arrived with the two #6949 rooting fixes in the 2026-08-11 batch (#7811, #7815), and #7825 — same batch — closed the hole that had been letting a PR's own checkout carry the comparison baseline, so the ratchet only started seeing them once it landed.

All fifteen convert to RuntimeHandle::across_{mut,const}. No ceiling was raised and raw_handle_debt_baseline.txt is untouched — the total lands back on 998 exactly, and regex/replace_fn.rs returns to its recorded ceiling of 3.

module before after disposition
disposable.rs 3 0 converted; module stays unlisted
builtins/formatting/boxed_primitives.rs 1 0 converted; module stays unlisted
messaging.rs 1 0 converted; module stays unlisted
regex/replace_fn.rs 13 3 10 converted; back at its existing ceiling

Why replace_fn.rs did not need a ceiling raise

#7838 proposed raising it 3 → 13 on the grounds that the thirteen are the one shape raw_handle_debt_files.txt sanctions joining the list for — a loop whose collection window is a user-visible callback, where cur_str = || string_as_str(handle.get_raw_const_ptr(..)) re-derives at every access and across_* (one call ↔ one re-read) cannot express it.

That is true of three of them, not thirteen — and those three are pre-existing, which is exactly what the ceiling of 3 was recorded for. git show 6af7e5840^:crates/perry-runtime/src/regex/replace_fn.rs | grep -c get_raw_ is 3; at 6af7e5840 it is 13. So #7811 added 10, and all ten are the plain across_* shape: root → one allocating js_string_coerce → read-for-the-call. They convert mechanically.

Four of the ten are two-receiver call sites (s + pattern, s + re). Two receivers compose by nesting across_const — the inner call runs the coercion and re-reads b, the outer then re-reads a — which is how path::value_args::with_two_headers already does it. Two small private combinators (with_two_receivers_across, with_receiver_across) carry them so the nesting is written once.

Two conversions are not purely mechanical

Writing the ordering out made a latent stale-address use visible in disposable.rs (js_suppressed_error_new). Both are the #7192 shape — the store is in-frame but after a call that allocates — and neither is reachable without evacuation actually moving the receiver, so this is ordering hygiene rather than an observed crash:

  • set_nonenum filed attributes under a possibly-stale address. It called crate::object::set_property_attrs(obj as usize, ..) after js_object_set_field_by_name, which allocates when the object grows. That side table is keyed on the address, so a pre-call copy does not fault — it files the attributes where nothing will look them up, and error / suppressed / message silently become enumerable on a SuppressedError that grew during the set. Now re-read across the field-set.
  • The returned NaN-box was built before the prototype lookup. js_nanbox_pointer(obj) was computed, then builtin_prototype_value("SuppressedError") ran, then the box was returned. A NaN-box is a frozen address the collector cannot rewrite, so the returned value named from-space if that lookup collected. The box is now built last, after every allocating call.

Validation

$ python3 scripts/raw_handle_debt.py ; echo $?
bare raw-handle reads: 998 (baseline 998)
per-module: 110 module(s) within ceilings; every other runtime module is locked at zero
0

(the script's own exit code, not a pipeline's). --no-raise-vs origin/main, the mode CI runs, also exits 0: baseline 998 -> 998, 110 -> 110 module ceiling(s), none raised. Release build of -p perry -p perry-runtime-static -p perry-stdlib-static is clean; the perry-runtime test suite is still running and I will post the result.

Summary by CodeRabbit

  • Bug Fixes
    • Improved runtime stability when creating boxed strings, suppressed errors, and broadcast channels.
    • Fixed potential failures during regular-expression replacement when values require conversion or memory cleanup occurs.
    • Improved error handling and object safety during operations that may trigger memory management.
    • Preserved existing behavior for empty strings, symbol conversion errors, callable replacements, and regular-expression processing.

Ralph Küpper added 2 commits August 11, 2026 12:52
…ss_* (#7838)

The raw-handle debt ratchet went red on main: 1,013 bare reads against a
baseline of 998, with four per-module violations. The +15 all arrived with the
two #6949 rooting fixes in the 2026-08-11 batch, and #7825 -- same batch --
closed the hole that had been letting a PR's own checkout carry the comparison
baseline, so the ratchet only started seeing them once it landed.

All fifteen convert. None needed a ceiling raise and the baseline is untouched:
the total lands back on 998 exactly, and regex/replace_fn.rs returns to its
recorded ceiling of 3.

  #7815 (5 sites, three modules, all previously unlisted -> all now zero)
    disposable.rs, messaging.rs, builtins/formatting/boxed_primitives.rs.
    Each is the exact shape across_* exists for: allocate the receiver, run one
    allocating coercion, then write through a re-read pointer.

  #7811 (10 sites in regex/replace_fn.rs)
    NOT the sanctioned "loop across a user-visible trap" shape that
    raw_handle_debt_files.txt permits joining the list for -- that shape is the
    three PRE-EXISTING reads (`cur_str` in the two string-replacer loops, plus
    the subject re-read in call_string_replace_callback), which is what the
    ceiling of 3 was recorded for. The ten new ones are plain
    root -> one allocating js_string_coerce -> read-for-the-call, which across_*
    expresses directly. Two small local combinators carry the four two-receiver
    call sites; two receivers compose by NESTING across_const, the same way
    path::value_args::with_two_headers already does it.

Two of the conversions are not purely mechanical, because writing the ordering
out made a latent stale-address use visible in disposable.rs:

  - set_nonenum ran crate::object::set_property_attrs(obj as usize, ..) AFTER
    js_object_set_field_by_name, which allocates when the object grows. That
    side table is keyed on the address, so a pre-call copy does not fault -- it
    files the attributes under an address nothing looks up, and `error` /
    `suppressed` / `message` silently become enumerable on a SuppressedError
    that grew during the set.

  - js_nanbox_pointer(obj) was built BEFORE the SuppressedError.prototype
    lookup and returned afterwards. A NaN-box is a frozen address the collector
    cannot rewrite, so the returned value named from-space if the lookup
    collected. The box is now built last, after every allocating call.

Both are the #7192 shape (the store is in-frame but after a call that
allocates), and neither is reachable without evacuation moving the receiver, so
this is ordering hygiene rather than an observed crash.

Verified: scripts/raw_handle_debt.py exits 0 at 998/998, "110 module(s) within
ceilings; every other runtime module is locked at zero".
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime now uses RuntimeHandle::across_mut and related helpers across boxed-string, broadcast-channel, SuppressedError, and replacement paths. SuppressedError also delays pointer boxing until after prototype linking.

Changes

GC pointer safety

Layer / File(s) Summary
Root and refresh constructor pointers
changelog.d/7840-raw-handle-debt-across.md, crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs, crates/perry-runtime/src/messaging.rs
Boxed-string and broadcast-channel construction keeps objects rooted across coercion and uses relocated pointers afterward. The changelog records the raw-handle debt conversions.
Order SuppressedError allocations
crates/perry-runtime/src/disposable.rs
Property setup and prototype linking reacquire the object after allocations. NaN-box construction now occurs after prototype linking.
Root replacement receivers
crates/perry-runtime/src/regex/replace_fn.rs
Replacement helpers preserve one or two receivers across coercions and pass refreshed pointers to string and RegExp replacement paths.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • PerryTS/perry#7811: Introduced the related GC-rooting fixes in RegExp replacement paths.
  • PerryTS/perry#7389: Introduced RuntimeHandle::across_* patterns used by this change.
  • PerryTS/perry#7455: Converted related GC-sensitive raw-pointer handling to RuntimeHandle::across_*.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes convert all 15 targeted reads, preserve the ceiling of 3, and restore the ratchet baseline required by issue #7838.
Out of Scope Changes check ✅ Passed The implementation and changelog updates directly support the raw-handle debt fix and related GC-safety corrections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the runtime fix and the conversion of bare raw-handle reads to across_* APIs.
Description check ✅ Passed The description gives a detailed summary, concrete changes, related issue, and validation results, although it omits the template headings and checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7838-raw-handle-debt-ratchet

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/disposable.rs (1)

485-512: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Root all NaN-boxed input values across allocations.

obj_handle roots only obj. It does not root error, suppressed, or message.

set_nonenum receives value as an unrooted f64, then js_string_from_bytes can collect before js_object_set_field_by_name consumes it. The earlier set_nonenum calls can also collect before Line 504 reads message. If an input contains a movable heap value, these paths can use a stale pointer after evacuation.

Root error, suppressed, and message in scope before the first allocation. Root each value passed to set_nonenum before key creation, then reload it with get_nanbox_f64() after that allocation. Apply the same rule to the coerced message value.

Based on learnings: NaN-boxed f64 values must be rooted across allocating operations and reloaded before reuse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/disposable.rs` around lines 485 - 512, Root error,
suppressed, and message in the RuntimeHandleScope before any allocation, and
update set_nonenum to root each value before js_string_from_bytes, then reload
it with get_nanbox_f64() before js_object_set_field_by_name. Apply the same
rooting and reload behavior to the coerced message value, preserving the
existing undefined and string-coercion logic.

Source: Learnings

🧹 Nitpick comments (1)
changelog.d/7840-raw-handle-debt-across.md (1)

1-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rewrite the fragment as a final release note.

This fragment mixes final runtime behavior with CI baseline history, issue chronology, a git show command, and an abandoned ceiling proposal. These details describe development work, not the shipped behavior. Replace them with one concise entry that states the RuntimeHandle::across_* conversions and the SuppressedError ordering fixes. Keep ### Fixed and do not add version metadata.

Based on learnings: changelog fragments should describe final shipped behavior as one coherent release-note entry and should not include development-slice narratives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7840-raw-handle-debt-across.md` around lines 1 - 48, Rewrite the
fragment under ### Fixed as one concise release-note entry describing the
shipped RuntimeHandle::across_mut/across_const conversions and the
SuppressedError ordering fixes. Remove CI baseline counts, issue chronology, git
commands, ceiling proposals, and other development-history details; do not add
version metadata.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs`:
- Around line 311-324: Preserve GC handle roots across all allocations: in
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs:311-324, update
install_string_wrapper_indices and install_string_wrapper_length to root and
reload both the object and string handles; in
crates/perry-runtime/src/messaging.rs:616-617, root name_ptr and root/reload the
object in set_field across key(name) and in install_method across closure_value.
- Around line 311-324: Update the boxed String installation within the
obj_handle.across_mut closure to use refreshed GC handles throughout
allocations. Root the result of js_string_coerce, retain obj_handle across each
allocation, and reload object and character pointers after js_string_from_bytes,
js_string_coerce, js_string_char_at, and key-array growth. Root each
intermediate character string until its field store completes, and pass handles
rather than raw pointer snapshots to these helpers.

In `@crates/perry-runtime/src/messaging.rs`:
- Around line 616-617: In the allocation flow around js_object_alloc and
set_field, create RuntimeHandleScope and obj_handle immediately after allocating
the object, then refresh obj from obj_handle.get_raw_mut_ptr() after every
allocation before accessing it. Root the coerced name_ptr before subsequent
allocations, including install_method, refresh it afterward, and use the
refreshed pointer when setting the "name" field.
- Around line 616-619: Update the name-coercion path around obj_handle and
set_field to root the coerced name_value with scope.root_nanbox_f64, passing
get_nanbox_f64() after any helper allocation. Preserve obj_handle as the
receiver handle and pass it into set_field so the write uses the current
receiver; apply the same rooting and current-receiver handling to closure_value
and install_method.

---

Outside diff comments:
In `@crates/perry-runtime/src/disposable.rs`:
- Around line 485-512: Root error, suppressed, and message in the
RuntimeHandleScope before any allocation, and update set_nonenum to root each
value before js_string_from_bytes, then reload it with get_nanbox_f64() before
js_object_set_field_by_name. Apply the same rooting and reload behavior to the
coerced message value, preserving the existing undefined and string-coercion
logic.

---

Nitpick comments:
In `@changelog.d/7840-raw-handle-debt-across.md`:
- Around line 1-48: Rewrite the fragment under ### Fixed as one concise
release-note entry describing the shipped RuntimeHandle::across_mut/across_const
conversions and the SuppressedError ordering fixes. Remove CI baseline counts,
issue chronology, git commands, ceiling proposals, and other development-history
details; do not add version metadata.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7778c1e7-3196-4718-b166-d29385ca1be0

📥 Commits

Reviewing files that changed from the base of the PR and between ab1bd46 and c0a7e53.

📒 Files selected for processing (5)
  • changelog.d/7840-raw-handle-debt-across.md
  • crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
  • crates/perry-runtime/src/disposable.rs
  • crates/perry-runtime/src/messaging.rs
  • crates/perry-runtime/src/regex/replace_fn.rs

Comment on lines +311 to +324
let (ptr, obj) = obj_handle.across_mut::<crate::object::ObjectHeader, _>(|| {
// `new String()` (no args) is spec'd to box "", not "undefined".
if has_arg == 0 {
crate::string::js_string_from_bytes(std::ptr::null(), 0)
} else {
// ECMA-262 §22.1.1 step 2b: ToString(value) — throws TypeError for Symbol.
if unsafe { crate::symbol::js_is_symbol(value) } != 0 {
crate::collection_iter::throw_type_error(
"Cannot convert a Symbol value to a string",
);
}
js_string_coerce(value)
}
js_string_coerce(value)
};
let obj = obj_handle.get_raw_mut_ptr::<crate::object::ObjectHeader>();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  'across_mut|let \(.*obj\)|obj_handle|get_raw_mut_ptr|set_field|install_method|boxed_object' \
  crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs \
  crates/perry-runtime/src/messaging.rs

Repository: PerryTS/perry

Length of output: 20264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- boxed_primitives.rs ---'
sed -n '280,380p' crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs

printf '%s\n' '--- messaging.rs ---'
sed -n '590,635p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- RuntimeHandle and across_mut definitions ---'
rg -n -C 12 \
  'fn across_mut|struct RuntimeHandle|root_raw_mut_ptr|get_raw_mut_ptr|RuntimeHandleScope' \
  crates/perry-runtime/src

printf '%s\n' '--- allocation-capable callees used in the affected regions ---'
rg -n -C 5 \
  'fn (register_boxed_primitive_payload|install_string_wrapper|attach_boxed_primitive_prototype|js_object_set_field_by_name|js_object_define_accessor|js_string_coerce|js_string_from_bytes)' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

checks = {
    "crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": (309, 370),
    "crates/perry-runtime/src/messaging.rs": (614, 627),
}

for filename, (start, end) in checks.items():
    lines = Path(filename).read_text().splitlines()
    segment = lines[start-1:end]
    print(f"--- {filename}:{start}-{end} ---")
    across = next((i for i, line in enumerate(segment, start) if "across_mut" in line), None)
    if across is None:
        print("ERROR: no across_mut call found")
        continue

    post_across = [(i, line.strip()) for i, line in enumerate(segment, start)
                   if i > across and re.search(r'\bobj\b', line)]
    print(f"across_mut line: {across}")
    for i, line in post_across:
        print(f"{i}: {line}")
    unsafe_snapshot_uses = [
        (i, line) for i, line in post_across
        if "obj_handle.get_raw_mut_ptr()" not in line and
           not line.lstrip().startswith("//")
    ]
    print(f"post-across raw obj uses requiring review: {len(unsafe_snapshot_uses)}")
PY

Repository: PerryTS/perry

Length of output: 1796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- affected boxed-string constructor ---'
sed -n '300,334p' crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs

printf '%s\n' '--- affected broadcast-channel constructor ---'
sed -n '602,630p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- handle API ---'
rg -n -l 'pub struct RuntimeHandle|fn across_mut|fn root_raw_mut_ptr|fn get_raw_mut_ptr' crates/perry-runtime/src/gc
for file in $(rg -l 'pub struct RuntimeHandle|fn across_mut|fn root_raw_mut_ptr|fn get_raw_mut_ptr' crates/perry-runtime/src/gc); do
  echo "--- $file ---"
  rg -n -A 35 -B 8 'pub struct RuntimeHandle|fn across_mut|fn root_raw_mut_ptr|fn get_raw_mut_ptr' "$file"
done

printf '%s\n' '--- relevant callee definitions ---'
rg -n -l \
  'fn register_boxed_primitive_payload|fn install_string_wrapper_indices|fn install_string_wrapper_length|fn attach_boxed_primitive_prototype|fn js_object_set_field_by_name|fn js_string_coerce' \
  crates/perry-runtime/src
for file in $(rg -l \
  'fn register_boxed_primitive_payload|fn install_string_wrapper_indices|fn install_string_wrapper_length|fn attach_boxed_primitive_prototype|fn js_object_set_field_by_name|fn js_string_coerce' \
  crates/perry-runtime/src); do
  echo "--- $file ---"
  rg -n -A 45 -B 5 \
    'fn register_boxed_primitive_payload|fn install_string_wrapper_indices|fn install_string_wrapper_length|fn attach_boxed_primitive_prototype|fn js_object_set_field_by_name|fn js_string_coerce' \
    "$file"
done

Repository: PerryTS/perry

Length of output: 32578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

regions = {
    "crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": (311, 330),
    "crates/perry-runtime/src/messaging.rs": (617, 626),
}

for filename, (start, end) in regions.items():
    lines = Path(filename).read_text().splitlines()[start-1:end]
    print(f"--- {filename}:{start}-{end} ---")
    for lineno, line in enumerate(lines, start):
        if "across_mut" in line or "obj" in line or "name_ptr" in line:
            print(f"{lineno}: {line}")
PY

Repository: PerryTS/perry

Length of output: 1257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- messaging helper chain ---'
sed -n '45,75p' crates/perry-runtime/src/messaging.rs
sed -n '110,126p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- closure_value definition ---'
rg -n -l 'fn closure_value' crates/perry-runtime/src
for file in $(rg -l 'fn closure_value' crates/perry-runtime/src); do
  rg -n -A 30 -B 5 'fn closure_value' "$file"
done

printf '%s\n' '--- string-wrapper helper bodies ---'
sed -n '90,180p' crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs

printf '%s\n' '--- allocation and collection contract for string creation ---'
rg -n -A 22 -B 8 \
  'pub extern "C" fn js_string_from_bytes|fn js_string_from_bytes|js_string_from_bytes_with_capacity|gc_malloc|collect_minor|EVACUATE' \
  crates/perry-runtime/src/string crates/perry-runtime/src/gc \
  | head -n 260

Repository: PerryTS/perry

Length of output: 40579


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

cases = {
    "crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": (311, 330),
    "crates/perry-runtime/src/messaging.rs": (617, 626),
}

allocation_markers = (
    "js_string_from_bytes", "js_string_coerce", "closure_value",
    "js_object_set_field_by_name", "js_object_define_accessor",
)

for filename, (start, end) in cases.items():
    lines = Path(filename).read_text().splitlines()
    print(f"--- {filename} ---")
    for number in range(start, end + 1):
        text = lines[number - 1].strip()
        if not text:
            continue
        flags = []
        if re.search(r"\bobj\b", text) and "get_raw_mut_ptr" not in text:
            flags.append("raw_obj")
        if "ptr" in text and "get_raw" not in text:
            flags.append("raw_result")
        if any(marker in text for marker in allocation_markers):
            flags.append("allocation_capable")
        if flags:
            print(f"{number}: [{', '.join(flags)}] {text}")
PY

Repository: PerryTS/perry

Length of output: 1621


Preserve handle-based roots across every allocation.

across_mut returns one post-collection address. Later allocations can evacuate that object and the returned string pointer.

  • In boxed_primitives.rs, root and reload both the object and string handles. Apply this inside install_string_wrapper_indices and install_string_wrapper_length.
  • In messaging.rs, root name_ptr. Root and reload the object inside set_field across key(name) and inside install_method across closure_value.
📍 Affects 2 files
  • crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs#L311-L324 (this comment)
  • crates/perry-runtime/src/messaging.rs#L616-L617
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs` around
lines 311 - 324, Preserve GC handle roots across all allocations: in
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs:311-324, update
install_string_wrapper_indices and install_string_wrapper_length to root and
reload both the object and string handles; in
crates/perry-runtime/src/messaging.rs:616-617, root name_ptr and root/reload the
object in set_field across key(name) and in install_method across closure_value.

Source: Learnings


🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'install_string_wrapper_(indices|length)|js_string_(from_bytes|char_at|length)|js_object_set_field_by_name' \
  crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs

Repository: PerryTS/perry

Length of output: 6258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- RuntimeHandleScope and root APIs ---'
rg -n -C 12 \
  'struct RuntimeHandleScope|impl RuntimeHandleScope|root_string_ptr|root_raw_mut_ptr|across_mut|gc_register_mutable_root_scanner' \
  crates/perry-runtime/src

printf '%s\n' '--- String allocation and movement APIs ---'
rg -n -C 10 \
  'pub .*fn js_string_(from_bytes|coerce|char_at|length)|struct StringHeader|StringHeader' \
  crates/perry-runtime/src/string* crates/perry-runtime/src

printf '%s\n' '--- Relevant callers and registration order ---'
rg -n -C 8 \
  'install_string_wrapper_(indices|length)|register_boxed_primitive_payload|js_boxed_string_new' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Definitions ---'
rg -l \
  'struct RuntimeHandleScope|root_string_ptr|root_raw_mut_ptr|fn across_mut' \
  crates/perry-runtime/src | sort

printf '%s\n' '--- String definitions ---'
rg -l \
  'fn js_string_from_bytes|fn js_string_char_at|fn js_string_length|struct StringHeader' \
  crates/perry-runtime/src | sort

printf '%s\n' '--- Targeted call sites ---'
rg -n -C 6 \
  'install_string_wrapper_(indices|length)|register_boxed_primitive_payload|js_boxed_string_new' \
  crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs

Repository: PerryTS/perry

Length of output: 11912


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Runtime handle implementation ---'
file=$(rg -l 'pub struct RuntimeHandleScope|struct RuntimeHandleScope' crates/perry-runtime/src/gc)
cat -n "$file" | sed -n '1,280p'

printf '%s\n' '--- String allocation and access implementation ---'
for file in $(rg -l 'fn js_string_from_bytes|fn js_string_char_at|fn js_string_length|struct StringHeader' crates/perry-runtime/src/string); do
  printf '\n--- %s ---\n' "$file"
  rg -n -C 18 \
    'struct StringHeader|fn js_string_from_bytes|fn js_string_char_at|fn js_string_length|GC_TYPE_STRING|gc_alloc' \
    "$file"
done

printf '%s\n' '--- Object allocation and field insertion ---'
rg -n -C 16 \
  'pub .*fn js_object_alloc|fn js_object_set_field_by_name|fn register_boxed_primitive_payload|thread_local!|BOXED_PRIMITIVE_PAYLOADS' \
  crates/perry-runtime/src/object/alloc.rs \
  crates/perry-runtime/src/object/field_set_by_name \
  crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs

Repository: PerryTS/perry

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Handle reread methods ---'
file=crates/perry-runtime/src/gc/roots/runtime_handles.rs
rg -n -C 14 \
  'get_raw_mut_ptr|get_raw_const_ptr|set_raw|RuntimeHandleSlot|STRING_TAG' \
  "$file"

printf '%s\n' '--- Object allocation and movement-sensitive writes ---'
rg -n -C 12 \
  'arena_alloc_gc|js_object_set_field_by_name|set_builtin_property_attrs|object_set_static_prototype' \
  crates/perry-runtime/src/object/alloc.rs \
  crates/perry-runtime/src/object/field_set_by_name \
  crates/perry-runtime/src/object \
  crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs | head -n 500

printf '%s\n' '--- Exact string helper bodies ---'
cat -n crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs | sed -n '90,185p;294,333p'
cat -n crates/perry-runtime/src/string/char_ops.rs | sed -n '242,290p'
cat -n crates/perry-runtime/src/string/alloc.rs | sed -n '1,22p;124,145p;203,212p'

Repository: PerryTS/perry

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Exact wrapper helpers and constructor ---'
cat -n crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs | sed -n '90,185p;294,333p'

printf '%s\n' '--- Exact field setter implementation ---'
rg -l \
  'pub .*fn js_object_set_field_by_name|fn js_object_set_field_by_name' \
  crates/perry-runtime/src/object/field_set_by_name crates/perry-runtime/src/object
for file in $(rg -l \
  'pub .*fn js_object_set_field_by_name|fn js_object_set_field_by_name' \
  crates/perry-runtime/src/object/field_set_by_name crates/perry-runtime/src/object); do
  printf '\n--- %s ---\n' "$file"
  rg -n -C 20 \
    'js_object_set_field_by_name|js_array_alloc|js_array_push|arena_alloc_gc|js_string_from_bytes|keys_array' \
    "$file"
done

printf '%s\n' '--- Pointer-returning string coercion ---'
rg -n -C 24 \
  'fn js_string_coerce|js_string_coerce|js_get_string_pointer_unified|js_string_char_at' \
  crates/perry-runtime/src/value crates/perry-runtime/src/string

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Field setter entry and allocation points ---'
cat -n crates/perry-runtime/src/object/field_set_by_name.rs | sed -n '33,230p'
rg -n -C 10 \
  'fn js_object_set_field_by_name|js_array_alloc|js_array_push|arena_alloc_gc|js_string_intern|RuntimeHandleScope' \
  crates/perry-runtime/src/object/field_set_by_name/tail.rs \
  crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs

printf '%s\n' '--- js_string_coerce definition ---'
rg -n -l 'fn js_string_coerce|pub.*js_string_coerce' crates/perry-runtime/src
for file in $(rg -l 'fn js_string_coerce|pub.*js_string_coerce' crates/perry-runtime/src); do
  printf '\n--- %s ---\n' "$file"
  rg -n -C 30 'fn js_string_coerce|js_string_coerce' "$file"
done

Repository: PerryTS/perry

Length of output: 49006


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

boxed = Path("crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs").read_text()
numbers = Path("crates/perry-runtime/src/builtins/numbers.rs").read_text()
setter = Path("crates/perry-runtime/src/object/field_set_by_name/tail.rs").read_text()

def section(text, start, end):
    a = text.index(start)
    b = text.index(end, a)
    return text[a:b]

length = section(
    boxed,
    "fn install_string_wrapper_length(",
    "/// String exotic objects",
)
indices = section(
    boxed,
    "fn install_string_wrapper_indices(",
    "pub fn scan_boxed_primitive_payload_roots_mut",
)
constructor = section(
    boxed,
    "pub extern \"C\" fn js_boxed_string_new(",
    "#[no_mangle]\npub extern \"C\" fn js_boxed_boolean_new",
)
coerce = section(
    numbers,
    "pub extern \"C\" fn js_string_coerce(",
    "/// True when [`js_string_coerce`]",
)

checks = {
    "object raw pointer is reused after the rooting callback":
        "let (ptr, obj) = obj_handle.across_mut" in constructor
        and "install_string_wrapper_indices(obj, ptr)" in constructor,
    "length reads string after key allocation":
        length.index("js_string_from_bytes") < length.index("js_string_length"),
    "indices reads character before key allocation":
        indices.index("js_string_char_at") < indices.index("js_string_from_bytes"),
    "indices uses character pointer after key allocation":
        indices.index("js_string_from_bytes") < indices.index("JSValue::string_ptr(ch)"),
    "field setter allocates while updating a new key":
        "js_array_alloc" in setter and "refresh_roots_after_alloc!()" in setter,
    "string coercion can allocate":
        "js_string_from_bytes(result.as_ptr()" in coerce
        and "js_string_materialize_to_heap" in coerce,
}

for name, result in checks.items():
    print(f"{'PASS' if result else 'FAIL'}: {name}")

assert all(checks.values())
PY

Repository: PerryTS/perry

Length of output: 459


Use refreshed GC handles throughout boxed String installation.

obj, ptr, and ch are raw pointers held across allocations. js_string_coerce, js_string_char_at, js_string_from_bytes, and key-array growth can evacuate them. Root the coerced string, keep the object handle active, reload both pointers after each allocation, and root each intermediate character string until its field store completes. Do not pass raw pointer snapshots through the helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs` around
lines 311 - 324, Update the boxed String installation within the
obj_handle.across_mut closure to use refreshed GC handles throughout
allocations. Root the result of js_string_coerce, retain obj_handle across each
allocation, and reload object and character pointers after js_string_from_bytes,
js_string_coerce, js_string_char_at, and key-array growth. Root each
intermediate character string until its field store completes, and pass handles
rather than raw pointer snapshots to these helpers.

Source: Learnings

Comment on lines +616 to +617
let (name_ptr, obj) = obj_handle
.across_mut::<object::ObjectHeader, _>(|| crate::builtins::js_string_coerce(name));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'across_mut|obj_handle|get_raw_mut_ptr|set_field|install_method|boxed_object' \
  crates/perry-runtime/src/messaging.rs

Repository: PerryTS/perry

Length of output: 9067


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- RuntimeHandleScope and across_mut definitions ---'
rg -n -C 12 \
  'struct RuntimeHandleScope|impl RuntimeHandleScope|fn root_raw_mut_ptr|fn across_mut|get_raw_mut_ptr' \
  crates/perry-runtime crates

printf '%s\n' '--- Relevant object mutation APIs ---'
rg -n -C 10 \
  'fn js_object_set_field_by_name|js_object_define_accessor|fn set_object_prototype|fn js_object_alloc' \
  crates/perry-runtime

printf '%s\n' '--- GC/rooting APIs and mutable-root scanner registration ---'
rg -n -C 8 \
  'gc_register_mutable_root_scanner|root_raw_mut_ptr|root_nanbox_f64|across_mut' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Definition files ---'
rg -l \
  'struct RuntimeHandleScope|pub struct RuntimeHandleScope|fn js_object_set_field_by_name|pub fn js_object_set_field_by_name' \
  crates/perry-runtime/src crates/perry-runtime/tests 2>/dev/null | sort -u

printf '%s\n' '--- Focused handle API matches ---'
rg -n \
  'struct RuntimeHandleScope|impl RuntimeHandleScope|root_raw_mut_ptr|across_mut|get_raw_mut_ptr' \
  crates/perry-runtime/src/gc crates/perry-runtime/src --glob '*.rs' \
  | grep -E '(^|/)(handles?|mod|runtime|scope|gc)(\.rs:|/)' | head -n 240

printf '%s\n' '--- Focused object API matches ---'
rg -n \
  'fn js_object_set_field_by_name|fn js_object_define_accessor|fn set_object_prototype|fn js_object_alloc' \
  crates/perry-runtime/src --glob '*.rs' | head -n 120

Repository: PerryTS/perry

Length of output: 413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Runtime handle implementation ---'
sed -n '1,280p' crates/perry-runtime/src/gc/roots/runtime_handles.rs

printf '%s\n' '--- Field setter implementation ---'
sed -n '1,260p' crates/perry-runtime/src/object/field_set_by_name.rs

printf '%s\n' '--- BroadcastChannel call site and nearby helpers ---'
sed -n '598,632p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- Allocation/coercion definitions ---'
rg -n -C 8 \
  'pub extern "C" fn js_string_coerce|fn js_string_coerce|pub fn js_string_coerce|pub extern "C" fn js_object_alloc|fn js_object_alloc|pub fn js_object_alloc' \
  crates/perry-runtime/src --glob '*.rs'

Repository: PerryTS/perry

Length of output: 40820


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Messaging helper definitions ---'
sed -n '1,135p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- String coercion body ---'
sed -n '625,705p' crates/perry-runtime/src/builtins/numbers.rs

printf '%s\n' '--- Object allocation body ---'
sed -n '119,163p' crates/perry-runtime/src/object/alloc.rs

printf '%s\n' '--- Setter tail and handle-related logic ---'
rg -n -C 12 \
  'RuntimeHandleScope|root_|js_object_set_field_by_name\(|arena_alloc|key\(' \
  crates/perry-runtime/src/object/field_set_by_name crates/perry-runtime/src/object/field_set_by_name.rs \
  | head -n 360

printf '%s\n' '--- Closure and string allocation helpers ---'
rg -n -C 10 \
  'fn closure_value|fn js_string_from_bytes|pub.*js_string_from_bytes|js_closure_alloc' \
  crates/perry-runtime/src/messaging.rs crates/perry-runtime/src --glob '*.rs' \
  | head -n 300

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- String allocation implementation ---'
rg -n -C 18 \
  'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' \
  crates/perry-runtime/src/string crates/perry-runtime/src --glob '*.rs' | head -n 180

printf '%s\n' '--- Closure allocation implementation ---'
rg -n -C 20 \
  'pub extern "C" fn js_closure_alloc|pub fn js_closure_alloc|fn js_closure_alloc' \
  crates/perry-runtime/src/closure crates/perry-runtime/src --glob '*.rs' | head -n 180

printf '%s\n' '--- Root refresh macro and allocation points in setter tail ---'
rg -n -C 10 \
  'macro_rules! refresh_roots_after_alloc|refresh_roots_after_alloc!|js_string_intern|js_array_push|arena_alloc_gc' \
  crates/perry-runtime/src/object/field_set_by_name/tail.rs | head -n 320

printf '%s\n' '--- Existing messaging GC-rooting tests or references ---'
rg -n -C 12 \
  'broadcast_channel|js_broadcast_channel_new|6949|root_nanbox|root_string_ptr' \
  crates/perry-runtime/src/gc crates/perry-runtime/src/messaging.rs --glob '*.rs' | head -n 260

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

messaging = Path("crates/perry-runtime/src/messaging.rs").read_text()
coerce = Path("crates/perry-runtime/src/builtins/numbers.rs").read_text()
string_alloc = Path("crates/perry-runtime/src/string/alloc.rs").read_text()
string_mod = Path("crates/perry-runtime/src/string/mod.rs").read_text()
tail = Path("crates/perry-runtime/src/object/field_set_by_name/tail.rs").read_text()

checks = {
    "scope starts after constructor field write":
        messaging.index('set_field(\n        obj,\n        "constructor",\n        get_global_constructor("BroadcastChannel"),\n    );')
        < messaging.index('let scope = crate::gc::RuntimeHandleScope::new();', messaging.index('js_broadcast_channel_new')),
    "set_field allocates its key before calling setter":
        'object::js_object_set_field_by_name(obj, key(name), value);' in messaging,
    "ordinary string allocation reaches storage allocator":
        'js_string_from_bytes_with_capacity(data, len, len)' in string_alloc
        and 'let (ptr, data_ptr) = string_storage_alloc(capacity);' in string_alloc,
    "non-string coercion returns a newly allocated string":
        'js_string_from_bytes(result.as_ptr(), result.len() as u32)' in coerce,
    "setter roots only after entry":
        'let obj_handle = scope.root_raw_mut_ptr(obj);' in tail
        and 'let key_handle = scope.root_string_ptr(key);' in tail,
    "setter refreshes its own local object after internal allocations":
        'obj = obj_handle.get_raw_mut_ptr::<ObjectHeader>();' in tail,
    "messaging keeps coerced name as an unrooted raw-pointer NaN-box":
        'let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits());' in messaging
        and 'root_string_ptr(name_ptr)' not in messaging
        and 'root_nanbox_f64(name_value)' not in messaging,
}

for label, result in checks.items():
    print(f"{'PASS' if result else 'FAIL'}: {label}")

assert all(checks.values())
PY

printf '%s\n' '--- Storage allocator and exact coercion allocation path ---'
rg -n -C 16 \
  'fn string_storage_alloc|pub.*string_storage_alloc|js_string_from_bytes_with_capacity' \
  crates/perry-runtime/src/string --glob '*.rs' | head -n 180

Repository: PerryTS/perry

Length of output: 16489


Root obj and the coerced name across all allocations.

Create RuntimeHandleScope and obj_handle immediately after js_object_alloc. set_field allocates its key before receiving obj, and install_method allocates a closure. Reload obj_handle.get_raw_mut_ptr() after each allocation before using the object. Root name_ptr before the next allocation and use its refreshed value for the "name" field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/messaging.rs` around lines 616 - 617, In the
allocation flow around js_object_alloc and set_field, create RuntimeHandleScope
and obj_handle immediately after allocating the object, then refresh obj from
obj_handle.get_raw_mut_ptr() after every allocation before accessing it. Root
the coerced name_ptr before subsequent allocations, including install_method,
refresh it afterward, and use the refreshed pointer when setting the "name"
field.

Source: Learnings

Comment on lines +616 to 619
let (name_ptr, obj) = obj_handle
.across_mut::<object::ObjectHeader, _>(|| crate::builtins::js_string_coerce(name));
let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits());
set_field(obj, "name", name_value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'name_ptr|name_value|root_nanbox_f64|set_field|js_object_set_field_by_name' \
  crates/perry-runtime/src/messaging.rs crates/perry-runtime/src/atomics.rs

Repository: PerryTS/perry

Length of output: 19219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- messaging helpers and target ---'
sed -n '1,80p;598,628p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- handle and GC APIs ---'
rg -n -C 12 \
  'struct RuntimeHandleScope|fn root_nanbox_f64|fn root_raw_mut_ptr|fn across_mut|struct .*Handle|root_string_ptr' \
  crates/perry-runtime/src

printf '%s\n' '--- coercion and field-write implementations ---'
rg -n -C 16 \
  'fn js_string_coerce|js_string_coerce|js_object_set_field_by_name|fn key\\(' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- messaging helpers and target ---'
sed -n '1,80p;598,628p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- handle and GC APIs ---'
rg -n -C 12 \
  'struct RuntimeHandleScope|fn root_nanbox_f64|fn root_raw_mut_ptr|fn across_mut|struct .*Handle|root_string_ptr' \
  crates/perry-runtime/src

printf '%s\n' '--- coercion and field-write implementations ---'
rg -n -C 16 \
  'fn js_string_coerce|js_string_coerce|js_object_set_field_by_name|fn key\(' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate definitions ---'
rg -l \
  'pub struct RuntimeHandleScope|impl RuntimeHandleScope|fn js_string_coerce|pub extern "C" fn js_object_set_field_by_name|pub fn js_object_set_field_by_name' \
  crates/perry-runtime/src | sort

printf '%s\n' '--- exact target references ---'
rg -n -C 6 \
  'pub struct RuntimeHandleScope|root_nanbox_f64|root_raw_mut_ptr|across_mut' \
  crates/perry-runtime/src/gc.rs crates/perry-runtime/src/gc 2>/dev/null || true

printf '%s\n' '--- exact coercion references ---'
rg -n -C 10 \
  'fn js_string_coerce|pub extern "C" fn js_string_coerce|js_string_coerce' \
  crates/perry-runtime/src/builtins.rs crates/perry-runtime/src/builtins 2>/dev/null | head -n 180

printf '%s\n' '--- exact field setter references ---'
rg -n -C 12 \
  'js_object_set_field_by_name' \
  crates/perry-runtime/src/object.rs crates/perry-runtime/src/object 2>/dev/null | head -n 240

Repository: PerryTS/perry

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runtime handle implementation ---'
sed -n '1,115p;180,245p' crates/perry-runtime/src/gc/roots/runtime_handles.rs

printf '%s\n' '--- string coercion implementation ---'
rg -n 'js_string_coerce' crates/perry-runtime/src/builtins/numbers.rs
line="$(rg -n -m1 'js_string_coerce' crates/perry-runtime/src/builtins/numbers.rs | cut -d: -f1)"
start=$((line > 25 ? line - 25 : 1))
end=$((line + 65))
sed -n "${start},${end}p" crates/perry-runtime/src/builtins/numbers.rs

printf '%s\n' '--- field setter implementation ---'
rg -n 'js_object_set_field_by_name' crates/perry-runtime/src/object/field_set_by_name.rs
line="$(rg -n -m1 'js_object_set_field_by_name' crates/perry-runtime/src/object/field_set_by_name.rs | cut -d: -f1)"
start=$((line > 25 ? line - 25 : 1))
end=$((line + 180))
sed -n "${start},${end}p" crates/perry-runtime/src/object/field_set_by_name.rs

Repository: PerryTS/perry

Length of output: 21376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- messaging helper ---'
sed -n '55,72p;604,626p' crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- key-string allocation path ---'
rg -n -m 5 'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' \
  crates/perry-runtime/src
file="$(rg -l -m 1 'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' \
  crates/perry-runtime/src | head -n 1)"
line="$(rg -n -m 1 'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' "$file" | cut -d: -f1)"
start=$((line > 15 ? line - 15 : 1))
end=$((line + 90))
sed -n "${start},${end}p" "$file"

printf '%s\n' '--- read-only structural verifier ---'
python3 - <<'PY'
from pathlib import Path
p = Path("crates/perry-runtime/src/messaging.rs")
s = p.read_text()

helper = s[s.index("fn set_field("):s.index("fn get_global_constructor(")]
target = s[s.index("pub extern \"C\" fn js_broadcast_channel_new"):s.index("pub(crate) extern \"C\" fn js_broadcast_channel_constructor_call_error")]

checks = {
    "set_field passes raw obj before constructing key": "js_object_set_field_by_name(obj, key(name), value)" in helper,
    "key constructs a runtime string": "js_string_from_bytes(name.as_ptr(), name.len() as u32)" in s[s.index("fn key("):s.index("fn set_field(")],
    "target roots object across coercion": "let obj_handle = scope.root_raw_mut_ptr(obj);" in target,
    "target does not root coerced name value": "root_nanbox_f64(name_value)" not in target,
    "target uses raw name value for field write": 'set_field(obj, "name", name_value);' in target,
}
for name, ok in checks.items():
    print(f"{name}: {'yes' if ok else 'no'}")
raise SystemExit(0 if all(checks.values()) else 1)
PY

Repository: PerryTS/perry

Length of output: 8005


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- js_string_from_bytes allocator ---'
sed -n '1,95p' crates/perry-runtime/src/string/alloc.rs

printf '%s\n' '--- setter rooting and allocation-sensitive sections ---'
rg -n 'RuntimeHandleScope|root_raw|root_nanbox|js_string_from_bytes|js_object_alloc|gc_malloc|arena_alloc|set_object_keys_array|transition_cache_lookup' \
  crates/perry-runtime/src/object/field_set_by_name.rs \
  crates/perry-runtime/src/object/field_set_by_name/tail.rs \
  crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs

printf '%s\n' '--- setter tail start ---'
sed -n '1,220p' crates/perry-runtime/src/object/field_set_by_name/tail.rs

Repository: PerryTS/perry

Length of output: 19029


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- install_method and closure_value ---'
rg -n -C 18 \
  'fn closure_value|fn install_method|closure_value\\(' \
  crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- complete string allocation helper ---'
rg -n -m 3 'js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs
line="$(rg -n -m1 'pub extern "C" fn js_string_from_bytes_with_capacity|fn js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs | cut -d: -f1)"
start=$((line > 5 ? line - 5 : 1))
end=$((line + 100))
sed -n "${start},${end}p" crates/perry-runtime/src/string/alloc.rs

Repository: PerryTS/perry

Length of output: 303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- install_method and closure_value ---'
rg -n -C 18 -e 'fn closure_value' -e 'fn install_method' -e 'closure_value\(' \
  crates/perry-runtime/src/messaging.rs

printf '%s\n' '--- complete string allocation helper ---'
rg -n -m 3 'js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs
line="$(rg -n -m1 'pub extern "C" fn js_string_from_bytes_with_capacity|fn js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs | cut -d: -f1)"
start=$((line > 5 ? line - 5 : 1))
end=$((line + 100))
sed -n "${start},${end}p" crates/perry-runtime/src/string/alloc.rs

Repository: PerryTS/perry

Length of output: 8138


Keep the coerced value and receiver rooted across helper allocations.

key(name) allocates before js_object_set_field_by_name roots its arguments. This can evacuate both name_value and obj. closure_value can create the same gap in install_method. Root name_value with scope.root_nanbox_f64, pass get_nanbox_f64(), and pass obj_handle into set_field/install_method so each write uses a current receiver.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/messaging.rs` around lines 616 - 619, Update the
name-coercion path around obj_handle and set_field to root the coerced
name_value with scope.root_nanbox_f64, passing get_nanbox_f64() after any helper
allocation. Preserve obj_handle as the receiver handle and pass it into
set_field so the write uses the current receiver; apply the same rooting and
current-receiver handling to closure_value and install_method.

Source: Learnings

@proggeramlug
proggeramlug merged commit b1edd23 into main Aug 11, 2026
9 of 51 checks passed
@proggeramlug
proggeramlug deleted the fix/7838-raw-handle-debt-ratchet branch August 11, 2026 11:21
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Test result promised in the PR body, posting for the record now that this has merged:

$ RUST_TEST_THREADS=1 cargo test --release -p perry-runtime
test result: ok. 2108 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out
test result: ok. 0 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out

Green, single-threaded per #7791.

Also verified the gate holds on main after this landed — #7831 and #7836 merged on top of it, and #7836 is itself a rooting fix, which is the shape that broke the ratchet last time:

origin/main total bare reads = 998   baseline = 998   -> OK
per-module violations: 0

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.

raw-handle ratchet red on main: #7811/#7815 landed 15 bare reads the same batch's #7825 now counts

1 participant