Fix TextInput state revisions retaining their predecessor on Android - #58020
Fix TextInput state revisions retaining their predecessor on Android#58020entelostre wants to merge 1 commit into
Conversation
Summary: react#55719 added shadowViewFromShadowNode() to BaseTextInputShadowNode.h, nulling props/state on a fragment's parentShadowView so a text-input state revision does not retain the previous one. AndroidTextInputShadowNode is `final : public ConcreteViewShadowNode<...>` and builds its own fragment for the input's text value, so it never received that fix. ShadowView::state points at the AndroidTextInputState held when the fragment was built, and that fragment is stored into the next state by updateStateIfNeeded(), making revision N retain N-1. The chain grows one link per update and is destroyed by nested recursion, exhausting the native stack. Nothing reads Fragment::parentShadowView.props or .state - only tag, layoutMetrics and componentHandle are consumed - so clearing them is behaviour-preserving, including on the MapBuffer measurement path. Changelog: [Android] [Fixed] - Fix TextInput state revisions retaining their predecessor, causing a native stack overflow after many text updates
|
Hi @entelostre! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Summary:
#55719 added
shadowViewFromShadowNode()toBaseTextInputShadowNode.h, nullingprops/stateon a fragment'sparentShadowViewso a text-input state revision does not retain the previous one. It shipped in 0.85.0.AndroidTextInputShadowNodeisfinal : public ConcreteViewShadowNode<...>— it does not inheritBaseTextInputShadowNodeand builds its own fragment, so it never got the fix:ShadowView::statepoints at theAndroidTextInputStateheld when the fragment was built, andupdateStateIfNeeded()stores that fragment into the next state — so revision N retains N−1. The chain grows one link per update and is destroyed by nested recursion, exhausting the native stack (SIGSEGVon the guard page).Still present in 0.85-stable and 0.86-stable as well as
main. Android counterpart of #57193 (iOS, filed against 0.81.5, before #55719 shipped); that issue describes two problems — this patch addresses the unbounded retention, not the recursive destruction of the chain.The fix nulls the two fields inline rather than reusing
shadowViewFromShadowNode(), whose only copies are file-local inBaseTextShadowNode.cppand inBaseTextInputShadowNode.h— a base class this node does not inherit. Happy to lift it into a shared header instead if you prefer.Changelog:
[Android] [Fixed] - Fix TextInput native stack overflow caused by state revisions retaining their predecessor
Test Plan:
Safe: nothing in
ReactCommonreadsparentShadowView.propsor.state— after this patch the only occurrences are the two assignments it adds. Consumers usetag,layoutMetrics,componentHandleandeventEmitteronly. Measurement cache keys are unaffected:Fragment's hash and equality use.tag/.layoutMetricsdirectly and neverstd::hash<ShadowView>.Sufficient:
AndroidTextInputState::attributedStringBoxhas three writers — the initial state ({}, empty),updateStateIfNeeded()(storesgetAttributedString()or the previous box), and thefolly::dynamicconstructor (copies the previous box). With the base case empty andgetAttributedString()fixed here, induction gives that no fragment in either state field can carry a state pointer. Children are already covered byBaseTextShadowNode's own helper; the placeholder path uses{}.Evidence: symbolicated from a production crash (
llvm-addr2line -i, NDK 27, unstripped build-ID-matchedlibreactnative.so). The pattern repeats to stack exhaustion:~ShadowViewis inlined into~Fragment; itsstatemember is the link this patch removes.Measured on device. Two debug builds of the same app on a Samsung SM-G781B (Android 13), differing only by whether this patch is applied. Instrumentation in
updateStateIfNeeded()walkedstate → attributedStringBox → fragments → parentShadowView.stateand logged the chain length on every call:TextInputWithout the patch, chain length scaled with the number of state updates and showed no upper bound — 3 updates → 1, 26 → 11, 128 → 101, 920 → 757, with a peak of 791 across all inputs in a three-minute session. With the patch, every one of 4,673 samples read 0, under a slightly higher update load. At 791 links, teardown is on the order of 3,000 nested destructor frames.
Still not verified: that this eliminates the crash in the field. That needs a release with the fix and a crash-rate comparison. There is also no unit test — there is no host flavour of
components/textinput/platform/, no OSS CI job runs C++ tests, and Fantom does not linkrrc_textinput. Happy to add one if you can point me at a target that would run it, and to give full detail on any of the above.Affects 17 users across 16 Android device models (Lenovo, Samsung, Xiaomi, Oppo) in our production crash reporting, most recent 2026-08-14.