Skip to content

Commit d11055b

Browse files
javachemeta-codesync[bot]
authored andcommitted
Fix callback and promise teardown races
Summary: Keep JavaScript callback and promise values rooted while native owners retain only weak lifetime handles. Dispatch release through the JavaScript executor so native invocation and destruction cannot retain these values across runtime teardown. Apply the same ownership policy to asynchronous callbacks and promises. This builds on the investigation and reproducer contributed by HyunWoo Lee in #59579. Changelog: [General][Fixed] - Fix TurboModule callback and async promise cleanup racing runtime teardown [General][Deprecated] - Deprecate CallbackWrapper in favour of LongLivedObjectHandle Differential Revision: D128581452
1 parent 3aee67b commit d11055b

6 files changed

Lines changed: 941 additions & 93 deletions

File tree

‎packages/react-native/ReactCommon/react/bridging/CallbackWrapper.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ namespace facebook::react {
1919

2020
class CallInvoker;
2121

22-
// Helper for passing jsi::Function arg to other methods.
22+
/**
23+
* @deprecated Use LongLivedObjectHandle<jsi::Function> instead.
24+
* Access and release this legacy wrapper only on the JavaScript thread.
25+
*/
2326
class CallbackWrapper : public LongLivedObject {
2427
private:
2528
CallbackWrapper(jsi::Function &&callback, jsi::Runtime &runtime, std::shared_ptr<CallInvoker> jsInvoker)

‎packages/react-native/ReactCommon/react/bridging/Function.h‎

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#include <react/cxxstableapi/UmbrellaGuard.h>
1111

1212
#include <react/bridging/Base.h>
13-
#include <react/bridging/CallbackWrapper.h>
13+
#include <react/bridging/CallbackWrapper.h> // Retain the legacy transitive include.
1414

1515
#include <ReactCommon/CallInvoker.h>
1616
#include <ReactCommon/SchedulerPriority.h>
@@ -61,16 +61,16 @@ class AsyncCallback {
6161

6262
void callWithArgs(std::optional<SchedulerPriority> priority, Args... args) const noexcept
6363
{
64-
if (auto wrapper = callback_->wrapper_.lock()) {
65-
auto fn = [callback = callback_,
64+
auto callback = callback_;
65+
if (auto jsInvoker = callback->function_.lockInvoker()) {
66+
auto fn = [callback = std::move(callback),
6667
argsPtr = std::make_shared<std::tuple<Args...>>(std::make_tuple(std::forward<Args>(args)...))](
6768
jsi::Runtime &) { callback->apply(std::move(*argsPtr)); };
6869

69-
auto &jsInvoker = wrapper->jsInvoker();
7070
if (priority) {
71-
jsInvoker.invokeAsync(*priority, std::move(fn));
71+
jsInvoker->invokeAsync(*priority, std::move(fn));
7272
} else {
73-
jsInvoker.invokeAsync(std::move(fn));
73+
jsInvoker->invokeAsync(std::move(fn));
7474
}
7575
}
7676
}
@@ -79,57 +79,38 @@ class AsyncCallback {
7979
std::optional<SchedulerPriority> priority,
8080
std::function<void(jsi::Runtime &, jsi::Function &)> &&callImpl) const noexcept
8181
{
82-
if (auto wrapper = callback_->wrapper_.lock()) {
83-
// Capture callback_ and not wrapper_. If callback_ is deallocated or the
84-
// JSVM is shutdown before the async task is scheduled, the underlying
85-
// function will have been deallocated.
86-
auto fn = [callback = callback_, callImpl = std::move(callImpl)](jsi::Runtime &rt) {
87-
if (auto wrapper2 = callback->wrapper_.lock()) {
88-
callImpl(rt, wrapper2->callback());
82+
auto callback = callback_;
83+
if (auto jsInvoker = callback->function_.lockInvoker()) {
84+
// Retain native ownership until this task is destroyed, without retaining
85+
// a JavaScript value while the task is queued or discarded.
86+
auto fn = [callback = std::move(callback), callImpl = std::move(callImpl)](jsi::Runtime &rt) {
87+
if (auto function = callback->function_.lockOnJS()) {
88+
callImpl(rt, function.value());
8989
}
9090
};
9191

92-
auto &jsInvoker = wrapper->jsInvoker();
9392
if (priority) {
94-
jsInvoker.invokeAsync(*priority, std::move(fn));
93+
jsInvoker->invokeAsync(*priority, std::move(fn));
9594
} else {
96-
jsInvoker.invokeAsync(std::move(fn));
95+
jsInvoker->invokeAsync(std::move(fn));
9796
}
9897
}
9998
}
10099
};
101100

102-
// You must ensure that when invoking this you're located on the JS thread, or
103-
// have exclusive control of the JS VM context. If you cannot ensure this, use
104-
// AsyncCallback instead.
101+
// Invoke only on the JavaScript thread. Use AsyncCallback from other threads.
105102
template <typename R, typename... Args>
106103
class SyncCallback<R(Args...)> {
107104
public:
108105
SyncCallback(jsi::Runtime &rt, jsi::Function function, std::shared_ptr<CallInvoker> jsInvoker)
109-
: wrapper_(CallbackWrapper::createWeak(std::move(function), rt, std::move(jsInvoker)))
106+
: function_(rt, std::move(function), std::move(jsInvoker))
110107
{
111108
}
112109

113-
// Disallow copying, as we can no longer safely destroy the callback
114-
// from the destructor if there's multiple copies
115110
SyncCallback(const SyncCallback &) = delete;
116111
SyncCallback &operator=(const SyncCallback &) = delete;
117-
118-
// Allow move
119-
SyncCallback(SyncCallback &&other) noexcept : wrapper_(std::move(other.wrapper_)) {}
120-
121-
SyncCallback &operator=(SyncCallback &&other) noexcept
122-
{
123-
wrapper_ = std::move(other.wrapper_);
124-
return *this;
125-
}
126-
127-
~SyncCallback()
128-
{
129-
if (auto wrapper = wrapper_.lock()) {
130-
wrapper->destroy();
131-
}
132-
}
112+
SyncCallback(SyncCallback &&) noexcept = default;
113+
SyncCallback &operator=(SyncCallback &&) noexcept = default;
133114

134115
R operator()(Args... args) const
135116
{
@@ -138,21 +119,23 @@ class SyncCallback<R(Args...)> {
138119

139120
R call(Args... args) const
140121
{
141-
auto wrapper = wrapper_.lock();
122+
auto function = function_.lockOnJS();
142123

143-
// If the wrapper has been deallocated, we can no longer provide a return
144-
// value consistently, so our only option is to throw
145-
if (!wrapper) {
124+
// If the function has been deallocated, we can no longer provide a return
125+
// value consistently, so our only option is to throw.
126+
if (!function) {
146127
if constexpr (std::is_void_v<R>) {
147128
return;
148129
} else {
149130
throw std::runtime_error("Failed to call invalidated sync callback");
150131
}
151132
}
152133

153-
auto &callback = wrapper->callback();
154-
auto &rt = wrapper->runtime();
155-
auto jsInvoker = wrapper->jsInvokerPtr();
134+
// JavaScript can destroy this native callback during the call; use only
135+
// the retained value and local runtime/invoker references afterward.
136+
auto &callback = function.value();
137+
auto &rt = function.runtime();
138+
auto jsInvoker = function.jsInvokerPtr();
156139

157140
if constexpr (std::is_void_v<R>) {
158141
callback.call(rt, bridging::toJs(rt, std::forward<Args>(args), jsInvoker)...);
@@ -177,8 +160,7 @@ class SyncCallback<R(Args...)> {
177160
return call(std::move(std::get<Index>(args))...);
178161
}
179162

180-
// Held weakly so lifetime is managed by LongLivedObjectCollection.
181-
std::weak_ptr<CallbackWrapper> wrapper_;
163+
LongLivedObjectHandle<jsi::Function> function_;
182164
};
183165

184166
template <typename... Args>
@@ -191,7 +173,7 @@ struct Bridging<AsyncCallback<Args...>> {
191173

192174
static jsi::Function toJs(jsi::Runtime &rt, const AsyncCallback<Args...> &value)
193175
{
194-
return value.callback_->function_.getFunction(rt);
176+
return Bridging<SyncCallback<void(Args...)>>::toJs(rt, *value.callback_);
195177
}
196178
};
197179

@@ -205,7 +187,10 @@ struct Bridging<SyncCallback<R(Args...)>> {
205187

206188
static jsi::Function toJs(jsi::Runtime &rt, const SyncCallback<R(Args...)> &value)
207189
{
208-
return value.function_.getFunction(rt);
190+
if (auto function = value.function_.lockOnJS()) {
191+
return function.value().getFunction(rt);
192+
}
193+
throw jsi::JSError(rt, "Failed to get invalidated callback");
209194
}
210195
};
211196

‎packages/react-native/ReactCommon/react/bridging/LongLivedObject.cpp‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77

88
#include "LongLivedObject.h"
9+
10+
#include <ReactCommon/CallInvoker.h>
911
#include <unordered_map>
1012

1113
namespace facebook::react {
@@ -35,22 +37,28 @@ void LongLivedObjectCollection::add(std::shared_ptr<LongLivedObject> so) {
3537
}
3638

3739
void LongLivedObjectCollection::remove(const LongLivedObject* o) {
38-
std::scoped_lock lock(collectionMutex_);
39-
// Empty owner: lookup-only alias, with no ownership or control block.
40-
const std::shared_ptr<LongLivedObject> lookupKey{
41-
std::shared_ptr<LongLivedObject>{}, const_cast<LongLivedObject*>(o)};
40+
decltype(collection_)::node_type retired;
41+
{
42+
std::scoped_lock lock(collectionMutex_);
43+
// Empty owner: lookup-only alias, with no ownership or control block.
44+
const std::shared_ptr<LongLivedObject> lookupKey{
45+
std::shared_ptr<LongLivedObject>{}, const_cast<LongLivedObject*>(o)};
4246

43-
// A shared_ptr hashes and compares by the pointer it stores, so a
44-
// non-owning shared_ptr to `o` finds its entry without walking the set.
45-
auto entry = collection_.find(lookupKey);
46-
if (entry != collection_.end()) {
47-
collection_.erase(entry);
47+
// A shared_ptr hashes and compares by the pointer it stores, so a
48+
// non-owning shared_ptr to `o` finds its entry without walking the set.
49+
auto entry = collection_.find(lookupKey);
50+
if (entry != collection_.end()) {
51+
retired = collection_.extract(entry);
52+
}
4853
}
4954
}
5055

5156
void LongLivedObjectCollection::clear() {
52-
std::scoped_lock lock(collectionMutex_);
53-
collection_.clear();
57+
decltype(collection_) retired;
58+
{
59+
std::scoped_lock lock(collectionMutex_);
60+
retired.swap(collection_);
61+
}
5462
}
5563

5664
size_t LongLivedObjectCollection::size() const {
@@ -64,4 +72,22 @@ void LongLivedObject::allowRelease() {
6472
LongLivedObjectCollection::get(runtime_).remove(this);
6573
}
6674

75+
void detail::releaseLongLivedObjectOnJS(
76+
std::weak_ptr<LongLivedObject> object,
77+
std::weak_ptr<CallInvoker> jsInvoker) noexcept {
78+
if (object.expired()) {
79+
return;
80+
}
81+
if (auto invoker = jsInvoker.lock()) {
82+
invoker->invokeAsync([object = std::move(object)](jsi::Runtime&) {
83+
if (auto locked = object.lock()) {
84+
locked->allowRelease();
85+
}
86+
});
87+
}
88+
// If dispatch is dropped, runtime cleanup releases the collection. Never
89+
// fall back to acquiring or releasing a strong reference on the caller
90+
// thread.
91+
}
92+
6793
} // namespace facebook::react

0 commit comments

Comments
 (0)