Skip to content

[iOS] Fix concurrent modification crash#4008

Merged
m-bert merged 9 commits intomainfrom
@mbert/long-press-crash
Mar 5, 2026
Merged

[iOS] Fix concurrent modification crash#4008
m-bert merged 9 commits intomainfrom
@mbert/long-press-crash

Conversation

@m-bert
Copy link
Contributor

@m-bert m-bert commented Feb 27, 2026

Description

In reattachHandlersIfNeeded method we are iterating over handlers contained in registry. The following crash was reported:

NSDictionary was mutated while being enumerated

This PR adds @synchronized on registry._handlers so attempting to modify this value should be safe.

Note

For now we have no consistent reproduction. I'm also not sure where this concurrent modification takes place. It seems to be indirect, as previous keyframe in provided stack trace was flushOperations.

Test plan

Tested on the example code from #3964

Copilot AI review requested due to automatic review settings February 27, 2026 18:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a concurrent modification crash in the iOS gesture handler implementation. The crash occurred when reattachHandlersIfNeeded iterated over the handlers registry while the collection was being modified, causing a "NSDictionary was mutated while being enumerated" error. This method was introduced in PR #3964 to handle view recycling on the new architecture when parent views have display: none.

Changes:

  • Create a defensive copy of the handlers collection before iterating in reattachHandlersIfNeeded
  • Add a comment explaining the defensive copy to prevent concurrent modification

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +249 to +257
// Create copy to avoid mutating collection while enumerating.
NSArray<NSNumber *> *handlerTags = [_registry.handlers.allKeys copy];

// Re-bind handlers to their current native views. On Fabric, when a parent view has
// display:none and siblings change, the native UIView backing a component may be recycled
// and replaced. maybeBindHandler is a no-op if the view is nil or unchanged. This is only
// needed for handlers using the old api.
for (RNGestureHandler *handler in _registry.handlers.objectEnumerator) {
for (NSNumber *handlerTag in handlerTags) {
RNGestureHandler *handler = _registry.handlers[handlerTag];
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying handlers.allKeys avoids mutating during the subsequent for loop, but allKeys itself still has to enumerate the underlying mutable _handlers dictionary and can throw the same "mutated while being enumerated" exception if the registry is modified concurrently (e.g., dropAllHandlers/dropHandlerWithTag). To make this robust, access to the registry’s _handlers should be serialized (same queue) or protected with a lock/@synchronized when taking the snapshot and when mutating it elsewhere.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +253 to 266
NSDictionary *handlers = _registry.handlers;

@synchronized(handlers) {
for (RNGestureHandler *handler in handlers.objectEnumerator) {
if (handler.viewTag == nil || [handler usesNativeOrVirtualDetector]) {
continue;
}

[self maybeBindHandler:handler.tag
toViewWithTag:handler.viewTag
withActionType:handler.actionType
withHostDetector:nil];
[self maybeBindHandler:handler.tag
toViewWithTag:handler.viewTag
withActionType:handler.actionType
withHostDetector:nil];
}
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking on handlers here won’t reliably prevent the "NSDictionary was mutated while being enumerated" crash unless all code paths that mutate the same underlying dictionary also synchronize on the exact same lock object. Currently, the registry still removes/clears entries without any lock, so concurrent mutation can still happen during this enumeration. A more robust fix is to iterate over a snapshot (e.g. copy the values under a registry-owned lock) or move the synchronization/snapshotting into RNGestureHandlerRegistry so callers don’t need to lock its internal storage.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said below, creating handler is only part which is called form other thread.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@m-bert m-bert merged commit f46080a into main Mar 5, 2026
3 checks passed
@m-bert m-bert deleted the @mbert/long-press-crash branch March 5, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants