Skip to content

Commit 168ff7c

Browse files
fix(worker): keep a worker isolate alive while a __runOnMainThread callback locks it
RunMainThreadEntry read the target isolate from its entry, released the cache lock and only then took the isolate's Locker. A worker that ended in between removed the entry and disposed the isolate, and the main thread then locked freed memory. The main thread now holds the isolate from the moment it takes the entry until it releases the Locker, and a worker waits for those holds before disposing its isolate.
1 parent 9b12329 commit 168ff7c

4 files changed

Lines changed: 45 additions & 4 deletions

File tree

‎test-app/runtime/src/main/cpp/CallbackHandlers.cpp‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,24 @@ void CallbackHandlers::RunMainThreadEntry(uint64_t key) {
730730
return;
731731
}
732732
isolate = it->second.isolate_;
733-
}
733+
// Taken with the entry, under the same lock RemoveIsolateEntries
734+
// takes, so a worker either removes the entry first or waits for this
735+
// hold before disposing the isolate.
736+
++heldIsolates_[isolate];
737+
}
738+
// Declared before the Locker so that it lets go only after the Locker is
739+
// released.
740+
struct IsolateHold {
741+
Isolate *isolate;
742+
~IsolateHold() {
743+
std::lock_guard<std::mutex> lock(cacheMutex_);
744+
auto held = heldIsolates_.find(isolate);
745+
if (--held->second == 0) {
746+
heldIsolates_.erase(held);
747+
isolateReleased_.notify_all();
748+
}
749+
}
750+
} hold{isolate};
734751

735752
v8::Locker locker(isolate);
736753
Isolate::Scope isolate_scope(isolate);
@@ -1932,8 +1949,17 @@ void CallbackHandlers::RemoveIsolateEntries(v8::Isolate *isolate) {
19321949
}
19331950
}
19341951
}
1952+
1953+
void CallbackHandlers::WaitForMainThreadCallbacks(v8::Isolate *isolate) {
1954+
std::unique_lock<std::mutex> lock(cacheMutex_);
1955+
isolateReleased_.wait(lock, [isolate]() {
1956+
return heldIsolates_.find(isolate) == heldIsolates_.end();
1957+
});
1958+
}
19351959
robin_hood::unordered_map<uint64_t, CallbackHandlers::CacheEntry> CallbackHandlers::cache_;
19361960
std::mutex CallbackHandlers::cacheMutex_;
1961+
robin_hood::unordered_map<v8::Isolate *, int> CallbackHandlers::heldIsolates_;
1962+
std::condition_variable CallbackHandlers::isolateReleased_;
19371963

19381964

19391965
std::atomic_int64_t CallbackHandlers::count_ = {0};

‎test-app/runtime/src/main/cpp/CallbackHandlers.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <string>
55
#include <map>
6+
#include <condition_variable>
67
#include <mutex>
78
#include <vector>
89
#include "JEnv.h"
@@ -163,6 +164,14 @@ namespace tns {
163164

164165
static void RemoveIsolateEntries(v8::Isolate *isolate);
165166

167+
/*
168+
* Blocks until no __runOnMainThread callback still holds `isolate`.
169+
* A worker calls it after releasing its Locker and before disposing
170+
* the isolate: a callback that took the isolate before
171+
* RemoveIsolateEntries ran may be waiting on that Locker.
172+
*/
173+
static void WaitForMainThreadCallbacks(v8::Isolate *isolate);
174+
166175

167176
private:
168177
CallbackHandlers() {
@@ -248,6 +257,10 @@ namespace tns {
248257
// thread (multithreaded JS, workers), each under a different
249258
// isolate's Locker, so the Lockers provide no mutual exclusion
250259
static std::mutex cacheMutex_;
260+
// How many RunMainThreadEntry calls hold each isolate, from reading
261+
// its entry until they release its Locker; guarded by cacheMutex_
262+
static robin_hood::unordered_map<v8::Isolate *, int> heldIsolates_;
263+
static std::condition_variable isolateReleased_;
251264

252265

253266
};

‎test-app/runtime/src/main/cpp/Runtime.cpp‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,9 +1175,10 @@ void Runtime::DestroyRuntime() {
11751175
m_dispatchNativeUncaughtErrorFunc.Reset();
11761176
// Both hold v8::Global handles to JS callbacks, so their entries must be
11771177
// dropped here rather than in ~Runtime, which runs after Isolate::Dispose --
1178-
// resetting a Global then writes into a freed handle table. Doing it here
1179-
// also closes a window in which the main thread could take a Locker on this
1180-
// isolate (RunMainThreadEntry) after it had already been disposed.
1178+
// resetting a Global then writes into a freed handle table. A
1179+
// RunMainThreadEntry that has not taken its entry yet finds it gone; one that
1180+
// already has is waited for before the isolate is disposed
1181+
// (CallbackHandlers::WaitForMainThreadCallbacks).
11811182
CallbackHandlers::RemoveIsolateEntries(m_isolate);
11821183
FrameCallbacks::RemoveIsolateEntries(m_isolate);
11831184

‎test-app/runtime/src/main/cpp/WorkerWrapper.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr<WorkerWrapper> self) {
667667
isolate->RemoveNearHeapLimitCallback(WorkerWrapper::OnNearHeapLimit, 0);
668668
runtime_->DestroyRuntime();
669669
}
670+
CallbackHandlers::WaitForMainThreadCallbacks(isolate);
670671
isolate->Dispose();
671672
// Dispose freed the isolate's memory, so its address can be reused by
672673
// a concurrent Isolate::New - drop the platform's loop entry now, not

0 commit comments

Comments
 (0)