feat(ses): Support global lexicals
As distinct from endowments, extra properties of `globalThis`, global lexicals are constant free variables that are not reachable by computing properties of `globalThis`.
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -2,7 +2,11 @@ User-visible changes in SES:
## Next release
-* No changes yet.
+* The Compartment constructor now accepts a `globalLexicals` option.
+ The own enumerable properties of the global lexicals are captured
+ and presented as constants in the scope of all calls to `evaluate` and all
+ modules.
+ The global lexicals overshadow the global object.
## Release 0.8.0 (26-May-2020)

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -13,13 +13,22 @@ import * as babel from '@agoric/babel-standalone';
// Both produce:
// Error: 'default' is not exported by .../@agoric/babel-standalone/babel.js
import { makeModuleAnalyzer } from '@agoric/transform-module';
-import { assign, entries } from './commons.js';
+import {
+ assign,
+ create,
+ defineProperties,
+ entries,
+ getOwnPropertyNames,
+ getOwnPropertyDescriptors,
+ freeze,
+} from './commons.js';
import { createGlobalObject } from './global-object.js';
import { performEval } from './evaluate.js';
import { getCurrentRealmRec } from './realm-rec.js';
import { load } from './module-load.js';
import { link } from './module-link.js';
import { getDeferredExports } from './module-proxy.js';
+import { isValidIdentifierName } from './scope-constants.js';
// q, for quoting strings.
const q = JSON.stringify;
@@ -42,8 +51,8 @@ export class StaticModuleRecord {
this.imports = Object.keys(analysis.imports).sort();
- Object.freeze(this);
- Object.freeze(this.imports);
+ freeze(this);
+ freeze(this.imports);
moduleAnalyses.set(this, analysis);
}
@@ -90,7 +99,12 @@ const assertModuleHooks = compartment => {
export class Compartment {
constructor(endowments = {}, modules = {}, options = {}) {
// Extract options, and shallow-clone transforms.
- const { transforms = [], resolveHook, importHook } = options;
+ const {
+ transforms = [],
+ globalLexicals = {},
+ resolveHook,
+ importHook,
+ } = options;
const globalTransforms = [...transforms];
const realmRec = getCurrentRealmRec();
@@ -132,6 +146,17 @@ export class Compartment {
}
}
+ const invalidNames = getOwnPropertyNames(globalLexicals).filter(
+ name => !isValidIdentifierName(name),
+ );
+ if (invalidNames.length) {
+ throw new Error(
+ `Cannot create compartment with invalid names for global lexicals: ${invalidNames.join(
+ ', ',
+ )}; these names would not be lexically mentionable`,
+ );
+ }
+
privateFields.set(this, {
resolveHook,
importHook,
@@ -141,6 +166,15 @@ export class Compartment {
instances,
globalTransforms,
globalObject,
+ // The caller continues to own the globalLexicals object they passed to
+ // the compartment constructor, but the compartment only respects the
+ // original values and they are constants in the scope of evaluated
+ // programs and executed modules.
+ // This shallow copy captures only the values of enumerable own
+ // properties, erasing accessors.
+ // The snapshot is frozen to ensure that the properties are immutable
+ // when transferred-by-property-descriptor onto local scope objects.
+ globalLexicals: freeze({ ...globalLexicals }),
});
}
@@ -170,9 +204,20 @@ export class Compartment {
} = options;
const localTransforms = [...transforms];
- const { globalTransforms, globalObject } = privateFields.get(this);
+ const {
+ globalTransforms,
+ globalObject,
+ globalLexicals,
+ } = privateFields.get(this);
const realmRec = getCurrentRealmRec();
- return performEval(realmRec, source, globalObject, endowments, {
+
+ // TODO just pass globalLexicals as localObject
+ // https://github.com/Agoric/SES-shim/issues/365
+ const localObject = create(null);
+ defineProperties(localObject, getOwnPropertyDescriptors(globalLexicals));
+ defineProperties(localObject, getOwnPropertyDescriptors(endowments));
+
+ return performEval(realmRec, source, globalObject, localObject, {
globalTransforms,
localTransforms,
sloppyGlobalsMode,

MODIFY
packages/ses/src/evaluate.js
packages/ses/src/evaluate.js
@@ -17,7 +17,7 @@ export function performEval(
realmRec,
source,
globalObject,
- endowments = {},
+ localObject = {},
{
localTransforms = [],
globalTransforms = [],
@@ -32,13 +32,13 @@ export function performEval(
mandatoryTransforms,
]);
- const scopeHandler = createScopeHandler(realmRec, globalObject, endowments, {
+ const scopeHandler = createScopeHandler(realmRec, globalObject, localObject, {
sloppyGlobalsMode,
});
const scopeProxyRevocable = proxyRevocable(immutableObject, scopeHandler);
// Ensure that "this" resolves to the scope proxy.
- const constants = getScopeConstants(globalObject, endowments);
+ const constants = getScopeConstants(globalObject, localObject);
const evaluateFactory = makeEvaluateFactory(realmRec, constants);
const evaluate = apply(evaluateFactory, scopeProxyRevocable.proxy, []);

MODIFY
packages/ses/src/make-eval-function.js
packages/ses/src/make-eval-function.js
@@ -4,7 +4,7 @@ import { performEval } from './evaluate.js';
/**
* makeEvalFunction()
* A safe version of the native eval function which relies on
- * the safety of performEvaluate for confinement.
+ * the safety of performEval for confinement.
*/
export const makeEvalFunction = (realmRec, globalObject, options = {}) => {
// We use the the concise method syntax to create an eval without a

MODIFY
packages/ses/src/make-function-constructor.js
packages/ses/src/make-function-constructor.js
@@ -10,7 +10,7 @@ import { performEval } from './evaluate.js';
/**
* makeFunctionConstructor()
* A safe version of the native Function which relies on
- * the safety of performEvaluate for confinement.
+ * the safety of performEval for confinement.
*/
export function makeFunctionConstructor(realmRec, globaObject, options = {}) {
// Define an unused parameter to ensure Function.length === 1

MODIFY
packages/ses/src/module-instance.js
packages/ses/src/module-instance.js
import { performEval } from './evaluate.js';
import { getCurrentRealmRec } from './realm-rec.js';
import { getDeferredExports } from './module-proxy.js';
-import { create, entries, keys, freeze, defineProperty } from './commons.js';
+import {
+ create,
+ getOwnPropertyDescriptors,
+ entries,
+ keys,
+ freeze,
+ defineProperty,
+} from './commons.js';
// q, for enquoting strings in error messages.
const q = JSON.stringify;
@@ -29,9 +36,13 @@ export const makeModuleInstance = (
exportAlls,
} = moduleRecord;
+ const compartmentFields = privateFields.get(compartment);
+
+ const { globalLexicals } = compartmentFields;
+
const { exportsProxy, proxiedExports, activate } = getDeferredExports(
compartment,
- privateFields.get(compartment),
+ compartmentFields,
moduleAliases,
moduleSpecifier,
);
@@ -40,8 +51,10 @@ export const makeModuleInstance = (
// object (eventually proxied).
const exportsProps = create(null);
- // {_localName_: accessor} added to endowments for proxy traps
- const trappers = create(null);
+ // {_localName_: accessor} proxy traps for globalLexicals and live bindings.
+ // The globalLexicals object is frozen and the corresponding properties of
+ // localObject must be immutable, so we copy the descriptors.
+ const localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
// {_localName_: init(initValue) -> initValue} used by the
// rewritten code to initialize exported fixed bindings.
@@ -198,7 +211,7 @@ export const makeModuleInstance = (
localGetNotify[localName] = liveGetNotify;
if (setProxyTrap) {
- defineProperty(trappers, localName, {
+ defineProperty(localObject, localName, {
get,
set,
enumerable: true,
@@ -305,7 +318,7 @@ export const makeModuleInstance = (
realmRec,
functorSource,
globalObject,
- trappers, // "endowments" for live bindings.
+ localObject, // live bindings over global lexicals
{
localTransforms: [],
globalTransforms: [],

MODIFY
packages/ses/src/scope-constants.js
packages/ses/src/scope-constants.js
@@ -95,7 +95,7 @@ const identifierPattern = new RegExp('^[a-zA-Z_$][\\w$]*$');
* service if any of the names are keywords or keyword-like. This is
* safe and only prevent performance optimization.
*/
-function isValidIdentifierName(name) {
+export function isValidIdentifierName(name) {
// Ensure we have a valid identifier. We use regexpTest rather than
// /../.test() to guard against the case where RegExp has been poisoned.
return (

MODIFY
packages/ses/src/scope-handler.js
packages/ses/src/scope-handler.js
@@ -22,7 +22,7 @@ const alwaysThrowHandler = new Proxy(immutableObject, {
/**
* createScopeHandler()
* ScopeHandler manages a Proxy which serves as the global scope for the
- * performEvaluate operation (the Proxy is the argument of a 'with' binding).
+ * performEval operation (the Proxy is the argument of a 'with' binding).
* As described in createSafeEvaluator(), it has several functions:
* - allow the very first (and only the very first) use of 'eval' to map to
* the real (unsafe) eval function, so it acts as a 'direct eval' and can
@@ -37,7 +37,7 @@ const alwaysThrowHandler = new Proxy(immutableObject, {
export function createScopeHandler(
realmRec,
globalObject,
- endowments = {},
+ localObject = {},
{ sloppyGlobalsMode = false } = {},
) {
return {
@@ -67,11 +67,21 @@ export function createScopeHandler(
// fall through
}
- // Properties of the global.
- if (prop in endowments) {
- // Use reflect to defeat accessors that could be
- // present on the endowments object itself as `this`.
- return reflectGet(endowments, prop, globalObject);
+ // Properties of the localObject.
+ if (prop in localObject) {
+ // Use reflect to defeat accessors that could be present on the
+ // localObject object itself as `this`.
+ // This is done out of an overabundance of caution, as the SES shim
+ // only use the localObject carry globalLexicals and live binding
+ // traps.
+ // The globalLexicals are captured as a snapshot of what's passed to
+ // the Compartment constructor, wherein all accessors and setters are
+ // eliminated and the result frozen.
+ // The live binding traps do use accessors, and none of those accessors
+ // make use of their receiver.
+ // Live binding traps provide no avenue for user code to observe the
+ // receiver.
+ return reflectGet(localObject, prop, globalObject);
}
// Properties of the global.
@@ -79,17 +89,17 @@ export function createScopeHandler(
},
set(shadow, prop, value) {
- // Properties of the endowments.
- if (prop in endowments) {
- const desc = getOwnPropertyDescriptor(endowments, prop);
+ // Properties of the localObject.
+ if (prop in localObject) {
+ const desc = getOwnPropertyDescriptor(localObject, prop);
if ('value' in desc) {
// Work around a peculiar behavior in the specs, where
// value properties are defined on the receiver.
- return reflectSet(endowments, prop, value);
+ return reflectSet(localObject, prop, value);
}
// Ensure that the 'this' value on setters resolves
- // to the safeGlobal, not to the endowments object.
- return reflectSet(endowments, prop, value, globalObject);
+ // to the safeGlobal, not to the localObject object.
+ return reflectSet(localObject, prop, value, globalObject);
}
// Properties of the global.
@@ -122,7 +132,7 @@ export function createScopeHandler(
if (
sloppyGlobalsMode ||
prop === 'eval' ||
- prop in endowments ||
+ prop in localObject ||
prop in globalObject ||
prop in globalThis
) {

ADD
packages/ses/test/global-lexicals-evaluate.test.js
+/* global Compartment */
+import test from 'tape';
+import '../src/main.js';
+
+test('endowments own properties are mentionable', t => {
+ t.plan(1);
+
+ const endowments = { hello: 'World!' };
+ const modules = {};
+ const compartment = new Compartment(endowments, modules);
+
+ const whom = compartment.evaluate('hello');
+ t.equal(whom, 'World!');
+});
+
+test('endowments own properties are enumerable', t => {
+ t.plan(1);
+
+ const endowments = { hello: 'World!' };
+ const modules = {};
+ const compartment = new Compartment(endowments, modules);
+
+ const keys = compartment.evaluate('Object.keys(globalThis)');
+ t.deepEqual(keys, ['hello']);
+});
+
+test('endowments prototypically inherited properties are not mentionable', t => {
+ t.plan(1);
+
+ const endowments = { __proto__: { hello: 'World!' } };
+ const modules = {};
+ const compartment = new Compartment(endowments, modules);
+
+ t.throws(() => compartment.evaluate('hello'), /hello is not defined/);
+});
+
+test('endowments prototypically inherited properties are not enumerable', t => {
+ t.plan(1);
+
+ const endowments = { __proto__: { hello: 'World!' } };
+ const modules = {};
+ const compartment = new Compartment(endowments, modules);
+
+ const keys = compartment.evaluate('Object.keys(globalThis)');
+ t.deepEqual(keys, []);
+});
+
+test('global lexicals are mentionable', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const whom = compartment.evaluate('hello');
+ t.equal(whom, 'World!');
+});
+
+test('global lexicals are not enumerable from global object', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const keys = compartment.evaluate('Object.keys(globalThis)');
+ t.deepEqual(keys, []);
+});
+
+test('global lexicals are not reachable from global object', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const notHello = compartment.evaluate('globalThis.hello');
+ t.equal(notHello, undefined);
+});
+
+test('global lexicals prototypically inherited properties are not mentionable', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { __proto__: { hello: 'World!' } };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ t.throws(() => compartment.evaluate('hello'), /hello is not defined/);
+});
+
+test('global lexicals prototypically inherited properties are not reachable from global object', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { __proto__: { hello: 'World!' } };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const notHello = compartment.evaluate('globalThis.hello');
+ t.equal(notHello, undefined);
+});
+
+test('global lexicals prototypically inherited properties are not enumerable', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { __proto__: { hello: 'World!' } };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const keys = compartment.evaluate('Object.keys(globalThis)');
+ t.deepEqual(keys, []);
+});
+
+test('global lexicals overshadow global object', t => {
+ t.plan(1);
+
+ const endowments = { hello: 'Your name here' };
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const whom = compartment.evaluate('hello');
+ t.equal(whom, 'World!');
+});
+
+test('global lexicals are constant', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ t.throws(
+ () => compartment.evaluate('hello = "Dave."'),
+ /Assignment to constant/,
+ );
+});
+
+test('global lexicals are captured on construction', t => {
+ t.plan(1);
+
+ const endowments = {};
+ const modules = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ // Psych!
+ globalLexicals.hello = 'Something else';
+
+ const whom = compartment.evaluate('hello');
+ t.equal(whom, 'World!');
+});
+
+test('global lexical accessors are sampled once up front', t => {
+ t.plan(2);
+
+ let counter = 0;
+ const globalLexicals = {
+ get next() {
+ const result = counter;
+ counter += 1;
+ return result;
+ },
+ };
+
+ const endowments = {};
+ const modules = {};
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ const zero = compartment.evaluate('next');
+ t.equal(zero, 0);
+ const stillZero = compartment.evaluate('next');
+ t.equal(stillZero, 0);
+});
+
+test('global lexical accessors receive globalThis', t => {
+ t.plan(1);
+
+ let receiver;
+ const globalLexicals = Object.create(null, {
+ hello: {
+ get() {
+ receiver = this;
+ },
+ enumerable: true,
+ configurable: true,
+ },
+ goodbye: {
+ get() {
+ throw new Error('Non-enumerable properties should not be captured');
+ },
+ enumerable: false,
+ configurable: true,
+ },
+ });
+
+ const endowments = {};
+ const modules = {};
+ // eslint-disable-next-line no-unused-vars
+ const compartment = new Compartment(endowments, modules, { globalLexicals });
+
+ // Capturing globalLexicals.hello is a side-effect of Compartment
+ // construction.
+ // Code run in the compartment does not need to access the global lexical for
+ // it to be captured.
+ t.equal(receiver, globalLexicals);
+});

ADD
packages/ses/test/global-lexicals-modules.test.js
+/* global Compartment */
+
+import '../src/main.js';
+import test from 'tape';
+import { resolveNode, makeNodeImporter } from './node.js';
+
+const makeCompartment = (endowments, globalLexicals) => {
+ const makeImportHook = makeNodeImporter({
+ 'https://example.com/packages/example/main.js': `
+ export const whom = hello;
+ export const whomElse = hello;
+ export const keys = Object.keys(globalThis);
+ export const globalHello = globalThis.hello;
+ `,
+ 'https://example.com/packages/example/immutability.js': `
+ hello = 'Please throw';
+ `,
+ 'https://example.com/packages/example/collision.js': `
+ import { whom } from './main.js';
+ export default whom;
+ `,
+ });
+ const compartment = new Compartment(
+ endowments,
+ /* modules: */ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com/packages/example'),
+ globalLexicals,
+ },
+ );
+ return compartment;
+};
+
+test('endowments own properties are mentionable', async t => {
+ t.plan(1);
+
+ const endowments = { hello: 'World!' };
+ const compartment = makeCompartment(endowments);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { whom } = namespace;
+ t.equal(whom, 'World!');
+});
+
+test('endowments own properties are enumerable', async t => {
+ t.plan(1);
+
+ const endowments = { hello: 'World!' };
+ const compartment = makeCompartment(endowments);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { keys } = namespace;
+ t.deepEqual(keys, ['hello']);
+});
+
+test('endowments prototypically inherited properties are not mentionable', async t => {
+ t.plan(1);
+
+ const endowments = { __proto__: { hello: 'World!' } };
+ const compartment = makeCompartment(endowments);
+
+ try {
+ await compartment.import('./main.js');
+ } catch (error) {
+ t.ok(true);
+ }
+});
+
+test('endowments prototypically inherited properties are not enumerable', async t => {
+ t.plan(1);
+
+ const endowments = {
+ __proto__: { bye: 'Friend!' },
+ hello: 'World!',
+ };
+ const compartment = makeCompartment(endowments);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { keys } = namespace;
+ t.deepEqual(keys, ['hello']);
+});
+
+test('global lexicals are mentionable', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { whom } = namespace;
+ t.equal(whom, 'World!');
+});
+
+test('global lexicals are not enumerable from global object', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { keys } = namespace;
+ t.deepEqual(keys, []);
+});
+
+test('global lexicals are not reachable from global object', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { globalHello } = namespace;
+ t.equal(globalHello, undefined);
+});
+
+test('global lexicals prototypically inherited properties are not reachable from global object', async t => {
+ t.plan(1);
+
+ const endowments = { hello: 'World!' };
+ const globalLexicals = { __proto__: { hello: 'Other World!' } };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { globalHello } = namespace;
+ t.equal(globalHello, 'World!');
+});
+
+test('global lexicals prototypically inherited properties are not enumerable', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ const globalLexicals = {
+ __proto__: {
+ bye: 'So long and thanks for all the fish',
+ },
+ hello: 'World!',
+ };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { keys } = namespace;
+ t.deepEqual(keys, []);
+});
+
+test('global lexicals overshadow global object', async t => {
+ t.plan(1);
+
+ const endowments = { hello: 'Your name here' };
+ const globalLexicals = { hello: 'World!' };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { whom } = namespace;
+ t.equal(whom, 'World!');
+});
+
+test('global lexicals are constant', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ try {
+ await compartment.import('./immutability.js');
+ } catch (error) {
+ t.ok(true);
+ }
+});
+
+test('global lexicals are captured on construction', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ const globalLexicals = { hello: 'World!' };
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ // Psych!
+ globalLexicals.hello = 'Something else';
+
+ const { namespace } = await compartment.import('./main.js');
+ const { whom } = namespace;
+ t.equal(whom, 'World!');
+});
+
+test('global lexical accessors are sampled once up front', async t => {
+ t.plan(2);
+
+ let counter = 0;
+ const globalLexicals = {
+ get hello() {
+ const result = counter;
+ counter += 1;
+ return result;
+ },
+ };
+
+ const endowments = {};
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./main.js');
+ const { whom, whomElse } = namespace;
+ t.equal(whom, 0);
+ t.equal(whomElse, 0);
+});
+
+test('global lexical overshadowed by imported name', async t => {
+ t.plan(1);
+
+ const endowments = {};
+ // Freezing the given lexicals should not prevent overshadowing.
+ // The mechanism that allows this is the shallow copy of the
+ // globalLexicals performed in the compartment constructor.
+ const globalLexicals = Object.freeze({
+ hello: 'World!',
+ whom: 'Ignore me!',
+ });
+ const compartment = makeCompartment(endowments, globalLexicals);
+
+ const { namespace } = await compartment.import('./collision.js');
+ t.equal(namespace.default, 'World!');
+});

DELETE
packages/ses/test/leak-unsafe-eval.test.js
-import tap from 'tap';
-import sinon from 'sinon';
-import { Compartment } from '../src/compartment-shim.js';
-import stubFunctionConstructors from './stub-function-constructors.js';
-
-const { test } = tap;
-
-test('HostException in eval revokes unsafeEval', t => {
- t.plan(2);
-
- // Mimic repairFunctions.
- stubFunctionConstructors(sinon);
-
- // Prevent output
- sinon.stub(console, 'error').callsFake();
-
- const c = new Compartment();
-
- const endowments = { $$eval$$: null };
- try {
- c.evaluate(
- `
- function loop(){
- (0, eval)('1');
- loop();
- }
-
- try {
- loop();
- } catch(e) {}
-
- $$eval$$ = eval;
- `,
- { endowments },
- );
- // eslint-disable-next-line no-empty
- } catch (err) {}
-
- t.equal(endowments.$$eval$$, c.globalThis.eval, "should be realm's eval");
- t.notEqual(endowments.$$eval$$, globalThis.eval, 'should not be unsafe eval');
-
- sinon.restore();
-});
-
-test('HostException in Function revokes unsafeEval', t => {
- t.plan(2);
-
- // Mimic repairFunctions.
- stubFunctionConstructors(sinon);
-
- // Prevent output
- sinon.stub(console, 'error').callsFake();
-
- const c = new Compartment();
-
- const endowments = { $$eval$$: null };
- try {
- c.evaluate(
- `
- function loop(){
- Function('1');
- loop();
- }
-
- try {
- loop();
- } catch(e) {}
-
- $$eval$$ = eval;
- `,
- { endowments },
- );
- // eslint-disable-next-line no-empty
- } catch (err) {}
-
- t.equal(endowments.$$eval$$, c.globalThis.eval, "should be realm's eval");
- t.notEqual(endowments.$$eval$$, globalThis.eval, 'should not be unsafe eval');
-
- sinon.restore();
-});

feat(ses): Support global lexicals (#356)

As distinct from endowments, extra properties of `globalThis`, global lexicals are constant free variables that are not reachable by computing properties of `globalThis`.