fix(ses)!: Withdraw support for muli-lockdown
* fix(ses)!: Withdraw support for muli-lockdown
*BREAKING CHANGE*: Lockdown can no longer be called more than once.  Lockdown no longer returns a boolean to indicate whether it was effective (true) or redundant (false).
Fixes
MODIFY
packages/ses-integration-test/test/test-sanity.js
packages/ses-integration-test/test/test-sanity.js
@@ -3,7 +3,7 @@ import test from 'tape';
import 'ses';
test('sanity', t => {
- t.equal(lockdown(), true, 'lockdown runs successfully');
+ t.equal(lockdown(), undefined, 'lockdown runs successfully');
const c = new Compartment({ abc: 456 });
t.equal(c.evaluate('123'), 123, 'simple evaluate succeeds');
t.equal(c.evaluate('abc'), 456, 'endowment succeeds');

MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -13,8 +13,10 @@ User-visible changes in SES:
This overcomes the last *known* obstacle toward object capability containment.
- Lockdown will now read options from the environment as defined by the Node.js
`process.env` parameter space.
-- *BREAKING CHANGE*: It may no longer be safe to call `lockdown` more than once
- per realm (VM context or iframe).
+- *BREAKING CHANGE*: Lockdown may no longer be called more than once.
+ Lockdown no longer returns a boolean to indicate whether it was effective
+ (true) or redundant (false). Instead, Lockdown will return undefined for
+ its first invocation or throw an error otherwise.
# 0.14.4 (2021-10-14)

ADD
packages/ses/error-codes/SES_ALREADY_LOCKED_DOWN.md
+# Already locked down (`SES_ALREADY_LOCKED_DOWN`)
+
+Lockdown can only be applied once.
+The error code `SES_ALREADY_LOCKED_DOWN` should appear twice whenever it is
+raised, to indicate the call sites for the first and second occasion Lockdown
+was called.
+
+```
+(TypeError#1)
+TypeError#1: Already lockded down (SES_ALREADY_LOCKED_DOWN)
+
+ at repairIntrinsics
+ at lockdown
+ at example.js:2:1
+
+TypeError#1 ERROR_NOTE: Prior call (TypeError#2)
+Nested error under TypeError#1
+ TypeError#2: Prior lockdown (SES_ALREADY_LOCKED_DOWN)
+
+ at repairIntrinsics
+ at lockdown
+ at example.js:1:1
+```
+
+## Mitigation
+
+Remove all calls to Lockdown except one.
+Generally, the earliest call is the best, so removing the call from this
+error's stack trace should suffice, or at least reveal the next redundant
+Lockdown.
+Be sure not to remove all calls to Lockdown!
+
+## Related
+
+This error is distinct from `SES_MULTIPLE_INSTANCES`, which attempts to
+distinguish the case where Lockdown functions initialized by different
+instances of the SES shim were both called.
+In this case, Lockdown detects the other's behavior by its impact on the
+environment.

ADD
packages/ses/error-codes/SES_MULTIPLE_INSTANCES.md
+# Already locked down but not by this SES instance (`SES_MULTIPLE_INSTANCES`)
+
+Lockdown can only be applied once.
+
+In this case, Lockdown appears to have already been called,
+from a cursory inspection of some of the effects of calling Lockdown.
+That would suggest that there are multiple instances of the SES shim installed
+or another module is behaving like Lockdown.
+
+## Mitigation
+
+* Remove all calls to Lockdown except one.
+ Generally, the earliest call is the best, so removing the call from this
+ error's stack trace should suffice, or at least reveal the next redundant
+ Lockdown.
+ Be sure not to remove all calls to Lockdown!
+* If you do find that the error persists even with a single call to Lockdown,
+ there may be other copies of Lockdown initialized in the same realm (iframe,
+ VM context) or another library interfering with Lockdown by modifying the
+ primordial environment in a similar way to Lockdown, like freezing shared
+ primordials.
+
+## Related
+
+This error is distinct from `SES_ALREADY_LOCKED_DOWN`, which occurs in the
+simpler case: a single Lockdown function was called twice.

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
// @ts-check
import {
+ TypeError,
arrayFilter,
arrayMap,
globalThis,
is,
- keys,
ownKeys,
stringSplit,
} from './commons.js';
@@ -53,7 +53,8 @@ import { makeCompartmentConstructor } from './compartment-shim.js';
const { details: d, quote: q } = assert;
-let firstOptions;
+/** @type {Error=} */
+let priorLockdown;
// Build a harden() with an empty fringe.
// Gate it on lockdown.
@@ -64,8 +65,6 @@ let firstOptions;
*/
const harden = makeHardener();
-const alreadyHardenedIntrinsics = () => false;
-
/**
* @callback Transform
* @param {string} source
@@ -90,7 +89,7 @@ const alreadyHardenedIntrinsics = () => false;
/**
* @param {LockdownOptions} [options]
- * @returns {() => {}} repairIntrinsics
+ * @returns {() => void} repairIntrinsics
*/
export const repairIntrinsics = (options = {}) => {
// First time, absent options default to 'safe'.
@@ -117,7 +116,6 @@ export const repairIntrinsics = (options = {}) => {
// is useful. See
// [`stackFiltering` options](https://github.com/Agoric/SES-shim/blob/master/packages/ses/lockdown-options.md#stackfiltering-options)
// for an explanation.
- options = /** @type {LockdownOptions} */ ({ ...firstOptions, ...options });
const {
getEnvironmentOption: getenv,
@@ -166,36 +164,21 @@ export const repairIntrinsics = (options = {}) => {
d`lockdown(): non supported option ${q(extraOptionsNames)}`,
);
- // Asserts for multiple invocation of lockdown().
- if (firstOptions) {
- for (const name of keys(firstOptions)) {
assert(
- options[name] === firstOptions[name],
- d`lockdown(): cannot re-invoke with different option ${q(name)}`,
+ priorLockdown === undefined,
+ `Already locked down at ${priorLockdown} (SES_ALREADY_LOCKED_DOWN)`,
+ TypeError,
);
- }
- return alreadyHardenedIntrinsics;
- }
-
- firstOptions = {
- dateTaming, // deprecated
- errorTaming,
- mathTaming, // deprecated
- regExpTaming,
- localeTaming,
- consoleTaming,
- overrideTaming,
- overrideDebug,
- stackFiltering,
- domainTaming,
- __allowUnsafeMonkeyPatching__,
- };
+ priorLockdown = new TypeError('Prior lockdown (SES_ALREADY_LOCKED_DOWN)');
+ // Tease V8 to generate the stack string and release the closures the stack
+ // trace retained:
+ priorLockdown.stack;
/**
* Because of packagers and bundlers, etc, multiple invocations of lockdown
* might happen in separate instantiations of the source of this module.
* In that case, each one sees its own `firstOptions` variable, so the test
- * above will not detect that lockdown has already happened. Instead, we
+ * above will not detect that lockdown has already happened. We
* unreliably test some telltale signs that lockdown has run, to avoid
* trying to lock down a locked down environment. Although the test is
* unreliable, this is consistent with the SES threat model. SES provides
@@ -206,12 +189,7 @@ export const repairIntrinsics = (options = {}) => {
* tests without actually locking down counts as corrupting code.
*
* The specifics of what this tests for may change over time, but it
- * should be consistent with any setting of the lockdown options. We
- * do no checking that the state is consistent with current lockdown
- * options. So a call to lockdown with one set of options may silently
- * succeed with a state not reflecting those options, but only
- * if a previous lockdown happened from something other than this
- * instance of this module.
+ * should be consistent with any setting of the lockdown options.
*/
const seemsToBeLockedDown = () => {
return (
@@ -228,8 +206,9 @@ export const repairIntrinsics = (options = {}) => {
if (seemsToBeLockedDown()) {
// eslint-disable-next-line @endo/no-polymorphic-call
- console.log('Seems to already be locked down. Skipping second lockdown');
- return alreadyHardenedIntrinsics;
+ throw new TypeError(
+ `Already locked down but not by this SES instance (SES_MULTIPLE_INSTANCES)`,
+ );
}
/**
@@ -317,6 +296,11 @@ export const repairIntrinsics = (options = {}) => {
/**
* 3. HARDEN to share the intrinsics.
+ *
+ * We define hardenIntrinsics here so that options are in scope, but return
+ * it to the caller because we intend to eventually allow vetted shims to run
+ * between repairs and the hardening of intrinsics and so we can benchmark
+ * repair separately from hardening.
*/
function hardenIntrinsics() {
@@ -353,6 +337,6 @@ export const repairIntrinsics = (options = {}) => {
* @param {LockdownOptions} [options]
*/
export const lockdown = (options = {}) => {
- const maybeHardenIntrinsics = repairIntrinsics(options);
- return maybeHardenIntrinsics();
+ const hardenIntrinsics = repairIntrinsics(options);
+ hardenIntrinsics();
};

DELETE
packages/ses/test/test-lockdown-allow.js
-import test from 'ava';
-import '../index.js';
-
-test('lockdown returns boolean or throws in downgraded SES', t => {
- t.plan(6);
-
- t.truthy(
- lockdown({
- dateTaming: 'unsafe',
- errorTaming: 'unsafe',
- mathTaming: 'unsafe',
- regExpTaming: 'unsafe',
- }),
- undefined,
- 'return true when called from JS with options',
- );
-
- t.falsy(
- lockdown({
- dateTaming: 'unsafe',
- errorTaming: 'unsafe',
- mathTaming: 'unsafe',
- regExpTaming: 'unsafe',
- }),
- undefined,
- 'return false when called from SES with the same options',
- );
-
- t.throws(
- () =>
- lockdown({
- dateTaming: 'safe',
- }),
- undefined,
- 'throws when attempting to tame Date',
- );
-
- t.throws(
- () =>
- lockdown({
- errorTaming: 'safe',
- }),
- undefined,
- 'throws when attempting to tame Error',
- );
-
- t.throws(
- () =>
- lockdown({
- mathTaming: 'safe',
- }),
- undefined,
- 'throws when attempting to tame Math',
- );
-
- t.throws(
- () =>
- lockdown({
- regExpTaming: 'safe',
- }),
- undefined,
- 'throws when attempting to tame RegExp',
- );
-});

ADD
packages/ses/test/test-lockdown-compartment.js
+import test from 'ava';
+import '../index.js';
+
+test('lockdown returns or throws', t => {
+ t.plan(3);
+
+ // Compartment constructor does not throw before lockdown.
+ (() => {
+ const c = new Compartment({}, {}, {});
+ const functionConstructor = c.evaluate('Function.prototype.constructor');
+ t.is(
+ functionConstructor,
+ Function,
+ 'before lockdown, compartments have the same privileged Function.prototype.constructor as the start compartment',
+ );
+ })();
+
+ lockdown();
+
+ // Compartment constructor does not throw after lockdown.
+ (() => {
+ const c = new Compartment({}, {}, {});
+ const cf = c.evaluate('Function.prototype.constructor');
+ t.not(
+ cf,
+ Function,
+ 'after lockdown, Function.prototype.constructor must be tamed inside compartments',
+ );
+
+ const d = new Compartment({}, {}, {});
+ const df = d.evaluate('Function.prototype.constructor');
+ t.is(
+ cf,
+ df,
+ 'after lockdown, every compartment must have the same tamed Function.prototype.constructor',
+ );
+ })();
+});

MODIFY
packages/ses/test/test-lockdown.js
packages/ses/test/test-lockdown.js
import test from 'ava';
import '../index.js';
-test('lockdown returns boolean or throws in SES', t => {
- // Compartment constructor does not throw before lockdown.
- (() => {
- const c = new Compartment({}, {}, {});
- const functionConstructor = c.evaluate('Function.prototype.constructor');
- t.is(
- functionConstructor,
- Function,
- 'before lockdown, compartments have the same privileged Function.prototype.constructor as the start compartment',
- );
- })();
+test('lockdown returns or throws', t => {
+ t.plan(3);
- t.truthy(lockdown(), 'return true when called from JS without options');
- t.falsy(
- lockdown(),
- 'return false when called from SES with the same options',
- );
- t.throws(
- () => lockdown({ dateTaming: 'unsafe' }),
- undefined,
- 'throws when attempting to untame Date',
- );
- t.throws(
- () => lockdown({ errorTaming: 'unsafe' }),
- undefined,
- 'throws when attempting to untame Error',
- );
- t.throws(
- () => lockdown({ mathTaming: 'unsafe' }),
- undefined,
- 'throws when attempting to untame Math',
- );
- t.throws(
- () => lockdown({ regExpTaming: 'unsafe' }),
- undefined,
- 'throws when attempting to untame RegExp',
- );
+ t.is(undefined, lockdown(), undefined, 'return undefined');
- // Compartment constructor does not throw after lockdown.
- (() => {
- const c = new Compartment({}, {}, {});
- const cf = c.evaluate('Function.prototype.constructor');
- t.not(
- cf,
- Function,
- 'after lockdown, Function.prototype.constructor must be tamed inside compartments',
- );
+ t.throws(lockdown, undefined, 'throw if called again, at all');
- const d = new Compartment({}, {}, {});
- const df = d.evaluate('Function.prototype.constructor');
- t.is(
- cf,
- df,
- 'after lockdown, every compartment must have the same tamed Function.prototype.constructor',
- );
- })();
+ t.throws(lockdown, undefined, 'throw if called again, at all');
});

fix(ses)!: Withdraw support for muli-lockdown (#921)

* fix(ses)!: Withdraw support for muli-lockdown

*BREAKING CHANGE*: Lockdown can no longer be called more than once.  Lockdown no longer returns a boolean to indicate whether it was effective (true) or redundant (false).

Fixes #814