Skip to content

fix(codegen,runtime): entry-initialize boxed slots — eliminate undef box-pointer operands (#4926)#4931

Merged
proggeramlug merged 1 commit into
mainfrom
fix/box-undef-ptr-4926
Jun 10, 2026
Merged

fix(codegen,runtime): entry-initialize boxed slots — eliminate undef box-pointer operands (#4926)#4931
proggeramlug merged 1 commit into
mainfrom
fix/box-undef-ptr-4926

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Fixes #4926 — the deferred source bug behind the #4898 react-reconciler SIGBUS (runtime gate shipped in #4902).

Root cause

The boxed-var Stmt::Let path hoisted the slot alloca to the entry block (so it dominates all uses), but the store of the box pointer into that slot only runs where the Let executes. Any boxed read/write on a control-flow path that skips the Let — switch fallthrough into a later case, sibling-branch closure capture — loaded an uninitialized slot. LLVM folds that load to undef, and regalloc substitutes whatever register happens to be live. Under typed-feedback (#854) density (react-reconciler emits ~2500 register_site calls in one minified function), that live value was the read-only "object_get_by_name_guard" cstring pointer → js_box_set wrote into __TEXT.__cstring → SIGBUS. PreallocateBoxes (#569) had the same non-dominating-store shape when nested inside If/Try/Labeled bodies.

Fix

  • codegen (stmt/let_stmt.rs, stmt/mod.rs): entry-initialize every boxed slot with TAG_UNDEFINED via entry_allocas_push_store — exactly what the non-boxed Let path already did. Skipped-init paths now read a defined sentinel that even the structural checks alone reject deterministically; no more undef, no more regalloc roulette.
  • runtime (box.rs): js_box_get on an unregistered pointer returns undefined instead of bare NaN — the JS value of a read-before-initialization variable (Perry has no TDZ). TAG_UNDEFINED is a quiet-NaN bit pattern, so numeric consumers behave exactly as before; typeof/null-checks now see undefined.
  • e2e test (crates/perry/tests/issue_4926_boxed_slot_skipped_init.rs): switch-case skipped-init boxed read/write must be deterministic undefined, taken paths unchanged.

Verification

  • Mechanism repro (switch-case let captured+mutated by a closure, entered at the later case): pre-fix PERRY_DEBUG=1 showed js_box_get: invalid box pointer 0x1 (undef materialized as garbage) and typeof x printed number; post-fix the pointer is exactly the sentinel 0x7ffc000000000001 and typeof x prints undefined.
  • react-reconciler (rr_min.tsx from react-reconciler 0.33 init crashes under Perry — SIGBUS on import (standalone) / 'undefined is not iterable' in createReconciler (in-ink); blocks all custom React renderers #4898): the stray js_box_set operand collapses from arbitrary __TEXT garbage (0x1016e9360) to the deterministic sentinel — proving the rr stray call was this exact mechanism and is now defined behavior end to end. createReconciler({...full hostconfig}) still builds a working reconciler.
  • Zero regressions: 225-file test_gap_* sweep vs node --experimental-strip-types — failure sets byte-identical between baseline (clean main) and fixed builds (190 pass / 35 pre-existing, the only output diffs being ASLR addresses and thread IDs in panic backtraces). Full perry-runtime suite green single-threaded (1013 passed; the 1 failure, date::tests::test_full_year_setters_revive_invalid_date_only, fails on main too). cargo fmt clean; all touched files under the 2000-line gate.

No version bump / CHANGELOG entry per maintainer convention (folded in at merge).

…box-pointer operands (#4926)

The boxed-var Stmt::Let path hoisted the slot alloca to the entry block
but stored the box pointer only where the Let executes. Any boxed
read/write on a path that skips the Let (switch fallthrough, sibling-
branch capture) loaded an uninitialized slot; LLVM folded the load to
undef and regalloc substituted whatever register was live — under
typed-feedback density that was a read-only guard-string constant,
the source of the #4898 SIGBUS.

- codegen: entry-init every boxed slot (Stmt::Let + PreallocateBoxes)
  with TAG_UNDEFINED, mirroring the non-boxed path. Skipped-init paths
  now read a defined sentinel that the runtime rejects
  deterministically.
- runtime: js_box_get on an unregistered pointer returns undefined
  (the JS read-before-initialization value; Perry has no TDZ) instead
  of bare NaN. TAG_UNDEFINED is a quiet-NaN bit pattern, so numeric
  consumers are unchanged.
- e2e test: switch-case skipped-init boxed read/write must be
  deterministic undefined.

Verified: react-reconciler stray js_box_set operand collapses from
arbitrary __TEXT garbage to the sentinel; 225-test gap sweep has
byte-identical failure sets vs baseline (zero regressions); runtime
suite green (1 pre-existing date failure, on main too).
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.

codegen: js_box_set/js_box_get emitted with undef box-pointer operand (mutable-capture box alloc elided on taken path) — source bug behind #4898

1 participant