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
Merged
6 changes: 5 additions & 1 deletion packages/ses/NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
57 changes: 51 additions & 6 deletions packages/ses/src/compartment-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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 }),
});
}

Expand Down Expand Up @@ -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,
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
2 changes: 1 addition & 1 deletion packages/ses/src/make-eval-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/ses/src/make-function-constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 19 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,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.
Expand Down Expand Up @@ -198,7 +211,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 +318,7 @@ export const makeModuleInstance = (
realmRec,
functorSource,
globalObject,
trappers, // "endowments" for live bindings.
localObject, // live bindings over 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
40 changes: 25 additions & 15 deletions packages/ses/src/scope-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,7 +37,7 @@ const alwaysThrowHandler = new Proxy(immutableObject, {
export function createScopeHandler(
realmRec,
globalObject,
endowments = {},
localObject = {},
{ sloppyGlobalsMode = false } = {},
) {
return {
Expand Down Expand Up @@ -67,36 +67,46 @@ 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.
return reflectGet(globalObject, prop);
},

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.
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 All @@ -122,7 +132,7 @@ export function createScopeHandler(
if (
sloppyGlobalsMode ||
prop === 'eval' ||
prop in endowments ||
prop in localObject ||
prop in globalObject ||
prop in globalThis
) {
Expand Down
Loading