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

Conversation

blagoev
Copy link
Contributor

@blagoev blagoev commented Dec 20, 2019

fix v8 GC access violation after napi ObjectWrap instance is left in zombie state by an exception in its constructor

closes this #635

…zombie state by exception in its constructor
@blagoev
Copy link
Contributor Author

blagoev commented Dec 20, 2019

Naming is intentionally left colorful.
Renaming to something like FinalizerCallbackHint or something like that will be more generic

@gabrielschulhof
Copy link
Contributor

@blagoev does #600 not also fix this?

@gabrielschulhof
Copy link
Contributor

@blagoev could you please add a test wherein the subclass throws an exception in the constructor?

@blagoev
Copy link
Contributor Author

blagoev commented Dec 30, 2019

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.

Copy link
Contributor

@gabrielschulhof gabrielschulhof left a 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;
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
Zombie* zombie;

Let's not expose more public fields. We can do a ping-pong with SetData() and Data().

napi-inl.h Outdated
Comment on lines 3826 to 3851
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;
}
}

Copy link
Contributor

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();
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
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;
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
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;
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
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.

napi-inl.h Show resolved Hide resolved
@@ -3697,10 +3697,26 @@ 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.

napi-inl.h Outdated
instance = new T(callbackInfo);
if (callbackInfo.Env().IsExceptionPending()) {
callbackInfo.zombie->isZombie = true;
return nullptr;
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
return nullptr;

It's OK to fall through to the return below.

@@ -172,11 +172,30 @@ class Test : public Napi::ObjectWrap<Test> {

std::string Test::s_staticMethodText;

#ifdef NAPI_CPP_EXCEPTIONS
Copy link
Contributor

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);
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
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);

Copy link
Contributor

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>.

@gabrielschulhof
Copy link
Contributor

@dananderson @dmitryash WDYT about this approach?

@gabrielschulhof
Copy link
Contributor

@blagoev argh! GitHub shows the comments in a weird order 😕 Best to look at the "Files changed" tab.

@blagoev
Copy link
Contributor Author

blagoev commented Dec 30, 2019

@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.

@gabrielschulhof
Copy link
Contributor

@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 still think we should avoid heap-allocating, because we can resolve the situation without needing a finalizer callback. Also, if we napi_remove_wrap() if the wrapping succeeds but the construction fails then we can avoid the need for adding to the finalizer callback.

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.

AFAICT no user code gets executed between the ping of setting the ObjectWrapPingPong as the callbackInfo data and the pong of setting it back to the user data because the ObjectWrap<T> constructor runs before the user subclass' constructor.

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.

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 napi_remove_wrap()) as suggestions. Let's go with it.

added class FinilizerHint friend to CallbackInfo in order to hide the implementation from user API
enabled tests for non exception enabled builds
@blagoev
Copy link
Contributor Author

blagoev commented Dec 31, 2019

@gabrielschulhof I have refactored this a bit. Please share your thoughts.

Copy link
Contributor

@gabrielschulhof gabrielschulhof left a 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.

@gabrielschulhof
Copy link
Contributor

@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;
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:
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!

@@ -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.

@@ -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);
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!

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.

Comment on lines +202 to +204
#ifdef NAPI_CPP_EXCEPTIONS
TestConstructorExceptions::Initialize(env, exports);
#endif
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.

@@ -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.

// `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!

@blagoev
Copy link
Contributor Author

blagoev commented Jan 3, 2020

closing this in favor of #600

@blagoev blagoev closed this Jan 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants