Repository navigation
[iOS][Fabric] App hangs forever in RCTSurfaceTouchHandler _registerTouches: - RCTIdentifierPool::dequeue() spins in an infinite loop when all touch identifiers are leaked #59104
Description
Description
On iOS with the New Architecture (Fabric), the app can freeze permanently on the main thread inside -[RCTSurfaceTouchHandler touchesBegan:withEvent:] → -[RCTSurfaceTouchHandler _registerTouches:]. The process never recovers and is eventually killed by the watchdog.
The hang is in RCTIdentifierPool<size>::dequeue() (React/Fabric/Utils/RCTIdentifierPool.h):
int dequeue() {
while (true) {
if (!usage[lastIndex]) {
usage[lastIndex] = true;
return lastIndex;
}
lastIndex = (lastIndex + 1) % size;
}
}RCTSurfaceTouchHandler uses a fixed-size RCTIdentifierPool (RCTIdentifierPool<11> in 0.81, RCTIdentifierPool<17> in 0.86). When all slots are marked as used, dequeue() has no exit condition and spins forever on the main thread.
The pool can reach this state because identifiers can leak without ever being returned:
-
_registerTouches:dequeues before checking for an existing entry.- (void)_registerTouches:(NSSet<UITouch *> *)touches { for (UITouch *touch in touches) { auto activeTouch = CreateTouchWithUITouch(touch, _rootComponentView, _viewOriginOffset); activeTouch.touch.identifier = _identifierPool.dequeue(); _activeTouches.emplace(touch, activeTouch); } }
if
touchesBegan:is delivered for aUITouchalready present in_activeTouches,emplaceis a no-op, but the identifier has already been taken from the pool. It is never enqueued again, because_unregisterTouches:only returns the identifier stored in the (original) map entry. -
resetonly resets the pool when_activeTouchesis non-empty.- (void)reset { [super reset]; if (!_activeTouches.empty()) { ... _activeTouches.clear(); _identifierPool.reset(); } }
Identifiers leaked via (1) are not represented in
_activeTouches, so if the recognizer resets while the map is empty, the leaked identifiers remain marked as used for the lifetime of the surface.
Once every slot has leaked, the next touch hangs the app. The larger pool in 0.86 only raises the threshold: the unbounded loop, the emplace leak and the conditional reset are unchanged in 0.86.3 and on main (packages/react-native/React/Fabric/Utils/RCTIdentifierPool.h and packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm)
Steps to reproduce
We have not found a deterministic UIKit sequence that triggers the leak on a real device. The hang itself reproduces deterministically on react-native@0.87.1 (iOS Simulator) by simulating a single leak per tap, which exhausts the pool:
- Clone the reproducer (link below) and install:
cd ReproducerApp npm install # postinstall applies patches/react-native+0.87.1.patch cd ios && bundle install && bundle exec pod install && cd .. npm start npm run ios
The Podfile sets RCT_USE_PREBUILT_RNCORE=0 and RCT_USE_RN_DEP=0 so React Native is built from source and the patched RCTSurfaceTouchHandler.mm is compiled.
- The patch only adds a second
[self _registerTouches:touches];call in-[RCTSurfaceTouchHandler touchesBegan:withEvent:]. This simulates UIKit deliveringtouchesBeganfor aUITouchthat is already registered:emplaceis a no-op, so exactly one pool identifier leaks per tap. - The app shows a full-screen
Pressablewith a tap counter. Tap it 16 times, the counter updates normally until 16 identifiers leaked, 1 free. - Tap 17: the real touch takes the last free identifier, the duplicate registration calls
dequeue()on a full pool, and the main thread hangs forever. The counter stops updating and the app no longer responds to input. - Pause in Xcode (Debug → Pause): the main thread is spinning in
RCTIdentifierPool<17>::dequeue()inlined into-[RCTSurfaceTouchHandler _registerTouches:], identical to the production stack below.
Expected: a bookkeeping inconsistency in the touch registry should, at worst, reuse a touch identifier not permanently freeze the main thread.
Actual: the app hangs indefinitely and is eventually killed by the watchdog.
React Native Version
0.87.1
Affected Platforms
Runtime - iOS
Output of npx @react-native-community/cli info
System:
OS: macOS 26.6.2
CPU: (12) arm64 Apple M2 Max
Memory: 9.08 GB / 64.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 24.9.0
path: ~/.nvm/versions/node/v24.9.0/bin/node
Yarn: Not Found
npm:
version: 11.6.0
path: ~/.nvm/versions/node/v24.9.0/bin/npm
Watchman:
version: 2026.07.06.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.17.0
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: Not Found
IDEs:
Android Studio: 2026.1 AI-261.23567.138.2611.15646644
Xcode:
version: 26.6/17F113
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.19
Ruby:
version: 3.3.3
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 19.1.0
wanted: 19.1.0
react-native:
installed: 0.81.5
wanted: 0.81.5
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
App Hanging: App hanging for at least 5000 ms.
Thread 0 (main, current)
React -[RCTSurfaceTouchHandler _registerTouches:] <- RCTIdentifierPool<11>::dequeue() inlined
React -[RCTSurfaceTouchHandler _registerTouches:]
React -[RCTSurfaceTouchHandler touchesBegan:withEvent:]
UIKitCore -[UIGestureRecognizer _componentsBegan:withEvent:]
UIKitCore -[UITouchesEvent _sendEventToGestureRecognizer:]
UIKitCore __47-[UIGestureEnvironment _updateForEvent:window:]_block_invoke
UIKitCore __47-[UIGestureEnvironment _updateForEvent:window:]_block_invoke.35
Gestures __swift_memcpy17_8
Gestures __swift_allocate_value_buffer
UIKitCore -[UIGestureEnvironment _updateForEvent:window:]
UIKitCore -[UIWindow sendEvent:]
UIKitCore -[UIApplication sendEvent:]
UIKitCore __dispatchPreprocessedEventFromEventQueue
UIKitCore __processEventQueue
UIKitCore updateCycleEntry
UIKitCore _UIUpdateSequenceRunNext
UIKitCore schedulerStepScheduledMainSectionContinue
UpdateCycle UC::DriverCore::continueProcessing
CoreFoundation __CFMachPortPerform
CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
CoreFoundation __CFRunLoopDoSource1
CoreFoundation __CFRunLoopRun
CoreFoundation _CFRunLoopRunSpecificWithOptions
GraphicsServices GSEventRunModal
UIKitCore -[UIApplication _run]
UIKitCore UIApplicationMain
<app> main
dyld start
Thread 5 (com.facebook.react.runtime.JavaScript) — idle
React +[RCTJSThreadManager runRunLoop]
CoreFoundation _CFRunLoopRunSpecificWithOptions
...
MANDATORY Reproducer
https://github.com/m-szczepanek/rn-touch-identifier-pool-hang
Screenshots and Videos
N/A — the app freezes with no visual change and touches stop being processed
Activity