From 566d9314b3d3dce3545e727345c2d59b83f9becd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Thu, 12 Feb 2026 07:53:13 -0800 Subject: [PATCH] Fix crash in `cloneMultiple` when families have no path to root (#55491) Summary: Fixes crash in `ShadowNode::cloneMultipleRecursive` which tried to access a missing root's family from the `childrenCount` map. The main issue is that in some cases there is no path between the given families to update and the root shadow node. The solution is to check after building the `childrenCount` map if it contains the root family and return `nullptr` otherwise. This behavior is closer to the `getAncestors` method which returns an empty array if there is no ancestor-descendant relationship. Due to this change, an additional check in `AnimationBackendCommitHook` is also required. ## Changelog: [General][Fixed] - Fixed crash in `cloneMultiple` when families have no path to root Reviewed By: zeyap Differential Revision: D92838809 --- .../AnimationBackendCommitHook.cpp | 10 +++- .../react/renderer/core/ShadowNode.cpp | 2 +- .../renderer/core/tests/ShadowNodeTest.cpp | 55 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp index 19898d233e61..4a00926b0e2d 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp @@ -35,8 +35,8 @@ RootShadowNode::Unshared AnimationBackendCommitHook::shadowTreeWillCommit( if (surfaceFamilies.empty()) { return newRootShadowNode; } - return std::static_pointer_cast( - newRootShadowNode->cloneMultiple( + auto clonedRootShadowNode = + std::static_pointer_cast(newRootShadowNode->cloneMultiple( surfaceFamilies, [&surfaceFamilies, &updates]( const ShadowNode& shadowNode, @@ -73,6 +73,12 @@ RootShadowNode::Unshared AnimationBackendCommitHook::shadowTreeWillCommit( .state = shadowNode.getState(), .runtimeShadowNodeReference = true}); })); + + if (clonedRootShadowNode == nullptr) { + return newRootShadowNode; + } + + return clonedRootShadowNode; } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp index 1490d8746549..9b704b1a3eb9 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp @@ -471,7 +471,7 @@ std::shared_ptr ShadowNode::cloneMultiple( } } - if (childrenCount.empty()) { + if (!childrenCount.contains(&this->getFamily())) { return nullptr; } diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/ShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/ShadowNodeTest.cpp index 2ac02a75c3e2..bc4c5eb2b54e 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/ShadowNodeTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/ShadowNodeTest.cpp @@ -374,3 +374,58 @@ TEST_F(ShadowNodeTest, cloneMultipleWithSingleFamily) { EXPECT_EQ(newNodeABA->getTag(), nodeABA_->getTag()); EXPECT_EQ(newNodeABA.get(), nodeABA_.get()); } + +TEST_F(ShadowNodeTest, cloneMultipleReturnsNullptrWhenFamilyHasNoPathToRoot) { + auto newProps = std::make_shared(); + // nodeZ_ is not part of nodeA_'s tree + auto result = nodeA_->cloneMultiple( + {&nodeZ_->getFamily()}, + [&](const ShadowNode& oldShadowNode, const ShadowNodeFragment& fragment) { + return oldShadowNode.clone({ + .props = newProps, + .children = fragment.children, + .state = fragment.state, + }); + }); + + // cloneMultiple should return nullptr when the family has no path to the root + EXPECT_EQ(result, nullptr); +} + +TEST_F(ShadowNodeTest, cloneMultipleWithMixOfValidAndInvalidFamilies) { + auto newProps = std::make_shared(); + // nodeAB_ is in the tree, nodeZ_ is not + auto result = nodeA_->cloneMultiple( + {&nodeAB_->getFamily(), &nodeZ_->getFamily()}, + [&](const ShadowNode& oldShadowNode, const ShadowNodeFragment& fragment) { + return oldShadowNode.clone({ + .props = newProps, + .children = fragment.children, + .state = fragment.state, + }); + }); + + // Should still succeed and clone the valid family (nodeAB_) + EXPECT_NE(result, nullptr); + EXPECT_EQ(result->getTag(), nodeA_->getTag()); + EXPECT_EQ(result->getProps(), newProps); + + auto newNodeAB = result->getChildren()[1]; + EXPECT_EQ(newNodeAB->getTag(), nodeAB_->getTag()); + EXPECT_EQ(newNodeAB->getProps(), newProps); +} + +TEST_F(ShadowNodeTest, cloneMultipleWithEmptyFamilySet) { + auto newProps = std::make_shared(); + auto result = nodeA_->cloneMultiple( + {}, + [&](const ShadowNode& oldShadowNode, const ShadowNodeFragment& fragment) { + return oldShadowNode.clone({ + .props = newProps, + .children = fragment.children, + .state = fragment.state, + }); + }); + + EXPECT_EQ(result, nullptr); +}