Skip to content

Commit ec59fe1

Browse files
authored
fix(runtime): unwrap Proxy receivers and arguments before native dispatch (#485)
* fix(runtime): unwrap Proxy receivers and arguments before native dispatch Native wrappers wrapped in a JS Proxy (e.g. by Vue's reactive()) expose no internal fields, so method calls were dispatched as class methods, property accessors returned undefined, toString returned an object, and passing a proxy as an argument aborted the process in GetCreationContext. - tns::UnwrapProxy walks Proxy::GetTarget chains; tns::GetValue resolves through it, so every wrapper lookup sees the target. - MethodCallback, property getter/setter and toString resolve a proxy receiver to its native target (or class constructor for methods) and throw a TypeError for revoked proxies or non-native targets. - WriteValue/WriteTypeValue/ToArray unwrap before marshalling and throw a TypeError for revoked proxies; ToObject and callback return values treat a revoked proxy as nil. - GetCreationContext call sites fall back to the current context instead of asserting when the object has none. * fix(runtime): key wrapper state by the Proxy target in SetValue/DeleteValue GetValue resolves through proxies while SetValue and DeleteValue addressed the object they were handed, so __releaseNativeCounterpart(proxy) deleted the target's wrapper and then cleared the slot on the Proxy, leaving the target's internal field dangling. All three now resolve to the same target. ResolveProxyReceiver accepts a function target only when it carries an ObjCClass wrapper; a proxied plain function is a TypeError instead of a static call on the metadata class. * fix(runtime): zero the whole return slot for empty callback results ArgConverter::SetValue wrote a single ffi_arg when a JS callback returned null, undefined or a revoked Proxy, so struct-valued returns handed native code stale bytes past the first word. Callers now pass cif->rtype->size and the empty branch clears the full slot. * fix(runtime): read JavaScript-backed Proxies through their traps JS-to-native conversion unwrapped every Proxy and read its target, which bypasses the get/ownKeys/has traps for ordinary objects and arrays, so native code saw different data than JS. Vue's reactive() get trap auto-unwraps refs, so dictionaryWithDictionary(reactive({ a: ref(1) })) produced { a: <RefImpl> } instead of { a: 1 }. Rule, applied at every conversion site via tns::ProxyReadSource: the unwrapped target is still used for classification and native-wrapper lookups (a revoked Proxy still throws a TypeError). When the target is an ordinary object or Array, the original Proxy is what gets read. Map, Set, WeakMap/WeakSet, ArrayBuffer/views, Date, RegExp, boxed primitives, Promises, functions and objects with internal fields stay on the target: their prototype methods reject a Proxy receiver, and reactive(new Map()) only works through the raw Map. Sites: - Interop::WriteValue / WriteTypeValue: array-like -> ArrayAdapter, DictionaryAdapter, constant arrays and all struct-from-object initializers read the Proxy. - Interop::ToObject: plain objects hand the Proxy to DictionaryAdapter; proxied arrays are snapshotted through the traps before ArrayAdapter (which needs a real v8::Array). A snapshot failure is reported like the adapters' other read failures and yields nil. - Interop::ToArray runs Array.prototype.slice on the Proxy and throws a NativeScriptException carrying the trap's error instead of asserting. - Interop::InitializeStruct and ArgConverter::GetInitializerArgs throw a catchable error when a field read throws instead of asserting. - ArgConverter::FindInitializer passes the Proxy to GetInitializerArgs. - ArgConverter::SetValue (callback return) hands the Proxy to DictionaryAdapter; struct returns stay on the target because a throw cannot leave the ffi closure. - DictionaryAdapter remembers the Proxy target that holds its claim, so dealloc still finds and frees the claim after the Proxy is revoked.
1 parent 87a1a4b commit ec59fe1

14 files changed

Lines changed: 619 additions & 119 deletions

‎NativeScript/runtime/AnimationFrame.mm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ void FireFrame(double timestampSeconds) {
225225
}
226226
entry->scheduled = false;
227227
Local<v8::Function> cb = entry->callback.Get(isolate);
228-
Local<Context> context = cb->GetCreationContextChecked(v8::Isolate::GetCurrent());
228+
Local<Context> context = tns::GetCreationContextOrCurrent(isolate, cb);
229229
Context::Scope contextScope(context);
230230
if (entry->raf) {
231231
Local<Value> argv[] = {v8::Number::New(isolate, performanceMillis)};

‎NativeScript/runtime/ArgConverter.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ class ArgConverter {
7171
static const ProtocolMeta* FindProtocolMeta(Protocol* protocol);
7272
static void MethodCallback(ffi_cif* cif, void* retValue, void** argValues,
7373
void* userData);
74+
// returnSize is the ABI size of the return slot (cif->rtype->size); an
75+
// empty, null or undefined value zeroes all of it.
7476
static void SetValue(v8::Local<v8::Context> context, void* retValue,
75-
v8::Local<v8::Value> value,
77+
size_t returnSize, v8::Local<v8::Value> value,
7678
const TypeEncoding* typeEncoding);
7779
// Returns (lazily creating) the per-isolate interop.escapeException brand
7880
// private stored in Caches. Empty handle if the isolate cache is invalid.

‎NativeScript/runtime/ArgConverter.mm‎

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "ArgConverter.h"
22
#include <Foundation/Foundation.h>
3+
#include <algorithm>
34
#include <sstream>
45
#include "DictionaryAdapter.h"
56
#include "Helpers.h"
@@ -438,7 +439,7 @@
438439
if (!success) {
439440
memset(retValue, 0, cif->rtype->size);
440441
} else {
441-
ArgConverter::SetValue(context, retValue, result, data->typeEncoding_);
442+
ArgConverter::SetValue(context, retValue, cif->rtype->size, result, data->typeEncoding_);
442443
}
443444
} else {
444445
memset(retValue, 0, cif->rtype->size);
@@ -450,15 +451,19 @@
450451
}
451452
}
452453

453-
void ArgConverter::SetValue(Local<Context> context, void* retValue, Local<Value> value,
454-
const TypeEncoding* typeEncoding) {
454+
void ArgConverter::SetValue(Local<Context> context, void* retValue, size_t returnSize,
455+
Local<Value> value, const TypeEncoding* typeEncoding) {
455456
if (typeEncoding->type == BinaryTypeEncodingType::VoidEncoding) {
456457
return;
457458
}
458459

460+
// Runs inside an ffi closure, where a C++ throw cannot propagate; a revoked
461+
// proxy returns nil. libffi sizes the return slot to at least ffi_arg even
462+
// for narrower types, and the full slot must be written.
463+
Local<Value> original = value;
464+
value = tns::UnwrapProxy(value);
459465
if (value.IsEmpty() || value->IsNullOrUndefined()) {
460-
void* nullPtr = nullptr;
461-
*(ffi_arg*)retValue = (unsigned long)nullPtr;
466+
memset(retValue, 0, std::max(returnSize, sizeof(ffi_arg)));
462467
return;
463468
}
464469

@@ -540,8 +545,9 @@
540545
*static_cast<id __strong*>(retValue) = data;
541546
return;
542547
} else {
543-
id adapter = [[DictionaryAdapter alloc] initWithJSObject:value.As<Object>()
544-
isolate:isolate];
548+
id adapter = [[DictionaryAdapter alloc]
549+
initWithJSObject:tns::ProxyReadSource(original, value).As<Object>()
550+
isolate:isolate];
545551
memset(retValue, 0, sizeof(id));
546552
*static_cast<id __strong*>(retValue) = adapter;
547553
// CFAutorelease(adapter);
@@ -651,7 +657,9 @@
651657
std::vector<Local<Value>> initializerArgs;
652658
std::string constructorTokens;
653659
if (info.Length() == 1 && info[0]->IsObject() && tns::GetValue(isolate, info[0]) == nullptr) {
654-
initializerArgs = GetInitializerArgs(info[0].As<Object>(), constructorTokens);
660+
Local<Value> initializer = tns::UnwrapProxyOrThrow(isolate, info[0]);
661+
initializerArgs = GetInitializerArgs(tns::ProxyReadSource(info[0], initializer).As<Object>(),
662+
constructorTokens);
655663
}
656664

657665
std::shared_ptr<Caches> cache = Caches::Get(isolate);
@@ -733,6 +741,8 @@
733741

734742
bool ArgConverter::CanInvoke(Local<Context> context, const TypeEncoding* typeEncoding,
735743
Local<Value> arg) {
744+
// A revoked proxy matches anything so marshalling reports it as such.
745+
arg = tns::UnwrapProxy(arg);
736746
if (arg.IsEmpty() || arg->IsNullOrUndefined()) {
737747
return true;
738748
}
@@ -811,26 +821,27 @@
811821
std::string& constructorTokens) {
812822
std::vector<Local<Value>> args;
813823
constructorTokens = "";
814-
Local<Context> context;
815-
bool success = obj->GetCreationContext(v8::Isolate::GetCurrent()).ToLocal(&context);
816-
tns::Assert(success);
817824
Isolate* isolate = v8::Isolate::GetCurrent();
825+
Local<Context> context = tns::GetCreationContextOrCurrent(isolate, obj);
818826
Local<v8::Array> properties;
819-
if (obj->GetOwnPropertyNames(context).ToLocal(&properties)) {
820-
std::stringstream ss;
821-
for (uint32_t i = 0; i < properties->Length(); i++) {
822-
Local<Value> propertyName;
823-
if (properties->Get(context, i).ToLocal(&propertyName)) {
824-
std::string name = tns::ToString(isolate, propertyName);
825-
ss << name << ":";
826-
Local<Value> propertyValue;
827-
bool ok = obj->Get(context, propertyName).ToLocal(&propertyValue);
828-
tns::Assert(ok, isolate);
829-
args.push_back(propertyValue);
827+
TryCatch tc(isolate);
828+
if (!obj->GetOwnPropertyNames(context).ToLocal(&properties)) {
829+
throw NativeScriptException(isolate, tc, "Cannot read initializer object");
830+
}
831+
std::stringstream ss;
832+
for (uint32_t i = 0; i < properties->Length(); i++) {
833+
Local<Value> propertyName;
834+
if (properties->Get(context, i).ToLocal(&propertyName)) {
835+
std::string name = tns::ToString(isolate, propertyName);
836+
ss << name << ":";
837+
Local<Value> propertyValue;
838+
if (!obj->Get(context, propertyName).ToLocal(&propertyValue)) {
839+
throw NativeScriptException(isolate, tc, "Cannot read initializer object");
830840
}
841+
args.push_back(propertyValue);
831842
}
832-
constructorTokens = ss.str();
833843
}
844+
constructorTokens = ss.str();
834845

835846
return args;
836847
}

‎NativeScript/runtime/ClassBuilder.mm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) {
931931
memset(retValue, 0, cif->rtype->size);
932932
} else {
933933
const TypeEncoding* typeEncoding = context->meta_->getter()->encodings()->first();
934-
ArgConverter::SetValue(v8Context, retValue, res, typeEncoding);
934+
ArgConverter::SetValue(v8Context, retValue, cif->rtype->size, res, typeEncoding);
935935
}
936936
}
937937
if (pendingThrow != nil) {

‎NativeScript/runtime/DictionaryAdapter.mm‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ @implementation DictionaryAdapter {
226226
// both isolate states; the field compare below guards the isolate-alive
227227
// path against a slot someone else overwrote.
228228
ObjCDataWrapper* dataWrapper_;
229+
// The Proxy target holding dataWrapper_ when object_ is a Proxy: a Proxy
230+
// revoked later no longer leads to it, and the claim must still be found.
231+
Persistent<Value>* claimTarget_;
229232
}
230233

231234
- (instancetype)initWithJSObject:(Local<Object>)jsObject isolate:(Isolate*)isolate {
@@ -240,6 +243,12 @@ - (instancetype)initWithJSObject:(Local<Object>)jsObject isolate:(Isolate*)isola
240243
self->dataWrapper_ = new ObjCDataWrapper(self);
241244
self->dataWrapper_->MarkAdapterClaim();
242245
tns::SetValue(isolate, jsObject, self->dataWrapper_);
246+
if (jsObject->IsProxy()) {
247+
Local<Value> target = tns::UnwrapProxy(jsObject);
248+
if (!target.IsEmpty()) {
249+
self->claimTarget_ = new Persistent<Value>(isolate, target);
250+
}
251+
}
243252
}
244253
}
245254

@@ -368,7 +377,8 @@ - (void)dealloc {
368377
// isolate is gone the field can no longer be read, so the claim is dropped
369378
// rather than freed blind.
370379
if (dataWrapper_ != nullptr) {
371-
Local<Value> value = self->object_->Get(isolate);
380+
Local<Value> value =
381+
claimTarget_ != nullptr ? claimTarget_->Get(isolate) : self->object_->Get(isolate);
372382
if (tns::GetValue(isolate, value) == dataWrapper_) {
373383
tns::DeleteValue(isolate, value);
374384
delete dataWrapper_;
@@ -379,6 +389,9 @@ - (void)dealloc {
379389
// vended by -keyEnumerator hold this adapter alive, so nothing can be
380390
// reading the handle by the time this runs.
381391
self->object_->Reset();
392+
if (claimTarget_ != nullptr) {
393+
claimTarget_->Reset();
394+
}
382395
} else if (dataWrapper_ != nullptr) {
383396
// The isolate is gone, and with it the JS object and every reader of the
384397
// claim; no other path deletes one (__releaseNativeCounterpart leaves
@@ -389,6 +402,8 @@ - (void)dealloc {
389402
dataWrapper_ = nullptr;
390403
}
391404
self->object_ = nullptr;
405+
delete claimTarget_;
406+
claimTarget_ = nullptr;
392407
delete self->wrapper_;
393408

394409
[super dealloc];

‎NativeScript/runtime/Helpers.h‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,38 @@ void SetPrivateValue(const v8::Local<v8::Object>& obj, const v8::Local<v8::Strin
258258
v8::Local<v8::Value> GetPrivateValue(const v8::Local<v8::Object>& obj,
259259
const v8::Local<v8::String>& propName);
260260

261+
// Follows a Proxy chain to its innermost target; empty when any link is
262+
// revoked. Non-proxies come back unchanged.
263+
inline v8::Local<v8::Value> UnwrapProxy(v8::Local<v8::Value> value) {
264+
while (!value.IsEmpty() && value->IsProxy()) {
265+
v8::Local<v8::Proxy> proxy = value.As<v8::Proxy>();
266+
if (proxy->IsRevoked()) {
267+
return v8::Local<v8::Value>();
268+
}
269+
value = proxy->GetTarget();
270+
}
271+
return value;
272+
}
273+
274+
// UnwrapProxy for values crossing into native code: a revoked proxy throws a
275+
// NativeScriptException carrying a TypeError.
276+
v8::Local<v8::Value> UnwrapProxyOrThrow(v8::Isolate* isolate, v8::Local<v8::Value> value);
277+
278+
// The value native code should read properties and elements from, given the
279+
// original value and its unwrapped target. A Proxy over an ordinary object or
280+
// Array is read through its traps, so native code sees what JS sees. Any other
281+
// target is read directly: native wrappers keep their state on the target, and
282+
// objects with internal slots (Map, Set, buffers, Date, RegExp, boxed
283+
// primitives, functions) reject a Proxy receiver in their own methods.
284+
v8::Local<v8::Value> ProxyReadSource(v8::Local<v8::Value> original, v8::Local<v8::Value> target);
285+
286+
// The object's creation context, or the isolate's current (else main) context
287+
// for objects that have none, such as proxies.
288+
v8::Local<v8::Context> GetCreationContextOrCurrent(v8::Isolate* isolate,
289+
const v8::Local<v8::Object>& obj);
290+
291+
// Set/Get/DeleteValue all resolve through proxies: wrapper state is keyed by
292+
// the Proxy target, so a proxied wrapper yields its target's wrapper.
261293
void SetValue(v8::Isolate* isolate, const v8::Local<v8::Object>& obj, BaseDataWrapper* value);
262294
BaseDataWrapper* GetValue(v8::Isolate* isolate, const v8::Local<v8::Value>& val);
263295

‎NativeScript/runtime/Helpers.mm‎

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -198,24 +198,64 @@
198198
return ok;
199199
}
200200

201+
Local<Value> tns::UnwrapProxyOrThrow(Isolate* isolate, Local<Value> value) {
202+
if (value.IsEmpty() || !value->IsProxy()) {
203+
return value;
204+
}
205+
Local<Value> target = tns::UnwrapProxy(value);
206+
if (target.IsEmpty()) {
207+
std::string message = "Cannot pass a revoked Proxy to native code";
208+
throw NativeScriptException(
209+
isolate, v8::Exception::TypeError(tns::ToV8String(isolate, message)), message);
210+
}
211+
return target;
212+
}
213+
214+
Local<Value> tns::ProxyReadSource(Local<Value> original, Local<Value> target) {
215+
if (original.IsEmpty() || !original->IsProxy() || target.IsEmpty() || !target->IsObject()) {
216+
return target;
217+
}
218+
if (target->IsArray()) {
219+
return original;
220+
}
221+
if (target->IsFunction() || target->IsMap() || target->IsSet() || target->IsWeakMap() ||
222+
target->IsWeakSet() || target->IsArrayBuffer() || target->IsArrayBufferView() ||
223+
target->IsSharedArrayBuffer() || target->IsDate() || target->IsRegExp() ||
224+
target->IsStringObject() || target->IsNumberObject() || target->IsBooleanObject() ||
225+
target->IsBigIntObject() || target->IsSymbolObject() || target->IsPromise() ||
226+
target.As<Object>()->InternalFieldCount() > 0) {
227+
return target;
228+
}
229+
return original;
230+
}
231+
232+
Local<Context> tns::GetCreationContextOrCurrent(Isolate* isolate, const Local<Object>& obj) {
233+
Local<Context> context;
234+
if (obj->GetCreationContext(isolate).ToLocal(&context)) {
235+
return context;
236+
}
237+
context = isolate->GetCurrentContext();
238+
if (context.IsEmpty()) {
239+
context = Caches::Get(isolate)->GetContext();
240+
}
241+
return context;
242+
}
243+
201244
void tns::SetPrivateValue(const Local<Object>& obj, const Local<v8::String>& propName,
202245
const Local<Value>& value) {
203-
Local<Context> context;
204-
bool success = obj->GetCreationContext(v8::Isolate::GetCurrent()).ToLocal(&context);
205-
tns::Assert(success);
206246
Isolate* isolate = v8::Isolate::GetCurrent();
247+
Local<Context> context = tns::GetCreationContextOrCurrent(isolate, obj);
207248
Local<Private> privateKey = Private::ForApi(isolate, propName);
208249

250+
bool success = false;
209251
if (!obj->SetPrivate(context, privateKey, value).To(&success) || !success) {
210252
tns::Assert(false, isolate);
211253
}
212254
}
213255

214256
Local<Value> tns::GetPrivateValue(const Local<Object>& obj, const Local<v8::String>& propName) {
215-
Local<Context> context;
216-
bool success = obj->GetCreationContext(v8::Isolate::GetCurrent()).ToLocal(&context);
217-
tns::Assert(success);
218257
Isolate* isolate = v8::Isolate::GetCurrent();
258+
Local<Context> context = tns::GetCreationContextOrCurrent(isolate, obj);
219259
Local<Private> privateKey = Private::ForApi(isolate, propName);
220260

221261
Maybe<bool> hasPrivate = obj->HasPrivate(context, privateKey);
@@ -243,10 +283,18 @@
243283
return false;
244284
}
245285

246-
void tns::SetValue(Isolate* isolate, const Local<Object>& obj, BaseDataWrapper* value) {
247-
if (obj.IsEmpty() || obj->IsNullOrUndefined()) {
286+
void tns::SetValue(Isolate* isolate, const Local<Object>& val, BaseDataWrapper* value) {
287+
if (val.IsEmpty() || val->IsNullOrUndefined()) {
288+
return;
289+
}
290+
291+
// Wrapper state lives on the Proxy target so that it is found by the same
292+
// identity GetValue resolves to.
293+
Local<Value> target = tns::UnwrapProxy(val);
294+
if (target.IsEmpty()) {
248295
return;
249296
}
297+
Local<Object> obj = target.As<Object>();
250298

251299
Local<External> ext = External::New(isolate, value, v8::kExternalPointerTypeTagDefault);
252300

@@ -263,6 +311,13 @@
263311
}
264312

265313
Local<Object> obj = val.As<Object>();
314+
if (obj->IsProxy()) {
315+
Local<Value> target = tns::UnwrapProxy(obj);
316+
if (target.IsEmpty()) {
317+
return nullptr;
318+
}
319+
obj = target.As<Object>();
320+
}
266321
if (obj->InternalFieldCount() > 0) {
267322
Local<Value> field = obj->GetInternalField(0).As<v8::Value>();
268323
if (field.IsEmpty() || field->IsNullOrUndefined() || !field->IsExternal()) {
@@ -502,11 +557,12 @@ void WriteDebugLine(tns::LogCategory category, const char* message) {
502557
}
503558

504559
void tns::DeleteValue(Isolate* isolate, const Local<Value>& val) {
505-
if (val.IsEmpty() || val->IsNullOrUndefined() || !val->IsObject()) {
560+
Local<Value> target = tns::UnwrapProxy(val);
561+
if (target.IsEmpty() || target->IsNullOrUndefined() || !target->IsObject()) {
506562
return;
507563
}
508564

509-
Local<Object> obj = val.As<Object>();
565+
Local<Object> obj = target.As<Object>();
510566
if (obj->InternalFieldCount() > 0) {
511567
obj->SetInternalField(0, v8::Undefined(isolate));
512568
return;
@@ -518,12 +574,10 @@ void WriteDebugLine(tns::LogCategory category, const char* message) {
518574
return;
519575
}
520576

521-
Local<Context> context;
522-
bool success = obj->GetCreationContext(v8::Isolate::GetCurrent()).ToLocal(&context);
523-
tns::Assert(success, isolate);
577+
Local<Context> context = tns::GetCreationContextOrCurrent(isolate, obj);
524578
Local<Private> privateKey = Private::ForApi(isolate, metadataKey);
525579

526-
success = obj->DeletePrivate(context, privateKey).FromMaybe(false);
580+
bool success = obj->DeletePrivate(context, privateKey).FromMaybe(false);
527581
tns::Assert(success, isolate);
528582
}
529583

@@ -537,18 +591,21 @@ void WriteDebugLine(tns::LogCategory category, const char* message) {
537591
}
538592

539593
bool tns::IsArrayOrArrayLike(Isolate* isolate, const Local<Value>& value) {
540-
if (value->IsArray()) {
594+
Local<Value> target = tns::UnwrapProxy(value);
595+
if (target.IsEmpty()) {
596+
return false;
597+
}
598+
599+
if (target->IsArray()) {
541600
return true;
542601
}
543602

544-
if (!value->IsObject()) {
603+
if (!target->IsObject()) {
545604
return false;
546605
}
547606

548-
Local<Object> obj = value.As<Object>();
549-
Local<Context> context;
550-
bool success = obj->GetCreationContext(v8::Isolate::GetCurrent()).ToLocal(&context);
551-
tns::Assert(success, isolate);
607+
Local<Object> obj = target.As<Object>();
608+
Local<Context> context = tns::GetCreationContextOrCurrent(isolate, obj);
552609
return obj->Has(context, ToV8String(isolate, "length")).FromMaybe(false);
553610
}
554611

0 commit comments

Comments
 (0)