enableSchedulerDelegateInvalidation defaults to false, leaving Scheduler's deferred delegate callbacks exposed to use-after-free (EXC_BAD_ACCESS on the JS thread during teardown)
还没有人认领这个 Issue。
- 主要语言
- C++
- 星标
- 127k
- 派生
- 25.3k
- 平均合并
- 1 天 23 小时
- 30 天内合并 PR
- 4
描述
Description
Scheduler::uiManagerDidDispatchCommand and Scheduler::uiManagerDidFinishTransaction copy the raw pointer delegate_ into a lambda that is deferred through runtimeScheduler_->scheduleRenderingUpdate(...). If the delegate is destroyed before that lambda is drained, the lambda dereferences freed memory.
On 0.87.1 the guard for this exists, but it is gated behind a feature flag that is off by default, so a default build is still exposed:
// Scheduler.cpp (v0.87.1)
auto guardEnabled =
ReactNativeFeatureFlags::enableSchedulerDelegateInvalidation();
runtimeScheduler_->scheduleRenderingUpdate(
shadowNode->getSurfaceId(),
[delegate = delegate_,
invalidated = delegateInvalidated_,
guardEnabled, /* ... */]() {
if (guardEnabled && *invalidated) {
return;
}
delegate->schedulerDidDispatchCommand(shadowView, commandName, args);
});
// ReactNativeFeatureFlagsDefaults.h (v0.87.1)
bool enableSchedulerDelegateInvalidation() override {
return false;
}
With the flag off, neither setDelegate() nor ~Scheduler() neutralises lambdas that already captured the old pointer, so on iOS RCTScheduler's dealloc — which calls setDelegate(nullptr) and then frees the delegate proxy — leaves the RuntimeScheduler free to drain a stale lambda on the JS thread.
What I am asking: is there a reason the guard is still opt-in, and is there a plan to enable it by default? If the guard is considered correct, a default of false means every released app is still exposed to this.
Three further observations from reading the sources:
maincurrently has no guard at all inuiManagerDidDispatchCommand— theguardEnabled/invalidatedcapture present in v0.86.3 and v0.87.1 is absent there. If that is not intentional, the fix may have been lost on main.- On 0.81.x the flag does not exist at all (neither in
ReactNativeFeatureFlags.hnor in the JS feature-flag API), so apps on that line have no mitigation available whatsoever. - The same raw-pointer capture pattern is used in
uiManagerDidFinishTransaction, which runs on every non-synchronous transaction, not just on commands.
Note on Expo: the reporting app uses Expo, but this is not an Expo defect — the faulting frame is React Native's own renderer C++ (Scheduler.cpp), which Expo does not patch. Filing here rather than on the Expo tracker for that reason.
Note on the version field: the production crash below was captured on 0.81.5, where no guard exists. I have filed this against 0.87.1 because I verified by reading v0.87.1's sources that the unguarded path is still the default there. I have not run 0.87.1 myself.
Steps to reproduce
This is a race with a window of a few milliseconds, so the steps below describe the production scenario rather than a deterministic recipe. Reconstructed from the breadcrumbs of the crashing session:
- Open a screen with a
ScrollViewcontaining several chainedTextInputs, whereonSubmitEditingcallsnextRef.current?.focus(). Eachfocus()goes throughUIManager.dispatchCommand, and therefore throughScheduler::uiManagerDidDispatchCommand. - Type, moving focus between fields so the keyboard shows and hides repeatedly.
- Background the app immediately after a focus change. (In the observed session the user left the app to fetch an emailed sign-in code.)
- The app crashes on the JS thread while the main thread is still inside the
UIApplicationDidEnterBackgroundobserver.
The simultaneity is visible in the report: the main thread is inside __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ at the moment the JS thread faults.
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: (14) arm64 Apple M3 Max
Memory: 177.64 MB / 36.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.14.0
path: /Users/garri/.nvm/versions/node/v22.14.0/bin/node
Yarn:
version: 1.22.22
path: /Users/garri/.nvm/versions/node/v22.14.0/bin/yarn
npm:
version: 11.4.2
path: /Users/garri/.nvm/versions/node/v22.14.0/bin/npm
Watchman:
version: 2026.07.27.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: 2025.3 AI-253.29346.138.2531.14850935
Xcode:
version: 26.5/17F42
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.18
path: /usr/bin/javac
Ruby:
version: 4.0.6
path: /opt/homebrew/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react: Not Found
react-native: Not Found
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: Not found
Additional context: Expo SDK 54, Hermes, New Architecture enabled (`newArchEnabled: true`),
`buildReactNativeFromSource: true` on iOS. Crash captured on react-native 0.81.5.
Stacktrace or Logs
EXC_BAD_ACCESS
KERN_INVALID_ADDRESS at 0x344e880dcd072e6e
Crashed thread: com.facebook.react.runtime.JavaScript
facebook::react::Scheduler::uiManagerDidDispatchCommand::lambda::operator() Scheduler.cpp:286
std::__1::__invoke[abi:ne200100]<T> invoke.h:179
std::__1::__invoke_void_return_wrapper<T>::__call[abi:ne200100]<T> invoke.h:251
std::__1::__invoke_r[abi:ne200100]<T> invoke.h:273
std::__1::__function::__alloc_func<T>::operator()[abi:ne200100] function.h:167
std::__1::__function::__func<T>::operator() function.h:319
std::__1::__function::__value_func<T>::operator()[abi:ne200100] function.h:436
std::__1::function<T>::operator() function.h:995
facebook::react::RCTMessageThread::runAsync RCTMessageThread.mm:44
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ CoreFoundation
__CFRunLoopDoBlocks CoreFoundation
__CFRunLoopRun CoreFoundation
_CFRunLoopRunSpecificWithOptions CoreFoundation
+[RCTJSThreadManager runRunLoop] RCTJSThreadManager.mm:102
__NSThread__start__ Foundation
_pthread_start libsystem_pthread.dylib
Main thread at the same moment, processing the background transition:
__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ CoreFoundation
___CFXRegistrationPost_block_invoke CoreFoundation
_CFXRegistrationPost CoreFoundation
_CFXNotificationPost CoreFoundation
__CFRunLoopRun CoreFoundation
Notes:
- Line 286 on 0.81.5 is exactly `delegate->schedulerDidDispatchCommand(shadowView, commandName, args);`
inside the deferred lambda.
- The faulting address is arbitrary garbage rather than a small offset from null, which is consistent
with freed-and-reused memory rather than a null delegate.
- Device: iPhone 15 Pro Max (iPhone16,2), iOS 27.0 (24A437, RELEASE kernel), App Store build,
crash ~48 s after cold start, while the app was moving to the background.
MANDATORY Reproducer
I do not have a standalone reproducer. This is a production crash; the window between queueing the lambda and the delegate being freed is a few milliseconds and I have not been able to force it on demand. I am filing anyway because the defect is verifiable statically rather than empirically: the deferred lambda captures a raw SchedulerDelegate*, the invalidation guard that would neutralise it is compiled in but disabled by default, and the symbolicated crash lands on exactly that dereference. If a running reproducer is a hard requirement, I would appreciate a pointer to the existing tracking issue for enableSchedulerDelegateInvalidation so this report can be attached there instead.
Screenshots and Videos
N/A
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
先阅读 Scheduler.cpp,重点关注 uiManagerDidDispatchCommand 和 uiManagerDidFinishTransaction,然后将引用的 v0.87.1 代码与 main 进行比较。检查 ReactNativeFeatureFlagsDefaults.h 和 RCTScheduler 的 teardown 路径;在默认配置下,延迟回调无法解引用已销毁的 Delegate 即表示完成。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- cpp, ios, react-native
- 领域
- mobile
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 活跃
- 描述清晰度
- 基本清楚
- 新手友好度
- 48/100