diff --git a/karma.config.js b/karma.config.js index aede4434..d4ea9344 100644 --- a/karma.config.js +++ b/karma.config.js @@ -5,17 +5,28 @@ const globby = require('globby'); const istanbul = require('rollup-plugin-istanbul'); const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const replaceRollupPlugin = require('@rollup/plugin-replace'); const path = require('node:path'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); process.env.CHROME_BIN = require('puppeteer').executablePath(); let testFilesPattern = './test/**/*.spec.js'; const basePath = path.resolve(__dirname, './'); -const matchArg = process.argv.indexOf('--match'); +const argv = yargs(hideBin(process.argv)) + .options({ + coverage: { type: 'boolean' }, + match: { type: 'string' }, + 'use-shadow-realm': { type: 'boolean' }, + }) + .hide('help') + .hide('version').argv; +const { coverage, match, useShadowRealm } = argv; -if (matchArg > -1) { - testFilesPattern = process.argv[matchArg + 1] || ''; +if (match) { + testFilesPattern = match; } if (globby.sync(testFilesPattern).length) { @@ -27,8 +38,6 @@ if (globby.sync(testFilesPattern).length) { process.exit(0); } -const coverage = process.argv.includes('--coverage'); - const customLaunchers = { ChromeHeadlessNoSandbox: { base: 'ChromeHeadless', @@ -74,6 +83,13 @@ module.exports = function (config) { nodeResolve({ preferBuiltins: true, }), + replaceRollupPlugin({ + include: ['./test/__bootstrap__/create-virtual-environment.js'], + preventAssignment: true, + values: { + 'process.env.USE_SHADOW_REALM': JSON.stringify(useShadowRealm), + }, + }), ], }, }; diff --git a/package.json b/package.json index 03f66ec5..4c5320b2 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@commitlint/config-conventional": "17.1.0", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-node-resolve": "13.0.5", - "@rollup/plugin-replace": "4.0.0", + "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-typescript": "8.4.0", "@types/eslint": "8.4.6", "@types/jest": "29.0.0", @@ -111,5 +111,7 @@ "node": "16.13.1", "yarn": "1.22.11" }, - "dependencies": {} + "dependencies": { + "yargs": "^17.6.2" + } } 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..c7571fb9 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,29 @@ export class VirtualEnvironment { private readonly blueGlobalThisPointer: Pointer; + private readonly redCallableDefineProperties: CallableDefineProperties; + private readonly redCallableEvaluate: CallableEvaluate; private readonly redCallableGetPropertyValuePointer: CallableGetPropertyValuePointer; + private readonly redCallableInstallDateProtoToJSON: CallableInstallDateProtoToJSON; + + private readonly redCallableInstallJSONStringify: CallableInstallJSONStringify; + + private readonly redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors; + private readonly redCallableLinkPointers: CallableLinkPointers; private readonly redCallableSetPrototypeOf: CallableSetPrototypeOf; +<<<<<<< HEAD +======= private readonly redCallableDefineProperties: CallableDefineProperties; private readonly redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors; +>>>>>>> 3cede24 (test: add minimum test for ShadowRealm virtual environment, with instructions) private readonly redCallableTrackAsFastTarget: CallableTrackAsFastTarget; private readonly redGlobalThisPointer: Pointer; @@ -105,22 +129,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 +177,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 +218,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 +256,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 +306,8 @@ export class VirtualEnvironment { } ReflectApply(redCallableDefineProperties, undefined, args); }; + this.redCallableInstallJSONStringify = (WindowJSONPointer: Pointer) => + redCallableInstallJSONStringify(WindowJSONPointer); this.redCallableInstallLazyPropertyDescriptors = ( targetPointer: Pointer, ...ownKeysAndUnforgeableGlobalThisKeys: PropertyKey[] @@ -286,6 +320,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 +341,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..3976c9aa 100644 --- a/packages/near-membrane-dom/src/browser-realm.ts +++ b/packages/near-membrane-dom/src/browser-realm.ts @@ -1,5 +1,6 @@ import { assignFilteredGlobalDescriptorsFromPropertyDescriptorMap, + CallableEvaluate, createBlueConnector, createRedConnector, getFilteredGlobalOwnKeys, @@ -11,6 +12,7 @@ import { ReflectApply, toSafeWeakMap, toSafeWeakSet, + SUPPORTS_SHADOW_REALM, TypeErrorCtor, WeakMapCtor, WeakSetCtor, @@ -45,6 +47,17 @@ const aliveIframes = toSafeWeakSet(new WeakSetCtor()); const blueCreateHooksCallbackCache = toSafeWeakMap(new WeakMapCtor()); let defaultGlobalOwnKeys: PropertyKey[] | null = null; +const defaultGlobalOwnKeysRegistry = ObjectAssign({ __proto__: null }); +let defaultGlobalPropertyDescriptorMap: PropertyDescriptorMap | null = null; + +const ObjectCtor = Object; +const { bind: FunctionProtoBind } = Function.prototype; +const { create: ObjectCreate, getOwnPropertyDescriptors: ObjectGetOwnPropertyDescriptors } = + ObjectCtor; + +// @ts-ignore: Prevent cannot find name 'ShadowRealm' error. +const ShadowRealmCtor = SUPPORTS_SHADOW_REALM ? ShadowRealm : undefined; +const ShadowRealmProtoEvaluate: CallableEvaluate | undefined = ShadowRealmCtor?.prototype?.evaluate; function createDetachableIframe(doc: Document): HTMLIFrameElement { const iframe = ReflectApply(DocumentProtoCreateElement, doc, ['iframe']) as HTMLIFrameElement; @@ -67,7 +80,7 @@ function createIframeVirtualEnvironment( if (typeof globalObject !== 'object' || globalObject === null) { throw new TypeErrorCtor('Missing global object virtualization target.'); } - const blueRefs = getCachedGlobalObjectReferences(globalObject); + const blueRefs = getCachedGlobalObjectReferences(globalObject)!; if (typeof blueRefs !== 'object' || blueRefs === null) { throw new TypeErrorCtor('Invalid virtualization target.'); } @@ -76,7 +89,7 @@ function createIframeVirtualEnvironment( endowments, globalObjectShape, instrumentation, - keepAlive = false, + keepAlive = true, liveTargetCallback, signSourceCallback, // eslint-disable-next-line prefer-object-spread @@ -150,6 +163,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. // @@ -178,4 +192,119 @@ function revokedProxyCallback(value: ProxyTarget): boolean { return aliveIframes.has(value as any); } -export default createIframeVirtualEnvironment; +function createShadowRealmVirtualEnvironment( + globalObject: WindowProxy & typeof globalThis, + options?: BrowserEnvironmentOptions +): VirtualEnvironment { + if (typeof globalObject !== 'object' || globalObject === null) { + throw new TypeErrorCtor('Missing global object virtualization target.'); + } + const blueRefs = getCachedGlobalObjectReferences(globalObject)!; + if (typeof blueRefs !== 'object' || blueRefs === null) { + throw new TypeErrorCtor('Invalid virtualization target.'); + } + const { + distortionCallback, + endowments, + globalObjectShape, + instrumentation, + liveTargetCallback, + // eslint-disable-next-line prefer-object-spread + } = ObjectAssign({ __proto__: null }, options); + const shouldUseDefaultGlobalOwnKeys = + typeof globalObjectShape !== 'object' || globalObjectShape === null; + if (shouldUseDefaultGlobalOwnKeys && defaultGlobalOwnKeys === null) { + const iframe = createDetachableIframe(globalObject.document); + const oneTimeWindow = ReflectApply(HTMLIFrameElementProtoContentWindowGetter, iframe, [])!; + + defaultGlobalOwnKeys = getFilteredGlobalOwnKeys(oneTimeWindow); + + ReflectApply(ElementProtoRemove, iframe, []); + + defaultGlobalPropertyDescriptorMap = { + __proto__: null, + } as unknown as PropertyDescriptorMap; + assignFilteredGlobalDescriptorsFromPropertyDescriptorMap( + defaultGlobalPropertyDescriptorMap, + ObjectGetOwnPropertyDescriptors(globalObject) + ); + for (let i = 0, { length } = defaultGlobalOwnKeys; i < length; i += 1) { + defaultGlobalOwnKeysRegistry[defaultGlobalOwnKeys[i]] = true; + } + for (const key in defaultGlobalPropertyDescriptorMap) { + if (!(key in defaultGlobalOwnKeysRegistry)) { + delete defaultGlobalPropertyDescriptorMap[key]; + } + } + } + + let blueConnector = blueCreateHooksCallbackCache.get(blueRefs.document) as + | Connector + | undefined; + + if (blueConnector === undefined) { + blueConnector = createBlueConnector(globalObject); + blueCreateHooksCallbackCache.set(blueRefs.document, blueConnector); + } + + const redConnector = createRedConnector( + ReflectApply(FunctionProtoBind, ShadowRealmProtoEvaluate, [new ShadowRealmCtor()]) + ); + + const env = new VirtualEnvironment({ + blueConnector, + distortionCallback, + instrumentation, + liveTargetCallback, + redConnector, + }); + + // Does this actually need to be done when using ShadowRealm? + linkIntrinsics(env, globalObject); + + env.link('globalThis'); + // Set globalThis.__proto__ in the sandbox to a proxy of + // globalObject.__proto__ and with this, the entire + // structure around window proto chain should be covered. + env.remapProto(globalObject, blueRefs.WindowProto); + + let unsafeBlueDescMap: PropertyDescriptorMap = defaultGlobalPropertyDescriptorMap!; + if (globalObject !== window) { + unsafeBlueDescMap = { __proto__: null } as unknown as PropertyDescriptorMap; + assignFilteredGlobalDescriptorsFromPropertyDescriptorMap( + unsafeBlueDescMap, + ObjectGetOwnPropertyDescriptors(globalObject) + ); + for (const key in unsafeBlueDescMap) { + if (!(key in defaultGlobalOwnKeysRegistry)) { + delete unsafeBlueDescMap[key]; + } + } + } + env.remapProperties(blueRefs.window, unsafeBlueDescMap); + if (endowments) { + const filteredEndowments: PropertyDescriptorMap = {}; + assignFilteredGlobalDescriptorsFromPropertyDescriptorMap(filteredEndowments, endowments); + removeWindowDescriptors(filteredEndowments); + env.remapProperties(blueRefs.window, filteredEndowments); + } + // We remap `blueRefs.WindowPropertiesProto` to an empty object because it + // is "magical" in that it provides access to elements by id. + env.remapProto(blueRefs.WindowProto, ObjectCreate(blueRefs.EventTargetProto)); + + return env; +} + +export default function ( + globalObject: WindowProxy & typeof globalThis, + options?: BrowserEnvironmentOptions +) { + const { + useShadowRealm = false, + // eslint-disable-next-line prefer-object-spread + } = ObjectAssign({ __proto__: null }, options); + if (useShadowRealm && SUPPORTS_SHADOW_REALM) { + return createShadowRealmVirtualEnvironment(globalObject, options); + } + return createIframeVirtualEnvironment(globalObject, options); +} diff --git a/packages/near-membrane-dom/src/types.ts b/packages/near-membrane-dom/src/types.ts index 5528b03f..f8ba5d11 100644 --- a/packages/near-membrane-dom/src/types.ts +++ b/packages/near-membrane-dom/src/types.ts @@ -13,4 +13,5 @@ export interface BrowserEnvironmentOptions { keepAlive?: boolean; liveTargetCallback?: LiveTargetCallback; signSourceCallback?: SignSourceCallback; + useShadowRealm?: boolean; } 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..e532dd6a 100644 --- a/packages/near-membrane-shared/src/constants.ts +++ b/packages/near-membrane-shared/src/constants.ts @@ -1,17 +1,34 @@ +import { SymbolFor } from './Symbol'; + +// Character constants. +export const CHAR_ELLIPSIS = '\u2026'; + +// Locker build constants. export const LOCKER_IDENTIFIER_MARKER = '$LWS'; + +// Near-membrane constants. +export const LOCKER_NEAR_MEMBRANE_SERIALIZED_VALUE_SYMBOL = SymbolFor( + '@@lockerNearMembraneSerializedValue' +); +export const LOCKER_NEAR_MEMBRANE_SYMBOL = SymbolFor('@@lockerNearMembrane'); + // This package is bundled by third-parties that have their own build time // replacement logic. Instead of customizing each build system to be aware // of this package we implement a two phase debug mode by performing small // 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'); -export const CHAR_ELLIPSIS = '\u2026'; + +// @ts-ignore: Prevent cannot find name 'ShadowRealm' error. +export const SUPPORTS_SHADOW_REALM = typeof ShadowRealm === 'function'; + +// 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/.eslintrc b/test/.eslintrc index 2bd03277..b0666293 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -1,6 +1,7 @@ { "globals": { - "jasmine": "readonly" + "createVirtualEnvironment": true, + "jasmine": "readonly" }, "ignorePatterns": [".eslintrc.js"], "overrides": [ diff --git a/test/__bootstrap__/create-virtual-environment.js b/test/__bootstrap__/create-virtual-environment.js new file mode 100644 index 00000000..d34024f2 --- /dev/null +++ b/test/__bootstrap__/create-virtual-environment.js @@ -0,0 +1,13 @@ +import { SUPPORTS_SHADOW_REALM } from '@locker/near-membrane-shared'; +import createVirtualEnvironment from '@locker/near-membrane-dom'; + +const useShadowRealm = process.env.USE_SHADOW_REALM; + +globalThis.createVirtualEnvironment = function (globalObject, options = {}) { + if (SUPPORTS_SHADOW_REALM && !('useShadowRealm' in options) && useShadowRealm) { + options.useShadowRealm = useShadowRealm; + } + console.log(options); + + return createVirtualEnvironment(globalObject, options); +}; diff --git a/test/distortions/getter.spec.js b/test/distortions/getter.spec.js index 2ebc0658..b7600380 100644 --- a/test/distortions/getter.spec.js +++ b/test/distortions/getter.spec.js @@ -1,5 +1,3 @@ -import createVirtualEnvironment from '@locker/near-membrane-dom'; - // getting reference to the function to be distorted const { get: hostGetter } = Object.getOwnPropertyDescriptor(ShadowRoot.prototype, 'host'); const { get: localStorageGetter } = Object.getOwnPropertyDescriptor(window, 'localStorage'); @@ -36,7 +34,7 @@ describe('Getter Function Distortion', () => { expect(hostGetter.call(elm)).toBe(null); `); }); - it('should work for global property accessors (issue #64)', () => { + fit('should work for global property accessors (issue #64)', () => { expect.assertions(1); env.evaluate(` diff --git a/test/dom/create-virtual-environment.spec.js b/test/dom/create-virtual-environment.spec.js index 63f6da78..fe122821 100644 --- a/test/dom/create-virtual-environment.spec.js +++ b/test/dom/create-virtual-environment.spec.js @@ -23,17 +23,27 @@ describe('createVirtualEnvironment', () => { const env = createVirtualEnvironment(window, {}); expect(() => env.evaluate('')).not.toThrow(); }); - it('options object has endowments, but is undefined', () => { - let endowments; - const env = createVirtualEnvironment(window, { endowments }); - expect(() => env.evaluate('')).not.toThrow(); - }); - it('options object has endowments, but is empty', () => { - const env = createVirtualEnvironment(window, { - endowments: {}, + for (const useShadowRealm of [true, false]) { + fit(`options object has endowments, but is undefined${ + useShadowRealm ? ' (Using ShadowRealm)' : '' + }`, () => { + let endowments; + const env = createVirtualEnvironment(window, { + endowments, + useShadowRealm, + }); + expect(() => env.evaluate('')).not.toThrow(); }); - expect(() => env.evaluate('')).not.toThrow(); - }); + it(`options object has endowments, but is empty${ + useShadowRealm ? ' (Using ShadowRealm)' : '' + }`, () => { + const env = createVirtualEnvironment(window, { + endowments: {}, + useShadowRealm, + }); + expect(() => env.evaluate('')).not.toThrow(); + }); + } }); describe('options.distortionCallback', () => { 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/environment/shadowrealm.spec.js b/test/environment/shadowrealm.spec.js new file mode 100644 index 00000000..2cc2a626 --- /dev/null +++ b/test/environment/shadowrealm.spec.js @@ -0,0 +1,32 @@ +import { SUPPORTS_SHADOW_REALM } from '@locker/near-membrane-shared'; +import createVirtualEnvironment from '@locker/near-membrane-dom'; + +if (SUPPORTS_SHADOW_REALM) { + const useShadowRealm = true; + const endowments = { a: 1 }; + // To run this test file: + // + // (First run) + // 1. Open the latest version of Firefox and type 'about:config' in the address bar. + // 2. Click "Accept risk and continue" + // 3. Type "shadow" into the search bar + // 4. On the line containing "javascript.options.experimental.shadow_realms", click the "toggle" button. + // 5. Restart Firefox if prompted to do so. + // + // Move to your terminal and run `yarn build` or `yarn build:dev` + // + // (Subsequent runs) + // 1. In the terminal, type: + // npx karma start karma.config.js --browsers Firefox --match "test/environment/shadowrealm.spec.js" + // + describe('ShadowRealm', () => { + it('can create a virtual environment backed by a ShadowRealm', () => { + expect(() => { + createVirtualEnvironment(window, { + endowments, + useShadowRealm, + }); + }).not.toThrow(); + }); + }); +} 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`. diff --git a/yarn.lock b/yarn.lock index e25949d2..7a440ed4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1954,6 +1954,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== +"@jridgewell/sourcemap-codec@^1.4.13": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -2964,13 +2969,13 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-replace@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-4.0.0.tgz#e34c457d6a285f0213359740b43f39d969b38a67" - integrity sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g== +"@rollup/plugin-replace@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz#45f53501b16311feded2485e98419acb8448c61d" + integrity sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA== dependencies: - "@rollup/pluginutils" "^3.1.0" - magic-string "^0.25.7" + "@rollup/pluginutils" "^5.0.1" + magic-string "^0.27.0" "@rollup/plugin-typescript@8.4.0": version "8.4.0" @@ -2997,6 +3002,15 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" + integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + "@sinclair/typebox@^0.24.1": version "0.24.28" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.28.tgz#15aa0b416f82c268b1573ab653e4413c965fe794" @@ -3124,6 +3138,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -4226,6 +4245,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -5263,7 +5291,7 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== -estree-walker@^2.0.1: +estree-walker@^2.0.1, estree-walker@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== @@ -7679,12 +7707,12 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.0.tgz#21be64954a4680e303a09e9468f880b98a0b3c7f" integrity sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ== -magic-string@^0.25.7: - version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" - integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== +magic-string@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== dependencies: - sourcemap-codec "^1.4.8" + "@jridgewell/sourcemap-codec" "^1.4.13" make-dir@^2.1.0: version "2.1.0" @@ -9775,11 +9803,6 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" -sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - spawn-wrap@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e" @@ -10926,7 +10949,7 @@ yargs-parser@^20.2.9: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0: +yargs-parser@^21.0.0, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -10987,6 +11010,19 @@ yargs@^17.3.1, yargs@^17.4.0: y18n "^5.0.5" yargs-parser "^21.0.0" +yargs@^17.6.2: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yargs@^4.8.1: version "4.8.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0"