-
Notifications
You must be signed in to change notification settings - Fork 465
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
Conversation
…zombie state by exception in its constructor
Naming is intentionally left colorful. |
@blagoev could you please add a test wherein the subclass throws an exception in the constructor? |
I will try to add a test for this since the #600 discussion seems did not result in a conclusion. And my version seems simpler. Still I will leave it up for a discussion which version should be merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we heap-allocate and wait for the finalizer callback to free our hint then we leak the hint if wrapping fails. Therefore it's simplest to avoid heap allocation for the purpose of tracking failure.
napi.h
Outdated
@@ -1414,6 +1418,8 @@ namespace Napi { | |||
void* Data() const; | |||
void SetData(void* data); | |||
|
|||
Zombie* zombie; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Zombie* zombie; |
Let's not expose more public fields. We can do a ping-pong with SetData()
and Data()
.
napi-inl.h
Outdated
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) { | ||
Zombie* zombie = (Zombie*)hint; | ||
bool isZombie = zombie->isZombie; | ||
delete zombie; | ||
if (isZombie) { | ||
return; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This becomes unnecessary.
napi-inl.h
Outdated
CallbackInfo callbackInfo(env, info); | ||
callbackInfo.zombie = new Zombie(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callbackInfo.zombie = new Zombie(); | |
ObjectWrapPingPong ping_pong({callbackInfo.Data(), false}); | |
callbackInfo.SetData(&ping_pong); |
napi-inl.h
Outdated
return callbackInfo.This(); | ||
} | ||
catch (...) { | ||
callbackInfo.zombie->isZombie = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callbackInfo.zombie->isZombie = true; | |
if (ping_pong.object_wrapping_failed == false) { | |
napi_status status = napi_remove_wrap(callbackInfo.Env(), callbackInfo.This(), nullptr); | |
NAPI_FATAL_IF_FAILED(status, | |
"ObjectWrap<T>::ConstructorCallbackWrapper", | |
"Failed to remove wrap from failed ObjectWrap instance construction"); | |
} |
napi-inl.h
Outdated
instance = new T(callbackInfo); | ||
if (callbackInfo.Env().IsExceptionPending()) { | ||
callbackInfo.zombie->isZombie = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callbackInfo.zombie->isZombie = true; | |
if (ping_pong.object_wrapping_failed == false) { | |
napi_status status = napi_remove_wrap(callbackInfo.Env(), callbackInfo.This(), nullptr); | |
NAPI_FATAL_IF_FAILED(status, | |
"ObjectWrap<T>::ConstructorCallbackWrapper", | |
"Failed to remove wrap from failed ObjectWrap instance construction"); | |
} |
I guess this part should probably be factored out and a call made to it from above and from here.
@@ -3697,10 +3697,26 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper( | |||
} | |||
|
|||
T* instance; | |||
napi_value wrapper = details::WrapCallback([&] { | |||
napi_value wrapper = details::WrapCallback([&] () -> napi_value { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
napi-inl.h
Outdated
instance = new T(callbackInfo); | ||
if (callbackInfo.Env().IsExceptionPending()) { | ||
callbackInfo.zombie->isZombie = true; | ||
return nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nullptr; |
It's OK to fall through to the return below.
test/objectwrap.cc
Outdated
@@ -172,11 +172,30 @@ class Test : public Napi::ObjectWrap<Test> { | |||
|
|||
std::string Test::s_staticMethodText; | |||
|
|||
#ifdef NAPI_CPP_EXCEPTIONS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should perform this test in both cases – if C++ exceptions are enabled, as well as if they are not enabled.
napi-inl.h
Outdated
@@ -3120,7 +3120,7 @@ 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); | |||
status = napi_wrap(env, wrapper, instance, FinalizeCallback, (void*)callbackInfo.zombie, &ref); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
status = napi_wrap(env, wrapper, instance, FinalizeCallback, (void*)callbackInfo.zombie, &ref); | |
ObjectWrapPingPong* ping_pong = static_cast<ObjectWrapPingPong*>(callbackInfo.Data()); | |
callbackInfo.SetData(ping_pong->data); | |
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can declare
struct ObjectWrapPingPong {
void* data = nullptr;
bool object_wrapping_failed = false;
};
in the private section of ObjectWrap<T>
.
@dananderson @dmitryash WDYT about this approach? |
@blagoev argh! GitHub shows the comments in a weird order 😕 Best to look at the "Files changed" tab. |
@gabrielschulhof The napi_wrap failing was good catch. I was going to propose fixing this with status = napi_wrap(env, wrapper, instance, FinalizeCallback, (void*)callbackInfo.zombie, &ref);
if (status != napi_ok && callbackInfo.zombie != nullptr) {
auto zombie = callbackInfo.zombie;
callbackInfo.zombie = nullptr;
delete zombie;
} I thought of doing it with SetData but then this is user API which might be already used for something else. So we would overwrite user data during the call. My proposal is dictated mostly with what the API already provides (ie the Finalizer hint field and the Callbackinfo passed as only argument) without introducing side effects to the user. The only thing I don't like (and you correctly pointed it out) is having this public field to handle something internal. But it's caused by this desire to make it work within the confines of the current API. I am not at all attached to my approach and I am for finding the best approach for this. So maybe if we hide this public field and access it with a friend internal class or something we can do it better. |
I still think we should avoid heap-allocating, because we can resolve the situation without needing a finalizer callback. Also, if we
AFAICT no user code gets executed between the ping of setting the
A solution based on friend classes would definitely be better than the ping pong, even if I'm right about there being no user code between the ping and the pong. Your approach is good, and I added the elements from #600 (namely the |
added class FinilizerHint friend to CallbackInfo in order to hide the implementation from user API enabled tests for non exception enabled builds
@gabrielschulhof I have refactored this a bit. Please share your thoughts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great way of adding a field to CallbackInfo
without exposing it! I still don't understand why we need to heap-allocate though. If we used napi_remove_wrap()
right after the constructor we could avoid having to allocate the hint.
@blagoev I updated the implementation in https://github.com/nodehs/node-addon-api/pull/600/files to use friend classes while avoiding heap allocation. I gave you co-authorship on the commit because I used friend classes the same way you used them in this PR. Please let me know what you think! |
@@ -1397,6 +1397,7 @@ namespace Napi { | |||
}; | |||
|
|||
class CallbackInfo { | |||
friend class FinalizerHint; |
There was a problem hiding this comment.
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: | ||
bool shouldFinalize; | ||
|
||
static void Init(CallbackInfo& info, bool shouldFinalize = true) { |
There was a problem hiding this comment.
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!
@@ -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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -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); | |||
status = napi_wrap(env, wrapper, instance, FinalizeCallback, (void*)finalizerHint, &ref); |
There was a problem hiding this comment.
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!
instance = new T(callbackInfo); | ||
if (callbackInfo.Env().IsExceptionPending()) { | ||
FinalizerHint* finalizerHint = FinalizerHint::Get(callbackInfo); | ||
if (finalizerHint != nullptr) { |
There was a problem hiding this comment.
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.
#ifdef NAPI_CPP_EXCEPTIONS | ||
TestConstructorExceptions::Initialize(env, exports); | ||
#endif |
There was a problem hiding this comment.
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.
@@ -256,9 +256,19 @@ const test = (binding) => { | |||
testFinalize(clazz); | |||
}; | |||
|
|||
const testConstructorExceptions = () => { | |||
const TestConstructorExceptions = binding.objectwrap.TestConstructorExceptions; | |||
console.log("Runnig test testConstructorExceptions"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.log("Runnig test testConstructorExceptions"); |
The console log is unnecessary.
// `Test` is needed for accessing exposed symbols | ||
testObj(new Test(), Test); | ||
testClass(Test); | ||
|
||
testConstructorExceptions(); | ||
} | ||
|
||
test(require(`./build/${buildType}/binding.node`)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test(require(`./build/${buildType}/binding.node`)); | |
test(require(`./build/${buildType}/binding.node`)); | |
test(require(`./build/${buildType}/binding_noexcept.node`)); |
Please test both scenarios!
closing this in favor of #600 |
fix v8 GC access violation after napi ObjectWrap instance is left in zombie state by an exception in its constructor
closes this #635