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

objectwrap: gracefully handle constructor exceptions #600

Closed
wants to merge 4 commits into from

Conversation

gabrielschulhof
Copy link
Contributor

Move the napi_wrap() step out of the
Napi::ObjectWrap<T>::ObjectWrap() constructor to ensure that no
native instance pointer is associated with the JavaScript object under
construction if the native constructor should cause a JavaScript
exception to be thrown.

Fixes: #599

@NickNaso
Copy link
Member

@gabrielschulhof If I well understood you delayed the creation of the object reference in a function ConstructorCallbackWrapper that then we use in the DefineClass to pass the callback to underlying napi_define_class N-API function. Is it right?

@dananderson
Copy link

@gabrielschulhof My initial solution to this was the changes you proposed; however, the solution removes some functionality that may break existing ObjectWrap users (maybe this is a reasonable tradeoff). With the proposed change, native subclasses of ObjectWrap can no longer reference the javascript via Value() and Unwrap() could not be used.

class MyClass : public ObjectWrap<MyClass> {
  MyClass(const CallbackInfo& info) : ObjectWrap<MyClass>(info) {
    // info.This() could be used, but the myDependency constructor could call Unwrap()
    myDependency.New({ this->Value() });
  }
};

The above code feels contrived, but the rough equivalent in javascript seems more common.

class MyClass {
  constructor() {
    this.dependency = new Dependency(this)
  }
}

An alternate solution would be to introduce an ObjectWrap overridable Construct() function. Subclasses would put constructor logic in the new function. Basically, separating the C++/napi_ref construction from the constructor logic. Then, ConstructCallWrapper could do:

instance = new T(); // <- registers napi_ref, same finailizer
instance->Construct();
return callbackInfo.This();

This could also be a breaking change and I have not thought through all of the use cases.

@gabrielschulhof
Copy link
Contributor Author

