Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ses): Support global lexicals #356

Merged
merged 14 commits into from
Jul 5, 2020
Next Next commit
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`.
  • Loading branch information
kriskowal committed Jul 4, 2020
commit 0bfee801501f1842adb6c1f3041f4ca57d985ba9
47 changes: 43 additions & 4 deletions packages/ses/src/compartment-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@ 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,
getOwnPropertyDescriptors,
getOwnPropertyNames,
defineProperties,
entries,
} 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;
Expand Down Expand Up @@ -90,7 +98,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();
Expand Down Expand Up @@ -132,6 +145,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,
Expand All @@ -141,6 +165,11 @@ 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.
globalLexicals: Object.freeze({ ...globalLexicals }),
});
}

Expand Down Expand Up @@ -170,9 +199,19 @@ 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 globalObject
// https://github.com/Agoric/SES-shim/issues/365
const localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
defineProperties(localObject, getOwnPropertyDescriptors(endowments));

return performEval(realmRec, source, globalObject, localObject, {
globalTransforms,
localTransforms,
sloppyGlobalsMode,
Expand Down
6 changes: 3 additions & 3 deletions packages/ses/src/evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function performEval(
realmRec,
source,
globalObject,
endowments = {},
localObject = {},
{
localTransforms = [],
globalTransforms = [],
Expand All @@ -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, []);

Expand Down
24 changes: 18 additions & 6 deletions packages/ses/src/module-instance.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
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;
Expand Down Expand Up @@ -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,
);
Expand All @@ -40,8 +51,9 @@ 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 globalThis, globalLexicals, and
// live bindings.
const localObject = create(null, getOwnPropertyDescriptors(globalLexicals));

// {_localName_: init(initValue) -> initValue} used by the
// rewritten code to initialize exported fixed bindings.
Expand Down Expand Up @@ -198,7 +210,7 @@ export const makeModuleInstance = (

localGetNotify[localName] = liveGetNotify;
if (setProxyTrap) {
defineProperty(trappers, localName, {
defineProperty(localObject, localName, {
get,
set,
enumerable: true,
Expand Down Expand Up @@ -305,7 +317,7 @@ export const makeModuleInstance = (
realmRec,
functorSource,
globalObject,
trappers, // "endowments" for live bindings.
localObject, // live bindings and global lexicals
{
localTransforms: [],
globalTransforms: [],
Expand Down
2 changes: 1 addition & 1 deletion packages/ses/src/scope-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
14 changes: 11 additions & 3 deletions packages/ses/src/scope-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function createScopeHandler(
realmRec,
globalObject,
endowments = {},
{ sloppyGlobalsMode = false } = {},
{ globalLexicals = {}, sloppyGlobalsMode = false } = {},
) {
return {
// The scope handler throws if any trap other than get/set/has are run
Expand Down Expand Up @@ -67,6 +67,13 @@ export function createScopeHandler(
// fall through
}

// Global lexicals.
if (prop in globalLexicals) {
// Use reflect to defeat accessors that could be present on the
// globalLexicals object itself as `this`. XXX?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, do we not need this protection in this one case because we created globalLexicals ourselves, and know that it doesn't contain any accessors? I'm happy to retain the reflectGet for safety, but we might amend the comment if we know that's why we're doing it, and point out the difference between globalLexicals (which we create, and is static) and endowments (which.. might come from the user, and might have accessors, which we're happy to let execute, but not let those accessors use our this object?)

return reflectGet(globalLexicals, prop, globalObject);
}

// Properties of the global.
if (prop in endowments) {
// Use reflect to defeat accessors that could be
Expand Down Expand Up @@ -96,7 +103,7 @@ export function createScopeHandler(
return reflectSet(globalObject, prop, value);
},

// we need has() to return false for some names to prevent the lookup from
// we need has() to return false for some names to prevent the lookup from
// climbing the scope chain and eventually reaching the unsafeGlobal
// object (globalThis), which is bad.

Expand Down Expand Up @@ -124,7 +131,8 @@ export function createScopeHandler(
prop === 'eval' ||
prop in endowments ||
prop in globalObject ||
prop in globalThis
prop in globalThis ||
prop in globalLexicals
) {
return true;
}
Expand Down
134 changes: 134 additions & 0 deletions packages/ses/test/global-lexicals-evaluate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* 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 reachable 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 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 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!');
});
Loading