Skip to content

Add typed feedback runtime foundation - #1752

Merged
proggeramlug merged 5 commits into
PerryTS:mainfrom
andrewtdiz:codex/typed-feedback-foundation
May 25, 2026
Merged

Add typed feedback runtime foundation#1752
proggeramlug merged 5 commits into
PerryTS:mainfrom
andrewtdiz:codex/typed-feedback-foundation

Conversation

@andrewtdiz

Copy link
Copy Markdown
Contributor

Summary

Part 1 of the typed-runtime follow-up stack. This adds the runtime and codegen foundation for typed feedback sites without enabling the larger guarded numeric layout work yet.

Stack

  1. This PR: typed feedback foundation
  2. Guarded dispatch specializations (codex/guarded-dispatch-specializations)
  3. GC scanning: add layout descriptors and pointer masks #1096 numeric layouts / GC pointer-free proof (codex/gc-1096-numeric-layouts)

This PR is an enabling slice for #1096, but does not close it by itself.

Verification

  • cargo check -p perry-runtime -p perry-codegen on this slice
  • Full rebased stack was also checked with cargo check -p perry-runtime -p perry-codegen

Existing warning noise remains.

@proggeramlug
proggeramlug merged commit fbfc5db into PerryTS:main May 25, 2026
10 checks passed
proggeramlug added a commit that referenced this pull request May 25, 2026
…e LTO (#1764) (#1765)

The typed-feedback instrumentation helpers (#1752) are #[no_mangle] but had no
#[used] retention anchors. Nothing in the Rust crate graph references them —
only codegen-emitted .ts object files do — so the auto-optimize build (whole-
program thin-LTO + strip=true) internalized and dead-stripped them, dangling
the codegen call at final link (`Undefined symbols: _js_typed_feedback_*`).
This blocked linking for every instrumented program under the default build,
including all node:async_hooks / AsyncLocalStorage tests.

Add #[used] typed fn-pointer statics for all 23 js_typed_feedback_* exports —
same retention mechanism as value/dyn_index.rs (KEEP_JS_DYN_INDEX_GET) /
process.rs (KEEP_JS_SETENV, #1344). (A [usize;N] ptrtoint array or [*const ();N]
pointer array do NOT survive thin-LTO; only individual typed fn-pointer statics
keep the symbol external — verified via llvm-nm + the four async-context tests
now linking and passing under auto-optimize.)
proggeramlug pushed a commit that referenced this pull request Jul 17, 2026
…pread calls (#6518)

The #6486 family: js_array_grow moves a push-grown array and leaves a
GC_FLAG_FORWARDED stub at the old address (#233), so a caller-held
pre-grow pointer raw-dereferenced as (*arr).length reads the forwarding
pointer's bytes as the element count. #6517 fixed the from-array
construction paths; this closes out the remaining readers from the
#6518 audit, each verified individually.

Confirmed bugs (negative-control verified — reverting these two files
crashes the new grown_array_crossing case):

- thread.rs parallel_map_impl / parallel_filter_impl: parallelMap and
  parallelFilter on a grown array read raw (*arr).length after the
  NaN-box strip.
- thread.rs serialize_array: a stale array pointer crossing the thread
  boundary (as an element of a crossed array, or as a worker's return
  value) serialized with a garbage length — the old GcHeader still
  reads GC_TYPE_ARRAY, so serialize_nanbox_for_thread dispatched
  straight into the raw read.

Hardened (raw deref real, stale caller not currently reachable
in-tree): value_call.rs js_closure_call_apply_with_spread — in-tree
codegen pre-resolves the spread source via js_array_like_to_array
(whose real-Array arm runs clean_arr_ptr), but the helper is
no_mangle + stdlib-FFI-declared, so its contract accepts a raw handle;
it now re-cleans rather than leaning on upstream cleaning for memory
safety.

Audited, no behavior change: typed_feedback.rs guards
(plain_array_index_guard, numeric_array_push_guard) already reject
GC_FLAG_FORWARDED headers before their raw length/capacity reads
(since #1752), so stale grown pointers deterministically fall back to
the chain-following slow path. Documented with an audit note marking
the arm load-bearing.

Tests: test_gap_6518_spread_call_grown_array.ts (node-parity,
byte-identical) pins end-to-end spread-of-grown-array behavior; new
grown_array_crossing case in run_thread_tests.sh covers the
perry/thread paths node cannot mirror (fails pre-fix, passes
post-fix).

An unrelated pre-existing bug found while writing the repros (spawn
capture of an async-fn local array crosses as empty) is filed as
#6520.
proggeramlug pushed a commit that referenced this pull request Jul 17, 2026
…pread calls (#6518)

The #6486 family: js_array_grow moves a push-grown array and leaves a
GC_FLAG_FORWARDED stub at the old address (#233), so a caller-held
pre-grow pointer raw-dereferenced as (*arr).length reads the forwarding
pointer's bytes as the element count. #6517 fixed the from-array
construction paths; this closes out the remaining readers from the
#6518 audit, each verified individually.

Confirmed bugs (negative-control verified — reverting these two files
crashes the new grown_array_crossing case):

- thread.rs parallel_map_impl / parallel_filter_impl: parallelMap and
  parallelFilter on a grown array read raw (*arr).length after the
  NaN-box strip.
- thread.rs serialize_array: a stale array pointer crossing the thread
  boundary (as an element of a crossed array, or as a worker's return
  value) serialized with a garbage length — the old GcHeader still
  reads GC_TYPE_ARRAY, so serialize_nanbox_for_thread dispatched
  straight into the raw read.

Hardened (raw deref real, stale caller not currently reachable
in-tree): value_call.rs js_closure_call_apply_with_spread — in-tree
codegen pre-resolves the spread source via js_array_like_to_array
(whose real-Array arm runs clean_arr_ptr), but the helper is
no_mangle + stdlib-FFI-declared, so its contract accepts a raw handle;
it now re-cleans rather than leaning on upstream cleaning for memory
safety.

Element reads at all touched sites (serialize_array, the map/filter
serialize loops, single_thread_map/filter, the spread arg copy) go
through js_array_get_f64 instead of raw pointer walks, following the
rule established in #6517 review: a sparse array (length > capacity,
far slots in ARRAY_NAMED_PROPS) legally passes clean_arr_ptr, so
walking length raw slots reads out of bounds. The accessor also
normalizes the TAG_HOLE sentinel to undefined, which the raw copies
leaked into arg buffers and cross-thread payloads.

Audited, no behavior change: typed_feedback.rs guards
(plain_array_index_guard, numeric_array_push_guard) already reject
GC_FLAG_FORWARDED headers before their raw length/capacity reads
(since #1752), so stale grown pointers deterministically fall back to
the chain-following slow path. Documented with an audit note marking
the arm load-bearing.

Tests: test_gap_6518_spread_call_grown_array.ts (node-parity,
byte-identical, incl. holes-spread-as-undefined) pins end-to-end
spread-of-grown-array behavior; new grown_array_crossing case in
run_thread_tests.sh covers the perry/thread paths node cannot mirror
(fails pre-fix, passes post-fix), incl. holes crossing as undefined.

An unrelated pre-existing bug found while writing the repros (spawn
capture of an async-fn local array crosses as empty) is filed as
#6520.
proggeramlug added a commit that referenced this pull request Jul 17, 2026
…pread calls (#6518) (#6521)

* fix(runtime): resolve array forwarding stubs in thread crossing and spread calls (#6518)

The #6486 family: js_array_grow moves a push-grown array and leaves a
GC_FLAG_FORWARDED stub at the old address (#233), so a caller-held
pre-grow pointer raw-dereferenced as (*arr).length reads the forwarding
pointer's bytes as the element count. #6517 fixed the from-array
construction paths; this closes out the remaining readers from the
#6518 audit, each verified individually.

Confirmed bugs (negative-control verified — reverting these two files
crashes the new grown_array_crossing case):

- thread.rs parallel_map_impl / parallel_filter_impl: parallelMap and
  parallelFilter on a grown array read raw (*arr).length after the
  NaN-box strip.
- thread.rs serialize_array: a stale array pointer crossing the thread
  boundary (as an element of a crossed array, or as a worker's return
  value) serialized with a garbage length — the old GcHeader still
  reads GC_TYPE_ARRAY, so serialize_nanbox_for_thread dispatched
  straight into the raw read.

Hardened (raw deref real, stale caller not currently reachable
in-tree): value_call.rs js_closure_call_apply_with_spread — in-tree
codegen pre-resolves the spread source via js_array_like_to_array
(whose real-Array arm runs clean_arr_ptr), but the helper is
no_mangle + stdlib-FFI-declared, so its contract accepts a raw handle;
it now re-cleans rather than leaning on upstream cleaning for memory
safety.

Element reads at all touched sites (serialize_array, the map/filter
serialize loops, single_thread_map/filter, the spread arg copy) go
through js_array_get_f64 instead of raw pointer walks, following the
rule established in #6517 review: a sparse array (length > capacity,
far slots in ARRAY_NAMED_PROPS) legally passes clean_arr_ptr, so
walking length raw slots reads out of bounds. The accessor also
normalizes the TAG_HOLE sentinel to undefined, which the raw copies
leaked into arg buffers and cross-thread payloads.

Audited, no behavior change: typed_feedback.rs guards
(plain_array_index_guard, numeric_array_push_guard) already reject
GC_FLAG_FORWARDED headers before their raw length/capacity reads
(since #1752), so stale grown pointers deterministically fall back to
the chain-following slow path. Documented with an audit note marking
the arm load-bearing.

Tests: test_gap_6518_spread_call_grown_array.ts (node-parity,
byte-identical, incl. holes-spread-as-undefined) pins end-to-end
spread-of-grown-array behavior; new grown_array_crossing case in
run_thread_tests.sh covers the perry/thread paths node cannot mirror
(fails pre-fix, passes post-fix), incl. holes crossing as undefined.

An unrelated pre-existing bug found while writing the repros (spawn
capture of an async-fn local array crosses as empty) is filed as
#6520.

* fix(runtime): root the closure across parallelMap/parallelFilter GC points (#6521 review)

CodeRabbit flagged that single_thread_map/single_thread_filter root the
input array and re-derive it every iteration (the callback can trigger
a moving minor GC) but passed the closure as a raw unrooted pointer —
a moved closure left later iterations calling through a dangling
capture block. Root it alongside the array (before the result
allocation, which is itself a GC point) and re-derive it per iteration,
mirroring util_promisify's closure-rooting discipline.

Also hardened the impl-level ordering the review didn't flag: in
parallel_map_impl/parallel_filter_impl the closure func_ptr deref and
downstream capture-serialization derefs sat AFTER the clean_arr_ptr
call this PR added, and clean_arr_ptr can force-materialize a lazy
array — a GC point. Validate + root the closure first, resolve the
array, then re-derive the closure from its handle.

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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.

2 participants