From dc879950d196dfd429229f1c4c8e743ef1799d11 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Fri, 8 Aug 2025 14:39:42 -0700 Subject: [PATCH] native modules: Guard against concurrent resolve/reject calls (#53151) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53151 If the native module calls the resolve/reject, resolve/resolve, reject/resolve, reject/reject concurrently, the turbomodule infra could run into a null pointer exception. This diff mitigates that problem. Changelog: [iOS][Fixed] - Fix concurrent calls into resolve/reject inside native modules Reviewed By: sanjay-io Differential Revision: D79824319 fbshipit-source-id: 675264781f303d12fc1eb9649ecdc78601b7720b --- .../ios/ReactCommon/RCTTurboModule.mm | 65 ++++++++++++++----- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm index 2a677974ca8..2b4dfe8c37f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm @@ -26,6 +26,8 @@ #import #import #import +#import + #import #import @@ -284,44 +286,71 @@ ObjCTurboModule::createPromise(jsi::Runtime &runtime, const std::string &methodN {rt, args[0].getObject(rt).getFunction(rt), std::move(jsInvoker)}); __block std::optional> reject( {rt, args[1].getObject(rt).getFunction(rt), std::move(jsInvoker)}); + __block std::shared_ptr mutex = std::make_shared(); RCTPromiseResolveBlock resolveBlock = ^(id result) { - if (!resolve || !reject) { - if (resolveWasCalled) { - RCTLogError(@"%s: Tried to resolve a promise more than once.", moduleMethod.c_str()); + std::optional> localResolve; + bool alreadyResolved = false; + bool alreadyRejected = false; + { + std::lock_guard lock(*mutex); + if (!resolve || !reject) { + alreadyResolved = resolveWasCalled; + alreadyRejected = !resolveWasCalled; } else { - RCTLogError( - @"%s: Tried to resolve a promise after it's already been rejected.", moduleMethod.c_str()); + resolveWasCalled = YES; + localResolve = std::move(resolve); + resolve = std::nullopt; + reject = std::nullopt; } + } + + if (alreadyResolved) { + RCTLogError(@"%s: Tried to resolve a promise more than once.", moduleMethod.c_str()); return; } - resolve->call([result](jsi::Runtime &rt, jsi::Function &jsFunction) { + if (alreadyRejected) { + RCTLogError(@"%s: Tried to resolve a promise after it's already been rejected.", moduleMethod.c_str()); + return; + } + + localResolve->call([result](jsi::Runtime &rt, jsi::Function &jsFunction) { jsFunction.call(rt, convertObjCObjectToJSIValue(rt, result)); }); - - resolveWasCalled = YES; - resolve = std::nullopt; - reject = std::nullopt; }; RCTPromiseRejectBlock rejectBlock = ^(NSString *code, NSString *message, NSError *error) { - if (!resolve || !reject) { - if (resolveWasCalled) { - RCTLogError(@"%s: Tried to reject a promise after it's already been resolved.", moduleMethod.c_str()); + std::optional> localReject; + bool alreadyResolved = false; + bool alreadyRejected = false; + { + std::lock_guard lock(*mutex); + if (!resolve || !reject) { + alreadyResolved = resolveWasCalled; + alreadyRejected = !resolveWasCalled; } else { - RCTLogError(@"%s: Tried to reject a promise more than once.", moduleMethod.c_str()); + resolveWasCalled = NO; + localReject = std::move(reject); + reject = std::nullopt; + resolve = std::nullopt; } + } + + if (alreadyResolved) { + RCTLogError(@"%s: Tried to reject a promise after it's already been resolved.", moduleMethod.c_str()); + return; + } + + if (alreadyRejected) { + RCTLogError(@"%s: Tried to reject a promise more than once.", moduleMethod.c_str()); return; } NSDictionary *jsErrorDetails = RCTJSErrorFromCodeMessageAndNSError(code, message, error); - reject->call([jsErrorDetails](jsi::Runtime &rt, jsi::Function &jsFunction) { + localReject->call([jsErrorDetails](jsi::Runtime &rt, jsi::Function &jsFunction) { jsFunction.call(rt, convertJSErrorDetailsToJSRuntimeError(rt, jsErrorDetails)); }); - resolveWasCalled = NO; - resolve = std::nullopt; - reject = std::nullopt; }; invokeCopy(resolveBlock, rejectBlock);