Skip to content

Commit 440d9b9

Browse files
committed
Fix use-after-free in ~SyncCallback when a callback is destroyed off the JS thread
1 parent dfed2d9 commit 440d9b9

1 file changed

Lines changed: 23 additions & 4 deletions

File tree

  • packages/react-native/ReactCommon/react/bridging

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ template <typename R, typename... Args>
106106
class SyncCallback<R(Args...)> {
107107
public:
108108
SyncCallback(jsi::Runtime &rt, jsi::Function function, std::shared_ptr<CallInvoker> jsInvoker)
109-
: wrapper_(CallbackWrapper::createWeak(std::move(function), rt, std::move(jsInvoker)))
109+
: wrapper_(CallbackWrapper::createWeak(std::move(function), rt, std::move(jsInvoker))),
110+
collection_(&LongLivedObjectCollection::get(rt)),
111+
wrapperPtr_(wrapper_.lock().get())
110112
{
111113
}
112114

@@ -116,18 +118,30 @@ class SyncCallback<R(Args...)> {
116118
SyncCallback &operator=(const SyncCallback &) = delete;
117119

118120
// Allow move
119-
SyncCallback(SyncCallback &&other) noexcept : wrapper_(std::move(other.wrapper_)) {}
121+
SyncCallback(SyncCallback &&other) noexcept
122+
: wrapper_(std::move(other.wrapper_)),
123+
collection_(std::exchange(other.collection_, nullptr)),
124+
wrapperPtr_(std::exchange(other.wrapperPtr_, nullptr))
125+
{
126+
}
120127

121128
SyncCallback &operator=(SyncCallback &&other) noexcept
122129
{
123130
wrapper_ = std::move(other.wrapper_);
131+
collection_ = std::exchange(other.collection_, nullptr);
132+
wrapperPtr_ = std::exchange(other.wrapperPtr_, nullptr);
124133
return *this;
125134
}
126135

127136
~SyncCallback()
128137
{
129-
if (auto wrapper = wrapper_.lock()) {
130-
wrapper->destroy();
138+
// May run on any thread (e.g. the fbjni destructor thread). Locking wrapper_
139+
// here could leave this thread with the last reference outside the
140+
// collection's lock, racing with clear() during runtime teardown. Remove by
141+
// address instead, so any release happens under that lock. expired() skips
142+
// wrappers that clear() already released, whose address may be reused.
143+
if (collection_ != nullptr && !wrapper_.expired()) {
144+
collection_->remove(wrapperPtr_);
131145
}
132146
}
133147

@@ -179,6 +193,11 @@ class SyncCallback<R(Args...)> {
179193

180194
// Held weakly so lifetime is managed by LongLivedObjectCollection.
181195
std::weak_ptr<CallbackWrapper> wrapper_;
196+
// Used by the destructor to release the wrapper without locking wrapper_.
197+
// The collection outlives the runtime: LongLivedObjectCollection::get() never
198+
// erases its entries.
199+
LongLivedObjectCollection *collection_;
200+
const LongLivedObject *wrapperPtr_;
182201
};
183202

184203
template <typename... Args>

0 commit comments

Comments
 (0)