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

Surface SES on globalThis #307

Merged
merged 6 commits into from
May 19, 2020
Merged
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
2 changes: 1 addition & 1 deletion packages/ses-integration-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
"depcheck": "depcheck",
"lint": "eslint '**/*.js'",
"lint-fix": "eslint --fix '**/*.js'",
"create-test-file-no-lib-cjs": "rollup --no-treeshake -c transform-tests/config/rollup.config.no-lib.js",
"test:pre-release": "node -r esm puppeteer-test/pre-release.test.js",
"test:post-release": "node -r esm puppeteer-test/post-release.test.js",
"create-test-file-no-lib-cjs": "rollup --no-treeshake -c transform-tests/config/rollup.config.no-lib.js",
"create-test-file-esm": "rollup --no-treeshake -c transform-tests/config/rollup.config.esm.js",
"create-test-file-cjs": "rollup --no-treeshake -c transform-tests/config/rollup.config.cjs.js",
"create-test-file-browserified-tape": "browserify transform-tests/output/test.no-lib.cjs.js > transform-tests/output/test.tape-no-lib.js --exclude 'ses' --ignore-missing",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ export default [
output: [
{
file: path.resolve(__dirname, "../../bundles/rollup.js"),
format: "iife",
name: "SES"
format: "iife"
}
],
plugins: [
Expand Down
6 changes: 3 additions & 3 deletions packages/ses-integration-test/test/sanity.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* global Compartment */
/* global Compartment, lockdown */
import test from "tape";

import * as SES from "ses";
import "ses";

test("sanity", t => {
t.equal(SES.lockdown(), true, "lockdown runs successfully");
t.equal(lockdown(), true, "lockdown runs successfully");
const c = new Compartment();
t.equal(c.evaluate("123"), 123, "simple evaluate succeeds");
t.equal(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default [
plugins: [
replace({
delimiters: ["", ""],
'import * as SES from "ses";': ""
'import "ses";': ""
}),
resolve({
only: ["@agoric/nat"]
Expand Down
10 changes: 9 additions & 1 deletion packages/ses/NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ User-visible changes in SES:

## Next release

* Adds support for modules to Compartments.
* SES no longer exports anything.
`Compartment`, `ModuleStaticRecord`, `lockdown`, and `harden` are all
introduced as properties of `globalThis`.
* The `Compartment` `global` getter is now named `globalThis`, consistent with
the specification proposal.
* Repair `Function.apply` and `TypeError.message` (as well as `.message` on
all the other Error types), to tolerate what the npm `es-abstract` module
does. This allows `tape` (version 4.x) to be loaded in a locked-down SES
Copy link
Contributor

Choose a reason for hiding this comment

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

Please make the reference to #293 a markdown link to #293

world, and also allows its `t.throws` assertion to work. `tape` (version
5.x) still has problems. (#293)
5.x) still has problems. [#293]

[#293]: https://github.com/Agoric/SES-shim/issues/293)

## Release 0.7.7 (27-Apr-2020)

Expand Down
121 changes: 91 additions & 30 deletions packages/ses/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,69 +27,130 @@ npm install ses

## Usage

### Module
### Lockdown

This example locks down the current realm, turning it into a starting
compartment.
Within a compartment, there is a `Compartment` constructor that conveys
"endownments" into the new compartment's global scope, and a `harden` method
that that object and any object reachable from its surface.
The compartment can import modules and evaluate programs.
SES introduces the `lockdown()` function.
Calling `lockdown()` alters the surrounding execution enviornment, or
**realm**, such that no two programs running in the same realm can observe or
interfere with each other until they have been introduced.

To this end, `lockdown()` freezes all objects accessible to any program in the
realm.
The set of accessible objects includes but is not limited to: `globalThis`,
`[].__proto__`, `{}.__proto__`, `(() => {}).__proto__` `(async () =>
{}).__proto__`, and the properties of any accessible object.

The `lockdown()` function also **tames** some of those accessible objects
that have powers that would otherwise allow programs to observe or interfere
with one another like clocks, random number generators, and regular
expressions.

```js
import 'ses';
import 'my-vetted-shim';

lockdown();

console.log(Object.isFrozen([].__proto__));
// true
```

### Harden

SES introduces the `harden` function.
*After* calling `lockdown`, the `harden` function ensures that every object in
the transitive closure over property and prototype access starting with that
object has been **frozen** by `Object.freeze`.
This means that the object can be passed among programs and none of those
programs will be able to tamper with the **surface** of that object graph.
They can only read the surface data and call the surface functions.

```js
import {lockdown} from "ses";
import 'ses';

lockdown();

let counter = 0;
const capability = harden({
inc() {
counter++;
},
});

console.log(Object.isFrozen(capability));
// true
console.log(Object.isFrozen(capability.inc));
// true
```

Note that although the **surface** of the capability is frozen, the capability
still closes over the mutable counter.
Hardening an object graph makes the surface immutable, but does not make
methods pure.


### Compartment

SES introduces the `Compartment` constructor.
A compartment is an evaluation and execution environment with its own
`globalThis` and wholly independent system of modules, but otherwise shares
the same batch of intrinsics like `Array` with the surrounding compartment.
The concept of a compartment implies the existence of a "start compartment",
the initial execution environment of a **realm**.

In the following example, we create a compartment endowed with a `print()`
function on `globalThis`.

```js
import 'ses';

const c = new Compartment({
print: harden(console.log),
print: harden(console.log),
});

c.evaluate(`
print("Hello! Hello?");
print('Hello! Hello?');
`);
```

The new compartment has a different global object than the start compartment.
The global object is initially mutable.
Locking down the start compartment hardened many of the intrinsics in global
scope.
After lockdown, no compartment can tamper with these intrinsics.
Many of these intrinsics are identical in the new compartment.
Locking down the realm hardened the objects in global scope.
After `lockdown`, no compartment can tamper with these **intrinsics** and
**undeniable** objects.
Many of these are identical in the new compartment.

```js
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid terms like "the property holds" below when you're not referring to object properties but in a context where you might be.

const c = new Compartment();
c.global === global; // false
c.global.JSON === JSON; // true
c.globalThis === globalThis; // false
c.globalThis.JSON === JSON; // true
```

The property holds among any other compartments.
Other pairs of compartments also share many identical intrinsics and undeniable
objects of the realm.
Each has a unique, initially mutable, global object.
Many intrinsics are shared.

```js
const c1 = new Compartment();
const c2 = new Compartment();
c1.global === c2.global; // false
c1.global.JSON === c2.global.JSON; // true
c1.globalThis === c2.globalThis; // false
c1.globalThis.JSON === c2.globalThis.JSON; // true
```

### Compartments

Any code executed within a compartment shares a set of module instances.
For modules to work within a compartment, the creator must provide
a `resolveHook` and an `importHook`.
The `resolveHook` determines how the compartment will infer the full module
specifier for another module from a referrer module and the module specifier
imported within that module.
The `importHook` accepts a module specifier and asynchronously returns a
specifier for another module from a referrer module and the import specifier.
The `importHook` accepts a full specifier and asynchronously returns a
`ModuleStaticRecord` for that module.

```js
import { Compartment, ModuleStaticRecord } from 'ses';
import 'ses';

const c1 = new Compartment({}, {}, {
resolveHook: (moduleSpecifier, moduleReferrer) => {
return new URL(moduleSpecifier, moduleReferrer).toString();
return resolve(moduleSpecifier, moduleReferrer);
},
importHook: async moduleSpecifier => {
const moduleLocation = locate(moduleSpecifier);
Expand All @@ -101,14 +162,14 @@ const c1 = new Compartment({}, {}, {

A compartment can also link a module in another compartment.
Each compartment has a `module` function that accepts a module specifier
and returns the ModuleNamespace for that module.
The ModuleNamespace is not useful for inspecting the exports of the
and returns the module exports namespace for that module.
The module exports namespace is not useful for inspecting the exports of the
module until that module has been imported, but it can be passed into the
module map of another Compartment, creating a link.

```js
const c2 = new Compartment({}, {
'https://example.com/packages/example/': c1.module('./main.js'),
'c1': c1.module('./main.js'),
}, {
resolveHook,
importHook,
Expand Down
2 changes: 2 additions & 0 deletions packages/ses/src/intrinsic-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,7 @@ export const intrinsicNames = [
'FunctionPrototypeConstructor',
'Compartment',
'CompartmentPrototype',
'ModuleStaticRecord',
'ModuleStaticRecordPrototype',
'harden',
];
2 changes: 2 additions & 0 deletions packages/ses/src/intrinsics-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ const globalIntrinsicNames = [

'globalThis',
'Compartment',
'CompartmentPrototype',
'ModuleStaticRecord',
'ModuleStaticRecordPrototype',
'harden',
];

Expand Down
59 changes: 23 additions & 36 deletions packages/ses/src/lockdown-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import makeHardener from '@agoric/make-hardener';

import { assert } from './assert.js';
import { getIntrinsics } from './intrinsics.js';
import whitelistIntrinsics from './whitelist-intrinsics.js';
import repairLegacyAccessors from './repair-legacy-accessors.js';
Expand All @@ -23,25 +24,29 @@ import tameGlobalDateObject from './tame-global-date-object.js';
import tameGlobalErrorObject from './tame-global-error-object.js';
import tameGlobalMathObject from './tame-global-math-object.js';
import tameGlobalRegExpObject from './tame-global-reg-exp-object.js';

import enablePropertyOverrides from './enable-property-overrides.js';
import { Compartment } from './compartment-shim.js';

let previousOptions;

function assert(condition, message) {
if (!condition) {
throw new TypeError(message);
}
}
// A successful lockdown call indicates that `harden` can be called and
// guarantee that the hardened object graph is frozen out to the fringe.
let lockedDown = false;

// Build a harden() with an empty fringe.
// Gate it on lockdown.
const lockdownHarden = makeHardener();

export const harden = ref => {
assert(lockedDown, `Cannot harden before lockdown`);
return lockdownHarden(ref);
};

export function lockdown(options = {}) {
const {
noTameDate = false,
noTameError = false,
noTameMath = false,
noTameRegExp = false,
registerOnly = false,
...extraOptions
} = options;

Expand All @@ -60,7 +65,6 @@ export function lockdown(options = {}) {
noTameError,
noTameMath,
noTameRegExp,
registerOnly,
};
if (previousOptions) {
// Assert that multiple invocation have the same value
Expand Down Expand Up @@ -88,30 +92,7 @@ export function lockdown(options = {}) {
tameGlobalRegExpObject(noTameRegExp);

/**
* 2. SHIM to expose the proposed APIs.
*/

// Build a harden() with an empty fringe.
const harden = makeHardener();

// Add the API to the global object.
Object.defineProperties(globalThis, {
harden: {
value: harden,
configurable: true,
writable: true,
enumerable: false,
},
Compartment: {
value: Compartment,
configurable: true,
writable: true,
enumerable: false,
},
});

/**
* 3. WHITELIST to standardize the environment.
* 2. WHITELIST to standardize the environment.
*/

// Extract the intrinsics from the global.
Expand All @@ -124,16 +105,22 @@ export function lockdown(options = {}) {
repairLegacyAccessors();

/**
* 4. HARDEN to share the intrinsics.
* 3. HARDEN to share the intrinsics.
*/

// Circumvent the override mistake.
const detachedProperties = enablePropertyOverrides(intrinsics);

// Finally register and optionally freeze all the intrinsics. This
// must be the operation that modifies the intrinsics.
harden(intrinsics, registerOnly);
harden(detachedProperties, registerOnly);
lockdownHarden(intrinsics);
lockdownHarden(detachedProperties);

// Having completed lockdown without failing, the user may now
// call `harden` and expect the object's transitively accessible properties
// to be frozen out to the fringe.
// Raise the `harden` gate.
lockedDown = true;

// Returning `true` indicates that this is a JS to SES transition.
return true;
Expand Down
11 changes: 9 additions & 2 deletions packages/ses/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

export { lockdown } from './lockdown-shim.js';
export { Compartment, ModuleStaticRecord } from './compartment-shim.js';
import { lockdown, harden } from './lockdown-shim.js';
import { Compartment, ModuleStaticRecord } from './compartment-shim.js';

Object.assign(globalThis, {
lockdown,
harden,
Compartment,
ModuleStaticRecord,
Comment on lines +19 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

All shimmed pervasively global names must be added to the appropriate whitelists. I don't see lockdown and ModuleStaticRecord (or StaticModuleRecord) in intrinsic-names.js

I do see ModuleStaticRecord in intrinsic-globals.js, but I don't see lockdown there.

lockdown missing from whitelist.js as well.

We should have a test that fails on such omission, because it is an easy mistake to keep making.

TODO(markm): the whole intrinsic management system now consists of nine separate files with too much redundancy, e.g., between intrinsic-names.js, intrinsic-globals.js, and whitelist.js. We should simplify, make it easier to maintain, and self-test consistency among what redundancy remains. Also, check-intrinsics.js seems vacuous. Bad edit from a previously meaningful state?

Copy link
Contributor

Choose a reason for hiding this comment

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

Filed TODO as #317

Copy link
Member Author

Choose a reason for hiding this comment

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

This is the kind of thing that I worry most about. Thanks for catching this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding lockdown to the whitelist caused tests to fail, so I’ll come back to it when we have a firm resolution as to whether it should preserve itself after lockdown.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok. But make sure the state as of merging this PR is consistent with itself.

});
4 changes: 2 additions & 2 deletions packages/ses/test/compartment.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global Compartment */
/* global Compartment, lockdown */
import test from 'tape';
import { lockdown } from '../src/lockdown-shim.js';
import '../src/main.js';

lockdown();

Expand Down
Loading