Add guards around nativeProps usage to prevent race conditions (#52646)

Summary:
Some third-party libraries, like react-native-reanimated, can clone nodes in a different thread while react-native is calling `setNativeProps_DEPRECATED`. This results in a race condition, where a stale pointer to `nativeProps_DEPRECATED` can be accessed, resulting in a crash. This usually manifests as a `EXC_BAD_ACCESS` crash on iOS. On Android it seems more rare. We've added a lock around accesses to nativeProps_DEPRECATED, but alternative options of fixing this can be considered too.

For more information see https://github.com/software-mansion/react-native-reanimated/issues/7666

## Changelog:

[INTERNAL] [FIXED] - Fixed crashes caused by race conditions when third-party libraries clone the shadow dom from a different thread

Pull Request resolved: https://github.com/react/react-native/pull/52646

Test Plan:
Due to this being a race condition that only manifests in rare circumstances, it's very difficult to create a reliable reproduction case. The issue mentioned above contains ThreadSanitizer logs that demonstrate this issue. TSan no longer complains with this patch applied, and we've not seen any additional issues from it after deploying it in production over the past week.

Added unit test covering the `nativeProps_DEPRECATED` merge logic in `UIManager::cloneNode` and `ShadowNode::clone`:

```
buck2 test //xplat/js/react-native-github/packages/react-native/ReactCommon/react/renderer/uimanager:tests -- --regex FabricUIManagerTest
```

Reviewed By: zeyap

Differential Revision: D110169424

Pulled By: javache

fbshipit-source-id: 6139253dcc2c33348a0c1a3bd01e695d15aa83bc
This commit is contained in:
OrfeasZ
2026-07-01 13:01:48 -07:00
committed by meta-codesync[bot]
parent d1c5ee0388
commit 923e7ddcaf
13 changed files with 239 additions and 74 deletions
@@ -137,27 +137,35 @@ std::shared_ptr<ShadowNode> ShadowNode::clone(
const ShadowNodeFragment& fragment) const {
const auto& family = *family_;
const auto& componentDescriptor = family.componentDescriptor_;
if (family.nativeProps_DEPRECATED != nullptr) {
auto propsParserContext = PropsParserContext{family_->getSurfaceId(), {}};
if (fragment.props == ShadowNodeFragment::propsPlaceholder()) {
// Clone existing `props_` with `family.nativeProps_DEPRECATED` to apply
// previously set props via `setNativeProps` API.
auto props = componentDescriptor.cloneProps(
propsParserContext, props_, RawProps(*family.nativeProps_DEPRECATED));
auto clonedNode = componentDescriptor.cloneShadowNode(
*this,
{.props = props,
.children = fragment.children,
.state = fragment.state});
return clonedNode;
} else {
// TODO: We might need to merge fragment.props with
// `family.nativeProps_DEPRECATED`.
return componentDescriptor.cloneShadowNode(*this, fragment);
std::optional<RawProps> propsOverride;
{
std::lock_guard<std::mutex> lock(family.nativePropsMutex);
if (family.nativeProps_DEPRECATED != nullptr) {
if (fragment.props == ShadowNodeFragment::propsPlaceholder()) {
propsOverride.emplace(*family.nativeProps_DEPRECATED);
} else {
// TODO: We might need to merge fragment.props with
// `family.nativeProps_DEPRECATED`.
}
}
} else {
return componentDescriptor.cloneShadowNode(*this, fragment);
}
if (propsOverride) {
// Clone existing `props_` with `family.nativeProps_DEPRECATED` to
// apply previously set props via `setNativeProps` API. The parsed
// result escapes the lock so the rest of the clone work runs
// unblocked.
auto propsParserContext = PropsParserContext{family_->getSurfaceId(), {}};
return componentDescriptor.cloneShadowNode(
*this,
{.props = componentDescriptor.cloneProps(
propsParserContext, props_, std::move(*propsOverride)),
.children = fragment.children,
.state = fragment.state});
}
return componentDescriptor.cloneShadowNode(*this, fragment);
}
std::shared_ptr<const ContextContainer> ShadowNode::getContextContainer()
@@ -8,6 +8,7 @@
#pragma once
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <react/renderer/core/EventEmitter.h>
@@ -114,6 +115,7 @@ class ShadowNodeFamily final : public jsi::NativeState {
* architecture and will be removed in the future.
*/
mutable std::unique_ptr<folly::dynamic> nativeProps_DEPRECATED;
mutable std::mutex nativePropsMutex;
/**
* @return tag for the ShadowNodeFamily.
@@ -118,49 +118,51 @@ std::shared_ptr<ShadowNode> UIManager::cloneNode(
auto& componentDescriptor = shadowNode.getComponentDescriptor();
auto& family = shadowNode.getFamily();
auto props = ShadowNodeFragment::propsPlaceholder();
if (!rawProps.isEmpty()) {
if (family.nativeProps_DEPRECATED != nullptr) {
// 1. update the nativeProps_DEPRECATED props.
//
// In this step, we want the most recent value for the props
// managed by setNativeProps.
// Values in `rawProps` patch (take precedence over)
// `nativeProps_DEPRECATED`. For example, if both
// `nativeProps_DEPRECATED` and `rawProps` contain key 'A'.
// Value from `rawProps` overrides what was previously in
// `nativeProps_DEPRECATED`. Notice that the `nativeProps_DEPRECATED`
// patch will not get more props from `rawProps`: if the key is not
// present in `nativeProps_DEPRECATED`, it will not be added.
//
// The result of this operation is the new `nativeProps_DEPRECATED`.
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>(mergeDynamicProps(
*family.nativeProps_DEPRECATED, // source
(folly::dynamic)rawProps, // patch
NullValueStrategy::Ignore));
std::optional<folly::dynamic> finalProps;
{
std::lock_guard<std::mutex> lock(family.nativePropsMutex);
if (family.nativeProps_DEPRECATED != nullptr) {
// 1. update the nativeProps_DEPRECATED props.
//
// In this step, we want the most recent value for the props
// managed by setNativeProps.
// Values in `rawProps` patch (take precedence over)
// `nativeProps_DEPRECATED`. For example, if both
// `nativeProps_DEPRECATED` and `rawProps` contain key 'A'.
// Value from `rawProps` overrides what was previously in
// `nativeProps_DEPRECATED`. Notice that the `nativeProps_DEPRECATED`
// patch will not get more props from `rawProps`: if the key is not
// present in `nativeProps_DEPRECATED`, it will not be added.
//
// The result of this operation is the new `nativeProps_DEPRECATED`.
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>(mergeDynamicProps(
*family.nativeProps_DEPRECATED, // source
(folly::dynamic)rawProps, // patch
NullValueStrategy::Ignore));
// 2. Compute the final set of props.
//
// This step takes the new props handled by `setNativeProps` and
// merges them in the `rawProps` managed by React.
// The new props handled by `nativeProps` now takes precedence
// on the props handled by React, as we want to make sure that
// all the props are applied to the component.
// We use these finalProps as source of truth for the component.
auto finalProps = mergeDynamicProps(
(folly::dynamic)rawProps, // source
*family.nativeProps_DEPRECATED, // patch
NullValueStrategy::Override);
// 3. Clone the props by using finalProps.
props = componentDescriptor.cloneProps(
propsParserContext, shadowNode.getProps(), RawProps(finalProps));
} else {
props = componentDescriptor.cloneProps(
propsParserContext, shadowNode.getProps(), std::move(rawProps));
// 2. Compute the final set of props.
//
// This step takes the new props handled by `setNativeProps` and
// merges them in the `rawProps` managed by React.
// The new props handled by `nativeProps` now takes precedence
// on the props handled by React, as we want to make sure that
// all the props are applied to the component.
// We use these finalProps as source of truth for the component.
finalProps = mergeDynamicProps(
(folly::dynamic)rawProps, // source
*family.nativeProps_DEPRECATED, // patch
NullValueStrategy::Override);
}
}
props = componentDescriptor.cloneProps(
propsParserContext,
shadowNode.getProps(),
finalProps ? RawProps(std::move(*finalProps)) : std::move(rawProps));
}
auto clonedShadowNode = componentDescriptor.cloneShadowNode(
@@ -448,19 +450,22 @@ void UIManager::setNativeProps_DEPRECATED(
const std::shared_ptr<const ShadowNode>& shadowNode,
RawProps rawProps) const {
auto& family = shadowNode->getFamily();
if (family.nativeProps_DEPRECATED) {
// Values in `rawProps` patch (take precedence over)
// `nativeProps_DEPRECATED`. For example, if both `nativeProps_DEPRECATED`
// and `rawProps` contain key 'A'. Value from `rawProps` overrides what
// was previously in `nativeProps_DEPRECATED`.
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>(mergeDynamicProps(
*family.nativeProps_DEPRECATED,
(folly::dynamic)rawProps,
NullValueStrategy::Override));
} else {
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>((folly::dynamic)rawProps);
{
std::lock_guard<std::mutex> lock(family.nativePropsMutex);
if (family.nativeProps_DEPRECATED) {
// Values in `rawProps` patch (take precedence over)
// `nativeProps_DEPRECATED`. For example, if both
// `nativeProps_DEPRECATED` and `rawProps` contain key 'A'. Value from
// `rawProps` overrides what was previously in `nativeProps_DEPRECATED`.
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>(mergeDynamicProps(
*family.nativeProps_DEPRECATED,
(folly::dynamic)rawProps,
NullValueStrategy::Override));
} else {
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>((folly::dynamic)rawProps);
}
}
shadowTreeRegistry_.visit(
@@ -8,10 +8,151 @@
#include <memory>
#include <gtest/gtest.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include <react/renderer/core/ShadowNodeFragment.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
#include <react/renderer/uimanager/UIManager.h>
using namespace facebook::react;
namespace facebook::react {
TEST(UIManagerTest, testSomething) {
// TODO
class FabricUIManagerTest : public ::testing::Test {
public:
FabricUIManagerTest() {
contextContainer_ = std::make_shared<ContextContainer>();
ComponentDescriptorProviderRegistry componentDescriptorProviderRegistry{};
auto componentDescriptorRegistry =
componentDescriptorProviderRegistry.createComponentDescriptorRegistry(
ComponentDescriptorParameters{
.eventDispatcher = EventDispatcher::Shared{},
.contextContainer = contextContainer_,
.flavor = nullptr});
componentDescriptorProviderRegistry.add(
concreteComponentDescriptorProvider<RootComponentDescriptor>());
componentDescriptorProviderRegistry.add(
concreteComponentDescriptorProvider<ViewComponentDescriptor>());
builder_ = std::make_unique<ComponentBuilder>(componentDescriptorRegistry);
RuntimeExecutor runtimeExecutor =
[](std::function<void(
facebook::jsi::Runtime & runtime)>&& /*callback*/) {};
uiManager_ =
std::make_unique<UIManager>(runtimeExecutor, contextContainer_);
uiManager_->setComponentDescriptorRegistry(componentDescriptorRegistry);
buildAndCommitTree();
}
void TearDown() override {
uiManager_->stopSurface(surfaceId_);
}
protected:
std::shared_ptr<RootShadowNode> buildTree() {
std::shared_ptr<RootShadowNode> rootNode;
// clang-format off
auto element =
Element<RootShadowNode>()
.tag(1)
.surfaceId(surfaceId_)
.reference(rootNode)
.props([] {
auto sharedProps = std::make_shared<RootProps>();
sharedProps->layoutConstraints = LayoutConstraints{
.minimumSize = {.width = 0, .height = 0},
.maximumSize = {.width = 500, .height = 500}};
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.tag(viewTag_)
.surfaceId(surfaceId_)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
sharedProps->nativeId = "initial";
sharedProps->opacity = 1.0;
return sharedProps;
})
});
// clang-format on
builder_->build(element);
return rootNode;
}
void buildAndCommitTree() {
auto rootNode = buildTree();
auto shadowTree = std::make_unique<ShadowTree>(
surfaceId_,
LayoutConstraints{},
LayoutContext{},
*uiManager_,
*contextContainer_);
shadowTreePtr_ = shadowTree.get();
shadowTree->commit(
[&rootNode](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootNode);
},
{true});
uiManager_->startSurface(
std::move(shadowTree),
"test",
folly::dynamic::object,
DisplayMode::Visible);
}
std::shared_ptr<const ShadowNode> currentViewNode() const {
auto root = shadowTreePtr_->getCurrentRevision().rootShadowNode;
return root->getChildren().front();
}
const ViewProps& viewPropsInTree() const {
return static_cast<const ViewProps&>(*currentViewNode()->getProps());
}
SurfaceId surfaceId_{0};
Tag viewTag_{42};
std::shared_ptr<ContextContainer> contextContainer_;
std::unique_ptr<ComponentBuilder> builder_;
std::unique_ptr<UIManager> uiManager_;
ShadowTree* shadowTreePtr_{nullptr};
};
// Demonstrates the merge between props set by `setNativeProps_DEPRECATED`
// (the legacy native-side prop override path) and props supplied by a
// regular React re-render via `cloneNode`. The native override must
// survive a React render that does not touch the overridden key, and
// other React-supplied keys must still apply.
TEST_F(FabricUIManagerTest, SetNativePropsMergesWithReactRender) {
ASSERT_EQ(viewPropsInTree().nativeId, "initial");
ASSERT_FLOAT_EQ(viewPropsInTree().opacity, 1.0);
// Native override on `opacity` only. The committed tree picks up the new
// opacity and leaves the unrelated `nativeId` untouched.
uiManager_->setNativeProps_DEPRECATED(
currentViewNode(), RawProps(folly::dynamic::object("opacity", 0.5)));
EXPECT_EQ(viewPropsInTree().nativeId, "initial");
EXPECT_FLOAT_EQ(viewPropsInTree().opacity, 0.5);
// Simulate a React re-render that updates `nativeID` only. The native
// `opacity` override must survive (it is not present in rawProps and
// therefore not refreshed away by React), and the new `nativeID` must
// apply on top.
auto rerendered = uiManager_->cloneNode(
*currentViewNode(),
/*children=*/nullptr,
RawProps(folly::dynamic::object("nativeID", "from-react")));
auto& merged = static_cast<const ViewProps&>(*rerendered->getProps());
EXPECT_EQ(merged.nativeId, "from-react");
EXPECT_FLOAT_EQ(merged.opacity, 0.5);
}
} // namespace facebook::react
@@ -4660,6 +4660,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -4474,6 +4474,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -4651,6 +4651,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -6861,6 +6861,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -6703,6 +6703,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -6852,6 +6852,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -3214,6 +3214,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -3068,6 +3068,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;
@@ -3205,6 +3205,7 @@ class facebook::react::ShadowNodeFamily : public facebook::jsi::NativeState {
public facebook::react::SharedEventEmitter getEventEmitter() const;
public facebook::react::SurfaceId getSurfaceId() const;
public facebook::react::Tag getTag() const;
public mutable std::mutex nativePropsMutex;
public std::shared_ptr<const facebook::react::State> getMostRecentState() const;
public using AncestorList = std::vector<std::pair<std::reference_wrapper<const facebook::react::ShadowNode>, int>>;
public using Shared = std::shared_ptr<facebook::react::ShadowNodeFamily>;