Skip to content

fix(gc): js_array_length is classified AllocNoReentry but its Proxy arm runs user JS (get trap + number coercion) #11522

Description

@proggeramlug

Problem

crates/perry-codegen/src/gc_call_effects.rs (~L280–291) lists js_array_length as AllocNoReentry ("allocates but never re-enters user code"). The audit comment says it "takes a typed *const ArrayHeader, not a JSValue". That's no longer true. js_array_length (crates/perry-runtime/src/array/indexing.rs ~L225) has a #5135 arm for a Proxy statically typed as an array (immer drafts):

if let Some(proxy) = array_ptr_as_proxy(arr) {
    let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6);
    ...
    let n = crate::builtins::js_number_coerce(crate::proxy::js_proxy_get(proxy, key_f64));

js_proxy_get runs the user's get trap, and js_number_coerce can run valueOf/Symbol.toPrimitive. Both are arbitrary user JS, which can allocate, collect, and move objects. Under the safepoint-only contract (gc_safepoint_only_contract_enabled()), AllocNoReentry callees are marked "gc-leaf-function" by lower_roots_for_rs4gc (crates/perry-codegen/src/function/precise_roots.rs), so the caller's live GC values are not relocated across the call. A collection inside the trap then leaves the caller holding stale from-space pointers.

A measurement over the runtime's call graph also flagged js_object_alloc_class_inline_keys / js_object_alloc_class_inline_keys_stamped and js_object_get_own_field_or_undef (the latter listed around ~L209) as reaching JS. Verify each one.

Fix

  • Move js_array_length out of AllocNoReentry to the unknown (collecting) class, or split it: keep a leaf fast path (the plain-array lane at the top) and route the Proxy/Set/Map/object arms through a separately named collecting entry point that codegen calls on the slow path.
  • Audit js_object_alloc_class_inline_keys* and js_object_get_own_field_or_undef the same way, and fix the classification or the code.
  • Update the audit comments so they state what was actually checked.
  • Consider a test that fails when a classified helper's call graph reaches a JS-invoking symbol (js_proxy_get, js_native_call_value, coercion entry points). That would make this whole class checkable.

Verify

  • A regression test: new Proxy([], { get(t, k) { if (k === "length") { /* allocate a lot and force a GC */ ... return 3 } return Reflect.get(t, k) } }), read .length through a path that lowers to js_array_length, with live heap values in the caller used afterwards. Run under PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1, and assert a copying minor ran. It must match node, and must fail (wrong output or a verifier panic) before the fix. Sabotage-check it.
  • cargo test --release -p perry-codegen (the gc_call_effects tests) and the runtime suite pass.

Rules for the PR (repo conventions)

  • Code + tests + a changelog.d/<PR>-<slug>.md fragment. No version bump (don't touch [workspace.package].version, the CLAUDE.md version line, or Cargo.lock versions).
  • Every new regression test must be sabotage-checked: revert the fix, confirm the test goes red, restore. Say so in the PR body.
  • perry-runtime tests must run with RUST_TEST_THREADS=1. Build -p perry -p perry-runtime-static -p perry-stdlib-static together (the .a archives come from the -static wrappers).
  • Run scripts/run_lint_gates.sh (or at least cargo fmt --all -- --check, scripts/check_file_size.sh, python3 scripts/raw_handle_debt.py, python3 scripts/addr_class_inventory.py, python3 scripts/gc_runtime_root_holders.py, python3 scripts/gc_rekeyed_key_tables.py).
  • Self-contained: no private bundle or special host needed. A normal Linux or macOS dev box is enough.

Filed from the 2026-09 side-table / RSS audit; line numbers are against main e379a7a and may drift.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regressionrustPull requests that update rust code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions