Skip to content

Commit

Permalink
n-api: support type-tagging objects
Browse files Browse the repository at this point in the history
`napi_instanceof()` is insufficient for reliably establishing the data
type to which a pointer stored with `napi_wrap()` or
`napi_create_external()` inside a JavaScript object points. Thus, we
need a way to "mark" an object with a value that, when later retrieved,
can unambiguously tell us whether it is safe to cast the pointer stored
inside it to a certain structure.

Such a check must survive loading/unloading/multiple instances of an
addon, so we use UUIDs chosen *a priori*.

Fixes: nodejs#28164
  • Loading branch information
Gabriel Schulhof committed Jul 29, 2020
1 parent 168b22b commit 654c61c
Show file tree
Hide file tree
Showing 12 changed files with 521 additions and 0 deletions.
84 changes: 84 additions & 0 deletions benchmark/napi/type-tag/binding.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include <assert.h>
#define NAPI_EXPERIMENTAL
#include <node_api.h>

#define NAPI_CALL(call) \
do { \
napi_status status = call; \
assert(status == napi_ok && #call " failed"); \
} while (0);

#define EXPORT_FUNC(env, exports, name, func) \
do { \
napi_value js_func; \
NAPI_CALL(napi_create_function((env), \
(name), \
NAPI_AUTO_LENGTH, \
(func), \
NULL, \
&js_func)); \
NAPI_CALL(napi_set_named_property((env), \
(exports), \
(name), \
js_func)); \
} while (0);

static const napi_type_tag tag = {
0xe7ecbcd5954842f6, 0x9e75161c9bf27282
};

static napi_value TagObject(napi_env env, napi_callback_info info) {
size_t argc = 4;
napi_value argv[4];
uint32_t n;
uint32_t index;
napi_handle_scope scope;

NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
NAPI_CALL(napi_get_value_uint32(env, argv[0], &n));
NAPI_CALL(napi_open_handle_scope(env, &scope));
napi_value objects[n];
for (index = 0; index < n; index++) {
NAPI_CALL(napi_create_object(env, &objects[index]));
}

// Time the object tag creation.
NAPI_CALL(napi_call_function(env, argv[1], argv[2], 0, NULL, NULL));
for (index = 0; index < n; index++) {
NAPI_CALL(napi_type_tag_object(env, objects[index], &tag));
}
NAPI_CALL(napi_call_function(env, argv[1], argv[3], 1, &argv[0], NULL));

NAPI_CALL(napi_close_handle_scope(env, scope));
return NULL;
}

static napi_value CheckObjectTag(napi_env env, napi_callback_info info) {
size_t argc = 4;
napi_value argv[4];
uint32_t n;
uint32_t index;
bool is_of_type;

NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
NAPI_CALL(napi_get_value_uint32(env, argv[0], &n));
napi_value object;
NAPI_CALL(napi_create_object(env, &object));
NAPI_CALL(napi_type_tag_object(env, object, &tag));

// Time the object tag checking.
NAPI_CALL(napi_call_function(env, argv[1], argv[2], 0, NULL, NULL));
for (index = 0; index < n; index++) {
NAPI_CALL(napi_check_object_type_tag(env, object, &tag, &is_of_type));
assert(is_of_type && " type mismatch");
}
NAPI_CALL(napi_call_function(env, argv[1], argv[3], 1, &argv[0], NULL));

return NULL;
}

NAPI_MODULE_INIT() {
EXPORT_FUNC(env, exports, "tagObject", TagObject);
EXPORT_FUNC(env, exports, "checkObjectTag", CheckObjectTag);
return exports;
}
8 changes: 8 additions & 0 deletions benchmark/napi/type-tag/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.c' ]
}
]
}
18 changes: 18 additions & 0 deletions benchmark/napi/type-tag/check-object-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';
const common = require('../../common.js');

let binding;
try {
binding = require(`./build/${common.buildType}/binding`);
} catch {
console.error(`${__filename}: Binding failed to load`);
process.exit(0);
}

const bench = common.createBenchmark(main, {
n: [1e5, 1e6, 1e7],
});

function main({ n }) {
binding.checkObjectTag(n, bench, bench.start, bench.end);
}
18 changes: 18 additions & 0 deletions benchmark/napi/type-tag/type-tag-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';
const common = require('../../common.js');

let binding;
try {
binding = require(`./build/${common.buildType}/binding`);
} catch {
console.error(`${__filename}: Binding failed to load`);
process.exit(0);
}

const bench = common.createBenchmark(main, {
n: [1e3, 1e4, 1e5],
});

function main({ n }) {
binding.tagObject(n, bench, bench.start, bench.end);
}
210 changes: 210 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,27 @@ minimum lifetimes explicitly.

For more details, review the [Object lifetime management][].

