Skip to content

fix(runtime): reject unregistered box pointers in js_box_set/get (#4898)#4902

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4898-typed-feedback-sigbus
Jun 10, 2026
Merged

fix(runtime): reject unregistered box pointers in js_box_set/get (#4898)#4902
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4898-typed-feedback-sigbus

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #4898react-reconciler@0.33 crashed at module init under Perry with a SIGBUS (exit 138) before any user statement ran. After this change the standalone repro prints reached and exits 0, and createReconciler({...}) constructs a working reconciler (createContainer is a function) where main SIGBUSes.

Root cause

Deep-triage (thanks @proggeramlug) pinpointed the faulting instruction as str d0, [x0] inside js_box_set, with x0 = the address of the read-only "object_get_by_name_guard" string constant in __TEXT.__cstring.

  • A miscompiled js_box_set call receives a box-pointer operand that is effectively undef/poison at the IR level (a mutable-capture box whose allocation was elided on the taken path).
  • Under the typed-feedback (chore: triage dead-code warnings (fields, variants, constants, dead assignments) #854) register_site instrumentation — react-reconciler emits ~2500 of these in one giant minified function — the value left live in the callee-saved register that regalloc reuses for the undef operand is the ..._guard diagnostic string passed to js_typed_feedback_register_site.
  • That constant is >=0x1000, untagged (top-16 zero), and 8-byte aligned, so it passed every structural check in is_plausible_box_ptr, and the write landed in read-only memory.

The IR itself never stores into the guard global (verified across all --trace llvm modules — every guard global is used only as a register_site arg); the bad pointer is an optimizer/regalloc artifact of the undef operand, which is why a defensive runtime check is the robust fix.

Fix

Gate js_box_set / js_box_get on a new is_registered_box_ptr, which additionally requires the address to be in BOX_REGISTRY (every pointer js_box_alloc ever minted). Boxes are never freed, so the registry is monotonic per thread: membership has no false negatives for a real box and no stale-reuse hazard. A pointer that was never alloc'd as a box is provably not a box and is skipped — the same correctness-safe silent no-op the structural checks already performed for bad pointers (#924).

Validation

  • import createReconciler from 'react-reconciler'; console.log('reached', ...)reached function, exit 0 (was exit 138).
  • createReconciler({...full host config...})reconciler built: object createContainer: function, exit 0 (was exit 138).
  • Closure mutable-capture smoke (counter inc/dec, accumulating adder, for-let loop captures) → unchanged, correct.
  • Two new unit tests (box_set_skips_unregistered_plausible_pointer, box_set_get_roundtrips_for_real_box).
  • Full perry-runtime suite passes (one pre-existing, unrelated date::tests::test_full_year_setters_revive_invalid_date_only failure also present on main).

Notes

  • This unblocks the standalone SIGBUS and the createReconciler factory call. The deeper codegen bug (emitting js_box_set with an undef box-pointer operand) is worth a separate follow-up, but the runtime gate is correctness-safe regardless of where the stray pointer originates.
  • No version bump / CHANGELOG entry per maintainer convention (folded in at merge).

react-reconciler@0.33 crashed at module init under Perry with a SIGBUS
(exit 138) before any user statement ran. The faulting instruction is
`str d0, [x0]` inside `js_box_set`, with x0 pointing at the read-only
`"object_get_by_name_guard"` string constant in __TEXT.__cstring.

Root cause: a miscompiled `js_box_set` call receives a box-pointer
operand that is effectively undef/poison at the IR level (a mutable-
capture box whose allocation was elided on the taken path). Under the
typed-feedback (#854) `register_site` instrumentation — react-reconciler
emits ~2500 of these in one giant minified function — the value left
live in the callee-saved register that regalloc reuses for the undef
operand is the `..._guard` diagnostic string. That constant is >=0x1000,
untagged (top-16 zero), and 8-byte aligned, so it passed every check in
`is_plausible_box_ptr` and the write landed in read-only memory.

Fix: gate `js_box_set`/`js_box_get` on `is_registered_box_ptr`, which
additionally requires the address to be in `BOX_REGISTRY` (every pointer
`js_box_alloc` ever minted). Boxes are never freed, so the registry is
monotonic per thread: membership has no false negatives for a real box
and no stale-reuse hazard. A pointer that was never alloc'd as a box is
provably not a box and is skipped — the same correctness-safe silent
no-op the structural checks already performed for bad pointers (#924).

Result: the standalone `import createReconciler from 'react-reconciler'`
repro prints "reached" and exits 0, and `createReconciler({...})` now
constructs a working reconciler (createContainer is a function) where
main SIGBUSes. Closure mutable-capture semantics are unchanged. Adds two
unit tests; full perry-runtime suite passes (one pre-existing, unrelated
date-test failure also present on main).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant