Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes v8 GC access violation for zombie objects #638

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
62 changes: 59 additions & 3 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2711,6 +2711,27 @@ inline Object FunctionReference::New(const std::vector<napi_value>& args) const
return scope.Escape(Value().New(args)).As<Object>();
}

class FinalizerHint {
private:
FinalizerHint() {}
public:
bool shouldFinalize;

static void Init(CallbackInfo& info, bool shouldFinalize = true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use non-const references in Node.js. Only const references and pointers. So, please turn this into a pointer!

info.finalizerHint = new FinalizerHint();
info.finalizerHint->shouldFinalize = shouldFinalize;
}

static void Clear(CallbackInfo& info) {
info.finalizerHint = nullptr;
}

static FinalizerHint* Get(const CallbackInfo& info) {
return info.finalizerHint;
}
};


////////////////////////////////////////////////////////////////////////////////
// CallbackInfo class
////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -3120,7 +3141,12 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
napi_status status;
napi_ref ref;
T* instance = static_cast<T*>(this);
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref);
FinalizerHint* finalizerHint = FinalizerHint::Get(callbackInfo);
Copy link
Contributor

@gabrielschulhof gabrielschulhof Jan 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FinalizerHint* finalizerHint = FinalizerHint::Get(callbackInfo);
FinalizerHint* finalizerHint = FinalizerHint::Get(callbackInfo);
if (finalizerHint == nullptr) {
Napi::Error::Fatal("ObjectWrap<T>::ObjectWrap", "Failed to retrieve required finalizer hint");
}

The hint is not optional. It should die if it's absent.

status = napi_wrap(env, wrapper, instance, FinalizeCallback, (void*)finalizerHint, &ref);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid C-style casts!

if (status != napi_ok && finalizerHint != nullptr) {
FinalizerHint::Clear(const_cast<Napi::CallbackInfo&>(callbackInfo));
delete finalizerHint;
}
NAPI_THROW_IF_FAILED_VOID(env, status);
gabrielschulhof marked this conversation as resolved.
Show resolved Hide resolved

Reference<Object>* instanceRef = instance;
Expand Down Expand Up @@ -3697,10 +3723,31 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
}

T* instance;
napi_value wrapper = details::WrapCallback([&] {
napi_value wrapper = details::WrapCallback([&] () -> napi_value {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
napi_value wrapper = details::WrapCallback([&] () -> napi_value {
napi_value wrapper = details::WrapCallback([&] {

We don't need this change, because it's OK to return callbackInfo.This(). After all, when control returns to the engine it will throw anyway, so the actual instance will not be relevant, and there won't be a leak on the native side either.

CallbackInfo callbackInfo(env, info);
FinalizerHint::Init(callbackInfo);
#ifdef NAPI_CPP_EXCEPTIONS
try {
instance = new T(callbackInfo);
return callbackInfo.This();
}
catch (...) {
FinalizerHint* finalizerHint = FinalizerHint::Get(callbackInfo);
if (finalizerHint != nullptr) {
finalizerHint->shouldFinalize = false;
}
throw;
}
#else
instance = new T(callbackInfo);
if (callbackInfo.Env().IsExceptionPending()) {
FinalizerHint* finalizerHint = FinalizerHint::Get(callbackInfo);
if (finalizerHint != nullptr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check should be a hard check. If finalizerHint is NULL, we must go to Napi::Error::Fatal. After all, we set it above.

finalizerHint->shouldFinalize = false;
}
}
return callbackInfo.This();
#endif
});

return wrapper;
Expand Down Expand Up @@ -3823,7 +3870,16 @@ inline napi_value ObjectWrap<T>::InstanceSetterCallbackWrapper(
}

template <typename T>
inline void ObjectWrap<T>::FinalizeCallback(napi_env env, void* data, void* /*hint*/) {
inline void ObjectWrap<T>::FinalizeCallback(napi_env env, void* data, void* hint) {
if (hint != nullptr) {
FinalizerHint* finalizerHint = (FinalizerHint*)hint;
bool shouldFinalize = finalizerHint->shouldFinalize;
delete finalizerHint;
if (!shouldFinalize) {
return;
}
}

T* instance = reinterpret_cast<T*>(data);
instance->Finalize(Napi::Env(env));
delete instance;
Expand Down
2 changes: 2 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,7 @@ namespace Napi {
};

class CallbackInfo {
friend class FinalizerHint;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to forward-declare this class at the top of the file.

public:
CallbackInfo(napi_env env, napi_callback_info info);
~CallbackInfo();
Expand Down Expand Up @@ -1424,6 +1425,7 @@ namespace Napi {
napi_value _staticArgs[6];
napi_value* _dynamicArgs;
void* _data;
FinalizerHint* finalizerHint;
};

class PropertyDescriptor {
Expand Down
24 changes: 24 additions & 0 deletions test/objectwrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,35 @@ class Test : public Napi::ObjectWrap<Test> {

std::string Test::s_staticMethodText;


class TestConstructorExceptions : public Napi::ObjectWrap<TestConstructorExceptions> {
public:
TestConstructorExceptions(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<TestConstructorExceptions>(info) {
#ifdef NAPI_CPP_EXCEPTIONS
throw Napi::Error::New(info.Env(), "Constructor exceptions should not cause v8 GC to fail");
#else
Napi::Error::New(info.Env(), "Constructor exceptions should not cause v8 GC to fail").ThrowAsJavaScriptException();
return;
#endif
}

static void Initialize(Napi::Env env, Napi::Object exports) {
exports.Set("TestConstructorExceptions", DefineClass(env, "TestConstructorExceptions", {}));
}
};



Napi::Object InitObjectWrap(Napi::Env env) {
testStaticContextRef = Napi::Persistent(Napi::Object::New(env));
testStaticContextRef.SuppressDestruct();

Napi::Object exports = Napi::Object::New(env);
Test::Initialize(env, exports);

#ifdef NAPI_CPP_EXCEPTIONS
TestConstructorExceptions::Initialize(env, exports);
#endif
Comment on lines +202 to +204
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This initialization should be unconditional.

return exports;
}
10 changes: 10 additions & 0 deletions test/objectwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,19 @@ const test = (binding) => {
testFinalize(clazz);
};

const testConstructorExceptions = () => {
const TestConstructorExceptions = binding.objectwrap.TestConstructorExceptions;
console.log("Runnig test testConstructorExceptions");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log("Runnig test testConstructorExceptions");

The console log is unnecessary.

assert.throws(() => { new TestConstructorExceptions(); });
global.gc();
console.log("Test testConstructorExceptions complete");
}

// `Test` is needed for accessing exposed symbols
testObj(new Test(), Test);
testClass(Test);

testConstructorExceptions();
}

test(require(`./build/${buildType}/binding.node`));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test(require(`./build/${buildType}/binding.node`));
test(require(`./build/${buildType}/binding.node`));
test(require(`./build/${buildType}/binding_noexcept.node`));

Please test both scenarios!

Expand Down