Skip to content

[iOS] Stop RCTIdentifierPool::dequeue from spinning forever when the pool is full - #58947

Open
kimseongchan0914 wants to merge 1 commit into
react:mainfrom
kimseongchan0914:fix/ios-identifier-pool-exhaustion-hang
Open

kimseongchan0914 wants to merge 1 commit into
react:mainfrom
kimseongchan0914:fix/ios-identifier-pool-exhaustion-hang

Conversation

@kimseongchan0914

Copy link
Copy Markdown

Summary:

Fixes #58441.

RCTIdentifierPool::dequeue() looks for a free identifier in an unbounded while (true) loop. When every identifier is taken, the loop never exits. RCTSurfaceTouchHandler and RCTSurfacePointerHandler call it on the main thread from touchesBegan:, so the app hangs at 100% CPU until the watchdog kills it (0x8BADF00D). The issue has a symbolicated production hang report.

The pool fills up when identifiers leak over a long-lived process, for example when a touch is missing from the local registry at unregister time.

This change:

  • Bounds the scan to one pass over the pool. After size steps lastIndex is back where it started, so scanning further can't find anything new.
  • If no identifier is free, reclaims them all and reuses one. A reused touch identifier is a transient glitch, while the endless loop takes the whole app down.
  • Initialises lastIndex, which was read as a std::bitset subscript before being assigned.

Whenever the old code terminated, the new code returns the same identifiers. This PR does not fix the identifier leak itself. It only stops a leak from turning into a hang. I'm happy to change what happens on exhaustion if you prefer a different policy.

Changelog:

[IOS] [FIXED] - Fix main thread hang in the touch handler when all touch identifiers are in use

Test Plan:

The bug reproduces against the header alone, so I tested it with standalone programs built with clang++ -std=c++20.

1. Full pool. Fill all 17 slots (the size both handlers use), then call dequeue() again:

facebook::react::RCTIdentifierPool<17> pool;
pool.reset();
for (int i = 0; i < 17; i++) pool.dequeue();
int id = pool.dequeue();
  • Before: never returns (killed after 5 seconds)
  • After: returns 16, and the next call returns 0

2. Same behaviour when the pool is not full. Ran the old and new headers side by side over 2,000 random dequeue/enqueue sequences of 200 operations each, never exhausting the pool, with the old lastIndex pinned to 0:

identical over 400000 operations

3. Formatting. clang-format with the repo's ObjC style reports no diff for the file.

Not run: yarn format-check-cpp, an iOS build, and an on-device check. The C++ API snapshots are unaffected because no public member changed.

🤖 Generated with Claude Code

…s full

dequeue() scanned for a free identifier in an unbounded loop. Once every
identifier was taken, which happens when the touch handlers leak them over
a long-lived process, the loop never exited and hung the main thread.

Bound the scan to one pass over the pool. If no identifier is free, reclaim
them all and reuse one. Also initialise lastIndex, which was read before
being assigned.

Fixes react#58441

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot 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 Oct 8, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied 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.

RCTIdentifierPool::dequeue() spins forever when the pool is exhausted, hanging the main thread and freezing the device

1 participant