RCTIdentifierPool::dequeue() spins forever when the pool is exhausted, hanging the main thread and freezing the device
还没有人认领这个 Issue。
- 主要语言
- C++
- 星标
- 127k
- 派生
- 25.3k
- 平均合并
- 1 天 23 小时
- 30 天内合并 PR
- 4
描述
Description
RCTIdentifierPool::dequeue() is an unbounded while (true) loop that never terminates
once every slot is occupied. When that happens the main thread spins at 100% CPU forever,
the app stops servicing scene updates, and iOS — which waits on the frontmost app — leaves
the whole device unresponsive until FrontBoard's watchdog kills the app (0x8BADF00D).
packages/react-native/React/Fabric/Utils/RCTIdentifierPool.h:
int dequeue() {
while (true) {
if (!usage[lastIndex]) {
usage[lastIndex] = true;
return lastIndex;
}
lastIndex = (lastIndex + 1) % size; // nothing in the loop ever frees a slot
}
}
Nothing inside the loop clears a bit in usage, so if all size bits are set the loop can
never exit. This is unchanged in 0.82, 0.85, 0.87 and main.
How the pool gets exhausted
RCTSurfaceTouchHandler uses RCTIdentifierPool<11>. Two paths leak a slot permanently:
_unregisterTouches:skips_identifierPool.enqueue(...)when the touch is not in
_activeTouches— thecontinueafter theRCTAssert.RCTAssertis compiled out in
Release, so in production this leaks silently with no diagnostic at all._registerTouches:always callsdequeue(), but_activeTouches.emplace(touch, ...)is a
no-op if thatUITouch *key already exists, so the freshly taken identifier is orphaned.
The registry desync that drives (1) is already reported in #53303, where it surfaces as a
crash because the assert fires in Debug. In Release it is invisible and simply leaks.
Slots only reset on process restart, so this accumulates over the lifetime of the process.
Evidence from a production hang
iPad (A16) / iPadOS 26.6 / RN 0.81.1, New Architecture, process alive ~43 hours.
Two reports for the same pid:
cpu_resource:90 seconds cpu time over 113 seconds (80% cpu average),Num threads: 1,
footprint 181 MB. Not memory pressure.- Hang report:
FRONTBOARD 0x8BADF00D— "scene-update watchdog transgression: app exhausted
real (wall clock) time allowance of 10.00 seconds".
Symbolicated main thread:
-[RCTSurfaceTouchHandler _registerTouches:] RCTSurfaceTouchHandler.mm:197
-[RCTSurfaceTouchHandler touchesBegan:withEvent:] RCTSurfaceTouchHandler.mm:308
-[UIGestureRecognizer _componentsBegan:withEvent:]
Line 197 is activeTouch.touch.identifier = _identifierPool.dequeue();
Register state at the crash PC proves the pool was full:
| Register | Value | Meaning |
|---|---|---|
x9 |
2047 = 0b11111111111 |
all 11 slots occupied |
x28 |
11 |
pool size |
x27 |
0x2E8BA2E8BA2E8BA3 |
magic constant for % 11 |
x10 / x11 |
3 / 8 |
lastIndex, 1 << lastIndex |
The PC was pinned to the tst/b.ne at the bottom of that loop in 14 of 15 CPU samples.
React Native Version
0.81.1 (verified unchanged in 0.82.0, 0.85.0, 0.87.0 and main)
Affected Platforms
iOS (New Architecture / Fabric)
Steps to reproduce
The loop is unbounded by inspection — no runtime reproducer is needed to see it cannot exit:
RCTIdentifierPool<11> pool;
for (int i = 0; i < 11; i++) pool.dequeue(); // fill every slot
pool.dequeue(); // never returns
In the field it is reached by leaking 11 touch identifiers over a long-lived process.
Suggested fix
Bound the scan. After size steps lastIndex is back where it started, so a further
iteration cannot find anything new:
int dequeue() {
for (size_t attempt = 0; attempt < size; attempt++) {
if (!usage[lastIndex]) {
usage[lastIndex] = true;
return lastIndex;
}
lastIndex = (lastIndex + 1) % size;
}
// Every slot taken. Reclaim rather than hang: a reused touch identifier is a
// transient glitch, an infinite loop takes the whole device down.
usage.reset();
usage[lastIndex] = true;
return lastIndex;
}
This is behaviour-preserving wherever the current code terminates — I ran 200k randomised
allocate/free sequences against both and the returned identifiers are identical. They differ
only in the case that currently hangs forever.
int lastIndex; is also uninitialised and is used as a std::bitset subscript before any
assignment; int lastIndex{0}; would be worth including.
Fixing the leak in RCTSurfaceTouchHandler is worthwhile too, but the unbounded loop is what
turns a leaked identifier into an unresponsive device.
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
先从 packages/react-native/React/Fabric/Utils/RCTIdentifierPool.h 开始,然后检查 RCTSurfaceTouchHandler.mm 中的 _registerTouches: 和 _unregisterTouches:。运行 issue 中的耗尽示例,或在可用时运行相关的现有测试。完成的标准是:当全部 11 个 slot 都已占用时,dequeue() 会返回,lastIndex 已初始化,并且正常的 allocate/free 行为保持不变。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- cpp, ios, react-native
- 领域
- mobile-dev
- Issue 类型
- 缺陷
- 难度
- 2/5
- 预计耗时
- 半天
- 活跃度
- 活跃
- 描述清晰度
- 描述清楚
- 新手友好度
- 82/100