From d62235471df85e6b718933f4c4e43157b307119b Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 1 Oct 2026 20:14:18 -0300 Subject: [PATCH 1/7] fix(runtime): unwrap Proxy receivers and arguments before Java dispatch Reactive frameworks (Vue's reactive()) wrap Java object wrappers in a JS Proxy. A Proxy has no internal fields and no creation context, so the first Java method call on one aborted the process in GetInternalField, and any private-value lookup on one aborted in GetCreationContext. - ObjectManager::UnwrapProxy follows Proxy-of-Proxy chains to the target (null when revoked); GetJSInstanceInfo, and therefore every GetJavaObjectByJsObject caller, resolves through it. - Method, field, 'super', 'class' and 'null' accessors dispatch on the Proxy target. A revoked Proxy, or one whose target is not a Java object for an instance member, throws a TypeError. - JsArgConverter, JsArgToArrayConverter, JSToJavaConverter and MethodCache::GetType marshal the target of a Proxy argument, so proxied Java objects and proxied JS arrays convert as their targets do; a revoked Proxy argument is a conversion error. - Implementation objects passed to extend() or an interface constructor are unwrapped. - CallSuper is read only from objects that have the field, so a plain object as `this` throws instead of aborting. - Creation-context lookups fall back to the current context instead of aborting; Symbol.hasInstance catches native exceptions; URLImpl checks the field count before reading its pointer. --- test-app/app/src/main/assets/app/mainpage.js | 1 + .../assets/app/tests/testProxyReceivers.js | 190 ++++++++++++++++++ .../runtime/src/main/cpp/CallbackHandlers.cpp | 4 +- .../runtime/src/main/cpp/FieldAccessor.cpp | 3 + .../runtime/src/main/cpp/JsArgConverter.cpp | 8 +- .../runtime/src/main/cpp/JsArgConverter.h | 2 +- .../src/main/cpp/JsArgToArrayConverter.cpp | 8 +- .../src/main/cpp/JsArgToArrayConverter.h | 2 +- .../runtime/src/main/cpp/MetadataNode.cpp | 151 ++++++++++---- test-app/runtime/src/main/cpp/MethodCache.cpp | 5 +- .../runtime/src/main/cpp/NumericCasts.cpp | 4 +- .../runtime/src/main/cpp/ObjectManager.cpp | 15 +- test-app/runtime/src/main/cpp/ObjectManager.h | 14 +- test-app/runtime/src/main/cpp/URLImpl.cpp | 3 + .../runtime/src/main/cpp/V8GlobalHelpers.cpp | 12 +- .../runtime/src/main/cpp/V8GlobalHelpers.h | 5 + .../cpp/conversions/JSToJavaConverter.cpp | 9 +- .../main/cpp/conversions/JSToJavaConverter.h | 2 +- 18 files changed, 379 insertions(+), 59 deletions(-) create mode 100644 test-app/app/src/main/assets/app/tests/testProxyReceivers.js diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 62aeb52eb..0d0f26200 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -64,6 +64,7 @@ require("./tests/testClassForNameDiscovery"); require("./tests/testInterfaceImplementation"); require("./tests/testRuntimeImplementedAPIs"); require("./tests/testsInstanceOfOperator"); +require("./tests/testProxyReceivers"); require("./tests/testReleaseNativeCounterpart"); require("./tests/testJSONObjects"); require("./tests/kotlin/companions/testCompanionObjectsSupport"); diff --git a/test-app/app/src/main/assets/app/tests/testProxyReceivers.js b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js new file mode 100644 index 000000000..27ea04185 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js @@ -0,0 +1,190 @@ +// Reactive frameworks (Vue's reactive(), MobX observables) hand Java wrappers +// around behind a JS Proxy, so the Proxy reaches the runtime both as `this` +// and as a method argument. +describe("Proxy receivers and arguments", function () { + + function passThrough() { + return { + get: function (target, key, receiver) { + return Reflect.get(target, key, receiver); + }, + set: function (target, key, value, receiver) { + return Reflect.set(target, key, value, receiver); + } + }; + } + + it("calls an instance method on the proxied Java object", function () { + var sb = new java.lang.StringBuilder(); + var proxy = new Proxy(sb, {}); + + proxy.append("abc"); + + expect(sb.toString()).toBe("abc"); + expect(proxy.length()).toBe(3); + }); + + it("calls an instance method through a Proxy with traps", function () { + var sb = new java.lang.StringBuilder(); + var proxy = new Proxy(sb, passThrough()); + + proxy.append("x").append("y"); + + expect(sb.toString()).toBe("xy"); + }); + + it("calls an instance method through nested Proxies", function () { + var sb = new java.lang.StringBuilder("a"); + var proxy = new Proxy(new Proxy(sb, {}), passThrough()); + + proxy.append("b"); + + expect(sb.toString()).toBe("ab"); + }); + + it("calls a static method through a Proxy of the class", function () { + var IntegerProxy = new Proxy(java.lang.Integer, {}); + var StringProxy = new Proxy(java.lang.String, passThrough()); + + expect(IntegerProxy.parseInt("5")).toBe(5); + expect(StringProxy.format("%d", [java.lang.Integer.valueOf(3)])).toBe("3"); + }); + + it("reads and writes a public Java field through a Proxy", function () { + var point = new android.graphics.Point(1, 2); + var proxy = new Proxy(point, passThrough()); + + expect(proxy.x).toBe(1); + expect(proxy.y).toBe(2); + + proxy.y = 7; + + expect(point.y).toBe(7); + expect(proxy.y).toBe(7); + }); + + it("converts a proxied Java object to the Java toString()", function () { + var sb = new java.lang.StringBuilder("hello"); + var proxy = new Proxy(sb, {}); + + expect(proxy.toString()).toBe("hello"); + expect(String(proxy)).toBe("hello"); + expect("" + proxy).toBe("hello"); + expect(`${proxy}`).toBe("hello"); + }); + + it("passes a proxied Java object as a method argument", function () { + var sb = new java.lang.StringBuilder("item"); + var list = new java.util.ArrayList(); + + list.add(new Proxy(sb, {})); + + expect(list.size()).toBe(1); + expect(list.get(0).equals(sb)).toBe(true); + expect(list.contains(new Proxy(new Proxy(sb, {}), {}))).toBe(true); + }); + + it("assigns a proxied Java object to a Java array element", function () { + var arr = Array.create(java.lang.Object, 1); + var sb = new java.lang.StringBuilder("el"); + + arr[0] = new Proxy(sb, {}); + + expect(arr[0].equals(sb)).toBe(true); + }); + + it("passes a proxied JS array of Java objects as a Java array", function () { + var a = new java.io.File("/a"); + var b = new java.io.File("/b"); + + var plain = java.util.Arrays.asList([a, b]); + var proxied = java.util.Arrays.asList(new Proxy([a, b], passThrough())); + + expect(plain.size()).toBe(2); + expect(proxied.size()).toBe(2); + expect(proxied.get(0).getPath()).toBe("/a"); + expect(proxied.get(1).equals(b)).toBe(true); + }); + + it("passes proxied elements of a JS array as Java objects", function () { + var a = new java.io.File("/a"); + var b = new java.io.File("/b"); + + var list = java.util.Arrays.asList(new Proxy([new Proxy(a, {}), b], {})); + + expect(list.size()).toBe(2); + expect(list.get(0).equals(a)).toBe(true); + }); + + it("throws when the receiver is a revoked Proxy", function () { + var sb = new java.lang.StringBuilder(); + var revocable = Proxy.revocable(sb, {}); + var append = revocable.proxy.append; + var xGetter = Object.getOwnPropertyDescriptor(android.graphics.Point.prototype, "x").get; + var pointRevocable = Proxy.revocable(new android.graphics.Point(1, 2), {}); + + revocable.revoke(); + pointRevocable.revoke(); + + expect(function () { append.call(revocable.proxy, "x"); }).toThrowError(TypeError, /revoked Proxy/); + expect(function () { xGetter.call(pointRevocable.proxy); }).toThrowError(TypeError, /revoked Proxy/); + expect(sb.length()).toBe(0); + }); + + it("throws when a revoked Proxy is passed as an argument", function () { + var revocable = Proxy.revocable(new java.lang.StringBuilder(), {}); + var list = new java.util.ArrayList(); + + revocable.revoke(); + + expect(function () { list.add(revocable.proxy); }).toThrow(); + expect(list.size()).toBe(0); + }); + + it("throws when the Proxy target is not a Java object", function () { + var sb = new java.lang.StringBuilder(); + var append = sb.append; + var toString = sb.toString; + + expect(function () { append.call(new Proxy({}, {}), "x"); }).toThrowError(TypeError, /not a Java object/); + expect(function () { toString.call(new Proxy([], {})); }).toThrowError(TypeError, /not a Java object/); + expect(sb.length()).toBe(0); + }); + + it("throws instead of crashing when the receiver is a plain object", function () { + var append = new java.lang.StringBuilder().append; + + expect(function () { append.call({}, "x"); }).toThrow(); + }); + + it("supports instanceof on a proxied Java object", function () { + var sb = new java.lang.StringBuilder(); + var proxy = new Proxy(sb, {}); + var revocable = Proxy.revocable(new java.lang.StringBuilder(), {}); + revocable.revoke(); + + // Class checks walk the Proxy's prototype chain (the target's); + // interface checks go through the runtime's Symbol.hasInstance. + expect(proxy instanceof java.lang.StringBuilder).toBe(true); + expect(proxy instanceof java.lang.CharSequence).toBe(true); + expect(proxy instanceof java.util.List).toBe(false); + expect(revocable.proxy instanceof java.lang.CharSequence).toBe(false); + expect(function () { return revocable.proxy instanceof java.lang.StringBuilder; }).toThrowError(TypeError); + }); + + it("calls overridden and super methods of an extended class instance through a Proxy", function () { + var MyObject = java.lang.Object.extend({ + toString: function () { + return "custom:" + (this.super.hashCode() === this.hashCode()); + } + }); + var instance = new MyObject(); + var proxy = new Proxy(instance, passThrough()); + + expect(proxy.toString()).toBe("custom:true"); + expect(String(proxy)).toBe("custom:true"); + expect(proxy.hashCode()).toBe(instance.hashCode()); + expect(proxy.super.hashCode()).toBe(instance.hashCode()); + expect(proxy.equals(instance)).toBe(true); + }); +}); diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 8d2b18135..00bd2576c 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -611,7 +611,7 @@ CallbackHandlers::GetImplementedInterfaces(JEnv &env, const Local &imple vector interfacesToImplement; auto isolate = v8::Isolate::GetCurrent(); - auto context = implementationObject->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, implementationObject); Local interfacesName = String::NewFromUtf8Literal(isolate, "interfaces"); Local prop; if (implementationObject->Get(context, interfacesName).ToLocal(&prop) && !prop.IsEmpty() && prop->IsArray()) { @@ -657,7 +657,7 @@ CallbackHandlers::GetMethodOverrides(JEnv &env, const Local &implementat vector methodNames; auto isolate = v8::Isolate::GetCurrent(); - auto context = implementationObject->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, implementationObject); auto propNames = implementationObject->GetOwnPropertyNames(context).ToLocalChecked(); for (int i = 0; i < propNames->Length(); i++) { auto name = propNames->Get(context, i).ToLocalChecked().As(); diff --git a/test-app/runtime/src/main/cpp/FieldAccessor.cpp b/test-app/runtime/src/main/cpp/FieldAccessor.cpp index a2b7bb274..4801568e6 100644 --- a/test-app/runtime/src/main/cpp/FieldAccessor.cpp +++ b/test-app/runtime/src/main/cpp/FieldAccessor.cpp @@ -333,6 +333,9 @@ void FieldAccessor::SetJavaField(Isolate* isolate, const Local& target, //TODO: validate valie is a string; result = ArgConverter::ConvertToJavaString(value); } else { + if (value->IsProxy() && !ObjectManager::UnwrapProxy(value)->IsObject()) { + throw NativeScriptException("Cannot assign a revoked Proxy to field '" + fieldMetadata.name + "'"); + } auto context = isolate->GetCurrentContext(); auto objectWithHiddenID = value->ToObject(context).ToLocalChecked(); result = objectManager->GetJavaObjectByJsObject(objectWithHiddenID); diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index e5d82f0bd..6a7ed1bd1 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -112,16 +112,22 @@ tns::BufferCastType JsArgConverter::GetCastType(const v8::Local &arg, int index) { +bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { bool success = false; char buff[1024]; const auto &typeSignature = m_tokens.at(index); + // A Proxy's target is what holds the Java link or the array elements. + auto arg = jsArg.IsEmpty() ? jsArg : ObjectManager::UnwrapProxy(jsArg); + if (arg.IsEmpty()) { SetConvertedObject(index, nullptr); success = false; + } else if (jsArg->IsProxy() && !arg->IsObject()) { + SetConvertedObject(index, nullptr); + sprintf(buff, "Cannot convert a revoked Proxy to %s at index %d", typeSignature.c_str(), index); } else if (arg->IsArray()) { success = typeSignature[0] == '['; diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.h b/test-app/runtime/src/main/cpp/JsArgConverter.h index ba4678d88..14a098e3e 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.h +++ b/test-app/runtime/src/main/cpp/JsArgConverter.h @@ -52,7 +52,7 @@ namespace tns { private: - bool ConvertArg(const v8::Local& arg, int index); + bool ConvertArg(const v8::Local& jsArg, int index); bool ConvertJavaScriptArray(const v8::Local& jsArr, int index); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp index 63216a06f..3e24d986a 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp @@ -58,7 +58,7 @@ JsArgToArrayConverter::JsArgToArrayConverter(const v8::FunctionCallbackInfo context, const Local &arg, int index) { +bool JsArgToArrayConverter::ConvertArg(Local context, const Local &jsArg, int index) { bool success = false; stringstream s; @@ -67,9 +67,15 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalIsProxy() && !arg->IsObject()) { + s << "Cannot marshal a revoked Proxy at index " << index << " to Java type."; + success = false; } else if (arg->IsInt32() && (returnType == Type::Int || returnType == Type::Null)) { jint value = arg->Int32Value(context).ToChecked(); auto javaObject = JType::NewInt(env, value); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h index 853989e79..b8479aa8b 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h @@ -38,7 +38,7 @@ class JsArgToArrayConverter { }; private: - bool ConvertArg(v8::Local context, const v8::Local& arg, int index); + bool ConvertArg(v8::Local context, const v8::Local& jsArg, int index); void SetConvertedObject(JEnv& env, int index, jobject obj, bool isGlobal = false); diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 83e77fa34..24787ef27 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -35,10 +35,9 @@ void MetadataNode::Init(Isolate* isolate) { isolate, ArgConverter::ConvertToV8String(isolate, "tns::PackageKey")); } -// Deliberately not V8GetPrivateValue: that one resolves the creation context, -// which hard-crashes on values that have none (a revoked proxy), and throws -// when the lookup comes back Nothing. Callers here take arbitrary values, so a -// failed read must also leave no exception pending for whoever calls next. +// Deliberately not V8GetPrivateValue: that one throws when the lookup comes +// back Nothing. Callers here take arbitrary values, so a failed read must also +// leave no exception pending for whoever calls next. static void* TryReadPrivateExternal(Isolate* isolate, const Local& value, Persistent* key) { if (key == nullptr) { return nullptr; @@ -60,6 +59,43 @@ static void* TryReadPrivateExternal(Isolate* isolate, const Local& value return hidden.As()->Value(v8::kExternalPointerTypeTagDefault); } +static NativeScriptException ProxyTypeError(Isolate* isolate, const string& message) { + return NativeScriptException(isolate, Exception::TypeError(ArgConverter::ConvertToV8String(isolate, message)), message); +} + +// Reactive frameworks put Java wrappers behind a Proxy, which carries none of +// the wrapper's internal fields or private symbols. +static Local ResolveProxyReceiver(Isolate* isolate, const Local& receiver, const char* verb, const string& member, bool requireJavaObject) { + if (!receiver->IsProxy()) { + return receiver; + } + + auto target = ObjectManager::UnwrapProxy(receiver); + if (!target->IsObject()) { + throw ProxyTypeError(isolate, string("Cannot ") + verb + " '" + member + "' on a revoked Proxy"); + } + + auto object = target.As(); + if (requireJavaObject && !ObjectManager::IsJsRuntimeObject(object)) { + throw ProxyTypeError(isolate, string("Cannot ") + verb + " '" + member + "' on a Proxy whose target is not a Java object"); + } + + return object; +} + +static Local ResolveImplementationObject(Isolate* isolate, const Local& implementationObject) { + if (!implementationObject->IsProxy()) { + return implementationObject; + } + + auto target = ObjectManager::UnwrapProxy(implementationObject); + if (!target->IsObject()) { + throw ProxyTypeError(isolate, "Cannot use a revoked Proxy as an implementation object"); + } + + return target.As(); +} + bool MetadataNode::TryGetInstanceTypeName(Isolate* isolate, const Local& value, std::string& out) { auto cache = GetMetadataNodeCache(isolate); auto node = static_cast(TryReadPrivateExternal(isolate, value, cache->MetadataKey)); @@ -211,7 +247,11 @@ bool MetadataNode::IsNodeTypeInterface() { } string MetadataNode::GetTypeMetadataName(Isolate* isolate, Local& value) { - auto data = GetTypeMetadata(isolate, value.As()); + auto target = ObjectManager::UnwrapProxy(value); + auto data = target->IsFunction() ? GetTypeMetadata(isolate, target.As()) : nullptr; + if (data == nullptr) { + throw NativeScriptException(string("Expected a Java class or interface constructor")); + } return data->name; } @@ -336,9 +376,12 @@ void MetadataNode::SetClassAccessor(Local& ctorFunction) { void MetadataNode::ClassAccessorGetterCallback(const FunctionCallbackInfo& info) { try { - auto thiz = info.This(); auto isolate = info.GetIsolate(); - auto data = GetTypeMetadata(isolate, thiz.As()); + auto thiz = ResolveProxyReceiver(isolate, info.This(), "read", "class", false); + auto data = thiz->IsFunction() ? GetTypeMetadata(isolate, thiz.As()) : nullptr; + if (data == nullptr) { + throw NativeScriptException(string("'class' can only be read from a Java class constructor")); + } auto value = CallbackHandlers::FindClass(isolate, data->name); info.GetReturnValue().Set(value); @@ -360,7 +403,7 @@ void MetadataNode::NullObjectAccessorGetterCallback(const FunctionCallbackInfo hiddenVal; V8GetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodeName(isolate), hiddenVal); if (hiddenVal.IsEmpty()) { @@ -405,9 +448,11 @@ void MetadataNode::NullValueOfCallback(const FunctionCallbackInfo& args) void MetadataNode::FieldAccessorGetterCallback(const FunctionCallbackInfo& info) { try { - auto thiz = info.This(); auto fieldCallbackData = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); auto &fieldCallbackMetadata = fieldCallbackData->metadata; + auto thiz = ResolveProxyReceiver(info.GetIsolate(), info.This(), + "read Java field", fieldCallbackMetadata.getName(), + !fieldCallbackMetadata.isStatic); auto objectManager = Runtime::GetRuntime(info.GetIsolate())->GetObjectManager(); @@ -437,9 +482,11 @@ void MetadataNode::FieldAccessorGetterCallback(const FunctionCallbackInfo void MetadataNode::FieldAccessorSetterCallback(const FunctionCallbackInfo& info) { auto value = info[0]; try { - auto thiz = info.This(); auto fieldCallbackData = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); auto &fieldCallbackMetadata = fieldCallbackData->metadata; + auto thiz = ResolveProxyReceiver(info.GetIsolate(), info.This(), + "write Java field", fieldCallbackMetadata.getName(), + !fieldCallbackMetadata.isStatic); auto objectManager = Runtime::GetRuntime(info.GetIsolate())->GetObjectManager(); @@ -532,8 +579,8 @@ void MetadataNode::PropertyAccessorSetterCallback(const FunctionCallbackInfo& info) { try { - auto thiz = info.This(); auto isolate = info.GetIsolate(); + auto thiz = ResolveProxyReceiver(isolate, info.This(), "read", "super", true); auto key = ArgConverter::ConvertToV8String(isolate, "supervalue"); Local hidenVal; V8GetPrivateValue(isolate, thiz, key, hidenVal); @@ -1148,6 +1195,9 @@ Persistent* MetadataNode::GetPersistentConstructorFunction(Isolate* is MetadataNode::TypeMetadata* MetadataNode::GetTypeMetadata(Isolate* isolate, const Local& value) { Local hiddenVal; V8GetPrivateValue(isolate, value, String::NewFromUtf8(isolate, "typemetadata").ToLocalChecked(), hiddenVal); + if (hiddenVal.IsEmpty() || !hiddenVal->IsExternal()) { + return nullptr; + } auto data = reinterpret_cast(hiddenVal.As()->Value(v8::kExternalPointerTypeTagDefault)); return data; @@ -1239,7 +1289,7 @@ void MetadataNode::InterfaceConstructorCallback(const v8::FunctionCallbackInfoIsObject()) { throw NativeScriptException(string("First argument must be implementation object")); } - implementationObject = info[0]->ToObject(context).ToLocalChecked(); + implementationObject = ResolveImplementationObject(isolate, info[0].As()); } else if (info.Length() == 2) { if (!info[0]->IsString()) { throw NativeScriptException(string("First argument must be string")); @@ -1249,7 +1299,7 @@ void MetadataNode::InterfaceConstructorCallback(const v8::FunctionCallbackInfoToString(context).ToLocalChecked(); - implementationObject = info[1]->ToObject(context).ToLocalChecked(); + implementationObject = ResolveImplementationObject(isolate, info[1].As()); } else { throw NativeScriptException(string("Invalid number of arguments")); } @@ -1360,16 +1410,17 @@ void MetadataNode::MethodCallback(const v8::FunctionCallbackInfo& inf } } - auto thiz = info.This(); + auto thiz = ResolveProxyReceiver(info.GetIsolate(), info.This(), + "call Java method", methodName, !first.isStatic); auto isSuper = false; - if (!first.isStatic) { + if (!first.isStatic && ObjectManager::IsJsRuntimeObject(thiz)) { auto superValue = thiz->GetInternalField(static_cast(ObjectManager::MetadataNodeKeys::CallSuper)).As(); isSuper = !superValue.IsEmpty() && superValue->IsTrue(); } if ((argLength == 0) && (methodName == V8StringConstants::VALUE_OF)) { - info.GetReturnValue().Set(thiz); + info.GetReturnValue().Set(info.This()); } else { bool isFromInterface = initialCallbackData->node->IsNodeTypeInterface(); CallbackHandlers::CallJavaMethod(thiz, *className, methodName, entry, isFromInterface, first.isStatic, isSuper, info); @@ -1452,7 +1503,7 @@ Local MetadataNode::GetImplementationObject(Isolate* isolate, const Loca return implementationObject; } - auto context = object->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, object); if (object->HasOwnProperty(context, V8StringConstants::GetIsPrototypeImplementationObject(isolate)).ToChecked()) { auto v8Prototype = V8StringConstants::GetPrototype(isolate); auto maybeHasOwnProperty = object->HasOwnProperty(context, v8Prototype); @@ -1726,6 +1777,7 @@ void MetadataNode::ExtendMethodCallback(const v8::FunctionCallbackInfo(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); @@ -1970,7 +2022,11 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio } MetadataNode* MetadataNode::GetNodeFromHandle(const Local& value) { - auto node = GetInstanceMetadata(Isolate::GetCurrent(), value); + auto target = ObjectManager::UnwrapProxy(value); + if (!target->IsObject()) { + return nullptr; + } + auto node = GetInstanceMetadata(Isolate::GetCurrent(), target.As()); return node; } @@ -2337,37 +2393,48 @@ void MetadataNode::RegisterSymbolHasInstanceCallback(Isolate* isolate, MetadataE } void MetadataNode::SymbolHasInstanceCallback(const v8::FunctionCallbackInfo& info) { - auto length = info.Length(); - if (length != 1) { - throw NativeScriptException(string("Symbol.hasInstance must take exactly 1 argument")); - } + try { + auto length = info.Length(); + if (length != 1) { + throw NativeScriptException(string("Symbol.hasInstance must take exactly 1 argument")); + } - auto arg = info[0]; - if (arg->IsNullOrUndefined()) { - info.GetReturnValue().Set(false); - return; - } + auto arg = info[0]; + if (!arg->IsObject()) { + info.GetReturnValue().Set(false); + return; + } - auto clazz = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); + auto clazz = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); - auto isolate = info.GetIsolate(); - auto context = isolate->GetCurrentContext(); - auto runtime = Runtime::GetRuntime(isolate); + auto isolate = info.GetIsolate(); + auto runtime = Runtime::GetRuntime(isolate); - auto objectManager = runtime->GetObjectManager(); - auto obj = objectManager->GetJavaObjectByJsObject(arg->ToObject(context).ToLocalChecked()); + auto objectManager = runtime->GetObjectManager(); + auto obj = objectManager->GetJavaObjectByJsObject(arg.As()); - if (obj.IsNull()) { - // Couldn't find a corresponding java instance counterpart. This could happen - // if the "instanceof" operator is invoked on a pure javascript instance - info.GetReturnValue().Set(false); - return; - } + if (obj.IsNull()) { + // Couldn't find a corresponding java instance counterpart. This could happen + // if the "instanceof" operator is invoked on a pure javascript instance + info.GetReturnValue().Set(false); + return; + } - JEnv env; - auto isInstanceOf = env.IsInstanceOf(obj, clazz); + JEnv env; + auto isInstanceOf = env.IsInstanceOf(obj, clazz); - info.GetReturnValue().Set(isInstanceOf); + info.GetReturnValue().Set(isInstanceOf); + } catch (NativeScriptException& e) { + e.ReThrowToV8(); + } catch (std::exception e) { + stringstream ss; + ss << "Error: c++ exception: " << e.what() << endl; + NativeScriptException nsEx(ss.str()); + nsEx.ReThrowToV8(); + } catch (...) { + NativeScriptException nsEx(std::string("Error: c++ exception!")); + nsEx.ReThrowToV8(); + } } std::string MetadataNode::GetJniClassName(MetadataEntry& entry) { diff --git a/test-app/runtime/src/main/cpp/MethodCache.cpp b/test-app/runtime/src/main/cpp/MethodCache.cpp index a94196313..97531ce2e 100644 --- a/test-app/runtime/src/main/cpp/MethodCache.cpp +++ b/test-app/runtime/src/main/cpp/MethodCache.cpp @@ -12,6 +12,7 @@ #include "NumericCasts.h" #include "NativeScriptException.h" #include "Runtime.h" +#include "ObjectManager.h" #include using namespace v8; @@ -132,8 +133,10 @@ string MethodCache::EncodeSignature(const string& className, const string& metho return sig; } -string MethodCache::GetType(Isolate* isolate, const v8::Local& value) { +string MethodCache::GetType(Isolate* isolate, const v8::Local& jsValue) { string type; + // Overloads resolve against what a Proxy wraps. + auto value = ObjectManager::UnwrapProxy(jsValue); if (value->IsObject()) { auto context = isolate->GetCurrentContext(); diff --git a/test-app/runtime/src/main/cpp/NumericCasts.cpp b/test-app/runtime/src/main/cpp/NumericCasts.cpp index 48e82c523..ba1806292 100644 --- a/test-app/runtime/src/main/cpp/NumericCasts.cpp +++ b/test-app/runtime/src/main/cpp/NumericCasts.cpp @@ -37,7 +37,7 @@ CastType NumericCasts::GetCastType(Isolate* isolate, const Local& object Local NumericCasts::GetCastValue(const Local& object) { auto isolate = v8::Isolate::GetCurrent(); - auto context = object->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, object); Local value; object->Get(context, V8StringConstants::GetValue(isolate)).ToLocal(&value); return value; @@ -364,7 +364,7 @@ void NumericCasts::MarkJsObject(Isolate* isolate, const Local& object, C auto key = ArgConverter::ConvertToV8String(isolate, s_castMarker); auto type = Integer::New(isolate, static_cast(castType)); V8SetPrivateValue(isolate, object, key, type); - auto context = object->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, object); object->Set(context, V8StringConstants::GetValue(isolate), value); DEBUG_WRITE("MarkJsObject: Marking js object: %d with cast type: %d", object->GetIdentityHash(), castType); diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index 56c54fdcd..afbebd1a6 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -131,13 +131,24 @@ JniLocalRef ObjectManager::GetJavaObjectByJsObject( ObjectManager::JSInstanceInfo* ObjectManager::GetJSInstanceInfo( const Local& object) { - JSInstanceInfo* jsInstanceInfo = nullptr; + if (object->IsProxy()) { + auto target = UnwrapProxy(object); + return target->IsObject() ? GetJSInstanceInfo(target.As()) + : nullptr; + } if (IsJsRuntimeObject(object)) { return GetJSInstanceInfoFromRuntimeObject(object); } return nullptr; } +Local ObjectManager::UnwrapProxy(Local value) { + while (value->IsProxy()) { + value = value.As()->GetTarget(); + } + return value; +} + ObjectManager::JSInstanceInfo* ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { HandleScope handleScope(m_isolate); @@ -307,7 +318,7 @@ bool ObjectManager::CloneLink(const Local& src, if (success) { auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - auto jsInfo = src->GetInternalField(jsInfoIdx); + auto jsInfo = UnwrapProxy(src).As()->GetInternalField(jsInfoIdx); dest->SetInternalField(jsInfoIdx, jsInfo); } diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index 23194133a..df0ed111b 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -62,7 +62,19 @@ class ObjectManager { bool CloneLink(const v8::Local& src, const v8::Local& dest); - bool IsJsRuntimeObject(const v8::Local& object); + /* + * Whether GetInternalField is safe to call on this exact object. False for + * a Proxy even when its target qualifies; unwrap first where a Proxy should + * be accepted. + */ + static bool IsJsRuntimeObject(const v8::Local& object); + + /* + * The innermost target of a Proxy chain; any other value is returned as is. + * A revoked Proxy anywhere in the chain yields null. Internal fields and + * private symbols live on the target, never on the Proxy. + */ + static v8::Local UnwrapProxy(v8::Local value); static std::string GetClassName(jobject javaObject); diff --git a/test-app/runtime/src/main/cpp/URLImpl.cpp b/test-app/runtime/src/main/cpp/URLImpl.cpp index dd3348387..6820cd25d 100644 --- a/test-app/runtime/src/main/cpp/URLImpl.cpp +++ b/test-app/runtime/src/main/cpp/URLImpl.cpp @@ -10,6 +10,9 @@ using namespace ada; URLImpl::URLImpl(url_aggregator url) : url_(url) {} URLImpl *URLImpl::GetPointer(v8::Local object) { + if (object->InternalFieldCount() < 1) { + return nullptr; + } auto ptr = object->GetAlignedPointerFromInternalField(0, v8::kEmbedderDataTypeTagDefault); if (ptr == nullptr) { return nullptr; diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp index adc523884..ba467dee0 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp @@ -64,10 +64,18 @@ std::string tns::GetNativeWrapperHint(Isolate* isolate, const Local& valu return "JavaObject"; } +Local tns::GetCreationContextOrCurrent(Isolate* isolate, const Local& obj) { + Local context; + if (obj->GetCreationContext(isolate).ToLocal(&context)) { + return context; + } + return isolate->GetCurrentContext(); +} + bool tns::V8GetPrivateValue(Isolate* isolate, const Local& obj, const Local& propName, Local& out) { auto privateKey = Private::ForApi(isolate, propName); - auto context = obj->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, obj); auto hasPrivate = obj->HasPrivate(context, privateKey); if (hasPrivate.IsNothing()) { @@ -93,7 +101,7 @@ bool tns::V8GetPrivateValue(Isolate* isolate, const Local& obj, const Lo bool tns::V8SetPrivateValue(Isolate* isolate, const Local& obj, const Local& propName, const Local& value) { auto privateKey = Private::ForApi(isolate, propName); - auto context = obj->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, obj); auto res = obj->SetPrivate(context, privateKey, value); if (res.IsNothing()) { diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h index 4f5e165c8..6483574b8 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h @@ -12,6 +12,11 @@ namespace tns { // name etc.); empty when the value is a plain JS object. Never runs JS. std::string GetNativeWrapperHint(v8::Isolate* isolate, const v8::Local& value); +// A Proxy (live or revoked) has no creation context, and ToLocalChecked on the +// empty result aborts the process. The current context serves the same +// purpose for private symbols and plain property access. +v8::Local GetCreationContextOrCurrent(v8::Isolate* isolate, const v8::Local& obj); + bool V8GetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, v8::Local& out); bool V8SetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, const v8::Local& value); diff --git a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp index ac9cefc5e..062c9e10b 100644 --- a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp +++ b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp @@ -111,8 +111,13 @@ JSToJavaConverter::JSToJavaConverter( } } -bool JSToJavaConverter::ConvertArg(const Local &arg, int index) { - +bool JSToJavaConverter::ConvertArg(const Local &jsArg, int index) { + // A Proxy's target is what holds the Java link or the array elements. + auto arg = ObjectManager::UnwrapProxy(jsArg); + if (jsArg->IsProxy() && !arg->IsObject()) { + m_jniArgRefsState.SetConvertedObject(index, nullptr); + return false; + } if (arg->IsNumber() || arg->IsNumberObject()) { return tns::ConvertJavaScriptNumber( diff --git a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h index 6dd13314a..73f455f28 100644 --- a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h +++ b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h @@ -41,7 +41,7 @@ namespace tns { private: - bool ConvertArg(const v8::Local &arg, int index); + bool ConvertArg(const v8::Local &jsArg, int index); v8::Isolate *m_isolate; std::string m_methodSignature; From 99f02e00b519cd4e405f8f9128f8fe7eef5d88fb Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 14:15:47 -0300 Subject: [PATCH 2/7] fix(runtime): bound converter error messages and fail loudly on a revoked Proxy JsArgConverter formatted its conversion errors into a fixed 1024-byte buffer with sprintf; a long type signature could overrun it. Every message now goes through snprintf with the buffer size. JSToJavaConverter's constructors never inspect ConvertArg's result, so the revoked-Proxy branch silently produced a null argument. It now throws a TypeError instead. The class has had no live callers since b5218c9f, which switched dispatch back to JsArgConverter, so this only matters if it is revived. --- test-app/runtime/src/main/cpp/JsArgConverter.cpp | 12 ++++++------ .../src/main/cpp/conversions/JSToJavaConverter.cpp | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index 6a7ed1bd1..431779995 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -127,7 +127,7 @@ bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { success = false; } else if (jsArg->IsProxy() && !arg->IsObject()) { SetConvertedObject(index, nullptr); - sprintf(buff, "Cannot convert a revoked Proxy to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert a revoked Proxy to %s at index %d", typeSignature.c_str(), index); } else if (arg->IsArray()) { success = typeSignature[0] == '['; @@ -137,25 +137,25 @@ bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { } if (!success) { - sprintf(buff, "Cannot convert array to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert array to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsNumber() || arg->IsNumberObject()) { success = ConvertJavaScriptNumber(arg, index); if (!success) { - sprintf(buff, "Cannot convert number to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert number to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsBoolean() || arg->IsBooleanObject()) { success = ConvertJavaScriptBoolean(arg, index); if (!success) { - sprintf(buff, "Cannot convert boolean to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert boolean to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsString() || arg->IsStringObject()) { success = ConvertJavaScriptString(arg, index); if (!success) { - sprintf(buff, "Cannot convert string to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert string to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsObject()) { auto context = m_isolate->GetCurrentContext(); @@ -362,7 +362,7 @@ bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { if (success) { SetConvertedObject(index, obj.Move(), obj.IsGlobal()); } else { - sprintf(buff, "Cannot convert object to %s at index %d", typeSignature.c_str(), + snprintf(buff, sizeof(buff), "Cannot convert object to %s at index %d", typeSignature.c_str(), index); } break; diff --git a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp index 062c9e10b..467d7686d 100644 --- a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp +++ b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp @@ -115,8 +115,10 @@ bool JSToJavaConverter::ConvertArg(const Local &jsArg, int index) { // A Proxy's target is what holds the Java link or the array elements. auto arg = ObjectManager::UnwrapProxy(jsArg); if (jsArg->IsProxy() && !arg->IsObject()) { - m_jniArgRefsState.SetConvertedObject(index, nullptr); - return false; + // The constructors do not inspect the result, so the failure must + // surface here rather than as a null argument. + string message = "Cannot pass a revoked Proxy to Java at index " + to_string(index); + throw NativeScriptException(m_isolate, Exception::TypeError(ArgConverter::ConvertToV8String(m_isolate, message)), message); } if (arg->IsNumber() || arg->IsNumberObject()) { From 4ad0d772f84b5fdf1c79b997acf6125a29c67355 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 14:34:04 -0300 Subject: [PATCH 3/7] fix(runtime): read JavaScript-backed Proxies through their traps Converting a proxied JS array to a Java array read the Proxy's target directly, bypassing its get trap. Java then received different elements than JavaScript sees: Vue's reactive() get trap unwraps ref() values and wraps nested objects, and none of that reached Java. The rule is now the same at every conversion site: classify a Proxy by its unwrapped target, but when the target is an ordinary JS array, read `length` and each index through the Proxy itself so its traps run. A trap that throws fails the conversion with the trap's error message instead of crashing. Java wrappers, Map/Set, ArrayBuffer and views, typed arrays, Date, boxed primitives and functions keep converting from the target: the Java link and their internal slots live on the target, their prototype methods reject a Proxy receiver, and reactive(new Map()) only works through the raw object. Changed: JsArgConverter's array branch (the live Java call path) and the equivalent branch in JSToJavaConverter, via a shared ObjectManager::ReadArrayThroughProxy snapshot. JsArgToArrayConverter has no JS-array branch, MethodCache::GetType only classifies, and field writes never convert JS arrays, so those stay on the target. Implementation objects passed to extend() and interface constructors are deliberately left on the target: they are stored long-term and their methods are looked up by name later, which needs its own design. --- .../assets/app/tests/testProxyReceivers.js | 80 +++++++++++++++++++ .../runtime/src/main/cpp/JsArgConverter.cpp | 27 ++++++- .../runtime/src/main/cpp/ObjectManager.cpp | 36 +++++++++ test-app/runtime/src/main/cpp/ObjectManager.h | 8 ++ .../cpp/conversions/JSToJavaConverter.cpp | 14 +++- 5 files changed, 159 insertions(+), 6 deletions(-) diff --git a/test-app/app/src/main/assets/app/tests/testProxyReceivers.js b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js index 27ea04185..eaa740408 100644 --- a/test-app/app/src/main/assets/app/tests/testProxyReceivers.js +++ b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js @@ -116,6 +116,86 @@ describe("Proxy receivers and arguments", function () { expect(list.get(0).equals(a)).toBe(true); }); + function echoIntArray(arr) { + var echoed = new com.tns.tests.JniReferenceLeakTest(null).echoIntArray(arr); + var result = []; + for (var i = 0; i < echoed.length; i++) { + result.push(echoed[i]); + } + return result; + } + + it("reads a proxied JS array through its get trap when converting to a Java array", function () { + var proxy = new Proxy([], { + get: function (target, key, receiver) { + if (key === "length") { + return 3; + } + if (key === "0" || key === "1" || key === "2") { + return Number(key) * 10; + } + return Reflect.get(target, key, receiver); + } + }); + + expect(echoIntArray(proxy)).toEqual([0, 10, 20]); + }); + + it("passes the values a reactive-style get trap unwraps from refs", function () { + function ref(value) { + return { __v_isRef: true, value: value }; + } + var proxy = new Proxy([ref(1), 2, ref(3)], { + get: function (target, key, receiver) { + var v = Reflect.get(target, key, receiver); + return v !== null && typeof v === "object" && v.__v_isRef ? v.value : v; + } + }); + + expect(echoIntArray(proxy)).toEqual([1, 2, 3]); + }); + + it("throws the get trap's error when converting a proxied JS array", function () { + var proxy = new Proxy([1, 2], { + get: function (target, key, receiver) { + if (key === "1") { + throw new Error("boom"); + } + return Reflect.get(target, key, receiver); + } + }); + + expect(function () { echoIntArray(proxy); }).toThrowError(/boom/); + }); + + it("converts a proxied Java object or typed array from its target, bypassing traps", function () { + var lying = { + get: function (target, key, receiver) { + if (key === "length") { + return 0; + } + if (key === "0") { + return 99; + } + var v = Reflect.get(target, key, target); + return typeof v === "function" ? v.bind(target) : v; + } + }; + var source = new java.util.ArrayList(); + source.add("a"); + source.add("b"); + var bytes = new Uint8Array([7, 8, 9]); + + var copy = new java.util.ArrayList(new Proxy(source, lying)); + var holder = new java.util.ArrayList(); + holder.add(new Proxy(bytes, lying)); + var buffer = holder.toString(); + + expect(copy.size()).toBe(2); + expect(copy.get(1)).toBe("b"); + expect(buffer).toMatch(/ByteBuffer.*cap=3/); + }); + it("throws when the receiver is a revoked Proxy", function () { var sb = new java.lang.StringBuilder(); var revocable = Proxy.revocable(sb, {}); diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index 431779995..5884dc8a5 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -119,7 +119,8 @@ bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { const auto &typeSignature = m_tokens.at(index); - // A Proxy's target is what holds the Java link or the array elements. + // Classify by the target, which holds the Java link and internal slots; + // array elements are read through the Proxy itself. auto arg = jsArg.IsEmpty() ? jsArg : ObjectManager::UnwrapProxy(jsArg); if (arg.IsEmpty()) { @@ -131,12 +132,30 @@ bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { } else if (arg->IsArray()) { success = typeSignature[0] == '['; + bool trapThrew = false; if (success) { - auto jsArr = Local::Cast(arg); - success = ConvertJavaScriptArray(jsArr, index); + Local jsArr; + if (jsArg->IsProxy()) { + TryCatch tc(m_isolate); + auto context = m_isolate->GetCurrentContext(); + if (!ObjectManager::ReadArrayThroughProxy(context, jsArg.As()).ToLocal(&jsArr)) { + trapThrew = true; + success = false; + string reason = tc.HasCaught() + ? ArgConverter::ConvertToString(tc.Exception()->ToString(context).FromMaybe(Local())) + : string("unknown error"); + snprintf(buff, sizeof(buff), "%s", reason.c_str()); + } + } else { + jsArr = arg.As(); + } + + if (success) { + success = ConvertJavaScriptArray(jsArr, index); + } } - if (!success) { + if (!success && !trapThrew) { snprintf(buff, sizeof(buff), "Cannot convert array to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsNumber() || arg->IsNumberObject()) { diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index afbebd1a6..bc779b905 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -149,6 +149,42 @@ Local ObjectManager::UnwrapProxy(Local value) { return value; } +MaybeLocal ObjectManager::ReadArrayThroughProxy(Local context, + Local proxy) { + auto isolate = Isolate::GetCurrent(); + EscapableHandleScope scope(isolate); + + Local lengthValue; + if (!proxy->Get(context, ArgConverter::ConvertToV8String(isolate, "length")) + .ToLocal(&lengthValue)) { + return {}; + } + double length; + if (!lengthValue->NumberValue(context).To(&length)) { + return {}; + } + if (!(length > 0)) { + length = 0; + } + // Java arrays are indexed by jint. + if (length > INT32_MAX) { + isolate->ThrowException(Exception::RangeError(ArgConverter::ConvertToV8String( + isolate, "Array length exceeds the maximum Java array size"))); + return {}; + } + + auto count = static_cast(length); + auto snapshot = Array::New(isolate, static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + Local element; + if (!proxy->Get(context, i).ToLocal(&element) || + snapshot->Set(context, i, element).IsNothing()) { + return {}; + } + } + return scope.Escape(snapshot); +} + ObjectManager::JSInstanceInfo* ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { HandleScope handleScope(m_isolate); diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index df0ed111b..89ebb373a 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -76,6 +76,14 @@ class ObjectManager { */ static v8::Local UnwrapProxy(v8::Local value); + /* + * Copies an array-like Proxy into a fresh Array by reading `length` and each + * index through the Proxy, so its traps decide what Java receives. Empty when + * a trap throws; the exception is left for the caller's TryCatch. + */ + static v8::MaybeLocal ReadArrayThroughProxy( + v8::Local context, v8::Local proxy); + static std::string GetClassName(jobject javaObject); static std::string GetClassName(jclass clazz); diff --git a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp index 467d7686d..f2effda1d 100644 --- a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp +++ b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp @@ -112,7 +112,8 @@ JSToJavaConverter::JSToJavaConverter( } bool JSToJavaConverter::ConvertArg(const Local &jsArg, int index) { - // A Proxy's target is what holds the Java link or the array elements. + // Classify by the target, which holds the Java link and internal slots; + // array elements are read through the Proxy itself. auto arg = ObjectManager::UnwrapProxy(jsArg); if (jsArg->IsProxy() && !arg->IsObject()) { // The constructors do not inspect the result, so the failure must @@ -144,9 +145,18 @@ bool JSToJavaConverter::ConvertArg(const Local &jsArg, int index) { m_jniArgRefsState ); } else if (arg->IsArray() && m_tokens->at(index)[0] == '[') { + Local elements = arg; + if (jsArg->IsProxy()) { + TryCatch tc(m_isolate); + Local snapshot; + if (!ObjectManager::ReadArrayThroughProxy(m_isolate->GetCurrentContext(), jsArg.As()).ToLocal(&snapshot)) { + throw NativeScriptException(tc); + } + elements = snapshot; + } return tns::ConvertJavaScriptArray( m_isolate, - arg, + elements, index, m_tokens, m_jniArgRefsState From 331791e87cc22af2b1bdfbb5c09bd79d9353e9df Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 15:35:44 -0300 Subject: [PATCH 4/7] fix(runtime): clone the JsInfo field from the object that owns it GetJSInstanceInfo falls back to the prototype's JsInfo field for the TypeScript class layout, where the registered instance sits one level above the object being used. CloneLink used that lookup to decide success but then copied only the source object's own field, so a TypeScript child's super object could end up with an undefined link. The field resolution now lives in one helper shared by the lookup and CloneLink, which copies whatever that helper resolves and reports failure when it is not an External. --- .../runtime/src/main/cpp/ObjectManager.cpp | 29 +++++++++++-------- test-app/runtime/src/main/cpp/ObjectManager.h | 7 +++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index bc779b905..139ba4f67 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -185,10 +185,7 @@ MaybeLocal ObjectManager::ReadArrayThroughProxy(Local context, return scope.Escape(snapshot); } -ObjectManager::JSInstanceInfo* -ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { - HandleScope handleScope(m_isolate); - +Local ObjectManager::GetJSInstanceInfoField(const Local& object) { const int jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); auto jsInfo = object->GetInternalField(jsInfoIdx).As(); if (jsInfo->IsUndefined()) { @@ -204,7 +201,14 @@ ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { } } } + return jsInfo; +} + +ObjectManager::JSInstanceInfo* +ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { + HandleScope handleScope(m_isolate); + auto jsInfo = GetJSInstanceInfoField(object); if (!jsInfo.IsEmpty() && jsInfo->IsExternal()) { auto external = jsInfo.As(); return static_cast(external->Value(v8::kExternalPointerTypeTagDefault)); @@ -348,17 +352,18 @@ void ObjectManager::Link(const Local& object, uint32_t javaObjectID, bool ObjectManager::CloneLink(const Local& src, const Local& dest) { - auto jsInfo = GetJSInstanceInfo(src); - - auto success = jsInfo != nullptr; + auto source = UnwrapProxy(src); + if (!source->IsObject() || !IsJsRuntimeObject(source.As())) { + return false; + } - if (success) { - auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - auto jsInfo = UnwrapProxy(src).As()->GetInternalField(jsInfoIdx); - dest->SetInternalField(jsInfoIdx, jsInfo); + auto jsInfo = GetJSInstanceInfoField(source.As()); + if (jsInfo.IsEmpty() || !jsInfo->IsExternal()) { + return false; } - return success; + dest->SetInternalField(static_cast(MetadataNodeKeys::JsInfo), jsInfo); + return true; } string ObjectManager::GetClassName(jobject javaObject) { diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index 89ebb373a..cc0ca9257 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -186,6 +186,13 @@ class ObjectManager { JSInstanceInfo* GetJSInstanceInfoFromRuntimeObject( const v8::Local& object); + /* + * The stored JsInfo field for a runtime object: its own field, or its + * prototype's when the TypeScript layout keeps the registered instance one + * level up. Undefined when neither holds one. + */ + static v8::Local GetJSInstanceInfoField(const v8::Local& object); + void ReleaseJSInstance(v8::Persistent* po, JSInstanceInfo* jsInstanceInfo); From e3634fe8fbe1ffb8eaffe1355587da3b47e3fa18 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 15:42:16 -0300 Subject: [PATCH 5/7] fix(runtime): release the native counterpart on the object that owns the link __releaseNativeCounterpart resolved the JSInstanceInfo through the argument (so a Proxy found its target's link) but then cleared the JsInfo internal field on the argument itself. For a Proxy that is a V8 API-check fatal; for a TypeScript-layout child the own field was already undefined, so the prototype that actually holds the link kept pointing at the freed JSInstanceInfo. The JsInfo lookup, CloneLink and the release path now share one GetJSInstanceInfoOwner helper that returns the object holding the External (the object itself or its registered-instance prototype), and the release clears the field on that owner. --- .../assets/app/tests/testProxyReceivers.js | 14 +++++ .../runtime/src/main/cpp/ObjectManager.cpp | 63 +++++++++++-------- test-app/runtime/src/main/cpp/ObjectManager.h | 9 +-- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/test-app/app/src/main/assets/app/tests/testProxyReceivers.js b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js index eaa740408..15b6a0687 100644 --- a/test-app/app/src/main/assets/app/tests/testProxyReceivers.js +++ b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js @@ -267,4 +267,18 @@ describe("Proxy receivers and arguments", function () { expect(proxy.super.hashCode()).toBe(instance.hashCode()); expect(proxy.equals(instance)).toBe(true); }); + + it("releases the native counterpart of the Proxy target", function () { + var target = new java.lang.Object(); + var proxy = new Proxy(target, passThrough()); + var revocable = Proxy.revocable(new java.lang.Object(), {}); + revocable.revoke(); + + global.__releaseNativeCounterpart(proxy); + + expect(function () { global.__releaseNativeCounterpart(target); }).toThrowError(/non native object/); + expect(function () { global.__releaseNativeCounterpart(proxy); }).toThrowError(/non native object/); + expect(function () { target.toString(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { global.__releaseNativeCounterpart(revocable.proxy); }).toThrowError(/non native object/); + }); }); diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index 139ba4f67..895f64620 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -185,36 +185,38 @@ MaybeLocal ObjectManager::ReadArrayThroughProxy(Local context, return scope.Escape(snapshot); } -Local ObjectManager::GetJSInstanceInfoField(const Local& object) { +Local ObjectManager::GetJSInstanceInfoOwner(const Local& object) { const int jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - auto jsInfo = object->GetInternalField(jsInfoIdx).As(); - if (jsInfo->IsUndefined()) { - // Typescript object layout has an object instance as child of the actual - // registered instance. checking for that - auto prototypeObject = object->GetPrototype().As(); - - if (!prototypeObject.IsEmpty() && prototypeObject->IsObject()) { - DEBUG_WRITE("GetJSInstanceInfo: need to check prototype :%d", - prototypeObject->GetIdentityHash()); - if (IsJsRuntimeObject(prototypeObject)) { - jsInfo = prototypeObject->GetInternalField(jsInfoIdx).As(); - } + if (object->GetInternalField(jsInfoIdx).As()->IsExternal()) { + return object; + } + + // Typescript object layout has an object instance as child of the actual + // registered instance. checking for that + auto prototypeObject = object->GetPrototype().As(); + if (!prototypeObject.IsEmpty() && prototypeObject->IsObject()) { + DEBUG_WRITE("GetJSInstanceInfo: need to check prototype :%d", + prototypeObject->GetIdentityHash()); + if (IsJsRuntimeObject(prototypeObject) && + prototypeObject->GetInternalField(jsInfoIdx).As()->IsExternal()) { + return prototypeObject; } } - return jsInfo; + + return Local(); } ObjectManager::JSInstanceInfo* ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { HandleScope handleScope(m_isolate); - auto jsInfo = GetJSInstanceInfoField(object); - if (!jsInfo.IsEmpty() && jsInfo->IsExternal()) { - auto external = jsInfo.As(); - return static_cast(external->Value(v8::kExternalPointerTypeTagDefault)); + auto owner = GetJSInstanceInfoOwner(object); + if (owner.IsEmpty()) { + return nullptr; } - return nullptr; + auto external = owner->GetInternalField(static_cast(MetadataNodeKeys::JsInfo)).As(); + return static_cast(external->Value(v8::kExternalPointerTypeTagDefault)); } bool ObjectManager::IsJsRuntimeObject(const v8::Local& object) { @@ -357,12 +359,13 @@ bool ObjectManager::CloneLink(const Local& src, return false; } - auto jsInfo = GetJSInstanceInfoField(source.As()); - if (jsInfo.IsEmpty() || !jsInfo->IsExternal()) { + auto owner = GetJSInstanceInfoOwner(source.As()); + if (owner.IsEmpty()) { return false; } - dest->SetInternalField(static_cast(MetadataNodeKeys::JsInfo), jsInfo); + auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); + dest->SetInternalField(jsInfoIdx, owner->GetInternalField(jsInfoIdx)); return true; } @@ -695,12 +698,19 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { throw NativeScriptException("Argument is not an object!"); } - JSInstanceInfo* jsInstanceInfo = GetJSInstanceInfo(object); - - if (jsInstanceInfo == nullptr) { + auto source = UnwrapProxy(object); + Local owner; + if (source->IsObject() && IsJsRuntimeObject(source.As())) { + owner = GetJSInstanceInfoOwner(source.As()); + } + if (owner.IsEmpty()) { throw NativeScriptException("Trying to release a non native object!"); } + auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); + auto jsInstanceInfo = static_cast( + owner->GetInternalField(jsInfoIdx).As()->Value(v8::kExternalPointerTypeTagDefault)); + JEnv env; env.CallVoidMethod(m_javaRuntimeObject, RELEASE_NATIVE_INSTANCE_METHOD_ID, jsInstanceInfo->JavaObjectID); @@ -714,8 +724,7 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { } delete jsInstanceInfo; - auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - object->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + owner->SetInternalField(jsInfoIdx, Undefined(m_isolate)); } ObjectManager::JavaScriptMarkingMode ObjectManager::GetMarkingMode() { diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index cc0ca9257..011f7d4ed 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -187,11 +187,12 @@ class ObjectManager { const v8::Local& object); /* - * The stored JsInfo field for a runtime object: its own field, or its - * prototype's when the TypeScript layout keeps the registered instance one - * level up. Undefined when neither holds one. + * The runtime object holding the JsInfo External for `object`: the object + * itself, or its prototype when the TypeScript layout keeps the registered + * instance one level up. Empty when neither holds one. Reads and clears of + * the link must go through this object, never through a Proxy or a child. */ - static v8::Local GetJSInstanceInfoField(const v8::Local& object); + static v8::Local GetJSInstanceInfoOwner(const v8::Local& object); void ReleaseJSInstance(v8::Persistent* po, JSInstanceInfo* jsInstanceInfo); From 2209816067be19e41a2f8d52a1b71b902f2474bf Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 16:01:31 -0300 Subject: [PATCH 6/7] fix(runtime): release an instance and its super object together The `super` object of an extended instance carries a copy of the instance's JsInfo External, so freeing the JSInstanceInfo through either object left the other one pointing at freed memory. The super object now records its owner, release resolves through that owner whichever object is passed, and the owner's super alias is cleared together with its own field. The proxied-array snapshot also stores each element with CreateDataProperty, so an indexed setter on Array.prototype cannot replace what the Proxy's get trap returned. --- .../app/tests/testReleaseNativeCounterpart.js | 28 +++++++++++++++++++ .../runtime/src/main/cpp/MetadataNode.cpp | 1 + .../runtime/src/main/cpp/ObjectManager.cpp | 26 +++++++++++++++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js b/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js index 727a82cb8..28b28dc15 100644 --- a/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js +++ b/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js @@ -68,6 +68,34 @@ describe("Test native counterpart release", function () { expect(errorMessage).toBe("Argument is not an object!"); }); + it("Releasing an extended instance also releases its super object", function () { + var MyObject = java.lang.Object.extend({ + toString: function () { return "custom"; } + }); + var instance = new MyObject(); + var sup = instance.super; + + global.__releaseNativeCounterpart(instance); + + expect(function () { sup.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { instance.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { global.__releaseNativeCounterpart(sup); }).toThrowError("Trying to release a non native object!"); + }); + + it("Releasing through the super object releases the instance", function () { + var MyObject = java.lang.Object.extend({ + toString: function () { return "custom"; } + }); + var instance = new MyObject(); + var sup = instance.super; + + global.__releaseNativeCounterpart(sup); + + expect(function () { instance.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { sup.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { global.__releaseNativeCounterpart(instance); }).toThrowError("Trying to release a non native object!"); + }); + it("Calling the __releaseNativeCounterpart function with 0 arguments should throw exception", function(){ var errorMessage = ""; diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 24787ef27..7d4e1f0e4 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -598,6 +598,7 @@ void MetadataNode::SuperAccessorGetterCallback(const FunctionCallbackInfo superValue->SetPrototype(context, thiz->GetPrototype().As()->GetPrototype().As()->GetPrototype()); V8SetPrivateValue(isolate, thiz, key, superValue); + V8SetPrivateValue(isolate, superValue, ArgConverter::ConvertToV8String(isolate, "superowner"), thiz); objectManager->CloneLink(thiz, superValue); DEBUG_WRITE("superValue.GetPrototype=%d", superValue->GetPrototype().As()->GetIdentityHash()); diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index 895f64620..11f5f5058 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -178,7 +178,7 @@ MaybeLocal ObjectManager::ReadArrayThroughProxy(Local context, for (uint32_t i = 0; i < count; i++) { Local element; if (!proxy->Get(context, i).ToLocal(&element) || - snapshot->Set(context, i, element).IsNothing()) { + snapshot->CreateDataProperty(context, i, element).IsNothing()) { return {}; } } @@ -699,9 +699,22 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { } auto source = UnwrapProxy(object); + if (!source->IsObject()) { + throw NativeScriptException("Trying to release a non native object!"); + } + auto instance = source.As(); + + // A `super` object shares its owner's JsInfo External; the owner is the one + // registered, so the release is resolved and recorded there. + Local superOwner; + V8GetPrivateValue(m_isolate, instance, ArgConverter::ConvertToV8String(m_isolate, "superowner"), superOwner); + if (!superOwner.IsEmpty() && superOwner->IsObject()) { + instance = superOwner.As(); + } + Local owner; - if (source->IsObject() && IsJsRuntimeObject(source.As())) { - owner = GetJSInstanceInfoOwner(source.As()); + if (IsJsRuntimeObject(instance)) { + owner = GetJSInstanceInfoOwner(instance); } if (owner.IsEmpty()) { throw NativeScriptException("Trying to release a non native object!"); @@ -725,6 +738,13 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { delete jsInstanceInfo; owner->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + + // Every alias of the freed External must be cleared with it. + Local superValue; + V8GetPrivateValue(m_isolate, instance, ArgConverter::ConvertToV8String(m_isolate, "supervalue"), superValue); + if (!superValue.IsEmpty() && superValue->IsObject() && IsJsRuntimeObject(superValue.As())) { + superValue.As()->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + } } ObjectManager::JavaScriptMarkingMode ObjectManager::GetMarkingMode() { From 86c55744dfbdb25afe45897c444c7a799c261214 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 18:08:53 -0300 Subject: [PATCH 7/7] fix(runtime): track super aliases on the object that owns the link The super alias was recorded on the object `.super` was read from. For a child whose registered instance is its prototype, that is not the object a release resolves to, so releasing the registered instance directly would miss the alias. The super accessor now resolves the link owner and appends the super object to an alias list kept on that owner; the super object's back-reference points at the owner as well. Release reads the list from the owner it resolved, so every alias is cleared no matter which object was passed in. --- .../runtime/src/main/cpp/MetadataNode.cpp | 20 ++++++++++++++++++- .../runtime/src/main/cpp/ObjectManager.cpp | 16 +++++++++++---- test-app/runtime/src/main/cpp/ObjectManager.h | 15 +++++++------- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 7d4e1f0e4..a981cc4e2 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -598,9 +598,27 @@ void MetadataNode::SuperAccessorGetterCallback(const FunctionCallbackInfo superValue->SetPrototype(context, thiz->GetPrototype().As()->GetPrototype().As()->GetPrototype()); V8SetPrivateValue(isolate, thiz, key, superValue); - V8SetPrivateValue(isolate, superValue, ArgConverter::ConvertToV8String(isolate, "superowner"), thiz); objectManager->CloneLink(thiz, superValue); + // The alias is tracked on the object that owns the link, which for + // the TypeScript layout is the prototype rather than `thiz`. + auto owner = ObjectManager::IsJsRuntimeObject(thiz) ? ObjectManager::GetJSInstanceInfoOwner(thiz) : Local(); + if (owner.IsEmpty()) { + owner = thiz; + } + V8SetPrivateValue(isolate, superValue, ArgConverter::ConvertToV8String(isolate, "superowner"), owner); + auto aliasesKey = ArgConverter::ConvertToV8String(isolate, "superaliases"); + Local aliasesValue; + V8GetPrivateValue(isolate, owner, aliasesKey, aliasesValue); + Local aliases; + if (!aliasesValue.IsEmpty() && aliasesValue->IsArray()) { + aliases = aliasesValue.As(); + } else { + aliases = Array::New(isolate); + V8SetPrivateValue(isolate, owner, aliasesKey, aliases); + } + aliases->CreateDataProperty(context, aliases->Length(), superValue).Check(); + DEBUG_WRITE("superValue.GetPrototype=%d", superValue->GetPrototype().As()->GetIdentityHash()); auto node = GetInstanceMetadata(isolate, thiz); diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index 11f5f5058..87a73caf0 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -740,10 +740,18 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { owner->SetInternalField(jsInfoIdx, Undefined(m_isolate)); // Every alias of the freed External must be cleared with it. - Local superValue; - V8GetPrivateValue(m_isolate, instance, ArgConverter::ConvertToV8String(m_isolate, "supervalue"), superValue); - if (!superValue.IsEmpty() && superValue->IsObject() && IsJsRuntimeObject(superValue.As())) { - superValue.As()->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + Local aliasesValue; + V8GetPrivateValue(m_isolate, owner, ArgConverter::ConvertToV8String(m_isolate, "superaliases"), aliasesValue); + if (!aliasesValue.IsEmpty() && aliasesValue->IsArray()) { + auto aliases = aliasesValue.As(); + auto context = m_isolate->GetCurrentContext(); + for (uint32_t i = 0; i < aliases->Length(); i++) { + Local alias; + if (aliases->Get(context, i).ToLocal(&alias) && alias->IsObject() && + IsJsRuntimeObject(alias.As())) { + alias.As()->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + } + } } } diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index 011f7d4ed..5fbe4afce 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -69,6 +69,14 @@ class ObjectManager { */ static bool IsJsRuntimeObject(const v8::Local& object); + /* + * The runtime object holding the JsInfo External for `object`: the object + * itself, or its prototype when the TypeScript layout keeps the registered + * instance one level up. Empty when neither holds one. Reads and clears of + * the link must go through this object, never through a Proxy or a child. + */ + static v8::Local GetJSInstanceInfoOwner(const v8::Local& object); + /* * The innermost target of a Proxy chain; any other value is returned as is. * A revoked Proxy anywhere in the chain yields null. Internal fields and @@ -186,13 +194,6 @@ class ObjectManager { JSInstanceInfo* GetJSInstanceInfoFromRuntimeObject( const v8::Local& object); - /* - * The runtime object holding the JsInfo External for `object`: the object - * itself, or its prototype when the TypeScript layout keeps the registered - * instance one level up. Empty when neither holds one. Reads and clears of - * the link must go through this object, never through a Proxy or a child. - */ - static v8::Local GetJSInstanceInfoOwner(const v8::Local& object); void ReleaseJSInstance(v8::Persistent* po, JSInstanceInfo* jsInstanceInfo);