Skip to content

[Fabric/iOS] RNGestureHandlerDetectorShadowNode can clone children with stale yoga ownership, causing YGNodeGetOwner assert #4051

@John-Colvin

Description

@John-Colvin

Description

Warning, the following text in this section is AI generated based on a session of me debugging this problem with GPT-5.4 in OpenCode, but the problem is very real & the proposed fix does work for me. Feel free to close, ignore or just tell me "go away" if you don't want issues like this, I understand it's a burden on maintainers to have to read through what might be slop.

Summary

On iOS, InterceptingGestureDetector / RNGestureHandlerDetectorShadowNode can crash during layout with:

react_native_assert(YGNodeGetOwner(childYogaNode) == &yogaNode_);

from YogaLayoutableShadowNode::layout.
The problem appears to be in RNGestureHandlerDetectorShadowNode::unflattenNode(...), which currently clones detector children with node->clone({}).

Crash

The native stack looks like:

facebook::react::YogaLayoutableShadowNode::layout
facebook::react::RNGestureHandlerDetectorShadowNode::layout
facebook::react::YogaLayoutableShadowNode::layout
...

The assert is in React Native here:

// YogaLayoutableShadowNode.cpp
react_native_assert(YGNodeGetOwner(childYogaNode) == &yogaNode_);

Root Cause

RNGestureHandlerDetectorShadowNode does custom child cloning/unflattening:

  • appendChild(...) calls unflattenNode(child)
  • replaceChild(...) calls unflattenNode(newChild)
  • unflattenNode(...) does node->clone({})
    The issue is that clone({}) interacts badly with RN Fabric's YogaLayoutableShadowNode clone semantics:
  1. RN clone ctor copies the source yoga node.
  2. It calls updateYogaChildrenOwnersIfNeeded(), which invalidates ownership of direct yoga children on the clone.
  3. Because the fragment passed to clone(...) has no explicit children, the clone also inherits yogaTreeHasBeenConfigured_ from the source node.
  4. Later, Fabric may skip reconfiguring that cloned subtree in configureYogaTree() because it appears "already configured".
  5. That means the cloned node's direct yoga children can still have stale/non-parent owners when layout runs.
  6. YogaLayoutableShadowNode::layout() then asserts when it sees a child yoga node that is not owned by the detector yoga node.
    So the problem is not just "layout churn" or app-specific structure. The detector is creating a cloned subtree whose descendant yoga ownership has been invalidated, while also preserving enough configuration state for RN to skip the repair path.

Relevant RNGH code

Current code in RNGestureHandlerDetectorShadowNode.cpp:

std::shared_ptr<const ShadowNode>
RNGestureHandlerDetectorShadowNode::unflattenNode(
    const std::shared_ptr<const ShadowNode> &node) {
  auto clonedNode = node->clone({});
  auto clonedNodeWithProtectedAccess =
      std::static_pointer_cast<RNGestureHandlerDetectorShadowNode>(clonedNode);
  clonedNodeWithProtectedAccess->traits_.set(ShadowNodeTraits::FormsView);
  clonedNodeWithProtectedAccess->traits_.set(
      ShadowNodeTraits::FormsStackingContext);
  return clonedNode;
}

Local Fix That Stops The Crash

Changing unflattenNode(...) to clone with an explicit children fragment fixes the crash locally for me:

std::shared_ptr<const ShadowNode>
RNGestureHandlerDetectorShadowNode::unflattenNode(
    const std::shared_ptr<const ShadowNode> &node) {
  auto clonedNode = node->clone({
      .props = ShadowNodeFragment::propsPlaceholder(),
      .children =
          std::make_shared<const std::vector<std::shared_ptr<const ShadowNode>>>(
              node->getChildren()),
      .state = ShadowNodeFragment::statePlaceholder(),
  });
  auto clonedNodeWithProtectedAccess =
      std::static_pointer_cast<RNGestureHandlerDetectorShadowNode>(clonedNode);
  clonedNodeWithProtectedAccess->traits_.set(ShadowNodeTraits::FormsView);
  clonedNodeWithProtectedAccess->traits_.set(
      ShadowNodeTraits::FormsStackingContext);
  return clonedNode;
}

My understanding is that this works because passing a real children fragment forces RN to rebuild the cloned subtree's yoga children instead of preserving stale configured state from the source clone.

Why I Think This Is The Correct Direction

This change:

  • fixes the crash locally
  • preserves gesture behaviour
  • directly targets the ownership/configuration mismatch between RNGH's synthetic child clone and RN Fabric's yoga ownership model
    Extra Note
    There is also another suspicious implementation detail in this file: in layout(...), arbitrary children are static_pointer_cast to RNGestureHandlerDetectorShadowNode just to access protected members:
auto childWithProtectedAccess =
    std::static_pointer_cast<const RNGestureHandlerDetectorShadowNode>(
        yogaChild);

That seems unsafe / UB if the child is not actually a detector node, though I do not think that is the direct cause of this specific assert.

Steps to reproduce

This bit is me, not AI:

It's really difficult for me to replicate this in something I can share, I only see it in my real project which is complex & not open source. The only hint I can give is that I have structure like this View A ( View B ( [many more nested] ( InterceptingGestureDetector ( ... )))) with a useLayoutEffect containing a measure callback on a ref to B that then sets state to set height on A. Weird setup, I know, but I need to round sizes to pixel boundaries. Removing that resize stops the crash. But I can't get it to crash in a smaller setup, although I admit I haven't spent hours on it.

A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.

sorry, I really can't work this out in a small setup.

Gesture Handler version

3.0.0-nightly-20260331-9d671cb78

React Native version

0.83.4

Platforms

iOS

JavaScript runtime

Hermes

Workflow

Using Expo Prebuild or an Expo development build

Architecture

New Architecture (Fabric)

Build type

Debug mode

Device

Real device

Device model

iPhone 15 Pro Max, iOS 26.4

Acknowledgements

Yes

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions