Skip to content

[Fabric][iOS] Differ creates a mounted view again when a child with negative zIndex moves in a nested flatten/unflatten #58647

Description

@pawicao

Description

The Fabric differ (Differentiator.cpp) makes incorrect mutations for one tree shape. It creates a view that is already mounted. Or it deletes a view that only moves.

The tree has three nested views: Q > U > F. Q and F have opacity. F has three children. The second child (K) has zIndex: -1.

When the opacity moves from F to U in one commit, U unflattens and F flattens. The children of F move to U. The opposite change also causes the bug.

Result:

  • iOS Debug: the app crashes on the first change. RCTComponentViewRegistry asserts: "Attempt to dequeue already registered component."
  • iOS Release: the app crashes with SIGSEGV in -[RCTMountingManager performTransaction:] (I saw this on 0.88.0-rc.2).
  • Android: the app does not crash. The Android mounting layer ignores a Create for a tag that it already has. The differ output is still incorrect.

If you remove zIndex: -1 from K, the differ output is correct.

Cause (suspected):

This below cause came from AI assisted analysis of the situation

In calculateShadowViewMutationsFlattener, the differ sorts the children by orderIndex. Thus it visits K before F. At that time, nothing has matched K, so K becomes a delete or create candidate. Then the nested (un)flattening of F matches K through a different ShadowViewNodePair. It records K in subVisitedNewMap or subVisitedOldMap. But the last loop over the candidates checks only treeChildPair.inOtherTree(). Thus the differ deletes or creates K.

A related bug is in the same function. In the "flatten parent, unflatten child" branch, auto unvisitedItPair = *unvisitedOtherNodesIt->second; keeps the address of a local copy in unvisitedRecursiveChildPairs. This pointer is not valid after the loop iteration.

We found this bug through crash reports in react-native-reanimated (Layout Animations). The reproducer does not use Reanimated or any other library.

Steps to reproduce

  1. Clone https://github.com/pawicao/rn-differ-zindex-flatten-repro.
  2. Go to the ReproducerApp directory.
  3. Run yarn install.
  4. Run cd ios && bundle install && bundle exec pod install && cd ...
  5. Run yarn ios.
  6. Wait approximately 0.5 seconds. The app changes the tree automatically.
  7. Notice the crash.

React Native Version

0.87.1

Affected Platforms

Runtime - iOS

Output of npx @react-native-community/cli info

System:
  OS: macOS 27.0
  CPU: (10) arm64 Apple M4
  Memory: 271.25 MB / 32.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.23.2
    path: /Users/oskarpawica/.nvm/versions/node/v22.23.2/bin/node
  Yarn:
    version: 1.22.22
    path: /Users/oskarpawica/.nvm/versions/node/v22.23.2/bin/yarn
  npm:
    version: 10.9.8
    path: /Users/oskarpawica/.nvm/versions/node/v22.23.2/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.0
      - iOS 27.0
      - macOS 27.0
      - tvOS 27.0
      - visionOS 27.0
      - watchOS 27.0
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.1 AI-251.26094.121.2513.14007798
  Xcode:
    version: 27.0/27A266a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.17
    path: /opt/homebrew/opt/openjdk@17/bin/javac
  Ruby:
    version: 3.4.5
    path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.2.0
    wanted: 20.2.0
  react:
    installed: 19.2.3
    wanted: 19.2.3
  react-native:
    installed: 0.87.1
    wanted: 0.87.1
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'RCTComponentViewRegistry: Attempt to dequeue already registered component.'

9  libobjc.A.dylib   objc_exception_throw
10 Foundation        -[NSAssertionHandler handleFailureInFunction:file:lineNumber:description:]
11 React             -[RCTComponentViewRegistry dequeueComponentViewWithComponentHandle:tag:]
12 React             RCTPerformMountInstructions(std::vector<facebook::react::ShadowViewMutation> const&, RCTComponentViewRegistry*, RCTMountingTransactionObserverCoordinator&, int)
13 React             -[RCTMountingManager performTransaction:]::$_2::operator()(facebook::react::MountingTransaction const&, facebook::react::SurfaceTelemetry const&) const
20 React             facebook::react::TelemetryController::pullTransaction(...)
21 React             -[RCTMountingManager performTransaction:]
22 React             -[RCTMountingManager initiateTransaction:]
23 React             __42-[RCTMountingManager scheduleTransaction:]_block_invoke
24 React             __RCTExecuteOnMainQueue_block_invoke

Mutations from the differ (tags from a trace of the same tree):

U unflattens, F flattens:
Remove 146 <- 148 @2 | Remove 142 <- 148 @1 | Remove 144 <- 148 @0 | Create 150 | Create 144 | Insert 150 -> 152 @1 | Insert 144 -> 150 @0 | ... | Delete 148
                                                                                  ^^^^^^^^^^ K is mounted

U flattens, F unflattens:
Remove 182 <- 186 @2 | Remove 178 <- 186 @1 | Remove 180 <- 186 @0 | Remove 186 <- 188 @0 | Delete 186 | Delete 180 | Create 184 | Insert 180 -> 184 @0 | ...
                                                                                                          ^^^^^^^^^^ K moves, but the differ deletes it

MANDATORY Reproducer

https://github.com/pawicao/rn-differ-zindex-flatten-repro

Screenshots and Videos

rn-differ-crash-ios.mp4

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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