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

N-API: Implement stricter wrapping #13872

Closed

Conversation

gabrielschulhof
Copy link
Contributor

@gabrielschulhof gabrielschulhof commented Jun 22, 2017

Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to napi_wrap().

Whereas the old criterion for identifying napi_wrap()-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
v8::External which itself contains a pointer to a global static string unique
to N-API to be a napi_wrap()-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with napi_wrap(), and it allows us to recognize that an
object has already undergone napi_wrap() and we can thus replace the old
v8::External with the new one such that, when the old one will be garbage-
collected, any finalize callback for its native pointer will also be called.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. node-api Issues and PRs related to the Node-API. labels Jun 22, 2017
// Search the object's prototype chain for the wrapper object. Usually the
// wrapper would be the first in the chain, but it is OK for other objects to
// be inserted in the prototype chain.
bool FindWrapper(v8::Local<v8::Object> obj,
Copy link
Contributor Author

@gabrielschulhof gabrielschulhof Jun 22, 2017

Choose a reason for hiding this comment

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

If we used a v8::FunctionTemplate instead of a simple string we could take advantage of v8::Object::FindInstanceInPrototypeChain(Local< FunctionTemplate> tmpl) instead of having to perform the descent ourselves.

The problem with that is that the FunctionTemplate it requires needs to be stored somewhere. Now, it can be stored on the napi_env but the problem with that is that the env is per-module. So, if one module does napi_wrap(), then the other module will not recognize that this has happened, because it will have its own copy of a FunctionTemplate serving the same purpose. Is this desirable or is this to be avoided?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nodejs/addon-api

Copy link
Member

Choose a reason for hiding this comment

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

I'd say it we are better off depending on something that prevents things from working across modules.

Copy link
Member

Choose a reason for hiding this comment

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

I meant to say not depending on something that prevents things from working across modules. I think that's why you avoided the function template in the PR so all should be good.

Copy link
Member

@bnoordhuis bnoordhuis Jul 12, 2017

Choose a reason for hiding this comment

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

So, if one module does napi_wrap(), then the other module will not recognize that this has happened, because it will have its own copy of a FunctionTemplate serving the same purpose.

It doesn't have to be. FunctionTemplates can be per-isolate (don't need to be per-context) so if you manage to associate some state with the isolate, you should be good.

As to how to do that: you could use v8::Isolate::SetData() but that's a scarce resource (only 4 slots and one is already taken.)

Perhaps a static std::map<v8::Isolate*, State*> guarded by a mutex? You only need to look it up once per napi_env because you can cache it inside the env. Lock contention shouldn't be an issue.

@mhdawson
Copy link
Member

will wait to do complete review until updated to return an error on second wrap as discussed in the N-API meeting today.

@gabrielschulhof
Copy link
Contributor Author

@mhdawson done now.

doc/api/n-api.md Outdated
*Note*: Calling `napi_wrap()` a second time on an object that already has a
native instance associated with it by virtue of a previous call to
`napi_wrap()` will have the effect that the new instance will replace the old
instance, and the old instance will eventually be freed by the JavaScript
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs to be updated to reflect an error will be thrown.

src/node_api.cc Outdated
// Store the pointer as an external in the wrapper. If an external is already
// there, this will replace it and the old external will eventually be gc-ed.
// At that point the associated pointer will be finalized if a finalize_cb was
// specified at the time of wrapping.
Copy link
Member

Choose a reason for hiding this comment

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

This should be updated as well. Since we should error out above.

