diff --git a/Core/Node-API/Include/Shared/napi/js_native_api.h b/Core/Node-API/Include/Shared/napi/js_native_api.h index 961b30f2..ff99cf9b 100644 --- a/Core/Node-API/Include/Shared/napi/js_native_api.h +++ b/Core/Node-API/Include/Shared/napi/js_native_api.h @@ -613,8 +613,9 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_is_detached_arraybuffer(napi_env env, napi_value value, bool* result); #endif // NAPI_VERSION >= 7 -#if NAPI_VERSION >= 8 // Type tagging +// [BABYLON-NATIVE-ADDITION]: exposed unconditionally, see napi_type_tag in +// js_native_api_types.h. NAPI_EXTERN napi_status NAPI_CDECL napi_type_tag_object( napi_env env, napi_value value, const napi_type_tag* type_tag); @@ -623,6 +624,8 @@ napi_check_object_type_tag(napi_env env, napi_value value, const napi_type_tag* type_tag, bool* result); + +#if NAPI_VERSION >= 8 NAPI_EXTERN napi_status NAPI_CDECL napi_object_freeze(napi_env env, napi_value object); NAPI_EXTERN napi_status NAPI_CDECL napi_object_seal(napi_env env, diff --git a/Core/Node-API/Include/Shared/napi/js_native_api_types.h b/Core/Node-API/Include/Shared/napi/js_native_api_types.h index bea78ecd..bb03a897 100644 --- a/Core/Node-API/Include/Shared/napi/js_native_api_types.h +++ b/Core/Node-API/Include/Shared/napi/js_native_api_types.h @@ -163,11 +163,13 @@ typedef enum { } napi_key_conversion; #endif // NAPI_VERSION >= 6 -#if NAPI_VERSION >= 8 +// [BABYLON-NATIVE-ADDITION]: type tags are implemented by every engine here, so +// they are exposed unconditionally rather than at NAPI_VERSION >= 8. The tree +// stays at NAPI_VERSION 5 because napi_get_version reports that macro and the +// rest of v6/v7/v8 is not implemented on all engines. typedef struct { uint64_t lower; uint64_t upper; } napi_type_tag; -#endif // NAPI_VERSION >= 8 #endif // SRC_JS_NATIVE_API_TYPES_H_ diff --git a/Core/Node-API/Include/Shared/napi/napi-inl.h b/Core/Node-API/Include/Shared/napi/napi-inl.h index 338a7f0a..0d3c5b75 100644 --- a/Core/Node-API/Include/Shared/napi/napi-inl.h +++ b/Core/Node-API/Include/Shared/napi/napi-inl.h @@ -1343,8 +1343,8 @@ inline TypeTaggable::TypeTaggable() : Value() {} inline TypeTaggable::TypeTaggable(napi_env _env, napi_value _value) : Value(_env, _value) {} -#if NAPI_VERSION >= 8 - +// [BABYLON-NATIVE-ADDITION]: exposed unconditionally, see napi_type_tag in +// js_native_api_types.h. inline void TypeTaggable::TypeTag(const napi_type_tag* type_tag) const { napi_status status = napi_type_tag_object(_env, _value, type_tag); NAPI_THROW_IF_FAILED_VOID(_env, status); @@ -1358,8 +1358,6 @@ inline bool TypeTaggable::CheckTypeTag(const napi_type_tag* type_tag) const { return result; } -#endif // NAPI_VERSION >= 8 - //////////////////////////////////////////////////////////////////////////////// // Object class //////////////////////////////////////////////////////////////////////////////// diff --git a/Core/Node-API/Include/Shared/napi/napi.h b/Core/Node-API/Include/Shared/napi/napi.h index 24b044eb..a671e42c 100644 --- a/Core/Node-API/Include/Shared/napi/napi.h +++ b/Core/Node-API/Include/Shared/napi/napi.h @@ -758,10 +758,11 @@ class Symbol : public Name { class TypeTaggable : public Value { public: -#if NAPI_VERSION >= 8 + // [BABYLON-NATIVE-ADDITION]: exposed unconditionally, see napi_type_tag in + // js_native_api_types.h. void TypeTag(const napi_type_tag* type_tag) const; bool CheckTypeTag(const napi_type_tag* type_tag) const; -#endif // NAPI_VERSION >= 8 + protected: TypeTaggable(); TypeTaggable(napi_env env, napi_value value); diff --git a/Core/Node-API/Source/env_quickjs.cc b/Core/Node-API/Source/env_quickjs.cc index 6cf36e18..25c77a0f 100644 --- a/Core/Node-API/Source/env_quickjs.cc +++ b/Core/Node-API/Source/env_quickjs.cc @@ -107,6 +107,18 @@ namespace Napi env_ptr->has_own_property_function = JS_UNDEFINED; } + if (!JS_IsUndefined(env_ptr->type_tag_map)) + { + JS_FreeValue(env_ptr->context, env_ptr->type_tag_get); + JS_FreeValue(env_ptr->context, env_ptr->type_tag_set); + JS_FreeValue(env_ptr->context, env_ptr->type_tag_has); + JS_FreeValue(env_ptr->context, env_ptr->type_tag_map); + env_ptr->type_tag_get = JS_UNDEFINED; + env_ptr->type_tag_set = JS_UNDEFINED; + env_ptr->type_tag_has = JS_UNDEFINED; + env_ptr->type_tag_map = JS_UNDEFINED; + } + // Free all remaining JSValues in the handle scope stack for (auto& ptr : env_ptr->handle_scope_stack) { diff --git a/Core/Node-API/Source/js_native_api_chakra.cc b/Core/Node-API/Source/js_native_api_chakra.cc index 6e5d3e72..afb92fe6 100644 --- a/Core/Node-API/Source/js_native_api_chakra.cc +++ b/Core/Node-API/Source/js_native_api_chakra.cc @@ -1,8 +1,10 @@ #include "js_native_api_chakra.h" +#include "js_native_api_type_tag.h" #include #include #include #include +#include #include #include #include @@ -1780,6 +1782,140 @@ napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) return napi_ok; } +// Type tags +// +// The tag lives in a WeakMap reachable only from napi_env__ (see +// js_native_api_type_tag.h). Non-object inputs are coerced exactly as the V8 +// port's CHECK_TO_OBJECT does, so all engines agree on those edge cases. +static napi_status EnsureTypeTagMap(napi_env env) { + if (env->type_tag_map != JS_INVALID_REFERENCE) { + return napi_ok; + } + + JsValueRef global = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsGetGlobalObject(&global)); + + JsPropertyIdRef weakMapId = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsCreatePropertyId(STR_AND_LENGTH("WeakMap"), &weakMapId)); + + JsValueRef weakMapCtor = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsGetProperty(global, weakMapId, &weakMapCtor)); + + JsValueType ctorType; + CHECK_JSRT(env, JsGetValueType(weakMapCtor, &ctorType)); + RETURN_STATUS_IF_FALSE(env, ctorType == JsFunction, napi_generic_failure); + + JsValueRef undefinedValue = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsGetUndefinedValue(&undefinedValue)); + + JsValueRef map = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsConstructObject(weakMapCtor, &undefinedValue, 1, &map)); + + JsPropertyIdRef getId = JS_INVALID_REFERENCE; + JsPropertyIdRef setId = JS_INVALID_REFERENCE; + JsPropertyIdRef hasId = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsCreatePropertyId(STR_AND_LENGTH("get"), &getId)); + CHECK_JSRT(env, JsCreatePropertyId(STR_AND_LENGTH("set"), &setId)); + CHECK_JSRT(env, JsCreatePropertyId(STR_AND_LENGTH("has"), &hasId)); + + JsValueRef get = JS_INVALID_REFERENCE; + JsValueRef set = JS_INVALID_REFERENCE; + JsValueRef has = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsGetProperty(map, getId, &get)); + CHECK_JSRT(env, JsGetProperty(map, setId, &set)); + CHECK_JSRT(env, JsGetProperty(map, hasId, &has)); + + // The map is unreachable from the JS heap, so it needs an explicit root. There + // is no matching JsRelease: Napi::Detach runs after JsDisposeRuntime (see + // AppRuntime_Chakra.cpp), so by then the runtime and every object in it are + // already gone and releasing would fault. Same reason the wrap symbol taken in + // Napi::Attach is never released. + CHECK_JSRT(env, JsAddRef(map, nullptr)); + CHECK_JSRT(env, JsAddRef(get, nullptr)); + CHECK_JSRT(env, JsAddRef(set, nullptr)); + CHECK_JSRT(env, JsAddRef(has, nullptr)); + + env->type_tag_map = map; + env->type_tag_get = get; + env->type_tag_set = set; + env->type_tag_has = has; + return napi_ok; +} + +napi_status napi_type_tag_object(napi_env env, + napi_value object, + const napi_type_tag* type_tag) { + CHECK_ENV(env); + CHECK_ARG(env, object); + CHECK_ARG(env, type_tag); + + CHECK_NAPI(EnsureTypeTagMap(env)); + + JsValueRef target = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsConvertValueToObject(reinterpret_cast(object), &target)); + + JsValueRef args[3] = {env->type_tag_map, target, JS_INVALID_REFERENCE}; + + JsValueRef tagged = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsCallFunction(env->type_tag_has, args, 2, &tagged)); + bool alreadyTagged = false; + CHECK_JSRT(env, JsBooleanToBool(tagged, &alreadyTagged)); + RETURN_STATUS_IF_FALSE(env, !alreadyTagged, napi_invalid_arg); + + char hex[napi_type_tag_util::kHexLength + 1]; + napi_type_tag_util::ToHex(type_tag, hex); + + JsValueRef tagValue = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsCreateString(hex, napi_type_tag_util::kHexLength, &tagValue)); + + args[2] = tagValue; + JsValueRef stored = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsCallFunction(env->type_tag_set, args, 3, &stored)); + + return napi_ok; +} + +napi_status napi_check_object_type_tag(napi_env env, + napi_value object, + const napi_type_tag* type_tag, + bool* result) { + CHECK_ENV(env); + CHECK_ARG(env, object); + CHECK_ARG(env, type_tag); + CHECK_ARG(env, result); + + *result = false; + + JsValueRef target = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsConvertValueToObject(reinterpret_cast(object), &target)); + + // Nothing has been tagged yet, so nothing can match. + if (env->type_tag_map == JS_INVALID_REFERENCE) { + return napi_ok; + } + + JsValueRef args[2] = {env->type_tag_map, target}; + JsValueRef stored = JS_INVALID_REFERENCE; + CHECK_JSRT(env, JsCallFunction(env->type_tag_get, args, 2, &stored)); + + JsValueType storedType; + CHECK_JSRT(env, JsGetValueType(stored, &storedType)); + if (storedType != JsString) { + return napi_ok; + } + + char actual[napi_type_tag_util::kHexLength + 1] = {}; + size_t written = 0; + CHECK_JSRT(env, JsCopyString(stored, actual, sizeof(actual), &written)); + + char expected[napi_type_tag_util::kHexLength + 1]; + napi_type_tag_util::ToHex(type_tag, expected); + + *result = written == napi_type_tag_util::kHexLength && + memcmp(actual, expected, napi_type_tag_util::kHexLength) == 0; + return napi_ok; +} + napi_status napi_create_external(napi_env env, void* data, napi_finalize finalize_cb, diff --git a/Core/Node-API/Source/js_native_api_chakra.h b/Core/Node-API/Source/js_native_api_chakra.h index 2dfa59de..14f87423 100644 --- a/Core/Node-API/Source/js_native_api_chakra.h +++ b/Core/Node-API/Source/js_native_api_chakra.h @@ -16,6 +16,13 @@ struct napi_env__ { JsPropertyIdRef wrap_property_id = JS_INVALID_REFERENCE; + // Type-tag store, created on first use. See js_native_api_type_tag.h for why + // this is a WeakMap held here rather than a property on the tagged object. + JsValueRef type_tag_map = JS_INVALID_REFERENCE; + JsValueRef type_tag_get = JS_INVALID_REFERENCE; + JsValueRef type_tag_set = JS_INVALID_REFERENCE; + JsValueRef type_tag_has = JS_INVALID_REFERENCE; + // Escapable scope bookkeeping: token -> whether that scope has escaped. Values // are rooted by the engine rather than by a scope here, so this exists only to // honour the one-escape-per-scope rule and to reject tokens that are not open. diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index 5c8583bc..860ba63e 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -1,4 +1,5 @@ #include "js_native_api_javascriptcore.h" +#include "js_native_api_type_tag.h" #include #include #include @@ -273,12 +274,17 @@ namespace { template static T* Query(napi_env env, JSObjectRef obj, JSValueRef* exception) { - const auto hasSentinel{JSObjectHasPropertyForKey(env->context, obj, T::GetKey(env), exception)}; - if (*exception || !hasSentinel) { + // Own property only. JSObjectHasPropertyForKey searches the prototype + // chain, so Object.create(realInstance) used to resolve to the real + // instance's sentinel and unwrap to its native pointer. + const JSValueRef key{T::GetKey(env)}; + const JSValueRef hasSentinelValue{JSObjectCallAsFunction( + env->context, env->has_own_property_function, obj, 1, &key, exception)}; + if (*exception || !JSValueToBoolean(env->context, hasSentinelValue)) { return nullptr; } - JSValueRef sentinelValue{JSObjectGetPropertyForKey(env->context, obj, T::GetKey(env), exception)}; + JSValueRef sentinelValue{JSObjectGetPropertyForKey(env->context, obj, key, exception)}; if (*exception) { return nullptr; } @@ -837,6 +843,30 @@ void napi_env__::init_symbol(JSValueRef &symbol, const char *description) { JSValueProtect(context, symbol); } +JSObjectRef napi_env__::resolve_has_own_property(JSGlobalContextRef context) { + JSValueRef exception{}; + + JSValueRef objectCtor{JSObjectGetProperty( + context, JSContextGetGlobalObject(context), JSString("Object"), &exception)}; + JSValueRef prototype{exception == nullptr + ? JSObjectGetProperty(context, JSValueToObject(context, objectCtor, &exception), JSString("prototype"), &exception) + : nullptr}; + JSValueRef hasOwnProperty{exception == nullptr + ? JSObjectGetProperty(context, JSValueToObject(context, prototype, &exception), JSString("hasOwnProperty"), &exception) + : nullptr}; + + JSObjectRef function{exception == nullptr && hasOwnProperty != nullptr + ? JSValueToObject(context, hasOwnProperty, &exception) + : nullptr}; + + if (exception != nullptr || function == nullptr || !JSObjectIsFunction(context, function)) { + throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype.hasOwnProperty"}; + } + + JSValueProtect(context, function); + return function; +} + void napi_env__::deinit_symbol(JSValueRef symbol) { JSValueUnprotect(context, symbol); } @@ -2021,6 +2051,126 @@ napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) return napi_ok; } +// Type tags +// +// The tag lives in a WeakMap reachable only from napi_env__ (see +// js_native_api_type_tag.h). Non-object inputs are coerced exactly as the V8 +// port's CHECK_TO_OBJECT does, so all engines agree on those edge cases. +static napi_status EnsureTypeTagMap(napi_env env) { + if (env->type_tag_map != nullptr) { + return napi_ok; + } + + JSValueRef exception{}; + + JSValueRef weakMapValue{JSObjectGetProperty( + env->context, JSContextGetGlobalObject(env->context), JSString("WeakMap"), &exception)}; + CHECK_JSC(env, exception); + + JSObjectRef weakMapCtor{JSValueToObject(env->context, weakMapValue, &exception)}; + CHECK_JSC(env, exception); + RETURN_STATUS_IF_FALSE(env, JSObjectIsConstructor(env->context, weakMapCtor), napi_generic_failure); + + JSObjectRef map{JSObjectCallAsConstructor(env->context, weakMapCtor, 0, nullptr, &exception)}; + CHECK_JSC(env, exception); + + const char* names[3]{"get", "set", "has"}; + JSObjectRef methods[3]{}; + for (int i = 0; i < 3; ++i) { + JSValueRef method{JSObjectGetProperty(env->context, map, JSString(names[i]), &exception)}; + CHECK_JSC(env, exception); + methods[i] = JSValueToObject(env->context, method, &exception); + CHECK_JSC(env, exception); + RETURN_STATUS_IF_FALSE(env, JSObjectIsFunction(env->context, methods[i]), napi_generic_failure); + } + + // The map is unreachable from the JS heap, so it needs an explicit root. + JSValueProtect(env->context, map); + JSValueProtect(env->context, methods[0]); + JSValueProtect(env->context, methods[1]); + JSValueProtect(env->context, methods[2]); + + env->type_tag_map = map; + env->type_tag_get = methods[0]; + env->type_tag_set = methods[1]; + env->type_tag_has = methods[2]; + return napi_ok; +} + +napi_status napi_type_tag_object(napi_env env, + napi_value object, + const napi_type_tag* type_tag) { + CHECK_ENV(env); + CHECK_ARG(env, object); + CHECK_ARG(env, type_tag); + + CHECK_NAPI(EnsureTypeTagMap(env)); + + JSValueRef exception{}; + JSObjectRef target{JSValueToObject(env->context, ToJSValue(object), &exception)}; + CHECK_JSC(env, exception); + + JSValueRef args[2]{target, nullptr}; + + JSValueRef tagged{JSObjectCallAsFunction( + env->context, env->type_tag_has, env->type_tag_map, 1, args, &exception)}; + CHECK_JSC(env, exception); + RETURN_STATUS_IF_FALSE(env, !JSValueToBoolean(env->context, tagged), napi_invalid_arg); + + char hex[napi_type_tag_util::kHexLength + 1]; + napi_type_tag_util::ToHex(type_tag, hex); + + args[1] = JSValueMakeString(env->context, JSString(hex, napi_type_tag_util::kHexLength)); + JSObjectCallAsFunction(env->context, env->type_tag_set, env->type_tag_map, 2, args, &exception); + CHECK_JSC(env, exception); + + return napi_ok; +} + +napi_status napi_check_object_type_tag(napi_env env, + napi_value object, + const napi_type_tag* type_tag, + bool* result) { + CHECK_ENV(env); + CHECK_ARG(env, object); + CHECK_ARG(env, type_tag); + CHECK_ARG(env, result); + + *result = false; + + JSValueRef exception{}; + JSObjectRef target{JSValueToObject(env->context, ToJSValue(object), &exception)}; + CHECK_JSC(env, exception); + + // Nothing has been tagged yet, so nothing can match. + if (env->type_tag_map == nullptr) { + return napi_ok; + } + + JSValueRef args[1]{target}; + JSValueRef stored{JSObjectCallAsFunction( + env->context, env->type_tag_get, env->type_tag_map, 1, args, &exception)}; + CHECK_JSC(env, exception); + + if (!JSValueIsString(env->context, stored)) { + return napi_ok; + } + + JSString storedString{JSString::Attach(JSValueToStringCopy(env->context, stored, &exception))}; + CHECK_JSC(env, exception); + + char actual[napi_type_tag_util::kHexLength + 1]{}; + size_t written{}; + storedString.CopyToUTF8(actual, sizeof(actual), &written); + + char expected[napi_type_tag_util::kHexLength + 1]; + napi_type_tag_util::ToHex(type_tag, expected); + + *result = written == napi_type_tag_util::kHexLength && + std::memcmp(actual, expected, napi_type_tag_util::kHexLength) == 0; + return napi_ok; +} + napi_status napi_create_external(napi_env env, void* data, napi_finalize finalize_cb, diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 8d8dbd02..52c9fe9c 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -21,6 +21,17 @@ struct napi_env__ { JSValueRef reference_info_symbol{}; JSValueRef wrapper_info_symbol{}; + // Object.prototype.hasOwnProperty, cached because JSC's C API has no + // own-property accessor. See NativeInfo::Query. + JSObjectRef has_own_property_function{}; + + // Type-tag store, created on first use. See js_native_api_type_tag.h for why + // this is a WeakMap held here rather than a property on the tagged object. + JSObjectRef type_tag_map{}; + JSObjectRef type_tag_get{}; + JSObjectRef type_tag_set{}; + JSObjectRef type_tag_has{}; + // Escapable scope bookkeeping: token -> whether that scope has escaped. Values // are rooted by the engine rather than by a scope here, so this exists only to // honour the one-escape-per-scope rule and to reject tokens that are not open. @@ -31,7 +42,9 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; - napi_env__(JSGlobalContextRef context) : context{context} { + napi_env__(JSGlobalContextRef context) + : context{context} + , has_own_property_function{resolve_has_own_property(context)} { napi_envs[context] = this; JSGlobalContextRetain(context); init_symbol(constructor_info_symbol, "BabylonNative_ConstructorInfo"); @@ -42,6 +55,13 @@ struct napi_env__ { ~napi_env__() { deinit_refs(); + if (type_tag_map != nullptr) { + JSValueUnprotect(context, type_tag_get); + JSValueUnprotect(context, type_tag_set); + JSValueUnprotect(context, type_tag_has); + JSValueUnprotect(context, type_tag_map); + } + JSValueUnprotect(context, has_own_property_function); deinit_symbol(wrapper_info_symbol); deinit_symbol(reference_info_symbol); deinit_symbol(function_info_symbol); @@ -65,6 +85,10 @@ struct napi_env__ { void deinit_refs(); void init_symbol(JSValueRef& symbol, const char* description); void deinit_symbol(JSValueRef symbol); + + // Resolved in the member initializer list rather than the constructor body so + // that a failure here cannot leave a half-registered env behind. + static JSObjectRef resolve_has_own_property(JSGlobalContextRef context); }; #define RETURN_STATUS_IF_FALSE(env, condition, status) \ diff --git a/Core/Node-API/Source/js_native_api_quickjs.cc b/Core/Node-API/Source/js_native_api_quickjs.cc index 6db7f662..8219bb83 100644 --- a/Core/Node-API/Source/js_native_api_quickjs.cc +++ b/Core/Node-API/Source/js_native_api_quickjs.cc @@ -1,4 +1,5 @@ #include "js_native_api_quickjs.h" +#include "js_native_api_type_tag.h" #include #if defined(__clang__) #pragma clang diagnostic push @@ -2465,14 +2466,16 @@ napi_status napi_run_script(napi_env env, napi_value script, const char* source_ // Wrap/Unwrap for native objects // -// Instances created by a napi class constructor carry the js_wrap_class_id -// class (see the constructor trampoline in ExternalCallback::Callback), which -// gives them an opaque slot plus the NapiWrap finalizer. In that case (the only -// one exercised in this codebase, since node-addon-api's ObjectWrap always -// wraps the constructor's `this`) we store the native pointer directly on the -// instance, so nothing is observable from JS: the prototype is untouched and no -// own-property is added. The prototype-chain fallback below only runs for the -// theoretical case of wrapping an object we did not construct. +// Only an instance created by a napi class constructor can be wrapped: the +// constructor trampoline in ExternalCallback::Callback gives it the +// js_wrap_class_id class, which carries an opaque slot plus the NapiWrap +// finalizer. The native pointer lives on the instance itself, so nothing is +// observable from JS -- the prototype is untouched and no own-property is added. +// +// Wrapping any other object used to splice a wrapper object into its prototype +// chain, and unwrap searched the chain for it. That made +// `Object.create(realInstance)` unwrap to the real instance's native pointer -- +// a type confusion. Both halves are gone; napi_wrap now rejects such an object. napi_status napi_wrap(napi_env env, napi_value js_object, void* native_object, napi_finalize finalize_cb, void* finalize_hint, napi_ref* result) { CHECK_ENV(env); CHECK_ARG(env, js_object); @@ -2482,29 +2485,13 @@ napi_status napi_wrap(napi_env env, napi_value js_object, void* native_object, n JSValue jsObject = ToJSValue(js_object); - ExternalData* externalData = new ExternalData(env, native_object, finalize_cb, finalize_hint); - - if (JS_GetClassID(jsObject) == js_wrap_class_id) { - // Fast path: the instance itself owns the opaque slot. Reject a double wrap - // rather than leaking the previously stored ExternalData. - if (JS_GetOpaque(jsObject, js_wrap_class_id) != nullptr) { - delete externalData; - return napi_set_last_error(env, napi_invalid_arg); - } - JS_SetOpaque(jsObject, externalData); - } else { - // Fallback: create a wrapper object carrying the opaque slot and splice it - // into the prototype chain. Not reached by ObjectWrap-based classes. - JSValue wrapper = JS_NewObjectClass(env->context, js_wrap_class_id); - JS_SetOpaque(wrapper, externalData); + RETURN_STATUS_IF_FALSE(env, JS_GetClassID(jsObject) == js_wrap_class_id, napi_invalid_arg); - JSValue prototype = JS_GetPrototype(env->context, jsObject); - JS_SetPrototype(env->context, wrapper, prototype); - JS_SetPrototype(env->context, jsObject, wrapper); + // Reject a double wrap rather than leaking the previously stored ExternalData. + RETURN_STATUS_IF_FALSE(env, JS_GetOpaque(jsObject, js_wrap_class_id) == nullptr, napi_invalid_arg); - JS_FreeValue(env->context, wrapper); - JS_FreeValue(env->context, prototype); - } + ExternalData* externalData = new ExternalData(env, native_object, finalize_cb, finalize_hint); + JS_SetOpaque(jsObject, externalData); if (result != nullptr) { CHECK_NAPI(napi_create_reference(env, js_object, 0, result)); @@ -2521,34 +2508,14 @@ napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) { JSValue jsObject = ToJSValue(js_object); - // Fast path: native pointer stored on the instance itself. - if (JS_GetClassID(jsObject) == js_wrap_class_id) { - ExternalData* externalData = reinterpret_cast(JS_GetOpaque(jsObject, js_wrap_class_id)); - *result = externalData ? externalData->Data() : nullptr; - napi_clear_last_error(env); - return napi_ok; - } + RETURN_STATUS_IF_FALSE(env, JS_GetClassID(jsObject) == js_wrap_class_id, napi_invalid_arg); - // Fallback: search the prototype chain for a legacy wrapper object. - JSValue current = JS_GetPrototype(env->context, jsObject); - - while (!JS_IsNull(current)) { - if (JS_GetClassID(current) == js_wrap_class_id) { - ExternalData* externalData = reinterpret_cast(JS_GetOpaque(current, js_wrap_class_id)); - *result = externalData ? externalData->Data() : nullptr; - JS_FreeValue(env->context, current); - napi_clear_last_error(env); - return napi_ok; - } - - JSValue proto = JS_GetPrototype(env->context, current); - JS_FreeValue(env->context, current); - current = proto; - } - - JS_FreeValue(env->context, current); // Free the final JS_NULL value - *result = nullptr; - return napi_set_last_error(env, napi_invalid_arg); + ExternalData* externalData = reinterpret_cast(JS_GetOpaque(jsObject, js_wrap_class_id)); + RETURN_STATUS_IF_FALSE(env, externalData != nullptr, napi_invalid_arg); + + *result = externalData->Data(); + napi_clear_last_error(env); + return napi_ok; } napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) { @@ -2557,60 +2524,166 @@ napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) JSValue jsObject = ToJSValue(js_object); - // Fast path: the instance owns the opaque slot. Detach the finalizer by - // clearing the opaque and deleting the adapter directly (without running the - // user finalize callback) so a later GC does not run it on native memory that - // is being handed back or has already been freed (e.g. an ObjectWrap - // constructor that threw during stack unwinding). - if (JS_GetClassID(jsObject) == js_wrap_class_id) { - ExternalData* externalData = reinterpret_cast(JS_GetOpaque(jsObject, js_wrap_class_id)); - if (result != nullptr) { - *result = externalData ? externalData->Data() : nullptr; - } - JS_SetOpaque(jsObject, nullptr); - delete externalData; - napi_clear_last_error(env); + RETURN_STATUS_IF_FALSE(env, JS_GetClassID(jsObject) == js_wrap_class_id, napi_invalid_arg); + + // Detach the finalizer by clearing the opaque and deleting the adapter + // directly (without running the user finalize callback) so a later GC does not + // run it on native memory that is being handed back or has already been freed + // (e.g. an ObjectWrap constructor that threw during stack unwinding). + ExternalData* externalData = reinterpret_cast(JS_GetOpaque(jsObject, js_wrap_class_id)); + if (result != nullptr) { + *result = externalData ? externalData->Data() : nullptr; + } + JS_SetOpaque(jsObject, nullptr); + delete externalData; + napi_clear_last_error(env); + return napi_ok; +} + +// Type tags +// +// The tag lives in a WeakMap reachable only from napi_env__ (see +// js_native_api_type_tag.h). Non-object inputs are coerced exactly as the V8 +// port's CHECK_TO_OBJECT does, so all engines agree on those edge cases. +static napi_status EnsureTypeTagMap(napi_env env) { + if (!JS_IsUndefined(env->type_tag_map)) { return napi_ok; } - // Fallback: walk the prototype chain looking for a legacy wrapper object. - // `parent` is the object whose prototype is `current`, so once the wrapper is - // found it can be spliced out of the chain. - JSValue parent = JS_DupValue(env->context, jsObject); - JSValue current = JS_GetPrototype(env->context, jsObject); + JSContext* ctx = env->context; - while (!JS_IsNull(current)) { - if (JS_GetClassID(current) == js_wrap_class_id) { - ExternalData* externalData = reinterpret_cast(JS_GetOpaque(current, js_wrap_class_id)); - if (result != nullptr) { - *result = externalData ? externalData->Data() : nullptr; - } + JSValue global = JS_GetGlobalObject(ctx); + JSValue weakMapCtor = JS_GetPropertyStr(ctx, global, "WeakMap"); + JS_FreeValue(ctx, global); + if (!JS_IsFunction(ctx, weakMapCtor)) { + JS_FreeValue(ctx, weakMapCtor); + return napi_set_last_error(env, napi_generic_failure); + } - JS_SetOpaque(current, nullptr); + JSValue map = JS_CallConstructor(ctx, weakMapCtor, 0, nullptr); + JS_FreeValue(ctx, weakMapCtor); + if (JS_IsException(map)) { + JS_FreeValue(ctx, map); + return napi_set_last_error(env, napi_pending_exception); + } - JSValue wrapperProto = JS_GetPrototype(env->context, current); - JS_SetPrototype(env->context, parent, wrapperProto); - JS_FreeValue(env->context, wrapperProto); + JSValue get = JS_GetPropertyStr(ctx, map, "get"); + JSValue set = JS_GetPropertyStr(ctx, map, "set"); + JSValue has = JS_GetPropertyStr(ctx, map, "has"); + if (!JS_IsFunction(ctx, get) || !JS_IsFunction(ctx, set) || !JS_IsFunction(ctx, has)) { + JS_FreeValue(ctx, get); + JS_FreeValue(ctx, set); + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, map); + return napi_set_last_error(env, napi_generic_failure); + } - delete externalData; + env->type_tag_map = map; + env->type_tag_get = get; + env->type_tag_set = set; + env->type_tag_has = has; + return napi_ok; +} - JS_FreeValue(env->context, current); - JS_FreeValue(env->context, parent); - napi_clear_last_error(env); - return napi_ok; - } +napi_status napi_type_tag_object(napi_env env, napi_value object, const napi_type_tag* type_tag) { + CHECK_ENV(env); + CHECK_ARG(env, object); + CHECK_ARG(env, type_tag); + + CHECK_NAPI(EnsureTypeTagMap(env)); - JS_FreeValue(env->context, parent); - parent = current; // transfer ownership - current = JS_GetPrototype(env->context, parent); + JSContext* ctx = env->context; + JSValue jsObject = JS_ToObject(ctx, ToJSValue(object)); + if (JS_IsException(jsObject)) { + JS_FreeValue(ctx, jsObject); + return napi_set_last_error(env, napi_pending_exception); } - JS_FreeValue(env->context, parent); - JS_FreeValue(env->context, current); // Free the final JS_NULL value - if (result != nullptr) { - *result = nullptr; + JSValueConst args[2]; + args[0] = jsObject; + + JSValue tagged = JS_Call(ctx, env->type_tag_has, env->type_tag_map, 1, args); + if (JS_IsException(tagged)) { + JS_FreeValue(ctx, tagged); + JS_FreeValue(ctx, jsObject); + return napi_set_last_error(env, napi_pending_exception); + } + const bool alreadyTagged = JS_ToBool(ctx, tagged) == 1; + JS_FreeValue(ctx, tagged); + if (alreadyTagged) { + JS_FreeValue(ctx, jsObject); + return napi_set_last_error(env, napi_invalid_arg); + } + + char hex[napi_type_tag_util::kHexLength + 1]; + napi_type_tag_util::ToHex(type_tag, hex); + + JSValue tagValue = JS_NewStringLen(ctx, hex, napi_type_tag_util::kHexLength); + if (JS_IsException(tagValue)) { + JS_FreeValue(ctx, tagValue); + JS_FreeValue(ctx, jsObject); + return napi_set_last_error(env, napi_generic_failure); + } + + args[1] = tagValue; + JSValue stored = JS_Call(ctx, env->type_tag_set, env->type_tag_map, 2, args); + JS_FreeValue(ctx, tagValue); + JS_FreeValue(ctx, jsObject); + if (JS_IsException(stored)) { + JS_FreeValue(ctx, stored); + return napi_set_last_error(env, napi_pending_exception); + } + JS_FreeValue(ctx, stored); + + napi_clear_last_error(env); + return napi_ok; +} + +napi_status napi_check_object_type_tag(napi_env env, napi_value object, const napi_type_tag* type_tag, bool* result) { + CHECK_ENV(env); + CHECK_ARG(env, object); + CHECK_ARG(env, type_tag); + CHECK_ARG(env, result); + + *result = false; + + JSContext* ctx = env->context; + JSValue jsObject = JS_ToObject(ctx, ToJSValue(object)); + if (JS_IsException(jsObject)) { + JS_FreeValue(ctx, jsObject); + return napi_set_last_error(env, napi_pending_exception); + } + + // Nothing has been tagged yet, so nothing can match. + if (JS_IsUndefined(env->type_tag_map)) { + JS_FreeValue(ctx, jsObject); + napi_clear_last_error(env); + return napi_ok; + } + + JSValueConst args[1] = { jsObject }; + JSValue stored = JS_Call(ctx, env->type_tag_get, env->type_tag_map, 1, args); + JS_FreeValue(ctx, jsObject); + if (JS_IsException(stored)) { + JS_FreeValue(ctx, stored); + return napi_set_last_error(env, napi_pending_exception); } - return napi_set_last_error(env, napi_invalid_arg); + + if (JS_IsString(stored)) { + size_t length = 0; + const char* str = JS_ToCStringLen(ctx, &length, stored); + if (str != nullptr) { + char hex[napi_type_tag_util::kHexLength + 1]; + napi_type_tag_util::ToHex(type_tag, hex); + *result = length == napi_type_tag_util::kHexLength && + memcmp(str, hex, napi_type_tag_util::kHexLength) == 0; + JS_FreeCString(ctx, str); + } + } + + JS_FreeValue(ctx, stored); + napi_clear_last_error(env); + return napi_ok; } // External values diff --git a/Core/Node-API/Source/js_native_api_quickjs.h b/Core/Node-API/Source/js_native_api_quickjs.h index 38f9118f..00c32b7f 100644 --- a/Core/Node-API/Source/js_native_api_quickjs.h +++ b/Core/Node-API/Source/js_native_api_quickjs.h @@ -28,6 +28,13 @@ struct napi_env__ { napi_extended_error_info last_error{ nullptr, nullptr, 0, napi_ok }; JSValue has_own_property_function = JS_UNDEFINED; + // Type-tag store, created on first use. See js_native_api_type_tag.h for why + // this is a WeakMap held here rather than a property on the tagged object. + JSValue type_tag_map = JS_UNDEFINED; + JSValue type_tag_get = JS_UNDEFINED; + JSValue type_tag_set = JS_UNDEFINED; + JSValue type_tag_has = JS_UNDEFINED; + const std::thread::id thread_id{std::this_thread::get_id()}; // Handle scope storage diff --git a/Core/Node-API/Source/js_native_api_type_tag.h b/Core/Node-API/Source/js_native_api_type_tag.h new file mode 100644 index 00000000..f79f7f23 --- /dev/null +++ b/Core/Node-API/Source/js_native_api_type_tag.h @@ -0,0 +1,38 @@ +#pragma once + +// [BABYLON-NATIVE-ADDITION] +// +// Shared helper for the QuickJS, Chakra and JavaScriptCore type-tag +// implementations. +// +// V8 stores the tag under a v8::Private, which script cannot reach at all. +// These three engines have no equivalent per-object native slot for an arbitrary +// object (JS_SetOpaque, JsSetExternalData and JSObjectSetPrivate all require an +// object of the port's own class), so each keeps a WeakMap that is reachable +// only from napi_env__ and stores the tag in it as the fixed-width hex string +// produced here. A hidden own property would not do: even under a symbol, +// Object.getOwnPropertySymbols hands script the key, and the tag could then be +// read off a real instance and replayed onto a spoofed object -- which is the +// type confusion the tag exists to prevent. + +#include + +#include + +namespace napi_type_tag_util { + +// 128 bits as hex, upper word first. +constexpr int kHexLength = 32; + +inline void ToHex(const napi_type_tag* tag, char out[kHexLength + 1]) { + constexpr char digits[] = "0123456789abcdef"; + const uint64_t words[2] = {tag->upper, tag->lower}; + for (int w = 0; w < 2; ++w) { + for (int i = 0; i < 16; ++i) { + out[w * 16 + i] = digits[(words[w] >> ((15 - i) * 4)) & 0xF]; + } + } + out[kHexLength] = '\0'; +} + +} // namespace napi_type_tag_util diff --git a/Core/Node-API/Source/js_native_api_v8.cc b/Core/Node-API/Source/js_native_api_v8.cc index 8105a0bf..985ef4ee 100644 --- a/Core/Node-API/Source/js_native_api_v8.cc +++ b/Core/Node-API/Source/js_native_api_v8.cc @@ -358,9 +358,15 @@ inline napi_status Unwrap(napi_env env, RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg); v8::Local obj = value.As(); - // [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property + // [BABYLON-NATIVE-ADDITION]: only an object created from a napi class template + // has an internal field. Reading field 0 off any other object and + // dereferencing the result is an access violation, and napi_remove_wrap leaves + // a null behind. Upstream needs neither check: it reads a private property, + // which any object can carry, and validates it with IsExternal(). + RETURN_STATUS_IF_FALSE(env, obj->InternalFieldCount() >= 1, napi_invalid_arg); Reference* reference = static_cast(obj->GetAlignedPointerFromInternalField(0)); + RETURN_STATUS_IF_FALSE(env, reference != nullptr, napi_invalid_arg); if (result) { *result = reference->Data(); @@ -567,6 +573,11 @@ inline napi_status Wrap(napi_env env, RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg); v8::Local obj = value.As(); + // [BABYLON-NATIVE-ADDITION]: writing field 0 of an object that has none + // corrupts memory, so only an object created from a napi class template can be + // wrapped. Upstream can wrap any object because it uses a private property. + RETURN_STATUS_IF_FALSE(env, obj->InternalFieldCount() >= 1, napi_invalid_arg); + v8impl::Reference* reference = nullptr; if (result != nullptr) { // The returned reference should be deleted via napi_delete_reference() @@ -2551,8 +2562,7 @@ napi_status NAPI_CDECL napi_create_external(napi_env env, } // [BABYLON-NATIVE-ADDITION] -// added preprocessor for NAPI_VERSION check. napi_type_tag only defined for NAPI_VERSION >= 8 -#if NAPI_VERSION >= 8 +// Exposed unconditionally; see napi_type_tag in js_native_api_types.h. napi_status NAPI_CDECL napi_type_tag_object(napi_env env, napi_value object, const napi_type_tag* type_tag) { @@ -2562,7 +2572,7 @@ napi_status NAPI_CDECL napi_type_tag_object(napi_env env, CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, obj, object); CHECK_ARG_WITH_PREAMBLE(env, type_tag); - auto key = NAPI_PRIVATE_KEY(context); + auto key = NAPI_TYPE_TAG_PRIVATE_KEY(context); auto maybe_has = obj->HasPrivate(context, key); CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe_has, napi_generic_failure); RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( @@ -2592,7 +2602,7 @@ napi_status NAPI_CDECL napi_check_object_type_tag(napi_env env, CHECK_ARG_WITH_PREAMBLE(env, result); auto maybe_value = - obj->GetPrivate(context, NAPI_PRIVATE_KEY(context)); + obj->GetPrivate(context, NAPI_TYPE_TAG_PRIVATE_KEY(context)); CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe_value, napi_generic_failure); v8::Local val = maybe_value.ToLocalChecked(); @@ -2620,7 +2630,6 @@ napi_status NAPI_CDECL napi_check_object_type_tag(napi_env env, return GET_RETURN_STATUS(env); } -#endif napi_status NAPI_CDECL napi_get_value_external(napi_env env, napi_value value, diff --git a/Core/Node-API/Source/js_native_api_v8_internals.h b/Core/Node-API/Source/js_native_api_v8_internals.h index 3b7e094c..a127d2c5 100644 --- a/Core/Node-API/Source/js_native_api_v8_internals.h +++ b/Core/Node-API/Source/js_native_api_v8_internals.h @@ -76,7 +76,13 @@ class PersistentToLocal { #endif // [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property -//#define NAPI_PRIVATE_KEY(context) \ -// (v8::Private::New(context->GetIsolate())) +// Only the type-tag key is left; napi_wrap uses an internal field. Private::ForApi +// interns one symbol per isolate -- Private::New would mint a fresh one on every +// call, so a tag written under it could never be found again. +#define NAPI_TYPE_TAG_PRIVATE_KEY(context) \ + (v8::Private::ForApi( \ + (context)->GetIsolate(), \ + NAPI_FIXED_ONE_BYTE_STRING((context)->GetIsolate(), \ + "node:napi:type_tag"))) #endif // SRC_JS_NATIVE_API_V8_INTERNALS_H_ diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index d1c2aa44..7391c895 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -772,6 +772,167 @@ TEST(NodeApi, AdjacentEscapableScopesEscapeIndependently) EXPECT_TRUE(bothEscapesAccepted.get_future().get()); } +// Type tags are the only portable way to answer "is this JS object one of my +// type". napi_unwrap returns void* and cannot distinguish one wrapped type from +// another, so without a tag a call site that unwraps an object because its +// prototype looks right reinterprets whatever native pointer it gets back. +namespace +{ + constexpr napi_type_tag ALPHA_TAG{0x9b1d0e2a4c6f8071ULL, 0x35d7a9c1e3f50628ULL}; + constexpr napi_type_tag BETA_TAG{0x2f4e6a8c0d1b3759ULL, 0x86420ace13579bdfULL}; + + class Alpha : public Napi::ObjectWrap + { + public: + static Napi::Function GetClass(Napi::Env env) + { + return DefineClass(env, "Alpha", {}); + } + + Alpha(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + } + }; + + class Beta : public Napi::ObjectWrap + { + public: + static Napi::Function GetClass(Napi::Env env) + { + return DefineClass(env, "Beta", {}); + } + + Beta(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + } + }; + + struct TypeTagResults + { + bool setupOk{false}; + bool alphaMatchesOwnTag{false}; + bool alphaRejectsOtherTag{false}; + bool untaggedSiblingRejectsTag{false}; + bool spoofStillUnwrapsToBeta{false}; + bool spoofRejectsAlphaTag{false}; + bool impostorRejectsAlphaTag{false}; + bool impostorUnwrapRejected{false}; + bool derivedUnwrapRejected{false}; + bool retagRejected{false}; + uint32_t ownNamesBefore{0}; + uint32_t ownNamesAfter{1}; + uint32_t ownSymbolsBefore{0}; + uint32_t ownSymbolsAfter{1}; + }; +} + +TEST(NodeApi, TypeTags) +{ + Babylon::AppRuntime runtime{}; + + std::promise resultsPromise; + + runtime.Dispatch([&resultsPromise](Napi::Env env) mutable { + TypeTagResults results{}; + napi_env nenv{env}; + + Napi::Object objectCtor{env.Global().Get("Object").As()}; + Napi::Function create{objectCtor.Get("create").As()}; + Napi::Function setPrototypeOf{objectCtor.Get("setPrototypeOf").As()}; + Napi::Function getOwnPropertyNames{objectCtor.Get("getOwnPropertyNames").As()}; + Napi::Function getOwnPropertySymbols{objectCtor.Get("getOwnPropertySymbols").As()}; + + const auto countOwn = [](Napi::Function& lister, Napi::Object& target) { + return lister.Call({target}).As().Length(); + }; + + Napi::Function alphaCtor{Alpha::GetClass(env)}; + Napi::Function betaCtor{Beta::GetClass(env)}; + Napi::Object alphaPrototype{alphaCtor.Get("prototype").As()}; + + Napi::Object alpha{alphaCtor.New({})}; + Napi::Object sibling{alphaCtor.New({})}; + Napi::Object beta{betaCtor.New({})}; + + void* alphaData{nullptr}; + void* betaData{nullptr}; + if (napi_unwrap(nenv, alpha, &alphaData) != napi_ok || + napi_unwrap(nenv, beta, &betaData) != napi_ok || + alphaData == nullptr || betaData == nullptr || alphaData == betaData) + { + resultsPromise.set_value(results); + return; + } + + results.ownNamesBefore = countOwn(getOwnPropertyNames, alpha); + results.ownSymbolsBefore = countOwn(getOwnPropertySymbols, alpha); + + if (napi_type_tag_object(nenv, alpha, &ALPHA_TAG) != napi_ok || + napi_type_tag_object(nenv, beta, &BETA_TAG) != napi_ok) + { + resultsPromise.set_value(results); + return; + } + + results.setupOk = true; + + results.ownNamesAfter = countOwn(getOwnPropertyNames, alpha); + results.ownSymbolsAfter = countOwn(getOwnPropertySymbols, alpha); + + results.alphaMatchesOwnTag = alpha.CheckTypeTag(&ALPHA_TAG); + results.alphaRejectsOtherTag = !alpha.CheckTypeTag(&BETA_TAG); + + // The tag belongs to the instance, not to the class, so another Alpha + // that was never tagged must not inherit it through the prototype. + results.untaggedSiblingRejectsTag = !sibling.CheckTypeTag(&ALPHA_TAG); + + results.retagRejected = napi_type_tag_object(nenv, alpha, &ALPHA_TAG) != napi_ok; + + // A genuinely wrapped Beta wearing Alpha's prototype. napi_unwrap + // succeeds and hands back Beta's pointer, because the object really is + // wrapped -- so only the tag can stop an Alpha call site using it. + setPrototypeOf.Call({beta, alphaPrototype}); + void* spoofData{nullptr}; + results.spoofStillUnwrapsToBeta = + napi_unwrap(nenv, beta, &spoofData) == napi_ok && spoofData == betaData; + results.spoofRejectsAlphaTag = !beta.CheckTypeTag(&ALPHA_TAG); + + // Never wrapped at all. + Napi::Object impostor{create.Call({alphaPrototype}).As()}; + results.impostorRejectsAlphaTag = !impostor.CheckTypeTag(&ALPHA_TAG); + void* impostorData{nullptr}; + results.impostorUnwrapRejected = napi_unwrap(nenv, impostor, &impostorData) != napi_ok; + + // Also never wrapped, but with a wrapped object on its prototype chain: + // an unwrap that searches the chain hands back Alpha's pointer. + Napi::Object derived{create.Call({alpha}).As()}; + void* derivedData{nullptr}; + results.derivedUnwrapRejected = napi_unwrap(nenv, derived, &derivedData) != napi_ok; + + resultsPromise.set_value(results); + }); + + const TypeTagResults results{resultsPromise.get_future().get()}; + + ASSERT_TRUE(results.setupOk); + EXPECT_TRUE(results.alphaMatchesOwnTag); + EXPECT_TRUE(results.alphaRejectsOtherTag); + EXPECT_TRUE(results.untaggedSiblingRejectsTag); + EXPECT_TRUE(results.retagRejected); + EXPECT_TRUE(results.spoofStillUnwrapsToBeta); + EXPECT_TRUE(results.spoofRejectsAlphaTag); + EXPECT_TRUE(results.impostorRejectsAlphaTag); + EXPECT_TRUE(results.impostorUnwrapRejected); + EXPECT_TRUE(results.derivedUnwrapRejected); + + // Tagging must not add anything script can see: an own property, even under + // a symbol, can be read off a real instance and replayed onto a spoofed one. + EXPECT_EQ(results.ownNamesBefore, results.ownNamesAfter); + EXPECT_EQ(results.ownSymbolsBefore, results.ownSymbolsAfter); +} + #endif int RunTests()