#### napi_type_tag
<!-- YAML
added: REPLACEME
-->

A 128-bit value stored as two unsigned 64-bit integers. It serves as a UUID
with which JavaScript objects can be "tagged" in order to ensure that they are
of a certain type. This is a stronger check than [`napi_instanceof`][], because
the latter can report a false positive if the object's prototype has been
manipulated. Type-tagging is most useful in conjunction with [`napi_wrap`][]
because it ensures that the pointer retrieved from a wrapped object can be
safely cast to the native type corresponding to the type tag that had been
previously applied to the JavaScript object.

```c
typedef struct {
uint64_t lower;
uint64_t upper;
} napi_type_tag;
```

### N-API callback types

#### napi_callback_info
Expand Down Expand Up @@ -4282,6 +4303,143 @@ if (is_instance) {

The reference must be freed once it is no longer needed.

There are occasions where `napi_instanceof()` is insufficient for ensuring that
a JavaScript object is a wrapper for a certain native type. This is the case
especially when wrapped JavaScript objects are passed back into the addon via
static methods rather than as the `this` value of prototype methods. In such
cases there is a chance that they may be unwrapped incorrectly.

```js
const myAddon = require('./build/Release/my_addon.node');

// `openDatabase()` returns a JavaScript object that wraps a native database
// handle.
const dbHandle = myAddon.openDatabase();

// `query()` returns a JavaScript object that wraps a native query handle.
const queryHandle = myAddon.query(dbHandle, 'Gimme ALL the things!');

// There is an accidental error in the line below. The first parameter to
// `myAddon.queryHasRecords()` should be the database handle (`dbHandle`), not
// the query handle (`query`), so the correct condition for the while-loop
// should be
//
// myAddon.queryHasRecords(dbHandle, queryHandle)
//
while (myAddon.queryHasRecords(queryHandle, dbHandle)) {
// retrieve records
}
```

In the above example `myAddon.queryHasRecords()` is a method that accepts two
arguments. The first is a database handle and the second is a query handle.
Internally, it unwraps the first argument and casts the resulting pointer to a
native database handle. It then unwraps the second argument and casts the
resulting pointer to a query handle. If the arguments are passed in the wrong
order, the casts will work, however, there is a good chance that the underlying
database operation will fail, or will even cause an invalid memory access.

To ensure that the pointer retrieved from the first argument is indeed a pointer
to a database handle and, similarly, that the pointer retrieved from the second
argument is indeed a pointer to a query handle, the implementation of
`queryHasRecords()` has to perform a type validation. Retaining the JavaScript
class constructor from which the database handle was instantiated and the
constructor from which the query handle was instantiated in `napi_ref`s can
help, because `napi_instanceof()` can then be used to ensure that the instances
passed into `queryHashRecords()` are indeed of the correct type.

Unfortunately, `napi_instanceof()` does not protect against prototype
manipulation. For example, the prototype of the database handle instance can be
set to the prototype of the constructor for query handle instances. In this
case, the database handle instance can appear as a query handle instance, and it
will pass the `napi_instanceof()` test for a query handle instance, while still
containing a pointer to a database handle.

To this end, N-API provides type-tagging capabilities.

A type tag is a 128-bit integer unique to the addon. N-API provides the
`napi_type_tag` structure for storing a type tag. When such a value is passed
along with a JavaScript object stored in a `napi_value` to
`napi_type_tag_object()`, the JavaScript object will be "marked" with the
pointer. The "mark" is invisible on the JavaScript side. When a JavaScript
object arrives into a native binding, `napi_check_object_type_tag()` can be used
along with the original pointer to determine whether the JavaScript object was
previously "marked" with the pointer. This creates a type-checking capability
of a higher fidelity than `napi_instanceof()` can provide, because such type-
tagging survives prototype manipulation.

Continuing the above example, the following skeleton addon implementation
illustrates the use of `napi_type_tag_object()` and
`napi_check_object_type_tag()`.

```c
// This value is the type tag for a database handle. The command
//
// uuidgen | sed -r -e 's/-//g' -e 's/(.{16})(.*)/0x\1, 0x\2/'
//
// can be used to obtain the two values with which to initialize the structure.
static const napi_type_tag DatabaseHandleTypeTag = {
0x1edf75a38336451d, 0xa5ed9ce2e4c00c38
};

// This value is the type tag for a query handle.
static const napi_type_tag QueryHandleTypeTag = {
0x9c73317f9fad44a3, 0x93c3920bf3b0ad6a
};

static napi_value
openDatabase(napi_env env, napi_callback_info info) {
napi_status status;
napi_value result;

// Perform the underlying action which results in a database handle.
DatabaseHandle* dbHandle = open_database();

// Create a new, empty JS object.
status = napi_create_object(env, &result);
if (status != napi_ok) return NULL;

// Tag the object to indicate that it holds a pointer to a `DatabaseHandle`.
status = napi_type_tag_object(env, result, &DatabaseHandleTypeTag);
if (status != napi_ok) return NULL;

// Store the pointer to the `DatabaseHandle` structure inside the JS object.
status = napi_wrap(env, result, dbHandle, NULL, NULL, NULL);
if (status != napi_ok) return NULL;

return result;
}

// Later when we receive a JavaScript object purporting to be a database handle
// we can use `napi_check_object_type_tag()` to ensure that it is indeed such a
// handle.

static napi_value
query(napi_env env, napi_callback_info info) {
napi_status status;
size_t argc = 2;
napi_value argv[2];
bool is_db_handle;

status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
if (status != napi_ok) return NULL;

// Check that the object passed as the first parameter has the previously
// applied tag.
status = napi_check_object_type_tag(env,
argv[0],
&DatabaseHandleTypeTag,
&is_db_handle);
if (status != napi_ok) return NULL;

// Throw a `TypeError` if it doesn't.
if (!is_db_handle) {
// Throw a TypeError.
return NULL;
}
}
```

### napi_define_class
<!-- YAML
added: v8.0.0
Expand Down Expand Up @@ -4455,6 +4613,57 @@ object `js_object` using `napi_wrap()` and removes the wrapping. If a finalize
callback was associated with the wrapping, it will no longer be called when the
JavaScript object becomes garbage-collected.

### napi_type_tag_object

> Stability: 1 - Experimental

<!-- YAML
added: REPLACEME
-->
```c
napi_status napi_type_tag_object(napi_env env,
napi_value js_object,
const void* type_tag);
```

* `[in] env`: The environment that the API is invoked under.
* `[in] js_object`: The JavaScript object to be marked.
* `[in] type_tag`: The tag with which the object is to be marked.

Returns `napi_ok` if the API succeeded.

Associates the value of the `type_tag` pointer with the JavaScript object.
`napi_check_object_type_tag()` can then be used to compare the tag that was
attached to the object with one owned by the addon to ensure that the object
has the right type.

### napi_check_object_type_tag

> Stability: 1 - Experimental

<!-- YAML
added: REPLACEME
-->
```c
napi_status napi_check_object_type_tag(napi_env env,
napi_value js_object,
const void* type_tag,
bool* result);
```

* `[in] env`: The environment that the API is invoked under.
* `[in] js_object`: The JavaScript object whose type tag to examine.
* `[in] type_tag`: The tag with which to compare any tag found on the object.
* `[out] result`: Whether the type tag given matched the type tag on the
object. `false` is also returned if no type tag was found on the object.

Returns `napi_ok` if the API succeeded.

Compares the pointer given as `type_tag` with any that can be found on
`js_object`. If no tag is found on `js_object` or, if a tag is found but it does
not match `type_tag`, then `result` is set to `false`. If a tag is found and it
matches `type_tag`, then `result` is set to `true`.

### napi_add_finalizer

<!-- YAML
Expand Down Expand Up @@ -5517,6 +5726,7 @@ This API may only be called from the main thread.
[`napi_get_reference_value`]: #n_api_napi_get_reference_value
[`napi_get_value_external`]: #n_api_napi_get_value_external
[`napi_has_property`]: #n_api_napi_has_property
[`napi_instanceof`]: #n_api_napi_instanceof
[`napi_is_error`]: #n_api_napi_is_error
[`napi_is_exception_pending`]: #n_api_napi_is_exception_pending
[`napi_make_callback`]: #n_api_napi_make_callback
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ constexpr size_t kFsStatsBufferLength =
V(contextify_context_private_symbol, "node:contextify:context") \
V(contextify_global_private_symbol, "node:contextify:global") \
V(decorated_private_symbol, "node:decorated") \
V(napi_type_tag, "node:napi:type_tag") \
V(napi_wrapper, "node:napi:wrapper") \
V(untransferable_object_private_symbol, "node:untransferableObject") \

Expand Down
9 changes: 9 additions & 0 deletions src/js_native_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,15 @@ NAPI_EXTERN napi_status napi_detach_arraybuffer(napi_env env,
NAPI_EXTERN napi_status napi_is_detached_arraybuffer(napi_env env,
napi_value value,
bool* result);
// Type tagging
NAPI_EXTERN napi_status napi_type_tag_object(napi_env env,
napi_value value,
const napi_type_tag* type_tag);

NAPI_EXTERN napi_status napi_check_object_type_tag(napi_env env,
napi_value value,
const napi_type_tag* type_tag,
bool* result);
#endif // NAPI_EXPERIMENTAL

EXTERN_C_END
Expand Down
Loading

0 comments on commit 654c61c

Please sign in to comment.