From f7711a3170c66889d64df2b4628b0ff0df4ccf3e Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Thu, 9 Feb 2023 13:32:36 -0800 Subject: [PATCH] feat: @W-12499107 add env.installRemapOverrides() for better json support (#418) --- .../src/__tests__/intrinsics.spec.ts | 1 + .../near-membrane-base/src/environment.ts | 107 ++++-- packages/near-membrane-base/src/membrane.ts | 328 +++++++++++------- packages/near-membrane-base/src/types.ts | 9 +- .../near-membrane-dom/src/browser-realm.ts | 3 +- packages/near-membrane-node/src/node-realm.ts | 1 + .../near-membrane-shared/src/NearMembrane.ts | 10 +- .../src/__tests__/NearMembrane.spec.js | 1 + .../near-membrane-shared/src/constants.ts | 15 +- packages/near-membrane-shared/src/types.ts | 2 +- test/dom/custom-element.spec.js | 8 +- test/membrane/json.spec.js | 95 +++++ test/membrane/membrane-symbols.spec.js | 42 +-- 13 files changed, 419 insertions(+), 203 deletions(-) create mode 100644 test/membrane/json.spec.js diff --git a/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts b/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts index d3a9cdf3..0bde739a 100644 --- a/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts +++ b/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { assignFilteredGlobalDescriptorsFromPropertyDescriptorMap, createBlueConnector, diff --git a/packages/near-membrane-base/src/environment.ts b/packages/near-membrane-base/src/environment.ts index 97d9bfa3..85f4ca77 100644 --- a/packages/near-membrane-base/src/environment.ts +++ b/packages/near-membrane-base/src/environment.ts @@ -10,24 +10,37 @@ import { import type { ProxyTarget } from '@locker/near-membrane-shared/types'; import type { CallableDefineProperties, + CallableDescriptorCallback, CallableEvaluate, CallableGetPropertyValuePointer, + CallableInstallDateProtoToJSON, + CallableInstallJSONStringify, CallableInstallLazyPropertyDescriptors, + CallableIsTargetLive, + CallableIsTargetRevoked, CallableLinkPointers, + CallableSerializeTarget, CallableSetPrototypeOf, + CallableTrackAsFastTarget, GetSelectedTarget, GetTransferableValue, HooksCallback, Pointer, VirtualEnvironmentOptions, - CallableTrackAsFastTarget, - CallableDescriptorCallback, } from './types'; const LOCKER_NEAR_MEMBRANE_UNDEFINED_VALUE_SYMBOL = Symbol.for( '@@lockerNearMembraneUndefinedValue' ); +const { prototype: DateProto } = Date; +const { toJSON: DateProtoToJSON } = DateProto; +const WindowJSON = JSON; + +const installableDateToJSON = function toJSON(this: Date) { + return ReflectApply(DateProtoToJSON, this, []); +}; + export class VirtualEnvironment { private readonly blueCallableGetPropertyValuePointer: CallableGetPropertyValuePointer; @@ -39,18 +52,22 @@ export class VirtualEnvironment { private readonly blueGlobalThisPointer: Pointer; + private readonly redCallableDefineProperties: CallableDefineProperties; + private readonly redCallableEvaluate: CallableEvaluate; private readonly redCallableGetPropertyValuePointer: CallableGetPropertyValuePointer; - private readonly redCallableLinkPointers: CallableLinkPointers; + private readonly redCallableInstallDateProtoToJSON: CallableInstallDateProtoToJSON; - private readonly redCallableSetPrototypeOf: CallableSetPrototypeOf; - - private readonly redCallableDefineProperties: CallableDefineProperties; + private readonly redCallableInstallJSONStringify: CallableInstallJSONStringify; private readonly redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors; + private readonly redCallableLinkPointers: CallableLinkPointers; + + private readonly redCallableSetPrototypeOf: CallableSetPrototypeOf; + private readonly redCallableTrackAsFastTarget: CallableTrackAsFastTarget; private readonly redGlobalThisPointer: Pointer; @@ -105,22 +122,24 @@ export class VirtualEnvironment { 18: blueCallablePreventExtensions, 19: blueCallableSet, 20: blueCallableSetPrototypeOf, - 21: blueCallableDebugInfo, + // 21: blueCallableDebugInfo, // 22: blueCallableDefineProperties, 23: blueCallableGetLazyPropertyDescriptorStateByTarget, 24: blueCallableGetPropertyValue, 25: blueCallableGetTargetIntegrityTraits, 26: blueCallableGetToStringTagOfTarget, - 27: blueCallableInstallErrorPrepareStackTrace, - // 28: blueCallableInstallLazyPropertyDescriptors, - 29: blueCallableIsTargetLive, - 30: blueCallableIsTargetRevoked, - 31: blueCallableSerializeTarget, - 32: blueCallableSetLazyPropertyDescriptorStateByTarget, - // 33: blueTrackAsFastTarget, - 34: blueCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, - 35: blueCallableBatchGetPrototypeOfWhenHasNoOwnProperty, - 36: blueCallableBatchGetPrototypeOfWhenHasNoOwnPropertyDescriptor, + // 27: blueCallableInstallDateProtoToJSON, + 28: blueCallableInstallErrorPrepareStackTrace, + // 29: blueCallableInstallJSONStringify, + // 30: blueCallableInstallLazyPropertyDescriptors, + 31: blueCallableIsTargetLive, + // 32: blueCallableIsTargetRevoked, + // 33: blueCallableSerializeTarget, + 34: blueCallableSetLazyPropertyDescriptorStateByTarget, + // 35: blueTrackAsFastTarget, + 36: blueCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, + 37: blueCallableBatchGetPrototypeOfWhenHasNoOwnProperty, + 38: blueCallableBatchGetPrototypeOfWhenHasNoOwnPropertyDescriptor, } = blueHooks!; let redHooks: Parameters; const redConnect = redConnector('red', (...hooks: Parameters) => { @@ -151,19 +170,21 @@ export class VirtualEnvironment { 21: redCallableDebugInfo, 22: redCallableDefineProperties, 23: redCallableGetLazyPropertyDescriptorStateByTarget, - 24: redCallableGetPropertyValue, + // 24: redCallableGetPropertyValue, 25: redCallableGetTargetIntegrityTraits, 26: redCallableGetToStringTagOfTarget, - 27: redCallableInstallErrorPrepareStackTrace, - 28: redCallableInstallLazyPropertyDescriptors, - 29: redCallableIsTargetLive, - 30: redCallableIsTargetRevoked, - 31: redCallableSerializeTarget, - 32: redCallableSetLazyPropertyDescriptorStateByTarget, - 33: redCallableTrackAsFastTarget, - 34: redCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, - 35: redCallableBatchGetPrototypeOfWhenHasNoOwnProperty, - 36: redCallableBatchGetPrototypeOfWhenHasNoOwnPropertyDescriptor, + 27: redCallableInstallDateProtoToJSON, + 28: redCallableInstallErrorPrepareStackTrace, + 29: redCallableInstallJSONStringify, + 30: redCallableInstallLazyPropertyDescriptors, + // 31: redCallableIsTargetLive, + 32: redCallableIsTargetRevoked, + 33: redCallableSerializeTarget, + 34: redCallableSetLazyPropertyDescriptorStateByTarget, + 35: redCallableTrackAsFastTarget, + 36: redCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, + 37: redCallableBatchGetPrototypeOfWhenHasNoOwnProperty, + 38: redCallableBatchGetPrototypeOfWhenHasNoOwnPropertyDescriptor, } = redHooks!; blueConnect( noop, // redGlobalThisPointer, @@ -190,12 +211,14 @@ export class VirtualEnvironment { redCallableDebugInfo, noop, // redCallableDefineProperties, redCallableGetLazyPropertyDescriptorStateByTarget, - redCallableGetPropertyValue, + noop, // redCallableGetPropertyValue, redCallableGetTargetIntegrityTraits, redCallableGetToStringTagOfTarget, + noop, // redCallableInstallDateProtoToJSON, redCallableInstallErrorPrepareStackTrace, + noop, // redCallableInstallJSONStringify noop, // redCallableInstallLazyPropertyDescriptors, - redCallableIsTargetLive, + noop as unknown as CallableIsTargetLive, // redCallableIsTargetLive, redCallableIsTargetRevoked, redCallableSerializeTarget, redCallableSetLazyPropertyDescriptorStateByTarget, @@ -226,17 +249,19 @@ export class VirtualEnvironment { blueCallablePreventExtensions, blueCallableSet, blueCallableSetPrototypeOf, - blueCallableDebugInfo, + noop, // blueCallableDebugInfo noop, // blueCallableDefineProperties, blueCallableGetLazyPropertyDescriptorStateByTarget, blueCallableGetPropertyValue, blueCallableGetTargetIntegrityTraits, blueCallableGetToStringTagOfTarget, blueCallableInstallErrorPrepareStackTrace, + noop, // blueCallableInstallDateProtoToJSON + noop, // blueCallableInstallJSONStringify noop, // blueCallableInstallLazyPropertyDescriptors, blueCallableIsTargetLive, - blueCallableIsTargetRevoked, - blueCallableSerializeTarget, + noop as unknown as CallableIsTargetRevoked, // blueCallableIsTargetRevoked, + noop as CallableSerializeTarget, // blueCallableSerializeTarget,, blueCallableSetLazyPropertyDescriptorStateByTarget, noop, // blueCallableTrackAsFastTarget, blueCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, @@ -274,6 +299,8 @@ export class VirtualEnvironment { } ReflectApply(redCallableDefineProperties, undefined, args); }; + this.redCallableInstallJSONStringify = (WindowJSONPointer: Pointer) => + redCallableInstallJSONStringify(WindowJSONPointer); this.redCallableInstallLazyPropertyDescriptors = ( targetPointer: Pointer, ...ownKeysAndUnforgeableGlobalThisKeys: PropertyKey[] @@ -286,6 +313,10 @@ export class VirtualEnvironment { } ReflectApply(redCallableInstallLazyPropertyDescriptors, undefined, args); }; + this.redCallableInstallDateProtoToJSON = ( + DateProtoPointer: Pointer, + DataProtoToJSONPointer: Pointer + ) => redCallableInstallDateProtoToJSON(DateProtoPointer, DataProtoToJSONPointer); this.redCallableTrackAsFastTarget = (targetPointer: Pointer) => redCallableTrackAsFastTarget(targetPointer); } @@ -303,6 +334,16 @@ export class VirtualEnvironment { } } + installRemapOverrides() { + const transferableWindowJSON = this.blueGetTransferableValue(WindowJSON) as Pointer; + this.redCallableTrackAsFastTarget(transferableWindowJSON); + this.redCallableInstallDateProtoToJSON( + this.blueGetTransferableValue(DateProto) as Pointer, + this.blueGetTransferableValue(installableDateToJSON) as Pointer + ); + this.redCallableInstallJSONStringify(transferableWindowJSON); + } + lazyRemapProperties( target: ProxyTarget, ownKeys: PropertyKey[], diff --git a/packages/near-membrane-base/src/membrane.ts b/packages/near-membrane-base/src/membrane.ts index b0951131..4aa1dc76 100644 --- a/packages/near-membrane-base/src/membrane.ts +++ b/packages/near-membrane-base/src/membrane.ts @@ -60,6 +60,7 @@ import type { CallableSet, CallableSetLazyPropertyDescriptorStateByTarget, CallableSetPrototypeOf, + CallableTrackAsFastTarget, ForeignPropertyDescriptor, GetSelectedTarget, GlobalThisGetter, @@ -67,9 +68,9 @@ import type { HooksOptions, Pointer, PointerOrPrimitive, + Primitive, SerializedValue, ShadowTarget, - CallableTrackAsFastTarget, } from './types'; const proxyTargetToLazyPropertyDescriptorStateMap: WeakMap = toSafeWeakMap( @@ -199,9 +200,6 @@ export function createMembraneMarshall( // BigInt is not supported in Safari 13.1. // https://caniuse.com/bigint const FLAGS_REG_EXP = IS_IN_SHADOW_REALM ? /\w*$/ : undefined; - // Minification safe reference to the private `BoundaryProxyHandler` - // 'serializedValue' property name. - let MINIFICATION_SAFE_SERIALIZED_VALUE_PROPERTY_NAME: PropertyKey | undefined; // Minification safe references to the private `BoundaryProxyHandler` // 'apply' and 'construct' trap variant's property names. let MINIFICATION_SAFE_TRAP_PROPERTY_NAMES: string[] | undefined; @@ -215,6 +213,9 @@ export function createMembraneMarshall( const { isView: ArrayBufferIsView } = ArrayBufferCtor; const BigIntProtoValueOf = SUPPORTS_BIG_INT ? BigInt.prototype.valueOf : undefined; const { valueOf: BooleanProtoValueOf } = Boolean.prototype; + const { + prototype: { toJSON: DateProtoToJSON }, + } = Date; const { toString: ErrorProtoToString } = ErrorCtor.prototype; const { bind: FunctionProtoBind, toString: FunctionProtoToString } = Function.prototype; const { stringify: JSONStringify } = JSON; @@ -287,7 +288,9 @@ export function createMembraneMarshall( // Install flags to ensure things are installed once per realm. let installedErrorPrepareStackTraceFlag = false; + let installedJSONStringify = false; let installedPropertyDescriptorMethodWrappersFlag = false; + // eslint-disable-next-line no-shadow const enum PreventExtensionsResult { None, @@ -336,6 +339,24 @@ export function createMembraneMarshall( return false; } + function proxyMaskFunction(func: Function, maskFunc: T): T { + const proxy = new ProxyCtor(maskFunc, { + apply(_target: T, thisArg: any, args: any[]) { + if (thisArg === proxy || thisArg === maskFunc) { + thisArg = func; + } + return ReflectApply(func, thisArg, args); + }, + construct(_target: T, args: any[], newTarget: Function) { + if (newTarget === proxy || newTarget === maskFunc) { + newTarget = func; + } + return ReflectConstruct(func, args, newTarget); + }, + }); + return proxy; + } + const installErrorPrepareStackTrace = LOCKER_UNMINIFIED_FLAG ? () => { if (installedErrorPrepareStackTraceFlag) { @@ -1853,7 +1874,9 @@ export function createMembraneMarshall( let globalThisGetter: GlobalThisGetter | undefined = keyToGlobalThisGetterRegistry![key]; if (globalThisGetter === undefined) { - // Wrap `unboundGlobalThisGetter` in bound function + // We can't access the original getter to mask + // with `proxyMaskFunction()`, so instead we wrap + // `unboundGlobalThisGetter` in bound function // to obscure the getter source as "[native code]". globalThisGetter = ReflectApply( FunctionProtoBind, @@ -2386,9 +2409,7 @@ export function createMembraneMarshall( setPrototypeOf: ProxyHandler['setPrototypeOf']; - readonly proxy: ShadowTarget; - - private serializedValue: SerializedValue | undefined; + private serialize: () => Primitive; private staticToStringTag: string; @@ -2405,6 +2426,8 @@ export function createMembraneMarshall( private readonly nonConfigurableDescriptorCallback: CallableNonConfigurableDescriptorCallback; + readonly proxy: ShadowTarget; + private readonly shadowTarget: ProxyTarget; // @ts-ignore: Prevent 'is declared but its value is never read' error. @@ -2514,7 +2537,7 @@ export function createMembraneMarshall( }; this.proxy = proxy; this.revoke = revoke; - this.serializedValue = undefined; + this.serialize = noop; this.shadowTarget = shadowTarget; this.staticToStringTag = 'Object'; // Define traps. @@ -2553,125 +2576,121 @@ export function createMembraneMarshall( } } else { if (foreignTargetTraits & TargetTraits.IsObject) { - // Lazily define serializedValue. + // Lazily define serialize method. let cachedSerializedValue: SerializedValue | undefined | symbol = LOCKER_NEAR_MEMBRANE_UNDEFINED_VALUE_SYMBOL; - const { serializedValue } = this; - if (MINIFICATION_SAFE_SERIALIZED_VALUE_PROPERTY_NAME === undefined) { - // A minification safe way to get the 'serializedValue' - // property name. - ({ 0: MINIFICATION_SAFE_SERIALIZED_VALUE_PROPERTY_NAME } = ObjectKeys({ - serializedValue, - })); - } - ReflectApply(ObjectProtoDefineGetter, this, [ - MINIFICATION_SAFE_SERIALIZED_VALUE_PROPERTY_NAME, - () => { - if ( - cachedSerializedValue === - LOCKER_NEAR_MEMBRANE_UNDEFINED_VALUE_SYMBOL - ) { - cachedSerializedValue = foreignCallableSerializeTarget( - this.foreignTargetPointer - ); - } - return cachedSerializedValue; - }, - ]); + this.serialize = () => { + if ( + cachedSerializedValue === + LOCKER_NEAR_MEMBRANE_UNDEFINED_VALUE_SYMBOL + ) { + cachedSerializedValue = foreignCallableSerializeTarget( + this.foreignTargetPointer + ); + } + return cachedSerializedValue; + }; } } } - // Internal shadow realm side utilities: - - private makeProxyLive() { - // Replace pending traps with live traps that can work with the - // target without taking snapshots. - this.deleteProperty = BoundaryProxyHandler.passthruDeletePropertyTrap; - this.defineProperty = BoundaryProxyHandler.passthruDefinePropertyTrap; - this.preventExtensions = BoundaryProxyHandler.passthruPreventExtensionsTrap; - this.set = BoundaryProxyHandler.passthruSetTrap; - this.setPrototypeOf = BoundaryProxyHandler.passthruSetPrototypeOfTrap; - } - - private makeProxyStatic() { - // Reset all traps except apply and construct for static proxies - // since the proxy target is the shadow target and all operations - // are going to be applied to it rather than the real target. - this.defineProperty = BoundaryProxyHandler.staticDefinePropertyTrap; - this.deleteProperty = BoundaryProxyHandler.staticDeletePropertyTrap; - this.get = BoundaryProxyHandler.staticGetTrap; - this.getOwnPropertyDescriptor = - BoundaryProxyHandler.staticGetOwnPropertyDescriptorTrap; - this.getPrototypeOf = BoundaryProxyHandler.staticGetPrototypeOfTrap; - this.has = BoundaryProxyHandler.staticHasTrap; - this.isExtensible = BoundaryProxyHandler.staticIsExtensibleTrap; - this.ownKeys = BoundaryProxyHandler.staticOwnKeysTrap; - this.preventExtensions = BoundaryProxyHandler.staticPreventExtensionsTrap; - this.set = BoundaryProxyHandler.staticSetTrap; - this.setPrototypeOf = BoundaryProxyHandler.staticSetPrototypeOfTrap; - - const { foreignTargetPointer, foreignTargetTraits, shadowTarget } = this; - if (useFastForeignTargetPath) { - fastForeignTargetPointers!.delete(foreignTargetPointer); - } - // We don't wrap `foreignCallableGetTargetIntegrityTraits()` - // in a try-catch because it cannot throw. - const targetIntegrityTraits = - foreignCallableGetTargetIntegrityTraits(foreignTargetPointer); - if (targetIntegrityTraits & TargetIntegrityTraits.Revoked) { - // the target is a revoked proxy, in which case we revoke - // this proxy as well. - this.revoke(); - return; - } - // A proxy can revoke itself when traps are triggered and break - // the membrane, therefore we need protection. - try { - copyForeignOwnPropertyDescriptorsAndPrototypeToShadowTarget( - foreignTargetPointer, - shadowTarget - ); - } catch { - // We don't wrap `foreignCallableIsTargetRevoked()` in a - // try-catch because it cannot throw. - if (foreignCallableIsTargetRevoked(foreignTargetPointer)) { - this.revoke(); - return; - } - } - if ( - foreignTargetTraits & TargetTraits.IsObject && - !(SymbolToStringTag in shadowTarget) - ) { - let toStringTag = 'Object'; - try { - toStringTag = foreignCallableGetToStringTagOfTarget(foreignTargetPointer); - // eslint-disable-next-line no-empty - } catch {} - this.staticToStringTag = toStringTag; - } - // Preserve the semantics of the target. - if (targetIntegrityTraits & TargetIntegrityTraits.IsFrozen) { - ObjectFreeze(shadowTarget); - } else { - if (targetIntegrityTraits & TargetIntegrityTraits.IsSealed) { - ObjectSeal(shadowTarget); - } else if (targetIntegrityTraits & TargetIntegrityTraits.IsNotExtensible) { - ReflectPreventExtensions(shadowTarget); - } - if (LOCKER_UNMINIFIED_FLAG) { - // We don't wrap `foreignCallableDebugInfo()` in a try-catch - // because it cannot throw. - foreignCallableDebugInfo( - 'Mutations on the membrane of an object originating ' + - 'outside of the sandbox will not be reflected on ' + - 'the object itself:', - foreignTargetPointer - ); - } - } - } + // Internal red/shadow realm side utilities: + + private makeProxyLive = IS_IN_SHADOW_REALM + ? function (this: BoundaryProxyHandler): void { + // Replace pending traps with live traps that can work with the + // target without taking snapshots. + this.deleteProperty = BoundaryProxyHandler.passthruDeletePropertyTrap; + this.defineProperty = BoundaryProxyHandler.passthruDefinePropertyTrap; + this.preventExtensions = BoundaryProxyHandler.passthruPreventExtensionsTrap; + this.set = BoundaryProxyHandler.passthruSetTrap; + this.setPrototypeOf = BoundaryProxyHandler.passthruSetPrototypeOfTrap; + } + : noop; + + private makeProxyStatic = IS_IN_SHADOW_REALM + ? function (this: BoundaryProxyHandler): void { + // Reset all traps except apply and construct for static proxies + // since the proxy target is the shadow target and all operations + // are going to be applied to it rather than the real target. + this.defineProperty = BoundaryProxyHandler.staticDefinePropertyTrap; + this.deleteProperty = BoundaryProxyHandler.staticDeletePropertyTrap; + this.get = BoundaryProxyHandler.staticGetTrap; + this.getOwnPropertyDescriptor = + BoundaryProxyHandler.staticGetOwnPropertyDescriptorTrap; + this.getPrototypeOf = BoundaryProxyHandler.staticGetPrototypeOfTrap; + this.has = BoundaryProxyHandler.staticHasTrap; + this.isExtensible = BoundaryProxyHandler.staticIsExtensibleTrap; + this.ownKeys = BoundaryProxyHandler.staticOwnKeysTrap; + this.preventExtensions = BoundaryProxyHandler.staticPreventExtensionsTrap; + this.set = BoundaryProxyHandler.staticSetTrap; + this.setPrototypeOf = BoundaryProxyHandler.staticSetPrototypeOfTrap; + + const { foreignTargetPointer, foreignTargetTraits, shadowTarget } = this; + if (useFastForeignTargetPath) { + fastForeignTargetPointers!.delete(foreignTargetPointer); + } + // We don't wrap `foreignCallableGetTargetIntegrityTraits()` + // in a try-catch because it cannot throw. + const targetIntegrityTraits = + foreignCallableGetTargetIntegrityTraits(foreignTargetPointer); + if (targetIntegrityTraits & TargetIntegrityTraits.Revoked) { + // the target is a revoked proxy, in which case we revoke + // this proxy as well. + this.revoke(); + return; + } + // A proxy can revoke itself when traps are triggered and break + // the membrane, therefore we need protection. + try { + copyForeignOwnPropertyDescriptorsAndPrototypeToShadowTarget( + foreignTargetPointer, + shadowTarget + ); + } catch { + // We don't wrap `foreignCallableIsTargetRevoked()` in a + // try-catch because it cannot throw. + if (foreignCallableIsTargetRevoked(foreignTargetPointer)) { + this.revoke(); + return; + } + } + if ( + foreignTargetTraits & TargetTraits.IsObject && + !(SymbolToStringTag in shadowTarget) + ) { + let toStringTag = 'Object'; + try { + toStringTag = + foreignCallableGetToStringTagOfTarget(foreignTargetPointer); + // eslint-disable-next-line no-empty + } catch {} + this.staticToStringTag = toStringTag; + } + // Preserve the semantics of the target. + if (targetIntegrityTraits & TargetIntegrityTraits.IsFrozen) { + ObjectFreeze(shadowTarget); + } else { + if (targetIntegrityTraits & TargetIntegrityTraits.IsSealed) { + ObjectSeal(shadowTarget); + } else if ( + targetIntegrityTraits & TargetIntegrityTraits.IsNotExtensible + ) { + ReflectPreventExtensions(shadowTarget); + } + if (LOCKER_UNMINIFIED_FLAG) { + // We don't wrap `foreignCallableDebugInfo()` in a try-catch + // because it cannot throw. + foreignCallableDebugInfo( + 'Mutations on the membrane of an object originating ' + + 'outside of the sandbox will not be reflected on ' + + 'the object itself:', + foreignTargetPointer + ); + } + } + } + : noop; // Logic implementation of all traps. @@ -3125,12 +3144,12 @@ export function createMembraneMarshall( if (nearMembraneSymbolFlag) { // Exit without performing a [[Get]] for near-membrane // symbols because we know when the nearMembraneSymbolFlag - // is on that there is no shadowed symbol value. + // is ON that there is no shadowed symbol value. if (key === LOCKER_NEAR_MEMBRANE_SYMBOL) { return true; } if (key === LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL) { - return this.serializedValue; + return this.serialize(); } } let activity: Activity | undefined; @@ -3740,6 +3759,7 @@ export function createMembraneMarshall( } if (IS_IN_SHADOW_REALM) { + // Initialize `fastForeignTargetPointers` weak map. clearFastForeignTargetPointers(); } // Export callable hooks to a foreign realm. @@ -4383,8 +4403,44 @@ export function createMembraneMarshall( throw pushErrorAcrossBoundary(error); } }, + // callableInstallDateProtoToJSON + IS_IN_SHADOW_REALM + ? (DateProtoPointer: Pointer, DataProtoToJSONPointer: Pointer) => { + DateProtoPointer(); + const dateProto = selectedTarget as typeof Date.prototype; + selectedTarget = undefined; + DataProtoToJSONPointer(); + const toJSON = selectedTarget as unknown as typeof DateProtoToJSON; + selectedTarget = undefined; + // eslint-disable-next-line no-extend-native + dateProto.toJSON = proxyMaskFunction( + toJSON, + DateProtoToJSON + ) as typeof DateProtoToJSON; + selectedTarget = undefined; + } + : noop, // callableInstallErrorPrepareStackTrace installErrorPrepareStackTrace, + // callableInstallJSONStringify + IS_IN_SHADOW_REALM + ? (WindowJSONPointer: Pointer) => { + if (installedJSONStringify) { + return; + } + installedJSONStringify = true; + WindowJSONPointer(); + const WindowJSON = selectedTarget as typeof JSON; + selectedTarget = undefined; + WindowJSON.stringify = proxyMaskFunction( + ( + ...args: Parameters + ): ReturnType => + ReflectApply(JSONStringify, JSON, args), + JSONStringify + ); + } + : noop, // callableInstallLazyPropertyDescriptors IS_IN_SHADOW_REALM ? ( @@ -4515,7 +4571,7 @@ export function createMembraneMarshall( } : (noop as CallableSetLazyPropertyDescriptorStateByTarget), // callableTrackAsFastTarget - !IS_IN_SHADOW_REALM + IS_IN_SHADOW_REALM ? (targetPointer: Pointer) => { targetPointer(); const target = selectedTarget!; @@ -4731,16 +4787,18 @@ export function createMembraneMarshall( 24: foreignCallableGetPropertyValue, 25: foreignCallableGetTargetIntegrityTraits, 26: foreignCallableGetToStringTagOfTarget, - 27: foreignCallableInstallErrorPrepareStackTrace, - // 28: callableInstallLazyPropertyDescriptors, - 29: foreignCallableIsTargetLive, - 30: foreignCallableIsTargetRevoked, - 31: foreignCallableSerializeTarget, - 32: foreignCallableSetLazyPropertyDescriptorStateByTarget, - // 33: foreignCallableTrackAsFastTarget, - 34: foreignCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, - 35: foreignCallableBatchGetPrototypeOfWhenHasNoOwnProperty, - 36: foreignCallableBatchGetPrototypeOfWhenHasNoOwnPropertyDescriptor, + // 27: callableInstallDateProtoToJSON, + 28: foreignCallableInstallErrorPrepareStackTrace, + // 29: callableInstallJSONStringify, + // 30: callableInstallLazyPropertyDescriptors, + 31: foreignCallableIsTargetLive, + 32: foreignCallableIsTargetRevoked, + 33: foreignCallableSerializeTarget, + 34: foreignCallableSetLazyPropertyDescriptorStateByTarget, + // 35: callableTrackAsFastTarget, + 36: foreignCallableBatchGetPrototypeOfAndGetOwnPropertyDescriptors, + 37: foreignCallableBatchGetPrototypeOfWhenHasNoOwnProperty, + 38: foreignCallableBatchGetPrototypeOfWhenHasNoOwnPropertyDescriptor, } = hooks); const applyTrapForZeroOrMoreArgs = createApplyOrConstructTrapForZeroOrMoreArgs( ProxyHandlerTraps.Apply diff --git a/packages/near-membrane-base/src/types.ts b/packages/near-membrane-base/src/types.ts index 0cbf8ba5..1412870f 100644 --- a/packages/near-membrane-base/src/types.ts +++ b/packages/near-membrane-base/src/types.ts @@ -84,7 +84,12 @@ export type CallableGetPrototypeOf = (targetPointer: Pointer) => PointerOrPrimit export type CallableGetTargetIntegrityTraits = (targetPointer: Pointer) => number; export type CallableGetToStringTagOfTarget = (targetPointer: Pointer) => string; export type CallableHas = (targetPointer: Pointer, key: PropertyKey) => boolean; +export type CallableInstallDateProtoToJSON = ( + DateProtoPointer: Pointer, + DataProtoToJSONPointer: Pointer +) => void; export type CallableInstallErrorPrepareStackTrace = () => void; +export type CallableInstallJSONStringify = (WindowJSONPointer: Pointer) => void; export type CallableInstallLazyPropertyDescriptors = ( targetPointer: Pointer, ...ownKeysAndUnforgeableGlobalThisKeys: PropertyKey[] @@ -164,7 +169,9 @@ export type HooksCallback = ( callableGetPropertyValue: CallableGetPropertyValue, callableGetTargetIntegrityTraits: CallableGetTargetIntegrityTraits, callableGetToStringTagOfTarget: CallableGetToStringTagOfTarget, + callableInstallDateProtoToJSON: CallableInstallDateProtoToJSON, callableInstallErrorPrepareStackTrace: CallableInstallErrorPrepareStackTrace, + callableInstallJSONStringify: CallableInstallJSONStringify, callableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors, callableIsTargetLive: CallableIsTargetLive, callableIsTargetRevoked: CallableIsTargetRevoked, @@ -189,7 +196,7 @@ export interface Instrumentation { export type LiveTargetCallback = (target: ProxyTarget, targetTraits: number) => boolean; export type Pointer = CallableFunction; export type PointerOrPrimitive = Pointer | Primitive; -export type Primitive = bigint | boolean | null | number | string | symbol | undefined; +export type Primitive = bigint | boolean | null | number | string | symbol | undefined | void; export type RevokedProxyCallback = (target: ProxyTarget) => boolean; export type SignSourceCallback = (sourceText: string) => string; export type { SerializedValue }; diff --git a/packages/near-membrane-dom/src/browser-realm.ts b/packages/near-membrane-dom/src/browser-realm.ts index b4c5dad3..71656355 100644 --- a/packages/near-membrane-dom/src/browser-realm.ts +++ b/packages/near-membrane-dom/src/browser-realm.ts @@ -76,7 +76,7 @@ function createIframeVirtualEnvironment( endowments, globalObjectShape, instrumentation, - keepAlive = false, + keepAlive = true, liveTargetCallback, signSourceCallback, // eslint-disable-next-line prefer-object-spread @@ -150,6 +150,7 @@ function createIframeVirtualEnvironment( // We intentionally skip remapping Window.prototype because there is nothing // in it that needs to be remapped. env.lazyRemapProperties(blueRefs.EventTargetProto, blueRefs.EventTargetProtoOwnKeys); + env.installRemapOverrides(); // We don't remap `blueRefs.WindowPropertiesProto` because it is "magical" // in that it provides access to elements by id. // diff --git a/packages/near-membrane-node/src/node-realm.ts b/packages/near-membrane-node/src/node-realm.ts index 121d98e0..80c62df2 100644 --- a/packages/near-membrane-node/src/node-realm.ts +++ b/packages/near-membrane-node/src/node-realm.ts @@ -74,5 +74,6 @@ export default function createVirtualEnvironment( assignFilteredGlobalDescriptorsFromPropertyDescriptorMap(filteredEndowments, endowments); env.remapProperties(globalObject, filteredEndowments); } + env.installRemapOverrides(); return env; } diff --git a/packages/near-membrane-shared/src/NearMembrane.ts b/packages/near-membrane-shared/src/NearMembrane.ts index 4c2de554..8a00a319 100644 --- a/packages/near-membrane-shared/src/NearMembrane.ts +++ b/packages/near-membrane-shared/src/NearMembrane.ts @@ -1,11 +1,9 @@ -import { SymbolFor } from './Symbol'; +import { + LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL, + LOCKER_NEAR_MEMBRANE_SYMBOL, +} from './constants'; import type { NearMembraneSerializedValue } from './types'; -const LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL = SymbolFor( - '@@lockerNearMembraneSerializedValue' -); -const LOCKER_NEAR_MEMBRANE_SYMBOL = SymbolFor('@@lockerNearMembrane'); - export function getNearMembraneSerializedValue(object: object): NearMembraneSerializedValue { return LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in object ? undefined diff --git a/packages/near-membrane-shared/src/__tests__/NearMembrane.spec.js b/packages/near-membrane-shared/src/__tests__/NearMembrane.spec.js index 158addb6..8e3d9f7e 100644 --- a/packages/near-membrane-shared/src/__tests__/NearMembrane.spec.js +++ b/packages/near-membrane-shared/src/__tests__/NearMembrane.spec.js @@ -3,6 +3,7 @@ import { getNearMembraneSerializedValue, isNearMembrane } from '../../dist/index const LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL = Symbol.for( '@@lockerNearMembraneSerializedValue' ); + describe('NearMembrane', () => { it('getNearMembraneSerializedValue', () => { // In the red realm this should return `undefined`. diff --git a/packages/near-membrane-shared/src/constants.ts b/packages/near-membrane-shared/src/constants.ts index 23edede8..e46fac3d 100644 --- a/packages/near-membrane-shared/src/constants.ts +++ b/packages/near-membrane-shared/src/constants.ts @@ -1,3 +1,6 @@ +import { SymbolFor } from './Symbol'; + +// Locker build constants. export const LOCKER_IDENTIFIER_MARKER = '$LWS'; // This package is bundled by third-parties that have their own build time // replacement logic. Instead of customizing each build system to be aware @@ -5,13 +8,23 @@ export const LOCKER_IDENTIFIER_MARKER = '$LWS'; // runtime checks to determine phase one, our code is unminified, and // phase two, the user opted-in to custom devtools formatters. Phase one // is used for light weight initialization time debug while phase two is -// reserved for post initialization runtime. +// reserved for post initialization runtime export const LOCKER_UNMINIFIED_FLAG = // eslint-disable-next-line @typescript-eslint/naming-convention /* istanbul ignore next */ `${(function LOCKER_UNMINIFIED_FLAG() { return LOCKER_UNMINIFIED_FLAG.name; })()}`.includes('LOCKER_UNMINIFIED_FLAG'); + +// Character constants. export const CHAR_ELLIPSIS = '\u2026'; + +// Near-membrane constants. +export const LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL = SymbolFor( + '@@lockerNearMembraneSerializedValue' +); +export const LOCKER_NEAR_MEMBRANE_SYMBOL = SymbolFor('@@lockerNearMembrane'); + +// Object brand constants. export const TO_STRING_BRAND_ARRAY = '[object Array]'; export const TO_STRING_BRAND_ARRAY_BUFFER = '[object ArrayBuffer]'; export const TO_STRING_BRAND_BIG_INT = '[object BigInt]'; diff --git a/packages/near-membrane-shared/src/types.ts b/packages/near-membrane-shared/src/types.ts index 09eacdd3..2295ef70 100644 --- a/packages/near-membrane-shared/src/types.ts +++ b/packages/near-membrane-shared/src/types.ts @@ -1,6 +1,6 @@ export type Getter = () => any; export type NearMembraneSerializedValue = bigint | boolean | number | string | symbol; -export type ProxyTarget = CallableFunction | any[] | object; +export type ProxyTarget = CallableFunction | NewableFunction | any[] | object; export type Setter = (value: any) => void; // eslint-disable-next-line no-shadow export const enum TargetTraits { diff --git a/test/dom/custom-element.spec.js b/test/dom/custom-element.spec.js index 6cbbf71a..d3c67343 100644 --- a/test/dom/custom-element.spec.js +++ b/test/dom/custom-element.spec.js @@ -8,13 +8,13 @@ class ExternalElement extends HTMLElement { } } +const envOptions = { + globalObjectShape: window, +}; + customElements.define('x-external', ExternalElement); describe('Outer Realm Custom Element', () => { - const envOptions = { - globalObjectShape: window, - }; - it('should be accessible within the sandbox', () => { expect.assertions(3); diff --git a/test/membrane/json.spec.js b/test/membrane/json.spec.js new file mode 100644 index 00000000..e10306b3 --- /dev/null +++ b/test/membrane/json.spec.js @@ -0,0 +1,95 @@ +import createVirtualEnvironment from '@locker/near-membrane-dom'; + +describe('JSON', () => { + it('JSON.stringify of blue objects with modified properties', () => { + expect.assertions(1); + + let takeInside; + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ + expect, + exposeTakeInside(func) { + takeInside = func; + }, + }), + }); + + env.evaluate(` + exposeTakeInside(function takeInside(outsideValue, expectedValue) { + // Modify red proxies. + outsideValue.red = true; + expect(JSON.stringify(outsideValue)).toBe(expectedValue); + }); + `); + + const outsideObject = { blue: true }; + takeInside(outsideObject, JSON.stringify({ blue: true, red: true })); + }); + + it('JSON.parse of red objects with modified properties', () => { + expect.assertions(1); + + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ + expect, + takeOutside(insideValue, expectedValue) { + // Modify blue proxies. + insideValue.blue = true; + expect(JSON.stringify(insideValue)).toBe(expectedValue); + }, + }), + }); + + env.evaluate(` + const insideObject = { red: true }; + takeOutside(insideObject, JSON.stringify({ red: true, blue: true })); + `); + }); + + it('JSON.stringify of blue objects with date and regexp properties', () => { + expect.assertions(1); + + let takeInside; + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ + expect, + exposeTakeInside(func) { + takeInside = func; + }, + }), + }); + + env.evaluate(` + exposeTakeInside(function takeInside(outsideValue, expectedValue) { + // Test red proxies. + expect(JSON.stringify(outsideValue)).toBe(expectedValue); + }); + `); + + const date = new Date(); + const regexp = /a/; + const outsideObject = { date, regexp }; + takeInside(outsideObject, JSON.stringify({ date, regexp })); + }); + + it('JSON.parse of red objects with date and regexp properties', () => { + expect.assertions(1); + + const env = createVirtualEnvironment(window, { + endowments: Object.getOwnPropertyDescriptors({ + expect, + takeOutside(insideValue, expectedValue) { + // Test blue proxies. + expect(JSON.stringify(insideValue)).toBe(expectedValue); + }, + }), + }); + + env.evaluate(` + const date = new Date(); + const regexp = /a/; + const insideObject = { date, regexp }; + takeOutside(insideObject, \`{"date":"\${date.toISOString()}","regexp":{}}\`); + `); + }); +}); diff --git a/test/membrane/membrane-symbols.spec.js b/test/membrane/membrane-symbols.spec.js index d7a7b97f..ace1c8f6 100644 --- a/test/membrane/membrane-symbols.spec.js +++ b/test/membrane/membrane-symbols.spec.js @@ -82,10 +82,10 @@ describe('@@lockerNearMembrane', () => { exposeTakeInside(func) { takeInside = func; }, - takeOutside(insideValue, expectedSymbolValue) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. expect(LOCKER_NEAR_MEMBRANE_SYMBOL in insideValue).toBe(true); - expect(insideValue[LOCKER_NEAR_MEMBRANE_SYMBOL]).toBe(expectedSymbolValue); + expect(insideValue[LOCKER_NEAR_MEMBRANE_SYMBOL]).toBe(expectedValue); }, }), }); @@ -93,10 +93,10 @@ describe('@@lockerNearMembrane', () => { env.evaluate(` const LOCKER_NEAR_MEMBRANE_SYMBOL = Symbol.for('@@lockerNearMembrane'); - exposeTakeInside(function takeInside(outsideValue, expectedSymbolValue) { + exposeTakeInside(function takeInside(outsideValue, expectedValue) { // Test red proxies. expect(LOCKER_NEAR_MEMBRANE_SYMBOL in outsideValue).toBe(true); - expect(outsideValue[LOCKER_NEAR_MEMBRANE_SYMBOL]).toBe(expectedSymbolValue); + expect(outsideValue[LOCKER_NEAR_MEMBRANE_SYMBOL]).toBe(expectedValue); }); `); @@ -204,14 +204,14 @@ describe('@@lockerNearMembraneSerializedValue', () => { exposeTakeInside(func) { takeInside = func; }, - takeOutside(insideValue, expectedSerialized) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. // To unlock the near-membrane symbol flag first perform a // has() trap check. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in insideValue).toBe(false); // Next, perform a get() trap call. expect(insideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSerialized + expectedValue ); // Performing a get() trap call without first performing a // has() trap check will produce `undefined`. @@ -334,14 +334,14 @@ describe('@@lockerNearMembraneSerializedValue', () => { injectPairs(func) { func(bluePairs); }, - takeOutside(insideValue, expectedSerialized) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. // To unlock the near-membrane symbol flag first perform a // has() trap check. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in insideValue).toBe(false); // Next, perform a get() trap call. expect(insideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSerialized + expectedValue ); // Performing a get() trap call without first performing a // has() trap check will produce `undefined`. @@ -375,8 +375,8 @@ describe('@@lockerNearMembraneSerializedValue', () => { magentaPairs = outsideMagentaPairs; }); // Test blue proxies. - for (const { 0: magentaValue, 1: expectedSerialized } of magentaPairs) { - takeOutside(magentaValue, expectedSerialized); + for (const { 0: magentaValue, 1: expectedValue } of magentaPairs) { + takeOutside(magentaValue, expectedValue); } `); @@ -393,14 +393,14 @@ describe('@@lockerNearMembraneSerializedValue', () => { exposeTakeInside(func) { takeInside = func; }, - takeOutside(insideValue, expectedSerialized) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. // To unlock the near-membrane symbol flag first perform a // has() trap check. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in insideValue).toBe(false); // Next, perform a get() trap call. expect(insideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSerialized + expectedValue ); // Performing a get() trap call without first performing a // has() trap check will produce `undefined`. @@ -494,14 +494,14 @@ describe('@@lockerNearMembraneSerializedValue', () => { exposeTakeInside(func) { takeInside = func; }, - takeOutside(insideValue, expectedSerialized) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. // To unlock the near-membrane symbol flag first perform a // has() trap check. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in insideValue).toBe(false); // Next, perform a get() trap call. expect(insideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSerialized + expectedValue ); // Performing a get() trap call without first performing a // has() trap check will produce `undefined`. @@ -581,7 +581,7 @@ describe('@@lockerNearMembraneSerializedValue', () => { `); }); - it('should not be detectable with custom @@lockerNearMembraneSerializedValue value', () => { + it('should not be detectable when customized', () => { expect.assertions(36); let takeInside; @@ -591,11 +591,11 @@ describe('@@lockerNearMembraneSerializedValue', () => { exposeTakeInside(func) { takeInside = func; }, - takeOutside(insideValue, expectedSymbolValue) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in insideValue).toBe(true); expect(insideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSymbolValue + expectedValue ); }, }), @@ -606,11 +606,11 @@ describe('@@lockerNearMembraneSerializedValue', () => { '@@lockerNearMembraneSerializedValue' ); - exposeTakeInside(function takeInside(outsideValue, expectedSymbolValue) { + exposeTakeInside(function takeInside(outsideValue, expectedValue) { // Test red proxies. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in outsideValue).toBe(true); expect(outsideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSymbolValue + expectedValue ); }); `); @@ -751,14 +751,14 @@ describe('@@lockerNearMembraneSerializedValue', () => { exposeTakeInside(func) { takeInside = func; }, - takeOutside(insideValue, expectedSerialized) { + takeOutside(insideValue, expectedValue) { // Test blue proxies. // To unlock the near-membrane symbol flag first perform a // has() trap check. expect(LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL in insideValue).toBe(false); // Next, perform a get() trap call. expect(insideValue[LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL]).toBe( - expectedSerialized + expectedValue ); // Performing a get() trap call without first performing a // has() trap check will produce `undefined`.