diff --git a/packages/endo/src/parse.js b/packages/endo/src/parse.js index c54cf3b6bc..b4c61ae906 100644 --- a/packages/endo/src/parse.js +++ b/packages/endo/src/parse.js @@ -57,7 +57,8 @@ export const parseCjs = (source, _specifier, location, packageLocation) => { return namespace; }); - functor( + functor.call( + exports, require, exports, module, diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md index 1d8b7b1676..10ee8097b5 100644 --- a/packages/ses/NEWS.md +++ b/packages/ses/NEWS.md @@ -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 diff --git a/packages/ses/lockdown.js b/packages/ses/lockdown.js index c1b8e9bc84..386b48ee81 100644 --- a/packages/ses/lockdown.js +++ b/packages/ses/lockdown.js @@ -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, }); diff --git a/packages/ses/package.json b/packages/ses/package.json index 6ecc3bdb25..5f29e2964b 100644 --- a/packages/ses/package.json +++ b/packages/ses/package.json @@ -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" diff --git a/packages/ses/ses.js b/packages/ses/ses.js index 62efe55c75..d90acc48c8 100644 --- a/packages/ses/ses.js +++ b/packages/ses/ses.js @@ -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, }); diff --git a/packages/ses/src/compartment-shim.js b/packages/ses/src/compartment-shim.js index 278cbfbd40..94c9ce0b07 100644 --- a/packages/ses/src/compartment-shim.js +++ b/packages/ses/src/compartment-shim.js @@ -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() { @@ -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 { @@ -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]'; }, @@ -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 = '', 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 - const moduleRecords = new Map(); - // Map - const instances = new Map(); - // Map - 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), ); @@ -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 @@ -292,10 +139,6 @@ export const makeCompartmentConstructor = (intrinsics, nativeBrander) => { }); } - defineProperties(Compartment, { - prototype: { value: CompartmentPrototype }, - }); - return Compartment; }; @@ -307,3 +150,7 @@ export const Compartment = makeCompartmentConstructor( getGlobalIntrinsics(globalThis), nativeBrander, ); + +defineProperties(Compartment, { + prototype: { value: CompartmentPrototype }, +}); diff --git a/packages/ses/src/global-object.js b/packages/ses/src/global-object.js index 071adc772d..8ede1611a7 100644 --- a/packages/ses/src/global-object.js +++ b/packages/ses/src/global-object.js @@ -14,7 +14,9 @@ export function initGlobalObject( globalObject, intrinsics, newGlobalPropertyNames, - { globalTransforms, nativeBrander, makeCompartmentConstructor }, + makeCompartmentConstructor, + compartmentPrototype, + { globalTransforms, nativeBrander }, ) { for (const [name, constant] of entries(constantProperties)) { defineProperty(globalObject, name, { @@ -57,12 +59,14 @@ export function initGlobalObject( }), }; - if (makeCompartmentConstructor) { - perCompartmentGlobals.Compartment = makeCompartmentConstructor( - intrinsics, - nativeBrander, - ); - } + perCompartmentGlobals.Compartment = makeCompartmentConstructor( + intrinsics, + nativeBrander, + ); + + defineProperty(perCompartmentGlobals.Compartment, 'prototype', { + value: compartmentPrototype, + }); // TODO These should still be tamed according to the whitelist before // being made available. diff --git a/packages/ses/src/lockdown-shim.js b/packages/ses/src/lockdown-shim.js index 1542c3d74c..9cf3d0ccbb 100644 --- a/packages/ses/src/lockdown-shim.js +++ b/packages/ses/src/lockdown-shim.js @@ -49,7 +49,11 @@ export const harden = ref => { const alreadyHardenedIntrinsics = () => false; -export function repairIntrinsics(makeCompartmentConstructor, options = {}) { +export function repairIntrinsics( + makeCompartmentConstructor, + compartmentPrototype, + options = {}, +) { // First time, absent options default to 'safe'. // Subsequent times, absent options default to first options. // Thus, all present options must agree with first options. @@ -132,10 +136,16 @@ export function repairIntrinsics(makeCompartmentConstructor, options = {}) { // Initialize the powerful initial global, i.e., the global of the // start compartment, from the intrinsics. - initGlobalObject(globalThis, intrinsics, initialGlobalPropertyNames, { - nativeBrander, + initGlobalObject( + globalThis, + intrinsics, + initialGlobalPropertyNames, makeCompartmentConstructor, - }); + compartmentPrototype, + { + nativeBrander, + }, + ); /** * 3. HARDEN to share the intrinsics. @@ -163,10 +173,14 @@ export function repairIntrinsics(makeCompartmentConstructor, options = {}) { return hardenIntrinsics; } -export const makeLockdown = (makeCompartmentConstructor = undefined) => { +export const makeLockdown = ( + makeCompartmentConstructor, + compartmentPrototype, +) => { const lockdown = (options = {}) => { const maybeHardenIntrinsics = repairIntrinsics( makeCompartmentConstructor, + compartmentPrototype, options, ); return maybeHardenIntrinsics(); diff --git a/packages/ses/src/module-instance.js b/packages/ses/src/module-instance.js index b5df02b49a..e1d24f8912 100644 --- a/packages/ses/src/module-instance.js +++ b/packages/ses/src/module-instance.js @@ -1,13 +1,5 @@ -import { performEval } from './evaluate.js'; import { getDeferredExports } from './module-proxy.js'; -import { - create, - getOwnPropertyDescriptors, - entries, - keys, - freeze, - defineProperty, -} from './commons.js'; +import { create, entries, keys, freeze, defineProperty } from './commons.js'; // q, for enquoting strings in error messages. const q = JSON.stringify; @@ -37,8 +29,6 @@ export const makeModuleInstance = ( const compartmentFields = privateFields.get(compartment); - const { globalLexicals } = compartmentFields; - const { exportsProxy, proxiedExports, activate } = getDeferredExports( compartment, compartmentFields, @@ -52,8 +42,8 @@ export const makeModuleInstance = ( // {_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)); + // localLexicals must be immutable, so we copy the descriptors. + const localLexicals = create(null); // {_localName_: init(initValue) -> initValue} used by the // rewritten code to initialize exported fixed bindings. @@ -210,7 +200,7 @@ export const makeModuleInstance = ( localGetNotify[localName] = liveGetNotify; if (setProxyTrap) { - defineProperty(localObject, localName, { + defineProperty(localLexicals, localName, { get, set, enumerable: true, @@ -326,16 +316,10 @@ export const makeModuleInstance = ( activate(); } - let optFunctor = performEval( - functorSource, + let optFunctor = compartment.evaluate(functorSource, { globalObject, - localObject, // live bindings over global lexicals - { - localTransforms: [], - globalTransforms: [], - sloppyGlobalsMode: false, - }, - ); + __moduleShimLexicals__: localLexicals, + }); let didThrow = false; let thrownError; function execute() { diff --git a/packages/ses/src/module-shim.js b/packages/ses/src/module-shim.js new file mode 100644 index 0000000000..7dd2f64cff --- /dev/null +++ b/packages/ses/src/module-shim.js @@ -0,0 +1,246 @@ +// This module exports both Compartment and StaticModuleRecord because they +// communicate through the moduleAnalyses private side-table. + +import babel from '@agoric/babel-standalone'; +import { makeModuleAnalyzer } from '@agoric/transform-module'; +import { + defineProperties, + entries, + freeze, + getOwnPropertyDescriptors, + keys, +} from './commons.js'; +import { load } from './module-load.js'; +import { link } from './module-link.js'; +import { getDeferredExports } from './module-proxy.js'; +import { getGlobalIntrinsics } from './intrinsics.js'; +import { tameFunctionToString } from './tame-function-tostring.js'; +import { InertCompartment, InertStaticModuleRecord } from './inert.js'; +import { + CompartmentPrototype, + makeCompartmentConstructor, +} from './compartment-shim.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 }); + + // `keys` below is Object.keys which shows only the names of string-named + // enumerable own properties. + // By contrast, Reflect.ownKeys also shows the names of symbol-named + // enumerable own properties. + // `sort` defaults to a comparator that stringifies the array elements in a + // manner which fails on symbol-named properties. + // Distinct symbols can have the same stringification. + // + // The other subtle reason this is correct is that analysis.imports should + // only have identifier-named own properties. + 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 }, +}); + +// 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(); + +// privateFields captures the private state for each compartment. +const privateFields = 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 ModularCompartmentPrototypeExtension = { + constructor: InertCompartment, + + 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; + }, +}; + +defineProperties( + CompartmentPrototype, + getOwnPropertyDescriptors(ModularCompartmentPrototypeExtension), +); + +// TODO wasteful to do it twice, once before lockdown and again during +// lockdown. The second is doubly indirect. We should at least flatten that. +const nativeBrander = tameFunctionToString(); + +const SuperCompartment = makeCompartmentConstructor( + getGlobalIntrinsics(globalThis), + nativeBrander, +); + +const ModularCompartment = function Compartment( + endowments = {}, + moduleMap = {}, + options = {}, +) { + if (new.target === undefined) { + throw new TypeError( + `Class constructor Compartment cannot be invoked without 'new'`, + ); + } + + const self = Reflect.construct( + SuperCompartment, + [endowments, moduleMap, options], + new.target, + ); + + const { resolveHook, importHook, moduleMapHook } = options; + + // Map + const moduleRecords = new Map(); + // Map + const instances = new Map(); + // Map + 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`, + ); + } + } + + privateFields.set(self, { + resolveHook, + importHook, + moduleMap, + moduleMapHook, + moduleRecords, + deferredExports, + instances, + }); + + return self; +}; + +defineProperties(ModularCompartment, { + prototype: { value: CompartmentPrototype }, +}); + +export { ModularCompartment as Compartment, CompartmentPrototype }; diff --git a/packages/ses/src/whitelist.js b/packages/ses/src/whitelist.js index bc1ada4900..c1cd2fca13 100644 --- a/packages/ses/src/whitelist.js +++ b/packages/ses/src/whitelist.js @@ -1794,10 +1794,6 @@ export const whitelist = { evaluate: fn, globalThis: getter, name: getter, - import: asyncFn, - load: asyncFn, - importNow: fn, - module: fn, // Should this be proposed? toString: fn, }, @@ -1805,6 +1801,18 @@ export const whitelist = { lockdown: fn, harden: fn, + '%InitialGetStackString%': fn, +}; + +export const modulesWhitelist = { + '%CompartmentPrototype%': { + ...whitelist['%CompartmentPrototype%'], + import: asyncFn, + load: asyncFn, + importNow: fn, + module: fn, + }, + StaticModuleRecord: { '[[Proto]]': '%FunctionPrototype%', prototype: '%StaticModuleRecordPrototype%', @@ -1822,6 +1830,4 @@ export const whitelist = { // Should this be proposed? toString: fn, }, - - '%InitialGetStackString%': fn, }; diff --git a/packages/ses/test/compartment-instance.test.js b/packages/ses/test/compartment-instance.test.js index ec6f89a5a6..84a4b76439 100644 --- a/packages/ses/test/compartment-instance.test.js +++ b/packages/ses/test/compartment-instance.test.js @@ -38,17 +38,7 @@ test('Compartment instance', t => { t.deepEqual(Reflect.ownKeys(c), [], 'static properties'); t.deepEqual( Reflect.ownKeys(Object.getPrototypeOf(c)).sort(), - [ - 'constructor', - 'evaluate', - 'import', - 'importNow', - 'load', - 'module', - 'name', - 'globalThis', - 'toString', - ].sort(), + ['constructor', 'evaluate', 'name', 'globalThis', 'toString'].sort(), 'prototype properties', ); diff --git a/packages/ses/test/compartment-prototype.test.js b/packages/ses/test/compartment-prototype.test.js index 7f5323c470..d8f47c7c1a 100644 --- a/packages/ses/test/compartment-prototype.test.js +++ b/packages/ses/test/compartment-prototype.test.js @@ -14,17 +14,7 @@ test('Compartment prototype', t => { t.deepEqual( Reflect.ownKeys(Compartment.prototype).sort(), - [ - 'constructor', - 'evaluate', - 'import', - 'importNow', - 'load', - 'module', - 'name', - 'globalThis', - 'toString', - ].sort(), + ['constructor', 'evaluate', 'name', 'globalThis', 'toString'].sort(), 'prototype properties', ); }); diff --git a/packages/ses/test/global-object.test.js b/packages/ses/test/global-object.test.js index 1448498990..997f1384b5 100644 --- a/packages/ses/test/global-object.test.js +++ b/packages/ses/test/global-object.test.js @@ -3,6 +3,10 @@ import sinon from 'sinon'; import { initGlobalObject } from '../src/global-object.js'; import stubFunctionConstructors from './stub-function-constructors.js'; import { sharedGlobalPropertyNames } from '../src/whitelist.js'; +import { + makeCompartmentConstructor, + CompartmentPrototype, +} from '../src/compartment-shim.js'; const { test } = tap; @@ -18,9 +22,16 @@ test('globalObject', t => { }; const globalObject = {}; - initGlobalObject(globalObject, intrinsics, sharedGlobalPropertyNames, { - nativeBrander(_) {}, - }); + initGlobalObject( + globalObject, + intrinsics, + sharedGlobalPropertyNames, + makeCompartmentConstructor, + CompartmentPrototype, + { + nativeBrander(_) {}, + }, + ); t.ok(globalObject instanceof Object); t.equal(Object.getPrototypeOf(globalObject), Object.prototype); @@ -28,7 +39,7 @@ test('globalObject', t => { t.notEqual(globalObject, globalThis); t.equal(globalObject.globalThis, globalObject); - t.equals(Object.getOwnPropertyNames(globalObject).length, 6); + t.equals(Object.getOwnPropertyNames(globalObject).length, 7); const descs = Object.getOwnPropertyDescriptors(globalObject); for (const [name, desc] of Object.entries(descs)) { diff --git a/packages/ses/test/import-commons.js b/packages/ses/test/import-commons.js index 0769ccda21..bedefabc09 100644 --- a/packages/ses/test/import-commons.js +++ b/packages/ses/test/import-commons.js @@ -1,4 +1,4 @@ -import { StaticModuleRecord } from '../src/compartment-shim.js'; +import { StaticModuleRecord } from '../src/module-shim.js'; // q, to quote strings in error messages. const q = JSON.stringify; diff --git a/packages/ses/test/import-gauntlet.test.js b/packages/ses/test/import-gauntlet.test.js index 0d1c0cc11f..6ec4eafedf 100644 --- a/packages/ses/test/import-gauntlet.test.js +++ b/packages/ses/test/import-gauntlet.test.js @@ -2,7 +2,7 @@ // modules using a single Compartment. import tap from 'tap'; -import { Compartment } from '../src/compartment-shim.js'; +import { Compartment } from '../src/module-shim.js'; import { resolveNode, makeNodeImporter } from './node.js'; const { test } = tap; diff --git a/packages/ses/test/import-stack-traces.test.js b/packages/ses/test/import-stack-traces.test.js index b95c54ef52..5aef636f07 100644 --- a/packages/ses/test/import-stack-traces.test.js +++ b/packages/ses/test/import-stack-traces.test.js @@ -1,5 +1,5 @@ import tap from 'tap'; -import { Compartment } from '../src/compartment-shim.js'; +import { Compartment } from '../src/module-shim.js'; import { resolveNode, makeNodeImporter } from './node.js'; const { test } = tap; diff --git a/packages/ses/test/import.test.js b/packages/ses/test/import.test.js index 06348a9a0a..b620181cc5 100644 --- a/packages/ses/test/import.test.js +++ b/packages/ses/test/import.test.js @@ -4,7 +4,7 @@ /* eslint max-lines: 0 */ import tap from 'tap'; -import { Compartment } from '../src/compartment-shim.js'; +import { Compartment } from '../src/module-shim.js'; import { resolveNode, makeNodeImporter } from './node.js'; import { makeImporter, makeStaticRetriever } from './import-commons.js'; diff --git a/packages/ses/test/module-compartment-instance.test.js b/packages/ses/test/module-compartment-instance.test.js new file mode 100644 index 0000000000..031e065b99 --- /dev/null +++ b/packages/ses/test/module-compartment-instance.test.js @@ -0,0 +1,56 @@ +import tap from 'tap'; +import sinon from 'sinon'; +import { Compartment } from '../src/module-shim.js'; +import stubFunctionConstructors from './stub-function-constructors.js'; + +const { test } = tap; + +test('Compartment instance', t => { + t.plan(9); + + // Mimic repairFunctions. + stubFunctionConstructors(sinon); + + const c = new Compartment(); + + t.equals(typeof c, 'object', 'typeof'); + t.ok(c instanceof Compartment, 'instanceof'); + t.notEquals( + c.constructor, + Compartment, + 'function Compartment() { [native code] }', + ); + + t.equals( + Object.getPrototypeOf(c), + Compartment.prototype, + 'Object.getPrototypeOf()', + ); + t.ok( + // eslint-disable-next-line no-prototype-builtins + Compartment.prototype.isPrototypeOf(c), + 'Compartment.prototype.isPrototypeOf()', + ); + + t.equals(c.toString(), '[object Compartment]', 'toString()'); + t.equals(c[Symbol.toStringTag], undefined, '"Symbol.toStringTag" property'); + + t.deepEqual(Reflect.ownKeys(c), [], 'static properties'); + t.deepEqual( + Reflect.ownKeys(Object.getPrototypeOf(c)).sort(), + [ + 'constructor', + 'evaluate', + 'import', + 'importNow', + 'load', + 'module', + 'name', + 'globalThis', + 'toString', + ].sort(), + 'prototype properties', + ); + + sinon.restore(); +}); diff --git a/packages/ses/test/module-compartment-prototype.test.js b/packages/ses/test/module-compartment-prototype.test.js new file mode 100644 index 0000000000..bef54aad89 --- /dev/null +++ b/packages/ses/test/module-compartment-prototype.test.js @@ -0,0 +1,30 @@ +import tap from 'tap'; +import { Compartment } from '../src/module-shim.js'; + +const { test } = tap; + +test('Compartment prototype', t => { + t.plan(2); + + t.notEquals( + Compartment.prototype.constructor, + Compartment, + 'The initial value of Compartment.prototype.constructor', + ); + + t.deepEqual( + Reflect.ownKeys(Compartment.prototype).sort(), + [ + 'constructor', + 'evaluate', + 'import', + 'importNow', + 'load', + 'module', + 'name', + 'globalThis', + 'toString', + ].sort(), + 'prototype properties', + ); +}); diff --git a/packages/ses/test/static-module-record-unit.test.js b/packages/ses/test/static-module-record-unit.test.js index 1184bd76e4..8c4383acba 100644 --- a/packages/ses/test/static-module-record-unit.test.js +++ b/packages/ses/test/static-module-record-unit.test.js @@ -1,5 +1,5 @@ import tap from 'tap'; -import { StaticModuleRecord } from '../src/compartment-shim.js'; +import { StaticModuleRecord } from '../src/module-shim.js'; const { test } = tap; diff --git a/packages/ses/test/whitelist-intrinsics.test.js b/packages/ses/test/whitelist-intrinsics.test.js index b63524369e..ab7d049cbd 100644 --- a/packages/ses/test/whitelist-intrinsics.test.js +++ b/packages/ses/test/whitelist-intrinsics.test.js @@ -1,6 +1,10 @@ import tap from 'tap'; import '../ses.js'; import { repairIntrinsics } from '../src/lockdown-shim.js'; +import { + makeCompartmentConstructor, + CompartmentPrototype, +} from '../src/compartment-shim.js'; const { test } = tap; @@ -21,7 +25,10 @@ test('whitelistPrototypes - on', t => { Object.prototype.hasOwnProperty.foo = 1; console.time('Benchmark repairIntrinsics()'); - const hardenIntrinsics = repairIntrinsics(); + const hardenIntrinsics = repairIntrinsics( + makeCompartmentConstructor, + CompartmentPrototype, + ); console.timeEnd('Benchmark repairIntrinsics()'); console.time('Benchmark hardenIntrinsics()'); diff --git a/packages/ses/test262/compartment-shim.js b/packages/ses/test262/compartment-shim.js index 104b2d0c12..6417dec0ff 100644 --- a/packages/ses/test262/compartment-shim.js +++ b/packages/ses/test262/compartment-shim.js @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import test262Runner from '@agoric/test262-runner'; -import { Compartment } from '../src/compartment-shim.js'; +import { Compartment } from '../src/module-shim.js'; export default function patchFunctionConstructors() { /* eslint-disable no-proto */