Skip to content

Commit

Permalink
feat: @W-16299313 PoC iframe keepAlive
Browse files Browse the repository at this point in the history
  • Loading branch information
rwaldron committed Aug 5, 2024
1 parent 565cf51 commit dcbe2db
Show file tree
Hide file tree
Showing 12 changed files with 58 additions and 211 deletions.
13 changes: 1 addition & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,16 @@ Since you can have multiple sandboxes associated to the Blue Realm, there is a p

In browsers, since we don't have a way to create a light-weight Realm that is synchronously accessible (that will be solved in part by the [stage 3 ShadowRealms Proposal](https://github.com/tc39/proposal-shadowrealm)), we are forced to use a same-domain iframe in order to isolate the code to be evaluated inside a sandbox for a particular window.

#### Detached iframes

Since the iframes have many ways to reach out to the opener/top window reference, we are forced to use a detached `iframe`, which is, on itself, a complication. A detached `iframe`'s window is a window that does not have any host behavior associated to it, in other words, this window does not have an origin after disconnecting the iframe, which means it can't execute any DOM API without throwing an error. Luckily for us, the JavaScript intrinsics, and all JavaScript language features specified by ECMA262 and ECMA402 are still alive and kicking in that iframe, except for one feature, dynamic imports in a form of `import(specifier)`.

To mitigate the issue with dynamic imports, we are forced to transpile the code that attempts to use this feature of the language, otherwise it will just fail to fetch the module because there is no origin available at the host level. However, transpiling dynamic imports is a very common way to bundle code for production systems today.

#### Unforgeables

The `window` reference in the detached iframe, just like any other `window` reference in browsers, contains various unforgeable descriptors, these are descriptors installed in Window, and other globals that are non-configurable, and therefor this library cannot remove them or replace them with a Red Proxy. Must notable, we have the window's prototype chain that is completely unforgeable:
The `window` reference in the iframe, just like any other `window` reference in browsers, contains various unforgeable descriptors, these are descriptors installed in Window, and other globals that are non-configurable, and therefor this library cannot remove them or replace them with a Red Proxy. Must notable, we have the window's prototype chain that is completely unforgeable:

```
window -> Window.prototype -> WindowProperties.prototype -> EventTarget.prototype
```

What we do in this case is to keep the identity of those unforgeable around, but changing the descriptors installing on them, and any other method that expects these identities to be passed to them. This make them effectively harmless because they don't give any power.

Additionally, there are other unforgeables like `location` that are host bounded, in that case, we don't have to do much since the detaching mechanism will automatically invalidate them.

These can only be virtualized via transpilation if they need to be available inside the sandbox. Such transpilation process is not provided as part of this library.

#### Requirements
Expand Down Expand Up @@ -93,13 +85,10 @@ We do not know the applications of this library just yet, but we suspect that th

* Debugging is still very challenging considering that dev-tools are still catching up with the Proxies. Chrome for example has differences displaying proxies in the console vs the watch panel.

Additionally, there is an existing bug in ChromeDev-tools that prevent a detached iframe to be debugged (https://bugs.chromium.org/p/chromium/issues/detail?id=1015462).

### WindowProxy

The `window` reference in the iframe, just like any other `window` reference in browsers, exhibit a bizarre behavior, the `WindowProxy` behavior. This has two big implications for this implementation when attempting to give access to other window references coming from same domain iframes (e.g.: sandboxing the main app + one iframe):

* each window will require a new detached iframe to sandbox each of them, but if the iframe navigates to another page, the window reference remains the same, but the internal of the non-observable real window are changing. Otherwise distortions defined for the sandbox will not apply to the identity of the methods from the same-domain iframe.
* GCing the sandbox when the iframe navigates out is tricky due to the fact that the original iframe's window reference remains the same, and it is used by few of the internal maps.

For those reasons, we do not support accessing other realm instances from within the sandbox at the moment.
Expand Down
2 changes: 0 additions & 2 deletions karma.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ const globby = require('globby');
const istanbul = require('rollup-plugin-istanbul');
const { nodeResolve } = require('@rollup/plugin-node-resolve');

process.env.CHROME_BIN = require('puppeteer').executablePath();

let testFilesPattern = './test/**/*.spec.js';

const basePath = path.resolve(__dirname, './');
Expand Down
12 changes: 1 addition & 11 deletions packages/near-membrane-base/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,22 +305,12 @@ export class VirtualEnvironment {
}
}

