Skip to content

[iOS][Fabric] SIGSEGV in createComponentViewWithComponentHandle: — unsynchronized map writes in _registerComponentIfPossible: race with main-thread mounting #58299

Description

@Pranjal-srivastav

Description

Summary

Release builds crash with SIGSEGV (fault address 0x18) on com.apple.main-thread in
-[RCTComponentViewFactory createComponentViewWithComponentHandle:] during Fabric mounting.
Observed in production on iOS 26.0.1, RN 0.86.2, New Architecture enabled, legacy interop layer in use.

The affected code is unchanged in 0.86.3 and 0.87.1 (I diffed
React/Fabric/Mounting/RCTComponentViewFactory.mm across tags — 0.86.3 is byte-identical;
0.87.1 only adds #ifdef RCT_REMOVE_LEGACY_COMPONENT_INTEROP around the interop fallback),
so this reproduces on latest stable.

Root cause

Two unsynchronized shared-state mutations, plus a release-only hard-crash path:

1. _registerComponentIfPossible: writes _componentViewClasses without holding _mutex.
It runs on the JS thread (via the hasComponentProvider binding installed by
RCTInstallNativeComponentRegistryBinding, and via the provider-request callback).
Fallbacks 1/2 are safe (they go through registerComponentViewClass:, which takes
std::unique_lock), but Fallback 3 (interop) and Fallback 4 (UnimplementedView) write
directly into _componentViewClasses and _registrationStatusMap with no lock
.
Meanwhile the main thread reads the same map in createComponentViewWithComponentHandle:
under a shared_lock — which protects nothing against a writer that never locks.
A concurrent insert can rehash the unordered_map mid-find.

2. On a lookup miss, release builds dereference the end() iterator.
RCTAssert(iterator != _componentViewClasses.end(), ...) is compiled out in release, and the
next line does iterator->second. libc++'s end() iterator has a null node pointer, so this
reads nullptr + 0x18 — matching the crash address exactly.

3. Same pattern in RCTLegacyViewManagerInteropComponentView isSupported: — Step 3
mutates the static NSMutableDictionary cache (_supportedLegacyViewComponents) from the
JS thread with no synchronization; NSMutableDictionary is not thread-safe.

Being a data race, it fires probabilistically — for us the rate jumped visibly on iOS 26
(changed thread timing), but the defect is version-independent.

Reproducer

  • Deterministic for the crash site: make _registerComponentIfPossible: skip one component
    name and mount it — release config crashes at the same address; debug shows the assert.
  • The race itself: Thread Sanitizer flags the unsynchronized _componentViewClasses access
    during cold start + navigation through interop-using screens.
  • Possibly related community reproducer (same RN version, same crash site during mounting):
    https://github.com/SpiGAndromeda/reproducer-rn-0862-symbolview-launch-crash

Fix we are shipping via patch-package

  1. _registerComponentIfPossible: takes std::unique_lock lock(_mutex) once at the top,
    covering the early-return check and Fallback 3/4 writes.
  2. To avoid self-deadlock (std::shared_mutex is non-recursive), registerComponentViewClass:
    is split into the public locking wrapper + a private _registerComponentViewClassLocked:
    body; Fallbacks 1/2 call the locked variant.
  3. _wasComponentRegistered: reads under a shared_lock.
  4. Release-build guard at the crash site: on end(), log via RCTLogError and mount
    RCTUnimplementedViewComponentView instead of dereferencing.
  5. isSupported: Step 3 wrapped in @synchronized on the dictionary it mutates.

Full patch attached below. Happy to open a PR if the approach looks right to maintainers.

Full patch (react-native+0.86.2, applied via patch-package)
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm
index 226781f..522a7cc 100644
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm
@@ -122,27 +122,30 @@ + (BOOL)isSupported:(NSString *)componentName
   NSArray<Class> *registeredModules = RCTGetModuleClasses();
   NSMutableDictionary<NSString *, Class> *supportedLegacyViewComponents =
       [RCTLegacyViewManagerInteropComponentView _supportedLegacyViewComponents];
-  if (supportedLegacyViewComponents[componentName] != NULL) {
-    return YES;
-  }
+  // Fix: static NSMutableDictionary mutated from multiple threads.
+  @synchronized(supportedLegacyViewComponents) {
+    if (supportedLegacyViewComponents[componentName] != NULL) {
+      return YES;
+    }
 
-  for (Class moduleClass in registeredModules) {
-    id<RCTBridgeModule> bridgeModule = (id<RCTBridgeModule>)moduleClass;
-    NSString *moduleName = [[bridgeModule moduleName] isEqualToString:@""]
-        ? [NSStringFromClass(moduleClass) stringByReplacingOccurrencesOfString:@"Manager" withString:@""]
-        : [bridgeModule moduleName];
+    for (Class moduleClass in registeredModules) {
+      id<RCTBridgeModule> bridgeModule = (id<RCTBridgeModule>)moduleClass;
+      NSString *moduleName = [[bridgeModule moduleName] isEqualToString:@""]
+          ? [NSStringFromClass(moduleClass) stringByReplacingOccurrencesOfString:@"Manager" withString:@""]
+          : [bridgeModule moduleName];
 
-    if (supportedLegacyViewComponents[moduleName] == NULL) {
-      supportedLegacyViewComponents[moduleName] = moduleClass;
-    }
+      if (supportedLegacyViewComponents[moduleName] == NULL) {
+        supportedLegacyViewComponents[moduleName] = moduleClass;
+      }
 
-    if ([moduleName isEqualToString:componentName] ||
-        [moduleName isEqualToString:[@"RCT" stringByAppendingString:componentName]]) {
-      return YES;
+      if ([moduleName isEqualToString:componentName] ||
+          [moduleName isEqualToString:[@"RCT" stringByAppendingString:componentName]]) {
+        return YES;
+      }
     }
-  }
 
-  return NO;
+    return NO;
+  }
 }
 
 + (void)supportLegacyViewManagersWithPrefix:(NSString *)prefix
diff --git a/node_modules/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm b/node_modules/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm
index 608d75c..d9f4c3b 100644
--- a/node_modules/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm
+++ b/node_modules/react-native/React/Fabric/Mounting/RCTComponentViewFactory.mm
@@ -48,6 +48,7 @@
 @interface RCTComponentViewFactory ()
 - (void)_registerComponentIfPossible:(const std::string &)name;
 - (BOOL)_wasComponentRegistered:(const std::string &)name;
+- (void)_registerComponentViewClassLocked:(Class<RCTComponentViewProtocol>)componentViewClass;
 @end
 
 // Allow JS runtime to register native components as needed. For static view configs.
@@ -115,12 +116,15 @@ - (RCTComponentViewClassDescriptor)_componentViewClassDescriptorFromClass:(Class
 
 - (BOOL)_wasComponentRegistered:(const std::string &)name
 {
+  std::shared_lock lock(_mutex);
   auto registrationResult = _registrationStatusMap.find(name);
   return registrationResult != _registrationStatusMap.end() && (registrationResult->second);
 }
 
 - (void)_registerComponentIfPossible:(const std::string &)name
 {
+  // Fix: fallback 3/4 below mutated the maps unguarded while the main thread reads them.
+  std::unique_lock lock(_mutex);
   if (_registrationStatusMap.find(name) != _registrationStatusMap.end()) {
     return;
   }
@@ -133,7 +137,7 @@ - (void)_registerComponentIfPossible:(const std::string &)name
   // Fallback 1: Call provider function for component view class.
   Class<RCTComponentViewProtocol> klass = RCTComponentViewClassWithName(name.c_str());
   if (klass != nullptr) {
-    [self registerComponentViewClass:klass];
+    [self _registerComponentViewClassLocked:klass];
     return;
   }
 
@@ -144,7 +148,7 @@ - (void)_registerComponentIfPossible:(const std::string &)name
     NSString *objcName = [NSString stringWithCString:name.c_str() encoding:NSUTF8StringEncoding];
     klass = self.thirdPartyFabricComponentsProvider.thirdPartyFabricComponents[objcName];
     if (klass != nullptr) {
-      [self registerComponentViewClass:klass];
+      [self _registerComponentViewClassLocked:klass];
       return;
     }
   }
@@ -185,7 +189,12 @@ - (void)registerComponentViewClass:(Class<RCTComponentViewProtocol>)componentVie
 {
   RCTAssert(componentViewClass, @"RCTComponentViewFactory: Provided `componentViewClass` is `nil`.");
   std::unique_lock lock(_mutex);
+  [self _registerComponentViewClassLocked:componentViewClass];
+}
 
+// Body of registerComponentViewClass:, callable by paths already holding _mutex
+// (std::shared_mutex is not recursive).
+- (void)_registerComponentViewClassLocked:(Class<RCTComponentViewProtocol>)componentViewClass
+{
   auto provider = [componentViewClass componentDescriptorProvider];
   _componentViewClasses[provider.handle] = [self _componentViewClassDescriptorFromClass:componentViewClass];
   _providerRegistry.add(provider);
@@ -209,6 +218,18 @@ - (RCTComponentViewDescriptor)createComponentViewWithComponentHandle:(facebook::
       @"ComponentView with componentHandle `%lli` (`%s`) not found.",
       componentHandle,
       (char *)componentHandle);
+  // Fix: RCTAssert is compiled out in release; dereferencing end() here was a SIGSEGV.
+  if (iterator == _componentViewClasses.end()) {
+    RCTLogError(@"ComponentView with componentHandle `%lli` not found, mounting fallback view.", componentHandle);
+    auto fallbackDescriptor = [self _componentViewClassDescriptorFromClass:[RCTUnimplementedViewComponentView class]];
+    Class fallbackViewClass = fallbackDescriptor.viewClass;
+    return RCTComponentViewDescriptor{
+        .view = [fallbackViewClass new],
+        .observesMountingTransactionWillMount = fallbackDescriptor.observesMountingTransactionWillMount,
+        .observesMountingTransactionDidMount = fallbackDescriptor.observesMountingTransactionDidMount,
+        .shouldBeRecycled = fallbackDescriptor.shouldBeRecycled,
+    };
+  }
   auto componentViewClassDescriptor = iterator->second;
   Class viewClass = componentViewClassDescriptor.viewClass;
 

Environment

  • react-native: 0.86.2 (code verified unchanged in 0.87.1)
  • New Architecture: enabled; legacy interop layer in use (~29 Paper view managers)
  • iOS 26.0.1, physical devices, release builds
  • Crash: SIGSEGV, fault address 0x0000000000000018, com.apple.main-thread

Steps to reproduce

This is a data race, so there is no tap-this-button repro. Three ways to observe it:

  1. Crash site, deterministic: in RCTComponentViewFactory.mm, make
    _registerComponentIfPossible: return early for one component name used by the app
    (simulating a lost race), build in Release configuration, and mount a screen using that
    component → SIGSEGV at fault address 0x18 in createComponentViewWithComponentHandle:.
    In Debug the RCTAssert fires instead, confirming the release/debug divergence.

  2. The race itself: enable Thread Sanitizer, cold-start an app that uses the legacy
    interop layer, and navigate quickly through screens during startup. TSan reports the
    unsynchronized read/write on _componentViewClasses (Fallback 3/4 of
    _registerComponentIfPossible: write it with _mutex not held, while the main thread
    reads it in createComponentViewWithComponentHandle:).

  3. In production: occurs spontaneously at low rate; frequency increased noticeably on
    iOS 26.0.x (changed thread timing). Possibly related community reproducer:
    https://github.com/SpiGAndromeda/reproducer-rn-0862-symbolview-launch-crash

React Native Version

0.86.2

Affected Platforms

Runtime - iOS

Areas

Fabric - The New Renderer

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

System:
  OS: macOS 26.5.2
  CPU: (12) arm64 Apple M4 Pro
  Memory: 135.67 MB / 24.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 25.6.1
    path: /opt/homebrew/bin/node
  Yarn:
    version: 1.22.22
    path: /opt/homebrew/bin/yarn
  npm:
    version: 11.9.0
    path: /opt/homebrew/bin/npm
  Watchman:
    version: 2026.01.12.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.5
      - iOS 26.5
      - macOS 26.5
      - tvOS 26.5
      - visionOS 26.5
      - watchOS 26.5
  Android SDK:
    API Levels:
      - "31"
      - "34"
      - "35"
      - "36"
    Build Tools:
      - 35.0.0
      - 36.0.0
    System Images:
      - android-30 | Google APIs ARM 64 v8a
      - android-30 | Google APIs Intel x86 Atom
      - android-30 | Google APIs Intel x86_64 Atom
      - android-35 | Google APIs ARM 64 v8a
      - android-37.2-beta3 | 16 KB Page Size Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2025.3 AI-253.29346.138.2531.14876573
  Xcode:
    version: 26.6/17F113
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.18
    path: /usr/bin/javac
  Ruby:
    version: 4.0.1
    path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.1.0
    wanted: 20.1.0
  react:
    installed: 19.2.3
    wanted: 19.2.3
  react-native:
    installed: 0.86.2
    wanted: 0.86.2
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000018
Triggered by Thread: 0 (com.apple.main-thread)
iOS 26.0.1, Release build, RN 0.86.2 (New Architecture, interop layer enabled)

0  Nubra  -[RCTComponentViewFactory createComponentViewWithComponentHandle:] (RCTComponentViewFactory.mm:212)
1  Nubra  -[RCTComponentViewRegistry _dequeueComponentViewWithComponentHandle:] (RCTComponentViewRegistry.mm:96)
2  Nubra  -[RCTComponentViewRegistry dequeueComponentViewWithComponentHandle:tag:] (RCTComponentViewRegistry.mm:52)
3  Nubra  std::__1::__function::__func<-[RCTMountingManager performTransaction:]::$_2, ...>::operator() (RCTMountingManager.mm:56)
4  Nubra  facebook::react::TelemetryController::pullTransaction(...) const (TelemetryController.cpp:40)
5  Nubra  -[RCTMountingManager performTransaction:] (function.h:241)
6  Nubra  -[RCTMountingManager initiateTransaction:] (RCTMountingManager.mm:248)
7  libdispatch.dylib  _dispatch_call_block_and_release
...
19 Nubra  main (AppDelegate.swift)

Frame 0 is the `iterator->second` dereference after
`_componentViewClasses.find(componentHandle)` returns `end()` —
the RCTAssert above it is compiled out in Release, and libc++'s
end() iterator holds a null node pointer, hence fault address 0x18.

MANDATORY Reproducer

https://github.com/SpiGAndromeda/reproducer-rn-0862-symbolview-launch-crash

Screenshots and Videos

Image

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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions