Visitar URL original
fix: isolate/runtime lifetime — worker startup race, teardown use-after-free, and leaks by edusperoni · Pull Request #2006 · NativeScript/android · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ add_library(
src/main/cpp/File.cpp
src/main/cpp/Interop.cpp
src/main/cpp/IsolateDisposer.cpp
src/main/cpp/IsolateTracked.cpp
src/main/cpp/JEnv.cpp
src/main/cpp/DesugaredInterfaceCompanionClassNameResolver.cpp
src/main/cpp/JType.cpp
Expand Down Expand Up @@ -203,6 +204,7 @@ add_library(
src/main/cpp/Profiler.cpp
src/main/cpp/ReadWriteLock.cpp
src/main/cpp/Runtime.cpp
src/main/cpp/RuntimeState.cpp
src/main/cpp/SimpleAllocator.cpp
src/main/cpp/SimpleProfiler.cpp
src/main/cpp/StructuredClone.cpp
Expand Down
22 changes: 5 additions & 17 deletions test-app/runtime/src/main/cpp/ArgConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "Runtime.h"
#include "V8GlobalHelpers.h"
#include "NativeScriptAssert.h"
#include "RuntimeState.h"
#include <sstream>

using namespace v8;
Expand Down Expand Up @@ -196,15 +197,11 @@ int64_t ArgConverter::ConvertToJavaLong(Isolate* isolate, const Local<Value>& va
}

ArgConverter::TypeLongOperationsCache* ArgConverter::GetTypeLongCache(v8::Isolate* isolate) {
TypeLongOperationsCache* cache;
auto itFound = s_type_long_operations_cache.find(isolate);
if (itFound == s_type_long_operations_cache.end()) {
cache = new TypeLongOperationsCache;
s_type_long_operations_cache.emplace(isolate, cache);
} else {
cache = itFound->second;
// Per runtime, so there is no shared table to race on; see RuntimeState.h.
auto* cache = RuntimeState::For<TypeLongOperationsCache>(isolate);
if (cache == nullptr) {
throw NativeScriptException("Long conversion cache requested after the runtime was torn down");
}

return cache;
}

Expand All @@ -222,12 +219,3 @@ u16string ArgConverter::ConvertToUtf16String(const v8::Local<String>& s) {



void ArgConverter::onDisposeIsolate(Isolate* isolate) {
auto itFound = s_type_long_operations_cache.find(isolate);
if (itFound != s_type_long_operations_cache.end()) {
delete itFound->second;
s_type_long_operations_cache.erase(itFound);
}
}

robin_hood::unordered_map<Isolate*, ArgConverter::TypeLongOperationsCache*> ArgConverter::s_type_long_operations_cache;
28 changes: 15 additions & 13 deletions test-app/runtime/src/main/cpp/ArgConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,27 @@ class ArgConverter {
return v8::String::NewFromTwoByte(isolate, ((const uint16_t*) utf16string.data())).ToLocalChecked();
}

static void onDisposeIsolate(v8::Isolate* isolate);
/*
* Per-runtime state (RuntimeState owns one of these per runtime, so it
* has to be constructible from outside ArgConverter). Destroyed with
* the runtime, while its isolate is still alive.
*/
struct TypeLongOperationsCache {
v8::Persistent<v8::Function>* LongNumberCtorFunc = nullptr;

v8::Persistent<v8::NumberObject>* NanNumberObject = nullptr;

~TypeLongOperationsCache() {
delete LongNumberCtorFunc;
delete NanNumberObject;
}
};

private:

// TODO: plamen5kov: rewrite logic for java long number operations in javascript (java long -> javascript number operations check)
static const long long JS_LONG_LIMIT = ((long long) 1) << 53;

struct TypeLongOperationsCache {
v8::Persistent<v8::Function>* LongNumberCtorFunc;

v8::Persistent<v8::NumberObject>* NanNumberObject;
};
//

static TypeLongOperationsCache* GetTypeLongCache(v8::Isolate* isolate);

inline static jstring ObjectToString(jobject object) {
Expand All @@ -146,11 +153,6 @@ class ArgConverter {

static void NativeScriptLongToStringFunctionCallback(const v8::FunctionCallbackInfo<v8::Value>& args);

/*
* "s_type_long_operations_cache" used to keep function
* dealing with operations concerning java long -> javascript number.
*/
static robin_hood::unordered_map<v8::Isolate*, TypeLongOperationsCache*> s_type_long_operations_cache;
};
}

Expand Down
19 changes: 5 additions & 14 deletions test-app/runtime/src/main/cpp/ErrorEvents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@ using namespace std;
using namespace tns;
using namespace v8;

/*
* Non-throwing runtime lookup, safe from V8 callbacks that may fire while a
* runtime is being torn down (Runtime::GetRuntime throws in that window).
*/
static Runtime* GetRuntimeOrNull(Isolate* isolate) {
return static_cast<Runtime*>(
isolate->GetData((uint32_t) Runtime::IsolateData::RUNTIME));
}

/*
* Native function handed to internal/error-events.js as `nativeReportFatal(error,
* stackString)`. It runs the terminal tail (shim + log) WITHOUT re-dispatching
Expand All @@ -38,7 +29,7 @@ static void NativeReportFatalCallback(const FunctionCallbackInfo<Value>& info) {

void ErrorEvents::Init(Local<Context> context) {
auto isolate = v8::Isolate::GetCurrent();
auto runtime = GetRuntimeOrNull(isolate);
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr) {
throw NativeScriptException("ErrorEvents::Init: no runtime for isolate");
}
Expand Down Expand Up @@ -81,7 +72,7 @@ void ErrorEvents::Init(Local<Context> context) {
bool ErrorEvents::DispatchError(Isolate* isolate, Local<Value> error,
const string& messageString,
const string& stack) {
auto runtime = GetRuntimeOrNull(isolate);
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr || runtime->DispatchErrorEventFunc().IsEmpty()) {
return false;
}
Expand All @@ -104,7 +95,7 @@ bool ErrorEvents::DispatchError(Isolate* isolate, Local<Value> error,
bool ErrorEvents::DispatchUnhandledRejection(Isolate* isolate,
Local<Promise> promise,
Local<Value> reason) {
auto runtime = GetRuntimeOrNull(isolate);
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr || runtime->DispatchUnhandledRejectionFunc().IsEmpty()) {
return false;
}
Expand All @@ -126,7 +117,7 @@ bool ErrorEvents::DispatchNativeUncaughtError(Isolate* isolate,
Local<Value> error,
const string& messageString,
const string& stack) {
auto runtime = GetRuntimeOrNull(isolate);
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr || runtime->DispatchNativeUncaughtErrorFunc().IsEmpty()) {
return false;
}
Expand All @@ -149,7 +140,7 @@ bool ErrorEvents::DispatchNativeUncaughtError(Isolate* isolate,
void ErrorEvents::DispatchRejectionHandled(Isolate* isolate,
Local<Promise> promise,
Local<Value> reason) {
auto runtime = GetRuntimeOrNull(isolate);
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr || runtime->DispatchRejectionHandledFunc().IsEmpty()) {
return;
}
Expand Down
3 changes: 1 addition & 2 deletions test-app/runtime/src/main/cpp/Events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ using namespace v8;

void Events::Init(Local<Context> context) {
auto isolate = v8::Isolate::GetCurrent();
auto runtime = static_cast<Runtime*>(
isolate->GetData((uint32_t) Runtime::IsolateData::RUNTIME));
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr) {
throw NativeScriptException("Events::Init: no runtime for isolate");
}
Expand Down
3 changes: 1 addition & 2 deletions test-app/runtime/src/main/cpp/FrameCallbacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,7 @@ void Dispatch(EntryId id, int64_t frameTimeNanos) {
}

Isolate* isolate = entry->isolate_;
Runtime* runtime = static_cast<Runtime*>(
isolate->GetData((uint32_t) Runtime::IsolateData::RUNTIME));
Runtime* runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr) {
return;
}
Expand Down
4 changes: 0 additions & 4 deletions test-app/runtime/src/main/cpp/IsolateDisposer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@

namespace tns {
void disposeIsolate(v8::Isolate *isolate) {
tns::ArgConverter::onDisposeIsolate(isolate);
tns::MetadataNode::onDisposeIsolate(isolate);
tns::Console::onDisposeIsolate(isolate);
tns::JSONObjectHelper::onDisposeIsolate(isolate);
tns::NsBuiltinModules::onDisposeIsolate(isolate);
tns::BuiltinLoader::onDisposeIsolate(isolate);
// clear all isolate bound objects
Expand Down
56 changes: 56 additions & 0 deletions test-app/runtime/src/main/cpp/IsolateTracked.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include "IsolateTracked.h"

#include "RuntimeState.h"
#include "robin_hood.h"

namespace tns {

namespace {
// The live instances of one runtime; see RuntimeState.h. Touched only on that
// runtime's own thread -- both the GC finalizer and the teardown sweep run
// there -- so it needs no synchronization.
struct TrackedInstances {
robin_hood::unordered_set<IsolateTracked*> live;
};
} // namespace

void IsolateTracked::BindFinalizer(v8::Isolate* isolate,
const v8::Local<v8::Object>& object) {
v8::HandleScope scopedHandle(isolate);
weakHandle_.Reset(isolate, object);
weakHandle_.SetWeak(this, Finalizer, v8::WeakCallbackType::kParameter);

auto* tracked = RuntimeState::For<TrackedInstances>(isolate);
if (tracked != nullptr) {
tracked->live.insert(this);
}
}

void IsolateTracked::Finalizer(const v8::WeakCallbackInfo<IsolateTracked>& data) {
IsolateTracked* self = data.GetParameter();

auto* tracked = RuntimeState::For<TrackedInstances>(data.GetIsolate());
if (tracked != nullptr) {
tracked->live.erase(self);
}

delete self;
}

void IsolateTracked::SweepAll(v8::Isolate* isolate) {
auto* tracked = RuntimeState::For<TrackedInstances>(isolate);
if (tracked == nullptr) {
return;
}

// Detached first: a destructor that somehow reached back into the registry
// must not mutate the set being walked.
auto survivors = std::move(tracked->live);
tracked->live.clear();

for (IsolateTracked* instance : survivors) {
delete instance;
}
}

} // namespace tns
44 changes: 44 additions & 0 deletions test-app/runtime/src/main/cpp/IsolateTracked.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef TEST_APP_ISOLATETRACKED_H
#define TEST_APP_ISOLATETRACKED_H

#include "v8.h"

namespace tns {

/*
* Base for self-owned native objects whose lifetime is bound to a single JS
* object through a weak handle (URL, URLSearchParams, URLPattern).
*
* An instance dies in exactly two places: the GC finalizer, or SweepAll at
* runtime teardown. The sweep is the point of this class -- V8 does not run
* weak callbacks when an isolate is disposed, so every instance still alive
* when a runtime goes away (a worker shutting down, say) would otherwise leak
* its native state, and URLPattern would leak its compiled v8::Global handles
* with it.
*
* Never delete a bound instance directly: both deletion paths own the registry
* bookkeeping. Subclasses only need a destructor that releases their own
* state; it runs while the isolate is still alive, so it may reset v8::Global
* handles, but it must not create new ones.
*/
class IsolateTracked {
public:
virtual ~IsolateTracked() = default;

void BindFinalizer(v8::Isolate* isolate, const v8::Local<v8::Object>& object);

/*
* Deletes every instance the GC never got to. Runs on the runtime's own
* thread while the isolate is alive.
*/
static void SweepAll(v8::Isolate* isolate);

private:
static void Finalizer(const v8::WeakCallbackInfo<IsolateTracked>& data);

v8::Global<v8::Object> weakHandle_;
};

} // namespace tns

#endif // TEST_APP_ISOLATETRACKED_H
47 changes: 45 additions & 2 deletions test-app/runtime/src/main/cpp/JEnv.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
#include "JEnv.h"

#include <shared_mutex>
#include <assert.h>
#include "Util.h"
#include "NativeScriptException.h"
#include "DesugaredInterfaceCompanionClassNameResolver.h"

using namespace tns;

/*
* The class caches are process-wide and string-keyed, holding JNI global refs,
* which is correct to share -- but every runtime resolves classes from its own
* thread and workers run concurrently. Shared for lookups, exclusive only to
* publish. NewGlobalRef/DeleteLocalRef stay outside the lock; if two threads
* resolve the same class, the first published wins and the loser releases its
* ref rather than leaking it.
*/
static std::shared_mutex classCacheMutex;
using namespace std;

JEnv::JEnv()
Expand Down Expand Up @@ -758,6 +770,8 @@ jclass JEnv::FindClass(const string &className) {

jclass JEnv::CheckForClassInCache(const string &className) {
jclass global_class = nullptr;

std::shared_lock<std::shared_mutex> lock(classCacheMutex);
auto itFound = s_classCache.find(className);

if (itFound != s_classCache.end()) {
Expand All @@ -769,14 +783,30 @@ jclass JEnv::CheckForClassInCache(const string &className) {

jclass JEnv::InsertClassIntoCache(const string &className, jclass &tmp) {
auto global_class = reinterpret_cast<jclass>(m_env->NewGlobalRef(tmp));
s_classCache.emplace(className, global_class);
m_env->DeleteLocalRef(tmp);

jclass published = nullptr;
{
std::unique_lock<std::shared_mutex> lock(classCacheMutex);
auto result = s_classCache.emplace(className, global_class);
if (!result.second) {
published = result.first->second;
}
}

if (published != nullptr) {
// Another thread resolved the same class first; keep its ref.
m_env->DeleteGlobalRef(global_class);
return published;
}

return global_class;
}

jthrowable JEnv::CheckForClassMissingCache(const string &className) {
jthrowable throwable = nullptr;

std::shared_lock<std::shared_mutex> lock(classCacheMutex);
auto itFound = s_missingClasses.find(className);

if (itFound != s_missingClasses.end()) {
Expand All @@ -788,9 +818,22 @@ jthrowable JEnv::CheckForClassMissingCache(const string &className) {

jthrowable JEnv::InsertClassIntoMissingCache(const string &className,const jthrowable &tmp) {
auto throwable = reinterpret_cast<jthrowable>(m_env->NewGlobalRef(tmp));
s_missingClasses.emplace(className, throwable);
m_env->DeleteLocalRef(tmp);

jthrowable published = nullptr;
{
std::unique_lock<std::shared_mutex> lock(classCacheMutex);
auto result = s_missingClasses.emplace(className, throwable);
if (!result.second) {
published = result.first->second;
}
}

if (published != nullptr) {
m_env->DeleteGlobalRef(throwable);
return published;
}

return throwable;
}

Expand Down
Loading