Skip to content

Fix ConcurrentModificationException in IntentModule.getInitialURL re-entrancy - #57667

Closed
nickcernera wants to merge 1 commit into
react:mainfrom
nickcernera:fix-intentmodule-concurrent-modification
Closed

Fix ConcurrentModificationException in IntentModule.getInitialURL re-entrancy#57667
nickcernera wants to merge 1 commit into
react:mainfrom
nickcernera:fix-intentmodule-concurrent-modification

Conversation

@nickcernera

Copy link
Copy Markdown
Contributor

Summary:

IntentModule.onHostResume() iterates pendingOpenURLPromises (an ArrayList) directly and calls getInitialURL() for each pending promise. When getCurrentActivity() returns null at that moment — e.g. a deep link or notification tap landing mid activity-transition during a rapid pause/resume — getInitialURL() re-enters waitForActivityAndGetInitialURL(), which calls pendingOpenURLPromises.add(promise) on the very list being iterated. The next iteration then throws java.util.ConcurrentModificationException:

java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.util.ArrayList$Itr.next(ArrayList.java:967)
at com.facebook.react.modules.intent.IntentModule$waitForActivityAndGetInitialURL$1.onHostResume(IntentModule.kt:90)

The synchronized(this@IntentModule) guard does not prevent this: the re-entrancy is on the same thread, which already holds the (reentrant) lock, so no second lock acquisition happens. The crash is the ArrayList iterator's modCount check, not a cross-thread race.

The fix snapshots the pending promises into a local copy, clears the shared list, and nulls the listener before draining. Re-queued promises then land in the now-empty pendingOpenURLPromises and register a fresh listener for the next resume, instead of mutating the list being iterated. Behaviour is otherwise unchanged.

Changelog:

[ANDROID] [FIXED] - Fix ConcurrentModificationException when getInitialURL re-enters during onHostResume

Test Plan:

Added IntentModuleTest.getInitialURL_onHostResumeWithNullActivity_doesNotThrowAndPreservesPromise, a Robolectric regression test that registers a pending promise while the current activity is null, then drives onHostResume so the drain re-queues, and asserts the drain does not throw and the promise is preserved (neither resolved nor rejected).

Ran locally against main with the exact reproduction scenario:

With the fix — passes:

$ ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \
--tests "com.facebook.react.modules.intent.IntentModuleTest"
BUILD SUCCESSFUL
# tests=1, failures=0, errors=0

Without the fix (reverting IntentModule.kt only) — fails with the exact crash, confirming the test is a genuine regression test:

IntentModuleTest > getInitialURL_onHostResumeWithNullActivity_doesNotThrowAndPreservesPromise FAILED
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.util.ArrayList$Itr.next(ArrayList.java:967)
at com.facebook.react.modules.intent.IntentModule$waitForActivityAndGetInitialURL$1.onHostResume(IntentModule.kt:90)

ktfmt (Meta style, matching the repo's ktfmt configuration) reports both changed files as already formatted.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 24, 2026
@facebook-github-toolsfacebook-github-toolsBot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 24, 2026
@meta-codesync

Copy link
Copy Markdown

@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D113595327.

@cortinicocortinico left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

@meta-codesyncmeta-codesyncBot added the Merged This PR has been merged. label Jul 27, 2026
@meta-codesync

Copy link
Copy Markdown

@fabriziocucci merged this pull request in 20d04ae.

@nickcernera
nickcernera deleted the fix-intentmodule-concurrent-modification branch July 27, 2026 20:47
cipolleschi pushed a commit that referenced this pull request Aug 24, 2026
…entrancy (#57667)
Summary:
`IntentModule.onHostResume()` iterates `pendingOpenURLPromises` (an `ArrayList`) directly and calls `getInitialURL()` for each pending promise. When `getCurrentActivity()` returns `null` at that moment — e.g. a deep link or notification tap landing mid activity-transition during a rapid pause/resume — `getInitialURL()` re-enters `waitForActivityAndGetInitialURL()`, which calls `pendingOpenURLPromises.add(promise)` on the very list being iterated. The next iteration then throws `java.util.ConcurrentModificationException`:
```
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.util.ArrayList$Itr.next(ArrayList.java:967)
at com.facebook.react.modules.intent.IntentModule$waitForActivityAndGetInitialURL$1.onHostResume(IntentModule.kt:90)
```
The `synchronized(this@IntentModule)` guard does not prevent this: the re-entrancy is on the same thread, which already holds the (reentrant) lock, so no second lock acquisition happens. The crash is the `ArrayList` iterator's `modCount` check, not a cross-thread race.
The fix snapshots the pending promises into a local copy, clears the shared list, and nulls the listener **before** draining. Re-queued promises then land in the now-empty `pendingOpenURLPromises` and register a fresh listener for the next resume, instead of mutating the list being iterated. Behaviour is otherwise unchanged.
## Changelog:
[ANDROID] [FIXED] - Fix ConcurrentModificationException when getInitialURL re-enters during onHostResume
Pull Request resolved: #57667
Test Plan:
Added `IntentModuleTest.getInitialURL_onHostResumeWithNullActivity_doesNotThrowAndPreservesPromise`, a Robolectric regression test that registers a pending promise while the current activity is `null`, then drives `onHostResume` so the drain re-queues, and asserts the drain does not throw and the promise is preserved (neither resolved nor rejected).
Ran locally against `main` with the exact reproduction scenario:
**With the fix** — passes:
```
$ ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \
--tests "com.facebook.react.modules.intent.IntentModuleTest"
BUILD SUCCESSFUL
# tests=1, failures=0, errors=0
```
**Without the fix** (reverting `IntentModule.kt` only) — fails with the exact crash, confirming the test is a genuine regression test:
```
IntentModuleTest > getInitialURL_onHostResumeWithNullActivity_doesNotThrowAndPreservesPromise FAILED
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.util.ArrayList$Itr.next(ArrayList.java:967)
at com.facebook.react.modules.intent.IntentModule$waitForActivityAndGetInitialURL$1.onHostResume(IntentModule.kt:90)
```
`ktfmt` (Meta style, matching the repo's `ktfmt` configuration) reports both changed files as already formatted.
Reviewed By: cortinico
Differential Revision: D113595327
Pulled By: fabriziocucci
fbshipit-source-id: b1247cb6e473fd7c550dda8b2b584d720ea3e46c
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.MergedThis PR has been merged.Shared with MetaApplied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickcernera@cortinico