@@ -0,0 +1,6 @@
{
Copy link
Member

Choose a reason for hiding this comment

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

I think we should add this test to test_general instead of using a new addon. Each addon makes the compile just a little bit longer.

@gabrielschulhof
Copy link
Contributor Author

@mhdawson I have addressed your comments.

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

// Search the object's prototype chain for the wrapper object. Usually the
// wrapper would be the first in the chain, but it is OK for other objects to
// be inserted in the prototype chain.
bool FindWrapper(v8::Local<v8::Object> obj,
Copy link
Member

Choose a reason for hiding this comment

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

I meant to say not depending on something that prevents things from working across modules. I think that's why you avoided the function template in the PR so all should be good.

@mhdawson
Copy link
Member

@mhdawson
Copy link
Member

mhdawson commented Jul 6, 2017

@gabrielschulhof sorry this dropped off my radar, needs a rebase and a new CI run.

@gabrielschulhof
Copy link
Contributor Author

I had an unrelated test failure when I ran make -j4 test:

Path: sequential/test-fs-readfile-tostring-fail
/boot/nix/node/node/test/sequential/test-fs-readfile-tostring-fail.js:56
  throw err;
  ^

AssertionError [ERR_ASSERTION]: false == true
    at /boot/nix/node/node/test/sequential/test-fs-readfile-tostring-fail.js:31:12
    at /boot/nix/node/node/test/common/index.js:518:15
    at tryToString (fs.js:512:3)
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:500:12)
Command: out/Release/node /boot/nix/node/node/test/sequential/test-fs-readfile-tostring-fail.js

@gabrielschulhof
Copy link
Contributor Author

@mhdawson I've rebased, but I suspect a test run now will likely run into this unrelated error.

@mhdawson
Copy link
Member

mhdawson commented Jul 6, 2017

Needs one more rebase since I landed the other PR we talked about first.

@gabrielschulhof
Copy link
Contributor Author

@mhdawson It's rebased now, and when I ran make -j4 test locally, this time it passed with flying colours :)

src/node_api.cc Outdated
@@ -673,6 +673,38 @@ v8::Local<v8::Object> CreateAccessorCallbackData(napi_env env,
return cbdata;
}

// Pointer used to identify items wrapped by N-API. Used by FindWrapper and
// napi_wrap().
char napi_wrap_name[] = "N-API Wrapper";
Copy link
Member

Choose a reason for hiding this comment

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

Nit: const char*?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left out the const because converting from const char* to void* requires a pile of casting:

reinterpret_cast<void*>(const_cast<char*>(v8impl::napi_wrap_name))

But anyway, I'll add it.

napi_value wrap(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value argv[2];
napi_ref payload, callback;
Copy link
Member

Choose a reason for hiding this comment

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

Is callback used anywhere?

@mhdawson
Copy link
Member

mhdawson commented Jul 7, 2017

Looks like @gabrielschulhof pushed a commit to address @TimothyGu's comments.

CI run - https://ci.nodejs.org/job/node-test-pull-request/9030/

@gabrielschulhof
Copy link
Contributor Author

Is this OK to land?

doc/api/n-api.md Outdated
@@ -2905,6 +2905,10 @@ required in order to enable correct proper of the reference.
Afterward, additional manipulation of the wrapper's prototype chain may cause
`napi_unwrap()` to fail.

*Note*: Calling `napi_wrap()` a second time on an object that already has a
native instance associated with it by virtue of a previous call to
`napi_wrap()` will cause an error to be thrown.
Copy link
Member

Choose a reason for hiding this comment

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

error to be returned?

src/node_api.cc Outdated

// Store the pointer as an external in the wrapper.
v8::Local<v8::Value> external = v8::External::New(isolate, native_object);
wrapper->SetInternalField(0, external);
Copy link
Member

Choose a reason for hiding this comment

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

Why are you overwriting the External created and stored just a few lines up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not. A few lines up I'm setting the other index (1) whereas here I'm setting (0).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, dang! Merge error. You're right!

Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to `napi_wrap()`.

Whereas the old criterion for identifying `napi_wrap()`-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
`v8::External` which itself contains a pointer to a global static string unique
to N-API to be a `napi_wrap()`-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with `napi_wrap()`, and it allows us to recognize that an
object has already undergone `napi_wrap()` and we can thus prevent a chain of
wrappers only the first of which is accessible from appearing in the prototype
chain, as would be the result of multiple calls to `napi_wrap()` using the same
object.
@gabrielschulhof
Copy link
Contributor Author

@TimothyGu should be good now, but it needs another CI run.

@TimothyGu
Copy link
Member

Running CI at https://ci.nodejs.org/job/node-test-pull-request/9078/. Might take a while though because of the backlog from the CI lockdown.

@evanlucas
Copy link
Contributor

Can you lowercase the subsystem in the commit?

@mhdawson
Copy link
Member

@evanlucas will do

@mhdawson
Copy link
Member

Landed as d5b397c

mhdawson pushed a commit that referenced this pull request Jul 12, 2017
Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to `napi_wrap()`.

Whereas the old criterion for identifying `napi_wrap()`-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
`v8::External` which itself contains a pointer to a global static string unique
to N-API to be a `napi_wrap()`-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with `napi_wrap()`, and it allows us to recognize that an
object has already undergone `napi_wrap()` and we can thus prevent a chain of
wrappers only the first of which is accessible from appearing in the prototype
chain, as would be the result of multiple calls to `napi_wrap()` using the same
object.

PR-URL: #13872
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Timothy Gu <[email protected]>
@mhdawson
Copy link
Member

@bnoordhuis I just noticed your comment about not recognizing the wrap between 2 modules after landing. I had discussed this with Gabriel and the way the check works that is not a problem since we don't check against the FunctionTemplate itself.

Having said that @gabrielschulhof does make sense for us to look at the suggestion to see if that makes thing better.

@mhdawson mhdawson closed this Jul 12, 2017
@addaleax addaleax mentioned this pull request Jul 18, 2017
addaleax pushed a commit that referenced this pull request Jul 18, 2017
Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to `napi_wrap()`.

Whereas the old criterion for identifying `napi_wrap()`-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
`v8::External` which itself contains a pointer to a global static string unique
to N-API to be a `napi_wrap()`-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with `napi_wrap()`, and it allows us to recognize that an
object has already undergone `napi_wrap()` and we can thus prevent a chain of
wrappers only the first of which is accessible from appearing in the prototype
chain, as would be the result of multiple calls to `napi_wrap()` using the same
object.

PR-URL: #13872
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Timothy Gu <[email protected]>
@gabrielschulhof gabrielschulhof deleted the stricter-wrapping branch July 19, 2017 12:14
Fishrock123 pushed a commit that referenced this pull request Jul 19, 2017
Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to `napi_wrap()`.

Whereas the old criterion for identifying `napi_wrap()`-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
`v8::External` which itself contains a pointer to a global static string unique
to N-API to be a `napi_wrap()`-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with `napi_wrap()`, and it allows us to recognize that an
object has already undergone `napi_wrap()` and we can thus prevent a chain of
wrappers only the first of which is accessible from appearing in the prototype
chain, as would be the result of multiple calls to `napi_wrap()` using the same
object.

PR-URL: #13872
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Timothy Gu <[email protected]>
gabrielschulhof pushed a commit to gabrielschulhof/node that referenced this pull request Jul 22, 2017
Store the `napi_env` on the global object at a private key. This gives
us one `napi_env` per context.

Re: nodejs#13872 (comment)
gabrielschulhof pushed a commit to gabrielschulhof/node that referenced this pull request Apr 10, 2018
Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to `napi_wrap()`.

Whereas the old criterion for identifying `napi_wrap()`-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
`v8::External` which itself contains a pointer to a global static string unique
to N-API to be a `napi_wrap()`-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with `napi_wrap()`, and it allows us to recognize that an
object has already undergone `napi_wrap()` and we can thus prevent a chain of
wrappers only the first of which is accessible from appearing in the prototype
chain, as would be the result of multiple calls to `napi_wrap()` using the same
object.

PR-URL: nodejs#13872
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Timothy Gu <[email protected]>
MylesBorins pushed a commit that referenced this pull request Apr 16, 2018
Use a stronger criterion to identify objects in the prototype chain that store
pointers to native data that were added by previous calls to `napi_wrap()`.

Whereas the old criterion for identifying `napi_wrap()`-injected prototype
chain objects was to consider an object with an internal field
count of 1 to be such an object, the new criterion is to consider an object
with an internal field count of 2 such that the second field holds a
`v8::External` which itself contains a pointer to a global static string unique
to N-API to be a `napi_wrap()`-injected prototype chain object.

This greatly reduces the possibility of returning a pointer that was not
previously added with `napi_wrap()`, and it allows us to recognize that an
object has already undergone `napi_wrap()` and we can thus prevent a chain of
wrappers only the first of which is accessible from appearing in the prototype
chain, as would be the result of multiple calls to `napi_wrap()` using the same
object.

Backport-PR-URL: #19447
PR-URL: #13872
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
Reviewed-By: Timothy Gu <[email protected]>
@MylesBorins MylesBorins mentioned this pull request Apr 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. node-api Issues and PRs related to the Node-API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants