react / react/react-native

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

Offen
#58,299 3 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Needs: Author Feedback Needs: Repro Needs: Version Info Platform: iOS Type: New Architecture
Vorherrschende Sprache
C++
Sterne
127k
Forks
25.3k
Ø Merge
1 T. 23 Std.
Gemergte PRs (30 T.)
4

Beschreibung

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

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit React/Fabric/Mounting/RCTComponentViewFactory.mm und React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm und konzentriere dich auf die benannten Registrierungs- und Mounting-Einstiegspunkte. Führe Thread Sanitizer während des Kaltstarts und der Navigation durch Legacy-Interop-Bildschirme aus, um die gemeldeten Zugriffe zu beobachten. Erledigt ist die Aufgabe, wenn der Race-Zustand nicht mehr gemeldet wird und ein fehlendes Component-Handle in Release nicht mehr einen end()-Iterator dereferenziert.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
cpp, ios, objective-c, react-native
Bereich
mobile-dev, performance
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Aktiv
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
35/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.