From e7486d4df6aeb7252bc6ae09d3cced9d4adfa076 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Fri, 14 Jun 2019 16:44:18 -0700 Subject: [PATCH] n-api: support type-tagging objects `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: https://github.com/nodejs/node/issues/28164 Co-authored-by: Anna Henningsen PR-URL: https://github.com/nodejs/node/pull/28237 Reviewed-By: Anna Henningsen Reviewed-By: Michael Dawson Reviewed-By: Colin Ihrig Signed-off-by: Gabriel Schulhof --- benchmark/napi/type-tag-check/binding.gyp | 8 + benchmark/napi/type-tag-check/index.js | 18 ++ benchmark/napi/type-tag/binding.c | 84 ++++++++ benchmark/napi/type-tag/binding.gyp | 8 + benchmark/napi/type-tag/check-object-tag.js | 18 ++ benchmark/napi/type-tag/index.js | 18 ++ doc/api/n-api.md | 213 +++++++++++++++++++ src/env.h | 1 + src/js_native_api.h | 10 + src/js_native_api_types.h | 7 + src/js_native_api_v8.cc | 69 ++++++ src/js_native_api_v8.h | 32 +++ test/js-native-api/test_object/test.js | 18 ++ test/js-native-api/test_object/test_object.c | 41 ++++ 14 files changed, 545 insertions(+) create mode 100644 benchmark/napi/type-tag-check/binding.gyp create mode 100644 benchmark/napi/type-tag-check/index.js create mode 100644 benchmark/napi/type-tag/binding.c create mode 100644 benchmark/napi/type-tag/binding.gyp create mode 100644 benchmark/napi/type-tag/check-object-tag.js create mode 100644 benchmark/napi/type-tag/index.js diff --git a/benchmark/napi/type-tag-check/binding.gyp b/benchmark/napi/type-tag-check/binding.gyp new file mode 100644 index 00000000000000..595ab325233661 --- /dev/null +++ b/benchmark/napi/type-tag-check/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ '../type-tag/binding.c' ] + } + ] +} diff --git a/benchmark/napi/type-tag-check/index.js b/benchmark/napi/type-tag-check/index.js new file mode 100644 index 00000000000000..346dfb7812dea1 --- /dev/null +++ b/benchmark/napi/type-tag-check/index.js @@ -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); +} diff --git a/benchmark/napi/type-tag/binding.c b/benchmark/napi/type-tag/binding.c new file mode 100644 index 00000000000000..7bc8b5d7502e8b --- /dev/null +++ b/benchmark/napi/type-tag/binding.c @@ -0,0 +1,84 @@ +#include +#define NAPI_EXPERIMENTAL +#include + +#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; +} diff --git a/benchmark/napi/type-tag/binding.gyp b/benchmark/napi/type-tag/binding.gyp new file mode 100644 index 00000000000000..413621ade335a1 --- /dev/null +++ b/benchmark/napi/type-tag/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ 'binding.c' ] + } + ] +} diff --git a/benchmark/napi/type-tag/check-object-tag.js b/benchmark/napi/type-tag/check-object-tag.js new file mode 100644 index 00000000000000..346dfb7812dea1 --- /dev/null +++ b/benchmark/napi/type-tag/check-object-tag.js @@ -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); +} diff --git a/benchmark/napi/type-tag/index.js b/benchmark/napi/type-tag/index.js new file mode 100644 index 00000000000000..3f85b9af8e7d59 --- /dev/null +++ b/benchmark/napi/type-tag/index.js @@ -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); +} diff --git a/doc/api/n-api.md b/doc/api/n-api.md index c3f158ab5b2b71..afb15bfea7214b 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -593,6 +593,27 @@ minimum lifetimes explicitly. For more details, review the [Object lifetime management][]. +#### napi_type_tag + + +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 @@ -4260,6 +4281,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 +type tag. 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 type tag to determine whether the JavaScript object was +previously "marked" with the type tag. This creates a type-checking capability +of a higher fidelity than `napi_instanceof()` can provide, because such type- +tagging survives prototype manipulation and addon unloading/reloading. + +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 + +> Stability: 1 - Experimental + +```c +napi_status napi_type_tag_object(napi_env env, + napi_value js_object, + const napi_type_tag* 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. + +If the object already has an associated type tag, this API will return +`napi_invalid_arg`. + +### napi_check_object_type_tag + + +> Stability: 1 - Experimental + +```c +napi_status napi_check_object_type_tag(napi_env env, + napi_value js_object, + const napi_type_tag* 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