Skip to content

Commit

Permalink
fix: update near-membrane-dom to use ShadowRealm, fallback to iframe
Browse files Browse the repository at this point in the history
feat: my attempt

fix: ensure that globalObjectShape is used in both paths when provided

refactor: split logic for better readability

refactor: support linkage of window.__proto__ chain in shadow realm

chore: revert debug

fix: restore globalShapeObject and globalObjectVirtualizationTarget behavior in createShadowRealmVirtualEnvironment
  • Loading branch information
rwaldron committed May 23, 2022
1 parent 0077472 commit efc13dc
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 10 deletions.
12 changes: 6 additions & 6 deletions packages/near-membrane-base/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,23 @@ export class VirtualEnvironment {

private readonly blueGetSelectedTarget: GetSelectedTarget;

private readonly blueGetTransferableValue: GetTransferableValue;
private blueGetTransferableValue: GetTransferableValue;

private readonly blueGlobalThisPointer: Pointer;
private blueGlobalThisPointer: Pointer;

private readonly redCallableEvaluate: CallableEvaluate;

private readonly redCallableGetPropertyValuePointer: CallableGetPropertyValuePointer;

private readonly redCallableLinkPointers: CallableLinkPointers;

private readonly redCallableSetPrototypeOf: CallableSetPrototypeOf;
private redCallableSetPrototypeOf: CallableSetPrototypeOf;

private readonly redCallableDefineProperties: CallableDefineProperties;
private redCallableDefineProperties: CallableDefineProperties;

private readonly redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors;
private redCallableInstallLazyPropertyDescriptors: CallableInstallLazyPropertyDescriptors;

private readonly redGlobalThisPointer: Pointer;
private redGlobalThisPointer: Pointer;

constructor(options: VirtualEnvironmentOptions) {
if (options === undefined) {
Expand Down
3 changes: 3 additions & 0 deletions packages/near-membrane-base/src/intrinsics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const { includes: ArrayProtoIncludes } = Array.prototype;
const { assign: ObjectAssign } = Object;
const { apply: ReflectApply, ownKeys: ReflectOwnKeys } = Reflect;

// @ts-ignore: Prevent cannot find name 'ShadowRealm' error.
export const SUPPORTS_SHADOW_REALM = typeof ShadowRealm === 'function';

/**
* This list must be in sync with ecma-262, anything new added to the global object
* should be considered, to decide whether or not they need remapping. The default
Expand Down
115 changes: 113 additions & 2 deletions packages/near-membrane-dom/src/browser-realm.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
assignFilteredGlobalDescriptorsFromPropertyDescriptorMap,
CallableEvaluate,
createBlueConnector,
createRedConnector,
getFilteredGlobalOwnKeys,
linkIntrinsics,
DistortionCallback,
Instrumentation,
PropertyKeys,
SUPPORTS_SHADOW_REALM,
VirtualEnvironment,
} from '@locker/near-membrane-base';

Expand All @@ -30,10 +32,15 @@ const IFRAME_SANDBOX_ATTRIBUTE_VALUE = 'allow-same-origin allow-scripts';
const ObjectCtor = Object;
const TypeErrorCtor = TypeError;
const { prototype: DocumentProto } = Document;
const { bind: FunctionProtoBind } = Function.prototype;
const { prototype: NodeProto } = Node;
const { remove: ElementProtoRemove, setAttribute: ElementProtoSetAttribute } = Element.prototype;
const { appendChild: NodeProtoAppendChild } = NodeProto;
const { assign: ObjectAssign } = ObjectCtor;
const {
assign: ObjectAssign,
create: ObjectCreate,
getOwnPropertyDescriptors: ObjectGetOwnPropertyDescriptors,
} = ObjectCtor;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { __lookupGetter__: ObjectProto__lookupGetter__ } = ObjectCtor.prototype as any;
const { apply: ReflectApply } = Reflect;
Expand All @@ -56,9 +63,14 @@ const HTMLIFrameElementProtoContentWindowGetter = ReflectApply(
const NodeProtoLastChildGetter = ReflectApply(ObjectProto__lookupGetter__, NodeProto, [
'lastChild',
])!;
// @ts-ignore: Prevent cannot find name 'ShadowRealm' error.
const ShadowRealmCtor = SUPPORTS_SHADOW_REALM ? ShadowRealm : undefined;
const ShadowRealmProtoEvaluate: CallableEvaluate | undefined = ShadowRealmCtor?.prototype?.evaluate;
const defaultGlobalOwnKeysRegistry = { __proto__: null };
const docRef = document;

let defaultGlobalOwnKeys: PropertyKeys | null = null;
let defaultGlobalPropertyDescriptorMap: PropertyDescriptorMap | null = null;

function createDetachableIframe(): HTMLIFrameElement {
const iframe = ReflectApply(DocumentProtoCreateElement, docRef, [
Expand Down Expand Up @@ -163,4 +175,103 @@ function createIframeVirtualEnvironment(
return env;
}

export default createIframeVirtualEnvironment;
function createShadowRealmVirtualEnvironment(
globalObject: WindowProxy & typeof globalThis,
globalObjectShape: object | null,
providedOptions?: BrowserEnvironmentOptions
): VirtualEnvironment {
if (typeof globalObject !== 'object' || globalObject === null) {
throw new TypeErrorCtor('Missing global object virtualization target.');
}
const {
distortionCallback,
endowments,
instrumentation,
// eslint-disable-next-line prefer-object-spread
} = ObjectAssign({ __proto__: null }, providedOptions);

// If a globalObjectShape has been explicitly specified, reset the
// defaultGlobalPropertyDescriptorMap to null. This will ensure that
// the provided globalObjectShape is used to re-create the cached
// defaultGlobalPropertyDescriptorMap.
if (globalObjectShape !== null) {
defaultGlobalPropertyDescriptorMap = null;
}
if (defaultGlobalPropertyDescriptorMap === null) {
let sourceShapeOrOneTimeWindow = globalObjectShape!;
let sourceIsIframe = false;
if (globalObjectShape === null) {
const oneTimeIframe = createDetachableIframe();
sourceShapeOrOneTimeWindow = ReflectApply(
HTMLIFrameElementProtoContentWindowGetter,
oneTimeIframe,
[]
)!;
sourceIsIframe = true;
}
defaultGlobalOwnKeys = getFilteredGlobalOwnKeys(sourceShapeOrOneTimeWindow);
if (sourceIsIframe) {
ReflectApply(ElementProtoRemove, sourceShapeOrOneTimeWindow, []);
}
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];
}
}
}
const blueRefs = getCachedGlobalObjectReferences(globalObject);
// Create a new environment.
const env = new VirtualEnvironment({
blueConnector: createBlueConnector(globalObject),
distortionCallback,
instrumentation,
redConnector: createRedConnector(
ReflectApply(FunctionProtoBind, ShadowRealmProtoEvaluate, [new ShadowRealmCtor()])
),
});
linkIntrinsics(env, globalObject);
// window
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 SUPPORTS_SHADOW_REALM
? createShadowRealmVirtualEnvironment
: createIframeVirtualEnvironment;
8 changes: 6 additions & 2 deletions test/environment/createvirtualenvironment.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SUPPORTS_SHADOW_REALM } from '@locker/near-membrane-base';
import createVirtualEnvironment from '@locker/near-membrane-dom';

describe('createVirtualEnvironment', () => {
Expand Down Expand Up @@ -29,8 +30,11 @@ describe('createVirtualEnvironment', () => {
});
it('options object has keepAlive: true', () => {
const count = window.frames.length;
const env = createVirtualEnvironment(window, { keepAlive: true });
expect(window.frames.length).toBe(count + 1);
const env = createVirtualEnvironment(window, {
globalObjectShape: window,
keepAlive: true,
});
expect(window.frames.length).toBe(SUPPORTS_SHADOW_REALM ? count : count + 1);
expect(() => env.evaluate('')).not.toThrow();
});
it('options object has keepAlive: false', () => {
Expand Down

0 comments on commit efc13dc

Please sign in to comment.