@dananderson good point! In that case, IINM the best we can do is, in ObjectWrap<T>::ConstructorCallbackWrapper:

  T* instance;
  napi_value wrapper = details::WrapCallback([&] {
    CallbackInfo callbackInfo(env, info);
#ifdef NAPI_CPP_EXCEPTIONS
    try {
      instance = new T(callbackInfo);
    } catch (const std::exception& e) {
      napi_remove_wrap(env, info.This(), instance);
      throw e;
    }
#else
    if (env.IsExceptionPending()) {
      napi_remove_wrap(env, info.This(), instance, ...);
      delete instance;
    }
#endif

Still, the scenario you present lends me to believe that the constructor chain which ultimately throws the exception may, in real addons, have lots of side effects which need to be cleaned up. At least with this modification it will fail with an exception rather than a crash, so that should spotlight the place where things need fixing, and should hopefully get people to look at all the side effects of their constructors.

@dananderson
Copy link

@gabrielschulhof I believe napi_remove_wrap() does not remove the finalizer (and C++this) from the napi_ref, so the GC will still try to delete the invalid pointer. I could be wrong, but this is my understanding from my quick look at the related V8 code (Reference). And, I did not see a way in V8 to disassociate the finalizer from the Reference.

Another solution is to use finalizer hint. Though, I think this is a bit messy and would require a heap allocation.

I don't see a great solution to this without breaking user code.

@gabrielschulhof
Copy link
Contributor Author

@dananderson AFAICT with the change in this PR the v8impl::Reference used for the wrapping is actually deleted twice.

When C++ exceptions are used, it is deleted when the error is thrown, because the superclass of ObjectWrap<T> is Reference<Object> which will call napi_delete_reference() as part of its destructor. At this point, the reference is not actually deleted, but it is marked as a self-deleting reference. Then, in the new code, napi_remove_wrap() is called on the JavaScript object. This causes the v8impl::Reference to be retrieved from the wrapped object and deleted again. By this point it is marked as self-deleting, so it will actually be deleted. As part of the v8impl::Reference destructor, its _persistent member is also destroyed. _persistent is a v8::Global so it will be Reset() as part of its destruction, causing the weak callback to be removed from the JS object.

When C++ exceptions are not used, napi_remove_wrap() is called first, as part of the explicit exception handling added in this PR. Then, when the instance is explicitly deleted, the Reference<T> destructor calls napi_delete_reference(). Again, v8impl::Reference::Delete() is called twice, to the same effect.

Either way, it looks like the failed object construction is now cleaned up correctly.

@gabrielschulhof
Copy link
Contributor Author

... although it looks like Node.js v8.x does not have all our v8impl::Reference fixes, because on that version the code does crash.

@dmitryash
Copy link
Contributor

I would prefer calling napi_wrap() directly in ConstructorCallbackWrapper() after T instance is constructed if it is possible:

std::unique_ptr<T> instance(new T(cb_info));
napi_wrap(...);
instance.release();
return args.This();

@gabrielschulhof
Copy link
Contributor Author

@dananderson pointed out that moving the napi_wrap() out of the constructor will prevent subclasses from calling Unwrap() in their constructor.

@dmitryash
Copy link
Contributor

Just to make sure. Is it really needed to call Unwrap() inside a constructor if there is this pointer? Calling Unwrap() looks similar to calling a virtual function, the object is not competely constructed yet. To get JS value is enough to get This() from callback info instance but with care. JS and C++ are different so that there is not direct mapping between them. Some constructs cannot be implemented directly. However, I do not like an idea if separare Construct() because it is not natural in C++.

@dmitryash
Copy link
Contributor

Moreover this code in ObjectWrap constructor may be an undefined behavior and incorrect

node-addon-api/napi-inl.h

Lines 2971 to 2973 in 24d75dd

T* instance = static_cast<T*>(this);
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref);
NAPI_THROW_IF_FAILED_VOID(env, status);

According to standard

static_cast of a pointer to storage without an object is only allowed when casting to (possibly cv-qualified) void*.

Object of T is not constructed but static_cast is used to cast to T*. So that napi_wrap() should not be called in base class, i.e ObjectWrap.

I'm not an expert in JS but this code looks broken and prints an unexpected value (however strictly defined):

class Dependency {
    constructor(my_class) {
        console.log("Value: " + my_class.value)
    }
}

class MyClass {
    constructor() {
        this.dependency = new Dependency(this)
        this.value = 5
    }
}

const c = new MyClass()

This code is similar to unwrapping and using this in constructor of C++ class and it may have unexpected results and I'm not sure that in C++ it is strictly defined (more likely undefined).
To recap, my opinion is not to allow users to call Unwrap within constructors. Instead Unwrapcan throw Error to detect code/design which probably does some bad things.

napi-inl.h Outdated Show resolved Hide resolved
gabrielschulhof pushed a commit to gabrielschulhof/node-addon-api that referenced this pull request Dec 31, 2019
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
gabrielschulhof pushed a commit to gabrielschulhof/node-addon-api that referenced this pull request Jan 1, 2020
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
gabrielschulhof pushed a commit to gabrielschulhof/node-addon-api that referenced this pull request Jan 1, 2020
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
gabrielschulhof pushed a commit to gabrielschulhof/node-addon-api that referenced this pull request Jan 1, 2020
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
@gabrielschulhof
Copy link
Contributor Author

CI:

Version Job Status
v14.x https://ci.nodejs.org/job/node-test-node-addon-api-new/1507/
v12.x https://ci.nodejs.org/job/node-test-node-addon-api-new/1508/
v10.x https://ci.nodejs.org/job/node-test-node-addon-api-new/1509/
v8.x https://ci.nodejs.org/job/node-test-node-addon-api-new/1510/

That's right – v8.x doesn't have the patch in core that makes references work properly! Good thing it's going EOL post haste 🙂

@@ -3106,11 +3137,11 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
napi_value wrapper = callbackInfo.This();
napi_status status;
napi_ref ref;
T* instance = static_cast<T*>(this);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dmitryash it's actually possible to avoid this cast, because this works just fine in the code below.

Copy link
Member

Choose a reason for hiding this comment

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

@gabrielschulhof While I agree that this is nicer this way, it also requires updating the cast in FinalizeCallback like #475 does

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@addaleax I think #475 will mostly supersede this PR.

@blagoev
Copy link
Contributor

blagoev commented Jan 2, 2020

@gabrielschulhof It seems to me both #638 and #600 latest changes have some drawbacks in the implementation, but I guess we can't make it ideal so lets go with these #600 last changes here. I wish to make some naming suggestions to make the names more generic. Please consider them but if you think it does not improve or it's not worth it, then just ignore them and merge after CI works. I will make comments inline with the code tab.
cheers

try {
new ConstructorExceptionTest();
} catch (anException) {
gotException = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we use assert.throws(() => then the test will fail if there is no exception in the constructor. And currently the global.gc() call will fail the process leaving npm to show a generic error message. I think it's slightly better if we fail the test if there is no exception from the constructor. Just a notch better. what do you think? Or just swap the assert before the gc call maybe.

@blagoev
Copy link
Contributor

blagoev commented Jan 2, 2020

@gabrielschulhof I will post the naming suggestion here for readability

ObjectWrapCleanup -> ObjectWrapCreationContext
ObjectWrapCleanup._wrap_worked -> ObjectWrapCreationContext._objectWrapped (i think c++ addon do not use snake style names or if it does then this becomes _object_wrapped)
ObjectWrapCleanup.MarkWrapOK() -> ObjectWrapCreationContext.SetObjectWrapped() (like setting a single bit)
ObjectWrapCleanup.RemoveWrap() -> ObjectWrapCreationContext.Cleanup()

CallbackInfo->_object_wrap_cleanup -> CallbackInfo->_objectWrapCreationContext (snake style or not)

My way of thinking about this is the caller of new T() needs to setup a creation context which might get used in the future for something else in addition to cleanup the wrapping on fail. For example some other creation parameter can be passed in before the wrapping occurs. Hence the change to more generic name.

Otherwise I think the implementation is great and fixes the issue. So 👍 for a merge. I am not sure about the CI.

cheers

@gabrielschulhof
Copy link
Contributor Author

@blagoev I went with ObjectWrapConstructionContext and equivalents.

@gabrielschulhof
Copy link
Contributor Author

This fix will not work on v8.x so we need to wait for #643 to land.

Gabriel Schulhof and others added 3 commits January 6, 2020 16:27
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
@gabrielschulhof
Copy link
Contributor Author

gabrielschulhof commented Jan 7, 2020

@KevinEady @mhdawson could you PTAL? I removed the "blocked" label, because the PR removing v8.x and v6.x support has landed.


const test = (binding) => {
const { ConstructorExceptionTest } = binding.objectwrapConstructorException;
assert.throws(() => (new ConstructorExceptionTest()));
Copy link
Member

Choose a reason for hiding this comment

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

Could this be improved to validate that we get the same exception as we expect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can do.

Copy link
Member

@mhdawson mhdawson left a comment

Choose a reason for hiding this comment

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

LGTM

@gabrielschulhof
Copy link
Contributor Author

gabrielschulhof pushed a commit that referenced this pull request Jan 9, 2020
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: #599
Co-authored-by: blagoev <[email protected]>
PR-URL: #600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
@gabrielschulhof
Copy link
Contributor Author

Landed in 86384f9.

gabrielschulhof pushed a commit to gabrielschulhof/node-addon-api that referenced this pull request May 11, 2020
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
gabrielschulhof pushed a commit to gabrielschulhof/node-addon-api that referenced this pull request Jun 12, 2020
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs#600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
@gabrielschulhof gabrielschulhof deleted the issue-599 branch February 1, 2021 16:39
@gabrielschulhof gabrielschulhof restored the issue-599 branch February 1, 2021 16:40
@gabrielschulhof gabrielschulhof deleted the issue-599 branch March 7, 2021 18:44
kevindavies8 added a commit to kevindavies8/node-addon-api-Develop that referenced this pull request Aug 24, 2022
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs/node-addon-api#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs/node-addon-api#600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Marlyfleitas added a commit to Marlyfleitas/node-api-addon-Development that referenced this pull request Aug 26, 2022
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs/node-addon-api#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs/node-addon-api#600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
wroy7860 added a commit to wroy7860/addon-api-benchmark-node that referenced this pull request Sep 19, 2022
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs/node-addon-api#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs/node-addon-api#600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
johnfrench3 pushed a commit to johnfrench3/node-addon-api-git that referenced this pull request Aug 11, 2023
Ensure that no native instance pointer is associated with the
JavaScript object under construction if the native constructor causes a
JavaScript exception to be thrown.

Two different cases must be taken into consideration:

1. The exception in the constructor was caused by
   `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed.
2. The exception in the constructor was caused by the constructor of
   the subclass of `ObjectWrap<T>` after `napi_wrap()` was already
   successful.

Fixes: nodejs/node-addon-api#599
Co-authored-by: blagoev <[email protected]>
PR-URL: nodejs/node-addon-api#600
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
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.

GC Crash With ObjectWrap + Exceptions
8 participants