Skip to content

[iOS][Fabric] Stack overflow on Hermes GC thread ("hades") recursively destroying chained TextInputState revisions #57193

@Lawliang

Description

@Lawliang

Description

After a session of normal typing/interaction with <TextInput> components, the app crashes with:

EXC_BAD_ACCESS (SIGBUS), KERN_PROTECTION_FAILURE
"Thread stack size exceeded due to excessive recursion"
Faulting thread: "hades" (Hermes GC executor)

The faulting stack is a repeating recursive destructor cycle (~36 frames per link), unwinding a linked chain of text-input state revisions:

TextInputState::~TextInputState()
  → AttributedString::~AttributedString()        (reactTreeAttributedString)
    → AttributedString::Fragment::~Fragment()
      → ShadowView::~ShadowView()                (Fragment::parentShadowView)
        → shared_ptr<State const>::~shared_ptr   (ShadowView::state)
          → ConcreteState<TextInputState>::~ConcreteState()
            → TextInputState::~TextInputState()  ← previous revision, recurse

Mechanism (verified against 0.81.5 sources)

Each text-input state revision retains the previous revision:

  • TextInputState stores the input's content as AttributedString reactTreeAttributedString
    (ReactCommon/react/renderer/components/textinput/TextInputState.h:45)
  • Each AttributedString::Fragment carries a full ShadowView parentShadowView
    (ReactCommon/react/renderer/attributedstring/AttributedString.h:33)
  • ShadowView retains State::Shared state — the state the node had when the fragment
    was built, i.e. the previous TextInputState

So every state revision (keystroke, native text update, re-commit) appends one link to an
unbounded chain. Nothing trims it. When the last reference is finally dropped from JS, the
Hermes GC releases it on its background executor thread ("hades"), whose stack is small —
and the chain is destroyed via nested destructor recursion, one ~36-frame cascade per
revision. A few hundred to a few thousand accumulated revisions on one input is enough to
overflow that thread's stack.

Two distinct problems compound here:

  1. Unbounded retention: state revision N should not transitively retain revisions
    N-1…0 through reactTreeAttributedString fragments' parentShadowView.state.
  2. Recursive destruction on a small-stack thread: even bounded chains are destroyed
    recursively; on the GC executor thread the headroom is far less than the main/JS thread,
    so the crash appears "randomly" depending on which thread drops the last reference.

Steps to reproduce

Observed in a production-style app (Expo SDK 54, new architecture), not yet minimized:

  1. Mount a controlled multiline <TextInput value={text} onChangeText={setText} />
  2. Generate many state revisions on that one mount (typing/editing — hundreds+)
  3. Unmount / navigate away and let the Hermes GC collect the retained ShadowNode reference
  4. Crash on the hades thread with the stack above

Full .ips crash report available on request.

Expected behavior

Old TextInputState revisions are released incrementally (or destroyed iteratively), and
no recursion proportional to edit count occurs on the GC thread.

Environment

  • React Native: 0.81.5 (Fabric / new architecture, Hermes)
  • Expo SDK 54 (dev client)
  • Observed on: iOS 26.3 simulator (iPhone 16e), macOS 26.3 / MacBookPro18,1 — but the
    mechanism is platform-independent C++ in the renderer; nothing simulator-specific
  • The TextInputs involved are ordinary controlled inputs (no custom native code touching them)

Crash excerpt (faulting thread, first link of the cycle)

 0 React  std::__1::__shared_count::__release_shared()
 4 React  facebook::react::State::~State()
 5 React  facebook::react::ConcreteState<facebook::react::TextInputState>::~ConcreteState()
15 React  facebook::react::ShadowView::~ShadowView()
17 React  facebook::react::AttributedString::Fragment::~Fragment()
26 React  facebook::react::AttributedString::~AttributedString()
28 React  facebook::react::TextInputState::~TextInputState()
34 React  std::__1::__shared_count::__release_shared()   ← next link, repeats to stack exhaustion
…
   hermes::vm::HadesGC::collectOGInBackground()
   hermes::vm::HadesGC::Executor::worker()
   _pthread_start

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Author FeedbackNeeds: ReproThis issue could be improved with a clear list of steps to reproduce the issue.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions