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

Endo #336

Closed
wants to merge 15 commits into from
Closed

Endo #336

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/endo/src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export const parseCjs = (source, _specifier, location, packageLocation) => {
return namespace;
});

functor(
functor.call(
exports,
require,
exports,
module,
Expand Down
6 changes: 6 additions & 0 deletions packages/ses/NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ User-visible changes in SES:

## Next release

* The `ses/lockdown` module and Rollup bundles now include a minimal
implementation of `Compartment` that supports `evaluate` but not loading
modules.
This is sufficient for containment of JavaScript programs, including
modules that have been pre-compiled to programs out-of-band, without
entraining a full JavaScript parser framework.
* Allows a compartment's `importHook` to return an "alias" if the returned
static module record has a different specifier than requested.
* Adds the `name` option to the `Compartment` constructor and `name` accessor
Expand Down
24 changes: 23 additions & 1 deletion packages/ses/lockdown.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
// Copyright (C) 2018 Agoric
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Importing the lower-layer "./lockdown.js" ensures that we run later and
// replace its global lockdown if an application elects to import both.
import { assign } from './src/commons.js';
import { makeLockdown, harden } from './src/lockdown-shim.js';
import {
makeCompartmentConstructor,
CompartmentPrototype,
Compartment,
} from './src/compartment-shim.js';

assign(globalThis, {
harden,
lockdown: makeLockdown(),
lockdown: makeLockdown(makeCompartmentConstructor, CompartmentPrototype),
Compartment,
});
3 changes: 2 additions & 1 deletion packages/ses/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"clean": "rm -rf dist",
"lint": "eslint '**/*.js'",
"lint-fix": "eslint --fix '**/*.js'",
"test": "yarn build && tap --no-esm --no-coverage --reporter spec 'test/**/*.test.js'",
"qt": "tap --no-esm --no-coverage --reporter spec 'test/**/*.test.js'",
"test": "yarn build && yarn qt",
"test262": "tap --no-esm --no-coverage --reporter spec test262/*.js",
"build": "rollup --config rollup.config.js",
"demo": "http-server -o /demos"
Expand Down
10 changes: 7 additions & 3 deletions packages/ses/ses.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@
import './lockdown.js';
import { assign } from './src/commons.js';
import { makeLockdown } from './src/lockdown-shim.js';
import { makeCompartmentConstructor } from './src/compartment-shim.js';
import { whitelist, modulesWhitelist } from './src/whitelist.js';
import {
makeCompartmentConstructor,
CompartmentPrototype,
Compartment,
StaticModuleRecord,
} from './src/compartment-shim.js';
} from './src/module-shim.js';

assign(whitelist, modulesWhitelist);

assign(globalThis, {
lockdown: makeLockdown(makeCompartmentConstructor),
lockdown: makeLockdown(makeCompartmentConstructor, CompartmentPrototype),
Compartment,
StaticModuleRecord,
});
225 changes: 36 additions & 189 deletions packages/ses/src/compartment-shim.js
Original file line number Diff line number Diff line change
@@ -1,98 +1,23 @@
// This module exports both Compartment and StaticModuleRecord because they
// communicate through the moduleAnalyses private side-table.
/* eslint max-classes-per-file: ["error", 2] */

import babel from '@agoric/babel-standalone';
import { makeModuleAnalyzer } from '@agoric/transform-module';
import {
assign,
create,
defineProperties,
entries,
freeze,
getOwnPropertyNames,
keys,
getOwnPropertyDescriptors,
} from './commons.js';
import { initGlobalObject } from './global-object.js';
import { performEval } from './evaluate.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';
import { sharedGlobalPropertyNames } from './whitelist.js';
import { getGlobalIntrinsics } from './intrinsics.js';
import { tameFunctionToString } from './tame-function-tostring.js';
import { InertCompartment, InertStaticModuleRecord } from './inert.js';

// q, for quoting strings.
const q = JSON.stringify;

const analyzeModule = makeModuleAnalyzer(babel);

// moduleAnalyses are the private data of a StaticModuleRecord.
// We use moduleAnalyses in the loader/linker to look up
// the analysis corresponding to any StaticModuleRecord constructed by an
// importHook.
const moduleAnalyses = new WeakMap();

/**
* StaticModuleRecord captures the effort of parsing and analyzing module text
* so a cache of StaticModuleRecords may be shared by multiple Compartments.
*/
export function StaticModuleRecord(string, url) {
if (new.target === undefined) {
return new StaticModuleRecord(string, url);
}

const analysis = analyzeModule({ string, url });

this.imports = keys(analysis.imports).sort();

freeze(this);
freeze(this.imports);

moduleAnalyses.set(this, analysis);
}

const StaticModuleRecordPrototype = {
constructor: InertStaticModuleRecord,
toString() {
return '[object StaticModuleRecord]';
},
};

defineProperties(StaticModuleRecord, {
prototype: { value: StaticModuleRecordPrototype },
});

defineProperties(InertStaticModuleRecord, {
prototype: { value: StaticModuleRecordPrototype },
});
import { InertCompartment } from './inert.js';

// privateFields captures the private state for each compartment.
const privateFields = new WeakMap();

// moduleAliases associates every public module exports namespace with its
// corresponding compartment and specifier so they can be used to link modules
// across compartments.
// The mechanism to thread an alias is to use the compartment.module function
// to obtain the exports namespace of a foreign module and pass it into another
// compartment's moduleMap constructor option.
const moduleAliases = new WeakMap();

// Compartments do not need an importHook or resolveHook to be useful
// as a vessel for evaluating programs.
// However, any method that operates the module system will throw an exception
// if these hooks are not available.
const assertModuleHooks = compartment => {
const { importHook, resolveHook } = privateFields.get(compartment);
if (typeof importHook !== 'function' || typeof resolveHook !== 'function') {
throw new TypeError(
`Compartment must be constructed with an importHook and a resolveHook for it to be able to load modules`,
);
}
};

const CompartmentPrototype = {
export const CompartmentPrototype = {
constructor: InertCompartment,

get globalThis() {
Expand All @@ -112,12 +37,18 @@ const CompartmentPrototype = {
*/
evaluate(source, options = {}) {
// Perform this check first to avoid unecessary sanitizing.
// TODO Maybe relax string check and coerce instead:
// https://github.com/tc39/proposal-dynamic-code-brand-checks
if (typeof source !== 'string') {
throw new TypeError('first argument of evaluate() must be a string');
}

// Extract options, and shallow-clone transforms.
const { transforms = [], sloppyGlobalsMode = false } = options;
const {
transforms = [],
sloppyGlobalsMode = false,
__moduleShimLexicals__ = undefined,
} = options;
const localTransforms = [...transforms];

const {
Expand All @@ -126,71 +57,22 @@ const CompartmentPrototype = {
globalLexicals,
} = privateFields.get(this);

return performEval(source, globalObject, globalLexicals, {
let localObject = globalLexicals;
if (__moduleShimLexicals__ !== undefined) {
localObject = create(null, getOwnPropertyDescriptors(globalLexicals));
defineProperties(
localObject,
getOwnPropertyDescriptors(__moduleShimLexicals__),
);
}

return performEval(source, globalObject, localObject, {
globalTransforms,
localTransforms,
sloppyGlobalsMode,
});
},

module(specifier) {
if (typeof specifier !== 'string') {
throw new TypeError('first argument of module() must be a string');
}

assertModuleHooks(this);

const { exportsProxy } = getDeferredExports(
this,
privateFields.get(this),
moduleAliases,
specifier,
);

return exportsProxy;
},

async import(specifier) {
if (typeof specifier !== 'string') {
throw new TypeError('first argument of import() must be a string');
}

assertModuleHooks(this);

return load(privateFields, moduleAliases, this, specifier).then(() => {
const namespace = this.importNow(specifier);
return { namespace };
});
},

async load(specifier) {
if (typeof specifier !== 'string') {
throw new TypeError('first argument of load() must be a string');
}

assertModuleHooks(this);

return load(privateFields, moduleAliases, this, specifier);
},

importNow(specifier) {
if (typeof specifier !== 'string') {
throw new TypeError('first argument of importNow() must be a string');
}

assertModuleHooks(this);

const moduleInstance = link(
privateFields,
moduleAnalyses,
moduleAliases,
this,
specifier,
);
moduleInstance.execute();
return moduleInstance.exportsProxy;
},

toString() {
return '[object Compartment]';
},
Expand All @@ -206,58 +88,30 @@ export const makeCompartmentConstructor = (intrinsics, nativeBrander) => {
* Each Compartment constructor is a global. A host that wants to execute
* code in a context bound to a new global creates a new compartment.
*/
function Compartment(endowments = {}, moduleMap = {}, options = {}) {
function Compartment(endowments = {}, _moduleMap = {}, options = {}) {
// Extract options, and shallow-clone transforms.
const {
name = '<unknown>',
transforms = [],
globalLexicals = {},
resolveHook,
importHook,
moduleMapHook,
} = options;
const globalTransforms = [...transforms];

const globalObject = {};
initGlobalObject(globalObject, intrinsics, sharedGlobalPropertyNames, {
globalTransforms,
nativeBrander,
initGlobalObject(
globalObject,
intrinsics,
sharedGlobalPropertyNames,
makeCompartmentConstructor,
});
Compartment.prototype,
{
globalTransforms,
nativeBrander,
},
);

assign(globalObject, endowments);

// Map<FullSpecifier, ModuleCompartmentRecord>
const moduleRecords = new Map();
// Map<FullSpecifier, ModuleInstance>
const instances = new Map();
// Map<FullSpecifier, {ExportsProxy, ProxiedExports, activate()}>
const deferredExports = new Map();

// Validate given moduleMap.
// The module map gets translated on-demand in module-load.js and the
// moduleMap can be invalid in ways that cannot be detected in the
// constructor, but these checks allow us to throw early for a better
// developer experience.
for (const [specifier, aliasNamespace] of entries(moduleMap)) {
if (typeof aliasNamespace === 'string') {
// TODO implement parent module record retrieval.
throw new TypeError(
`Cannot map module ${q(specifier)} to ${q(
aliasNamespace,
)} in parent compartment`,
);
} else if (moduleAliases.get(aliasNamespace) === undefined) {
// TODO create and link a synthetic module instance from the given
// namespace object.
throw ReferenceError(
`Cannot map module ${q(
specifier,
)} because it has no known compartment in this realm`,
);
}
}

const invalidNames = getOwnPropertyNames(globalLexicals).filter(
identifier => !isValidIdentifierName(identifier),
);
Expand All @@ -271,13 +125,6 @@ export const makeCompartmentConstructor = (intrinsics, nativeBrander) => {

privateFields.set(this, {
name,
resolveHook,
importHook,
moduleMap,
moduleMapHook,
moduleRecords,
deferredExports,
instances,
globalTransforms,
globalObject,
// The caller continues to own the globalLexicals object they passed to
Expand All @@ -292,10 +139,6 @@ export const makeCompartmentConstructor = (intrinsics, nativeBrander) => {
});
}

defineProperties(Compartment, {
prototype: { value: CompartmentPrototype },
});

return Compartment;
};

Expand All @@ -307,3 +150,7 @@ export const Compartment = makeCompartmentConstructor(
getGlobalIntrinsics(globalThis),
nativeBrander,
);

defineProperties(Compartment, {
prototype: { value: CompartmentPrototype },
});
Loading