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
This commit is contained in:
Ramanpreet Nara
2025-08-08 14:39:42 -07:00
committed by Facebook GitHub Bot
parent e0ea781908
commit dc879950d1
@@ -26,6 +26,8 @@
#import <objc/runtime.h>
#import <atomic>
#import <iostream>
#import <mutex>
#import <sstream>
#import <vector>
@@ -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<AsyncCallback<>> reject(
{rt, args[1].getObject(rt).getFunction(rt), std::move(jsInvoker)});
__block std::shared_ptr<std::mutex> mutex = std::make_shared<std::mutex>();
RCTPromiseResolveBlock resolveBlock = ^(id result) {
if (!resolve || !reject) {
if (resolveWasCalled) {
RCTLogError(@"%s: Tried to resolve a promise more than once.", moduleMethod.c_str());
std::optional<AsyncCallback<>> localResolve;
bool alreadyResolved = false;
bool alreadyRejected = false;
{
std::lock_guard<std::mutex> 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<AsyncCallback<>> localReject;
bool alreadyResolved = false;
bool alreadyRejected = false;
{
std::lock_guard<std::mutex> 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);