lazyRemapProperties(
target: ProxyTarget,
ownKeys: PropertyKey[],
unforgeableGlobalThisKeys?: PropertyKey[]
) {
lazyRemapProperties(target: ProxyTarget, ownKeys: PropertyKey[]) {
if ((typeof target === 'object' && target !== null) || typeof target === 'function') {
const args: Parameters<CallableInstallLazyPropertyDescriptors> = [
this.blueGetTransferableValue(target) as Pointer,
];
ReflectApply(ArrayProtoPush, args, ownKeys);
if (unforgeableGlobalThisKeys?.length) {
// Use `LOCKER_NEAR_MEMBRANE_UNDEFINED_VALUE_SYMBOL` to delimit
// `ownKeys` and `unforgeableGlobalThisKeys`.
args[args.length] = LOCKER_NEAR_MEMBRANE_UNDEFINED_VALUE_SYMBOL;
ReflectApply(ArrayProtoPush, args, unforgeableGlobalThisKeys);
}
ReflectApply(this.redCallableInstallLazyPropertyDescriptors, undefined, args);
}
}
Expand Down
64 changes: 28 additions & 36 deletions packages/near-membrane-dom/src/browser-realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ import {
DocumentProtoClose,
DocumentProtoCreateElement,
DocumentProtoOpen,
ElementProtoRemove,
ElementProtoAttachShadow,
ElementProtoSetAttribute,
HTMLElementProtoStyleGetter,
HTMLIFrameElementProtoContentWindowGetter,
IS_OLD_CHROMIUM_BROWSER,
NodeProtoAppendChild,
NodeProtoLastChildGetter,
} from '@locker/near-membrane-shared-dom';
Expand All @@ -37,25 +36,36 @@ import {
getCachedGlobalObjectReferences,
filterWindowKeys,
removeWindowDescriptors,
unforgeablePoisonedWindowKeys,
} from './window';

const IFRAME_SANDBOX_ATTRIBUTE_VALUE = 'allow-same-origin allow-scripts';

const revoked = toSafeWeakSet(new WeakSetCtor<GlobalObject | Node>());
const blueCreateHooksCallbackCache = toSafeWeakMap(new WeakMapCtor<Document, Connector>());

function createDetachableIframe(doc: Document): HTMLIFrameElement {
const iframe = ReflectApply(DocumentProtoCreateElement, doc, ['iframe']) as HTMLIFrameElement;
let iframeStash: ShadowRoot;

function createShadowHiddenIframe(doc: Document): HTMLIFrameElement {
// It is impossible to test whether the NodeProtoLastChildGetter branch is
// reached in a normal Karma test environment.
const parent: Element =
ReflectApply(DocumentProtoBodyGetter, doc, []) ??
/* istanbul ignore next */ ReflectApply(NodeProtoLastChildGetter, doc, []);

if (!iframeStash) {
const host = ReflectApply(DocumentProtoCreateElement, doc, ['div']) as HTMLDivElement;

iframeStash = ReflectApply(ElementProtoAttachShadow, host, [
{ mode: 'closed' },
]) as ShadowRoot;

ReflectApply(NodeProtoAppendChild, parent, [host]);
}
const iframe = ReflectApply(DocumentProtoCreateElement, doc, ['iframe']) as HTMLIFrameElement;
const style: CSSStyleDeclaration = ReflectApply(HTMLElementProtoStyleGetter, iframe, []);
style.display = 'none';
ReflectApply(ElementProtoSetAttribute, iframe, ['sandbox', IFRAME_SANDBOX_ATTRIBUTE_VALUE]);
ReflectApply(NodeProtoAppendChild, parent, [iframe]);
ReflectApply(NodeProtoAppendChild, iframeStash, [iframe]);
return iframe;
}

Expand All @@ -76,13 +86,12 @@ function createIframeVirtualEnvironment(
endowments,
globalObjectShape,
instrumentation,
keepAlive = true,
liveTargetCallback,
maxPerfMode = false,
signSourceCallback,
// eslint-disable-next-line prefer-object-spread
} = ObjectAssign({ __proto__: null }, providedOptions) as BrowserEnvironmentOptions;
const iframe = createDetachableIframe(blueRefs.document);
const iframe = createShadowHiddenIframe(blueRefs.document);
const redWindow: GlobalObject = ReflectApply(
HTMLIFrameElementProtoContentWindowGetter,
iframe,
Expand Down Expand Up @@ -115,7 +124,7 @@ function createIframeVirtualEnvironment(
distortionCallback,
instrumentation,
liveTargetCallback,
revokedProxyCallback: keepAlive ? revokedProxyCallback : undefined,
revokedProxyCallback,
signSourceCallback,
});
linkIntrinsics(env, globalObject);
Expand All @@ -139,12 +148,7 @@ function createIframeVirtualEnvironment(
blueRefs.window,
shouldUseDefaultGlobalOwnKeys
? (defaultGlobalOwnKeys as PropertyKey[])
: filterWindowKeys(getFilteredGlobalOwnKeys(globalObjectShape, maxPerfMode)),
// Chromium based browsers have a bug that nulls the result of `window`
// getters in detached iframes when the property descriptor of `window.window`
// is retrieved.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1305302
keepAlive ? undefined : unforgeablePoisonedWindowKeys
: filterWindowKeys(getFilteredGlobalOwnKeys(globalObjectShape, maxPerfMode))
);
if (endowments) {
const filteredEndowments: PropertyDescriptorMap = {};
Expand All @@ -161,27 +165,15 @@ function createIframeVirtualEnvironment(
env.lazyRemapProperties(blueRefs.EventTargetProto, blueRefs.EventTargetProtoOwnKeys);
// We don't remap `blueRefs.WindowPropertiesProto` because it is "magical"
// in that it provides access to elements by id.
//
// Once we get the iframe info ready, and all mapped, we can proceed to
// detach the iframe only if `options.keepAlive` isn't true.
if (keepAlive) {
// @TODO: Temporary hack to preserve the document reference in Firefox.
// https://bugzilla.mozilla.org/show_bug.cgi?id=543435
const { document: redDocument } = redWindow;
// Revoke the proxies of the redDocument and redWindow to prevent access.
revoked.add(redDocument);
revoked.add(redWindow);
ReflectApply(DocumentProtoOpen, redDocument, []);
ReflectApply(DocumentProtoClose, redDocument, []);
} else {
if (IS_OLD_CHROMIUM_BROWSER) {
// For Chromium < v86 browsers we evaluate the `window` object to
// kickstart the realm so that `window` persists when the iframe is
// removed from the document.
redIndirectEval('window');
}
ReflectApply(ElementProtoRemove, iframe, []);
}

// @TODO: Temporary hack to preserve the document reference in Firefox.
// https://bugzilla.mozilla.org/show_bug.cgi?id=543435 (reviewed 2024-08-02, still reproduces)
const { document: redDocument } = redWindow;
// Revoke the proxies of the redDocument and redWindow to prevent access.
revoked.add(redDocument);
revoked.add(redWindow);
ReflectApply(DocumentProtoOpen, redDocument, []);
ReflectApply(DocumentProtoClose, redDocument, []);
return env;
}

Expand Down
1 change: 0 additions & 1 deletion packages/near-membrane-dom/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export interface BrowserEnvironmentOptions {
endowments?: PropertyDescriptorMap;
globalObjectShape?: object;
instrumentation?: Instrumentation;
keepAlive?: boolean;
liveTargetCallback?: LiveTargetCallback;
maxPerfMode?: boolean;
signSourceCallback?: SignSourceCallback;
Expand Down
8 changes: 1 addition & 7 deletions packages/near-membrane-dom/src/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SetCtor,
SetProtoHas,
} from '@locker/near-membrane-shared';
import { IS_CHROMIUM_BROWSER, rootWindow } from '@locker/near-membrane-shared-dom';
import { rootWindow } from '@locker/near-membrane-shared-dom';

interface CachedBlueReferencesRecord extends Object {
document: Document;
Expand All @@ -23,12 +23,6 @@ const blueDocumentToRecordMap: WeakMap<Document, CachedBlueReferencesRecord> = t
new WeakMap()
);

// Chromium based browsers have a bug that nulls the result of `window`
// getters in detached iframes when the property descriptor of `window.window`
// is retrieved.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1305302
export const unforgeablePoisonedWindowKeys = IS_CHROMIUM_BROWSER ? ['window'] : undefined;

export function getCachedGlobalObjectReferences(
globalObject: WindowProxy & typeof globalThis
): CachedBlueReferencesRecord | undefined {
Expand Down
7 changes: 5 additions & 2 deletions packages/near-membrane-shared-dom/src/Element.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export const { remove: ElementProtoRemove, setAttribute: ElementProtoSetAttribute } =
Element.prototype;
export const {
attachShadow: ElementProtoAttachShadow,
remove: ElementProtoRemove,
setAttribute: ElementProtoSetAttribute,
} = Element.prototype;
54 changes: 0 additions & 54 deletions packages/near-membrane-shared-dom/src/constants.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/near-membrane-shared-dom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './constants';
export * from './Document';
export * from './DOMException';
export * from './Element';
Expand Down
Loading

0 comments on commit dcbe2db

Please sign in to comment.