From 2962b0d0dfbd08b536bb8ee3028bbfdab43d7cba Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 25 Feb 2026 10:40:00 -0800 Subject: [PATCH] Fix SIGSEGV in ShadowNode::getTag() caused by use-after-free in findShadowNodeByTag_DEPRECATED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: A SIGSEGV crash is occurring in production when `ShadowNode::getTag()` is called on a destroyed ShadowNode during focus navigation (`FabricUIManagerBinding::findNextFocusableElement`). **Root cause**: `UIManager::findShadowNodeByTag_DEPRECATED` captures a **raw pointer** to the root shadow node inside a `tryCommit` callback, then dereferences it **after the lock is released**. Another thread can commit a new tree in between, destroying the old root and leaving a dangling pointer: ``` tryCommit([&](const RootShadowNode& old) { rootShadowNode = &old; // capture raw address return nullptr; // cancel commit, release lock }, {}); // !!! LOCK RELEASED — another thread can replace + destroy the old root rootShadowNode->getChildren(); // use-after-free → SIGSEGV ``` **Fix**: Replace the `tryCommit` + raw pointer pattern with `ShadowTree::getCurrentRevision()`, which returns a `ShadowTreeRevision` by value containing a `shared_ptr`. The `shared_ptr` copy keeps the root node alive for the entire traversal. **Why the old root gets destroyed**: After a commit, the old root's `shared_ptr` in `currentRevision_` is replaced. The `MountingCoordinator::push()` also overwrites `lastRevision_`. If no other holder remains (e.g. `baseRevision_` holds an earlier root), the old root's refcount drops to 0 and it is freed — while the finder thread may still hold a raw pointer to it. Changelog: [Internal] Differential Revision: D94376636 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 78 +++-- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../react/renderer/uimanager/UIManager.cpp | 36 ++- .../tests/FindShadowNodeByTagTest.cpp | 287 ++++++++++++++++++ .../ReactNativeFeatureFlags.config.js | 11 + .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 22 files changed, 476 insertions(+), 62 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/tests/FindShadowNodeByTagTest.cpp diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 71dbe51fbb87..3e0e99e6af11 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<477777b9a795b57f3bb3eaeb030738a9>> */ /** @@ -354,6 +354,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableVirtualViewDebugFeatures(): Boolean = accessor.enableVirtualViewDebugFeatures() + /** + * Fix a use-after-free race condition in findShadowNodeByTag_DEPRECATED by using getCurrentRevision() instead of tryCommit() with a raw pointer. + */ + @JvmStatic + public fun fixFindShadowNodeByTagRaceCondition(): Boolean = accessor.fixFindShadowNodeByTagRaceCondition() + /** * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 307703b6beca..bbd27b4d6433 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -74,6 +74,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableViewRecyclingForViewCache: Boolean? = null private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var enableVirtualViewDebugFeaturesCache: Boolean? = null + private var fixFindShadowNodeByTagRaceConditionCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null private var fixTextClippingAndroid15useBoundsForWidthCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null @@ -590,6 +591,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fixFindShadowNodeByTagRaceCondition(): Boolean { + var cached = fixFindShadowNodeByTagRaceConditionCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fixFindShadowNodeByTagRaceCondition() + fixFindShadowNodeByTagRaceConditionCache = cached + } + return cached + } + override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean { var cached = fixMappingOfEventPrioritiesBetweenFabricAndReactCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index a78d0fce88a2..5aa0113e15e4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -136,6 +136,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableVirtualViewDebugFeatures(): Boolean + @DoNotStrip @JvmStatic public external fun fixFindShadowNodeByTagRaceCondition(): Boolean + @DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean @DoNotStrip @JvmStatic public external fun fixTextClippingAndroid15useBoundsForWidth(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 908a14911cf0..0950fb163232 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<65cdfdcbe22ff163b75e0b067bd72693>> + * @generated SignedSource<<523e3c35d4bd1fc85f2a3bb26b8aad3f>> */ /** @@ -131,6 +131,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableVirtualViewDebugFeatures(): Boolean = false + override fun fixFindShadowNodeByTagRaceCondition(): Boolean = false + override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false override fun fixTextClippingAndroid15useBoundsForWidth(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index d1855bb311de..656dbba20689 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7d8e2872030e38ccb038d9d7aab214a2>> + * @generated SignedSource<> */ /** @@ -78,6 +78,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableViewRecyclingForViewCache: Boolean? = null private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var enableVirtualViewDebugFeaturesCache: Boolean? = null + private var fixFindShadowNodeByTagRaceConditionCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null private var fixTextClippingAndroid15useBoundsForWidthCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null @@ -648,6 +649,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fixFindShadowNodeByTagRaceCondition(): Boolean { + var cached = fixFindShadowNodeByTagRaceConditionCache + if (cached == null) { + cached = currentProvider.fixFindShadowNodeByTagRaceCondition() + accessedFeatureFlags.add("fixFindShadowNodeByTagRaceCondition") + fixFindShadowNodeByTagRaceConditionCache = cached + } + return cached + } + override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean { var cached = fixMappingOfEventPrioritiesBetweenFabricAndReactCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index fd4d2c67b9b5..bb2f3ac62adf 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<460d442da5dc25a441b671e7b30e7e56>> + * @generated SignedSource<> */ /** @@ -131,6 +131,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableVirtualViewDebugFeatures(): Boolean + @DoNotStrip public fun fixFindShadowNodeByTagRaceCondition(): Boolean + @DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean @DoNotStrip public fun fixTextClippingAndroid15useBoundsForWidth(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index d1a6753476f0..15c606bb8072 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8d00a6310e0fae475007f11fa56a5a6f>> + * @generated SignedSource<<45063df01d7ce8726b4a7d901f1b3341>> */ /** @@ -363,6 +363,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fixFindShadowNodeByTagRaceCondition() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixFindShadowNodeByTagRaceCondition"); + return method(javaProvider_); + } + bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixMappingOfEventPrioritiesBetweenFabricAndReact"); @@ -811,6 +817,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableVirtualViewDebugFeatures( return ReactNativeFeatureFlags::enableVirtualViewDebugFeatures(); } +bool JReactNativeFeatureFlagsCxxInterop::fixFindShadowNodeByTagRaceCondition( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition(); +} + bool JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); @@ -1149,6 +1160,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableVirtualViewDebugFeatures", JReactNativeFeatureFlagsCxxInterop::enableVirtualViewDebugFeatures), + makeNativeMethod( + "fixFindShadowNodeByTagRaceCondition", + JReactNativeFeatureFlagsCxxInterop::fixFindShadowNodeByTagRaceCondition), makeNativeMethod( "fixMappingOfEventPrioritiesBetweenFabricAndReact", JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index d9ecbbcc8de2..f5cd383bdeeb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5ac93ed057017f8d1a388b8029614f18>> */ /** @@ -192,6 +192,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableVirtualViewDebugFeatures( facebook::jni::alias_ref); + static bool fixFindShadowNodeByTagRaceCondition( + facebook::jni::alias_ref); + static bool fixMappingOfEventPrioritiesBetweenFabricAndReact( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index eba0b2cc236f..37f971aad1bb 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<20f3c045c134a0dae21f8428c6f8714a>> + * @generated SignedSource<<9d81f74c5926706ee353813e594575e8>> */ /** @@ -242,6 +242,10 @@ bool ReactNativeFeatureFlags::enableVirtualViewDebugFeatures() { return getAccessor().enableVirtualViewDebugFeatures(); } +bool ReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition() { + return getAccessor().fixFindShadowNodeByTagRaceCondition(); +} + bool ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact() { return getAccessor().fixMappingOfEventPrioritiesBetweenFabricAndReact(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index ca2bbf6a1a82..8a1ca2766b22 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<20d4471389baccef0854624bb31550a5>> + * @generated SignedSource<<84e2800073ffab2313a4e27897c0c246>> */ /** @@ -309,6 +309,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableVirtualViewDebugFeatures(); + /** + * Fix a use-after-free race condition in findShadowNodeByTag_DEPRECATED by using getCurrentRevision() instead of tryCommit() with a raw pointer. + */ + RN_EXPORT static bool fixFindShadowNodeByTagRaceCondition(); + /** * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 12fcccce9c2f..aafd52dd5889 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<416f7a3c9d67d992e1c1cc6b7a89d1de>> + * @generated SignedSource<<8f9f3ced66040f8275073e5084765356>> */ /** @@ -1001,6 +1001,24 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewDebugFeatures() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fixFindShadowNodeByTagRaceCondition() { + auto flagValue = fixFindShadowNodeByTagRaceCondition_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(54, "fixFindShadowNodeByTagRaceCondition"); + + flagValue = currentProvider_->fixFindShadowNodeByTagRaceCondition(); + fixFindShadowNodeByTagRaceCondition_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAndReact() { auto flagValue = fixMappingOfEventPrioritiesBetweenFabricAndReact_.load(); @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(55, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextClippingAndroid15useBoundsForWidth( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "fixTextClippingAndroid15useBoundsForWidth"); + markFlagAsAccessed(56, "fixTextClippingAndroid15useBoundsForWidth"); flagValue = currentProvider_->fixTextClippingAndroid15useBoundsForWidth(); fixTextClippingAndroid15useBoundsForWidth_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(57, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "fuseboxEnabledRelease"); + markFlagAsAccessed(58, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(59, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "hideOffscreenVirtualViewsOnIOS"); + markFlagAsAccessed(60, "hideOffscreenVirtualViewsOnIOS"); flagValue = currentProvider_->hideOffscreenVirtualViewsOnIOS(); hideOffscreenVirtualViewsOnIOS_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(61, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "perfIssuesEnabled"); + markFlagAsAccessed(62, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "perfMonitorV2Enabled"); + markFlagAsAccessed(63, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1172,7 +1190,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "preparedTextCacheSize"); + markFlagAsAccessed(64, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(65, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(66, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(67, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(68, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipToPaddingWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "syncAndroidClipToPaddingWithOverflow"); + markFlagAsAccessed(69, "syncAndroidClipToPaddingWithOverflow"); flagValue = currentProvider_->syncAndroidClipToPaddingWithOverflow(); syncAndroidClipToPaddingWithOverflow_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(70, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(71, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(72, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(73, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1352,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "useFabricInterop"); + markFlagAsAccessed(74, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(75, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "useNestedScrollViewAndroid"); + markFlagAsAccessed(76, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useSharedAnimatedBackend"); + markFlagAsAccessed(77, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(78, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useTurboModuleInterop"); + markFlagAsAccessed(79, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "useTurboModules"); + markFlagAsAccessed(80, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1478,7 +1496,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "viewCullingOutsetRatio"); + markFlagAsAccessed(81, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "viewTransitionEnabled"); + markFlagAsAccessed(82, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1514,7 +1532,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "virtualViewPrerenderRatio"); + markFlagAsAccessed(83, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 606c99835000..28f655e04c0e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<85777d1bf9cf533a094526f0111b9451>> + * @generated SignedSource<<6ba661fd9ce6aeff6cc9269848230618>> */ /** @@ -86,6 +86,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableViewRecyclingForView(); bool enableVirtualViewContainerStateExperimental(); bool enableVirtualViewDebugFeatures(); + bool fixFindShadowNodeByTagRaceCondition(); bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); bool fixTextClippingAndroid15useBoundsForWidth(); bool fuseboxAssertSingleHostState(); @@ -126,7 +127,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 83> accessedFeatureFlags_; + std::array, 84> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -182,6 +183,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableViewRecyclingForView_; std::atomic> enableVirtualViewContainerStateExperimental_; std::atomic> enableVirtualViewDebugFeatures_; + std::atomic> fixFindShadowNodeByTagRaceCondition_; std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; std::atomic> fixTextClippingAndroid15useBoundsForWidth_; std::atomic> fuseboxAssertSingleHostState_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 7567e6bc597f..e17c94e91f53 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<861e4a47ad9b2aa4ac054059082724a0>> */ /** @@ -243,6 +243,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool fixFindShadowNodeByTagRaceCondition() override { + return false; + } + bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 12600a05e054..56c0e084c0f9 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<850aaecd3bb86f9c317b341d8165e74a>> + * @generated SignedSource<<774ffd15e1fd9a79bb7b3f6c4719ba23>> */ /** @@ -531,6 +531,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableVirtualViewDebugFeatures(); } + bool fixFindShadowNodeByTagRaceCondition() override { + auto value = values_["fixFindShadowNodeByTagRaceCondition"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixFindShadowNodeByTagRaceCondition(); + } + bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override { auto value = values_["fixMappingOfEventPrioritiesBetweenFabricAndReact"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 0c808bf136fe..cadf99532f4c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8627bb40ea07b1f305a431f52edee642>> + * @generated SignedSource<> */ /** @@ -79,6 +79,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableViewRecyclingForView() = 0; virtual bool enableVirtualViewContainerStateExperimental() = 0; virtual bool enableVirtualViewDebugFeatures() = 0; + virtual bool fixFindShadowNodeByTagRaceCondition() = 0; virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; virtual bool fixTextClippingAndroid15useBoundsForWidth() = 0; virtual bool fuseboxAssertSingleHostState() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 5861c6a67d22..2d845642de15 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<10ec5c102c5fb90b74e78d7198bf959a>> + * @generated SignedSource<<0356babbe4e65071e6cc56a06e68b944>> */ /** @@ -314,6 +314,11 @@ bool NativeReactNativeFeatureFlags::enableVirtualViewDebugFeatures( return ReactNativeFeatureFlags::enableVirtualViewDebugFeatures(); } +bool NativeReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition(); +} + bool NativeReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index cd28cac97b88..bfb867589ab0 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<2b82eb6d91d0b4437aa3b25f0488fe3e>> */ /** @@ -144,6 +144,8 @@ class NativeReactNativeFeatureFlags bool enableVirtualViewDebugFeatures(jsi::Runtime& runtime); + bool fixFindShadowNodeByTagRaceCondition(jsi::Runtime& runtime); + bool fixMappingOfEventPrioritiesBetweenFabricAndReact(jsi::Runtime& runtime); bool fixTextClippingAndroid15useBoundsForWidth(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 815283ff8f83..d49a865037ad 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -522,20 +522,30 @@ std::shared_ptr UIManager::findShadowNodeByTag_DEPRECATED( auto shadowNode = std::shared_ptr{}; shadowTreeRegistry_.enumerate([&](const ShadowTree& shadowTree, bool& stop) { + // Obtain a pointer to the root node. The flag-gated path uses + // getCurrentRevision() which keeps the root alive via shared_ptr for + // the entire traversal, fixing a use-after-free race condition. + RootShadowNode::Shared rootShadowNodeHolder; const RootShadowNode* rootShadowNode = nullptr; - // The public interface of `ShadowTree` discourages accessing a stored - // pointer to a root node because of the possible data race. - // To work around this, we ask for a commit and immediately cancel it - // returning `nullptr` instead of a new shadow tree. - // We don't want to add a way to access a stored pointer to a root - // node because this `findShadowNodeByTag` is deprecated. It is only - // added to make migration to the new architecture easier. - shadowTree.tryCommit( - [&](const RootShadowNode& oldRootShadowNode) { - rootShadowNode = &oldRootShadowNode; - return nullptr; - }, - {/* default commit options */}); + if (ReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition()) { + rootShadowNodeHolder = shadowTree.getCurrentRevision().rootShadowNode; + rootShadowNode = rootShadowNodeHolder.get(); + } else { + // TODO(T257154369): Remove after flag rollout. + // The public interface of `ShadowTree` discourages accessing a stored + // pointer to a root node because of the possible data race. + // To work around this, we ask for a commit and immediately cancel it + // returning `nullptr` instead of a new shadow tree. + // We don't want to add a way to access a stored pointer to a root + // node because this `findShadowNodeByTag` is deprecated. It is only + // added to make migration to the new architecture easier. + shadowTree.tryCommit( + [&](const RootShadowNode& oldRootShadowNode) { + rootShadowNode = &oldRootShadowNode; + return nullptr; + }, + {/* default commit options */}); + } if (rootShadowNode != nullptr) { const auto& children = rootShadowNode->getChildren(); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FindShadowNodeByTagTest.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FindShadowNodeByTagTest.cpp new file mode 100644 index 000000000000..3d3f49d4b41a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FindShadowNodeByTagTest.cpp @@ -0,0 +1,287 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class FindShadowNodeByTagTestFeatureFlags + : public ReactNativeFeatureFlagsDefaults { + public: + bool fixFindShadowNodeByTagRaceCondition() override { + return true; + } +}; + +class FindShadowNodeByTagTest : public ::testing::Test { + public: + FindShadowNodeByTagTest() { + ReactNativeFeatureFlags::override( + std::make_unique()); + + contextContainer_ = std::make_shared(); + + ComponentDescriptorProviderRegistry componentDescriptorProviderRegistry{}; + auto eventDispatcher = EventDispatcher::Shared{}; + auto componentDescriptorRegistry = + componentDescriptorProviderRegistry.createComponentDescriptorRegistry( + ComponentDescriptorParameters{ + .eventDispatcher = eventDispatcher, + .contextContainer = contextContainer_, + .flavor = nullptr}); + + componentDescriptorProviderRegistry.add( + concreteComponentDescriptorProvider()); + componentDescriptorProviderRegistry.add( + concreteComponentDescriptorProvider()); + + builder_ = std::make_unique(componentDescriptorRegistry); + + RuntimeExecutor runtimeExecutor = + [](std::function&& /*callback*/) {}; + uiManager_ = + std::make_unique(runtimeExecutor, contextContainer_); + uiManager_->setComponentDescriptorRegistry(componentDescriptorRegistry); + + buildAndCommitTree(); + } + + void TearDown() override { + if (!surfaceStopped_) { + uiManager_->stopSurface(surfaceId_); + } + ReactNativeFeatureFlags::dangerouslyReset(); + } + + protected: + std::shared_ptr buildTree() { + std::shared_ptr rootNode; + + // clang-format off + auto element = + Element() + .tag(1) + .surfaceId(surfaceId_) + .reference(rootNode) + .props([] { + auto sharedProps = std::make_shared(); + auto& props = *sharedProps; + props.layoutConstraints = LayoutConstraints{ + .minimumSize = {.width = 0, .height = 0}, + .maximumSize = {.width = 500, .height = 500}}; + auto& yogaStyle = props.yogaStyle; + yogaStyle.setDimension( + yoga::Dimension::Width, + yoga::StyleSizeLength::points(500)); + yogaStyle.setDimension( + yoga::Dimension::Height, + yoga::StyleSizeLength::points(500)); + return sharedProps; + }) + .children({ + Element() + .tag(viewTag_) + .surfaceId(surfaceId_) + .props([] { + auto sharedProps = std::make_shared(); + auto& yogaStyle = sharedProps->yogaStyle; + yogaStyle.setDimension( + yoga::Dimension::Width, + yoga::StyleSizeLength::points(100)); + yogaStyle.setDimension( + yoga::Dimension::Height, + yoga::StyleSizeLength::points(100)); + return sharedProps; + }) + }) + .finalize([](RootShadowNode& shadowNode) { + shadowNode.layoutIfNeeded(); + shadowNode.sealRecursive(); + }); + // clang-format on + + builder_->build(element); + return rootNode; + } + + void buildAndCommitTree() { + auto rootNode = buildTree(); + + auto layoutConstraints = LayoutConstraints{}; + auto layoutContext = LayoutContext{}; + auto shadowTree = std::make_unique( + surfaceId_, + layoutConstraints, + layoutContext, + *uiManager_, + *contextContainer_); + + shadowTreePtr_ = shadowTree.get(); + + shadowTree->commit( + [&rootNode](const RootShadowNode& /*oldRootShadowNode*/) { + return std::static_pointer_cast(rootNode); + }, + {true}); + + uiManager_->startSurface( + std::move(shadowTree), + "test", + folly::dynamic::object, + DisplayMode::Visible); + } + + SurfaceId surfaceId_{0}; + Tag viewTag_{42}; + bool surfaceStopped_{false}; + std::shared_ptr contextContainer_; + std::unique_ptr builder_; + std::unique_ptr uiManager_; + ShadowTree* shadowTreePtr_{nullptr}; +}; + +TEST_F(FindShadowNodeByTagTest, FindExistingNode) { + auto found = uiManager_->findShadowNodeByTag_DEPRECATED(viewTag_); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->getTag(), viewTag_); +} + +TEST_F(FindShadowNodeByTagTest, FindNonExistentNode) { + auto found = uiManager_->findShadowNodeByTag_DEPRECATED(9999); + EXPECT_EQ(found, nullptr); +} + +TEST_F( + FindShadowNodeByTagTest, + RawPointerFromTryCommitDanglesAfterSurfaceStop) { + // Observe root lifetime via weak_ptr + std::weak_ptr weakRoot; + { + auto rev = shadowTreePtr_->getCurrentRevision(); + weakRoot = rev.rootShadowNode; + } + ASSERT_FALSE(weakRoot.expired()); + + // Simulate the old (buggy) pattern: capture raw pointer via tryCommit. + // This is exactly what findShadowNodeByTag_DEPRECATED used to do. + const RootShadowNode* rawPtr = nullptr; + shadowTreePtr_->tryCommit( + [&](const RootShadowNode& oldRoot) { + rawPtr = &oldRoot; + return nullptr; // cancel commit + }, + {}); + ASSERT_NE(rawPtr, nullptr); + ASSERT_EQ(rawPtr, weakRoot.lock().get()); + + // Stop the surface — releases all internal references (ShadowTree's + // currentRevision_ and MountingCoordinator's baseRevision_) + { + auto tree = uiManager_->stopSurface(surfaceId_); + surfaceStopped_ = true; + // tree goes out of scope here, destroying ShadowTree + MountingCoordinator + } + + // Old root is now destroyed. rawPtr is dangling. + EXPECT_TRUE(weakRoot.expired()) + << "Old root should be destroyed after surface stop, " + "proving that the raw pointer captured from tryCommit is dangling"; +} + +TEST_F(FindShadowNodeByTagTest, SharedPtrFromRevisionSurvivesSurfaceStop) { + // The fixed pattern: getCurrentRevision() returns a shared_ptr copy + auto revision = shadowTreePtr_->getCurrentRevision(); + std::weak_ptr weakRoot = revision.rootShadowNode; + ASSERT_FALSE(weakRoot.expired()); + + // Stop the surface — releases all internal references + { + auto tree = uiManager_->stopSurface(surfaceId_); + surfaceStopped_ = true; + } + + // Old root is STILL alive — revision's shared_ptr keeps it alive + EXPECT_FALSE(weakRoot.expired()) + << "Revision's shared_ptr should keep the root alive"; + + // Safely traverse the old tree even after the surface was stopped + const auto& children = revision.rootShadowNode->getChildren(); + ASSERT_FALSE(children.empty()); + EXPECT_EQ(children.front()->getTag(), viewTag_); +} + +TEST_F(FindShadowNodeByTagTest, ConcurrentFindAndCommitStress) { + // Stress test: multiple threads finding nodes while others rapidly commit + // new same-family tree clones. With the old tryCommit + raw pointer pattern, + // a committer can destroy the root between the time the finder captures the + // raw pointer and dereferences it, causing a use-after-free detectable by + // ASAN/TSAN. + constexpr int kNumFinderThreads = 4; + constexpr int kNumCommitterThreads = 2; + constexpr auto kDuration = std::chrono::seconds(2); + + std::atomic stop{false}; + std::atomic findCount{0}; + std::atomic commitCount{0}; + + std::vector threads; + threads.reserve(kNumFinderThreads); + + // Finder threads: repeatedly search for the node by tag + for (int i = 0; i < kNumFinderThreads; i++) { + threads.emplace_back([&]() { + while (!stop.load(std::memory_order_relaxed)) { + auto found = uiManager_->findShadowNodeByTag_DEPRECATED(viewTag_); + if (found) { + EXPECT_EQ(found->getTag(), viewTag_); + findCount.fetch_add(1, std::memory_order_relaxed); + } + } + }); + } + + // Committer threads: rapidly replace the tree with same-family clones. + // Using ShadowNode::clone() is much faster than buildTree() (no layout/ + // allocation overhead), maximizing commit throughput and the probability + // of hitting the race window. + for (int i = 0; i < kNumCommitterThreads; i++) { + threads.emplace_back([&]() { + while (!stop.load(std::memory_order_relaxed)) { + shadowTreePtr_->commit( + [](const RootShadowNode& oldRoot) { + return std::static_pointer_cast( + oldRoot.ShadowNode::clone(ShadowNodeFragment{})); + }, + {}); + commitCount.fetch_add(1, std::memory_order_relaxed); + } + }); + } + + std::this_thread::sleep_for(kDuration); + stop.store(true, std::memory_order_relaxed); + for (auto& t : threads) { + t.join(); + } + + EXPECT_GT(findCount.load(), 0); + EXPECT_GT(commitCount.load(), 0); +} + +} // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index cf8407d6c82a..e8e429ba609c 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -625,6 +625,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + fixFindShadowNodeByTagRaceCondition: { + defaultValue: false, + metadata: { + dateAdded: '2026-02-25', + description: + 'Fix a use-after-free race condition in findShadowNodeByTag_DEPRECATED by using getCurrentRevision() instead of tryCommit() with a raw pointer.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, fixMappingOfEventPrioritiesBetweenFabricAndReact: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 7143af0bc992..7939dec2f939 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<97e082b7554dba5b4f387ef783d2d6e3>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -101,6 +101,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ enableViewRecyclingForView: Getter, enableVirtualViewContainerStateExperimental: Getter, enableVirtualViewDebugFeatures: Getter, + fixFindShadowNodeByTagRaceCondition: Getter, fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, fixTextClippingAndroid15useBoundsForWidth: Getter, fuseboxAssertSingleHostState: Getter, @@ -412,6 +413,10 @@ export const enableVirtualViewContainerStateExperimental: Getter = crea * Enables VirtualView debug features such as logging and overlays. */ export const enableVirtualViewDebugFeatures: Getter = createNativeFlagGetter('enableVirtualViewDebugFeatures', false); +/** + * Fix a use-after-free race condition in findShadowNodeByTag_DEPRECATED by using getCurrentRevision() instead of tryCommit() with a raw pointer. + */ +export const fixFindShadowNodeByTagRaceCondition: Getter = createNativeFlagGetter('fixFindShadowNodeByTagRaceCondition', false); /** * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 2a0efe4a94a5..128f27b7f32e 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<50a267a6a302105663a150f2f3a8ef0c>> + * @generated SignedSource<<3495a8cf9d3d820e37d082b3d62e2b91>> * @flow strict * @noformat */ @@ -79,6 +79,7 @@ export interface Spec extends TurboModule { +enableViewRecyclingForView?: () => boolean; +enableVirtualViewContainerStateExperimental?: () => boolean; +enableVirtualViewDebugFeatures?: () => boolean; + +fixFindShadowNodeByTagRaceCondition?: () => boolean; +fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; +fixTextClippingAndroid15useBoundsForWidth?: () => boolean; +fuseboxAssertSingleHostState?: () => boolean;