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(import-bundle): Support Endo zip hex bundle format #1983

Merged
merged 8 commits into from
Nov 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
3 changes: 3 additions & 0 deletions packages/ERTP/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/ertp",
"version": "0.8.0",
"description": "Electronic Rights Transfer Protocol (ERTP). A smart contract framework for exchanging electronic rights",
"parsers": {
"js": "mjs"
},
"main": "src/index.js",
"engines": {
"node": ">=11.0"
Expand Down
5 changes: 4 additions & 1 deletion packages/SwingSet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/swingset-vat",
"version": "0.10.0",
"description": "Vat/Container Launcher",
"parsers": {
"js": "mjs"
},
"main": "src/main.js",
"module": "src/index.js",
"engines": {
Expand Down Expand Up @@ -29,8 +32,8 @@
},
"dependencies": {
"@agoric/assert": "^0.1.0",
"@agoric/base64": "^0.0.0+1-dev",
"@agoric/babel-parser": "^7.6.4",
"@agoric/base64": "^0.0.0+1-dev",
"@agoric/bundle-source": "^1.1.10",
"@agoric/captp": "^1.6.0",
"@agoric/eventual-send": "^0.12.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/acorn-eventual-send/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/acorn-eventual-send",
"version": "2.0.10",
"description": "Eventual send (wavy dot) parser plugin for Acorn",
"parsers": {
"js": "mjs"
},
"main": "index.js",
"scripts": {
"build": "exit 0",
Expand Down
3 changes: 3 additions & 0 deletions packages/agoric-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "agoric",
"version": "0.10.1",
"description": "Manage the Agoric Javascript smart contract platform",
"parsers": {
"js": "mjs"
},
"main": "lib/main.js",
"bin": "bin/agoric",
"files": [
Expand Down
3 changes: 3 additions & 0 deletions packages/assert/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/assert",
"version": "0.1.0",
"description": "Assert expression support that protects sensitive data",
"parsers": {
"js": "mjs"
},
"main": "src/assert.js",
"engines": {
"node": ">=11.0"
Expand Down
3 changes: 3 additions & 0 deletions packages/base64/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/base64",
"version": "0.0.0+1-dev",
"description": "Transcodes base64",
"parsers": {
"js": "mjs"
},
"author": "Agoric",
"license": "Apache-2.0",
"type": "module",
Expand Down
5 changes: 5 additions & 0 deletions packages/bundle-source/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/bundle-source",
"version": "1.1.10",
"description": "Create source bundles from ES Modules",
"parsers": {
"js": "mjs"
},
"main": "src/index.js",
"scripts": {
"build": "exit 0",
Expand All @@ -19,7 +22,9 @@
},
"dependencies": {
"@agoric/acorn-eventual-send": "^2.0.10",
"@agoric/base64": "0.0.0+1-dev",
"@agoric/babel-parser": "^7.6.4",
"@agoric/compartment-mapper": "^0.2.3",
"@agoric/transform-eventual-send": "^1.3.5",
"@babel/generator": "^7.6.4",
"@babel/traverse": "^7.8.3",
Expand Down
19 changes: 18 additions & 1 deletion packages/bundle-source/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'fs';
import { rollup as rollup0 } from 'rollup';
import path from 'path';
import resolve0 from '@rollup/plugin-node-resolve';
Expand All @@ -6,17 +7,21 @@ import * as babelParser from '@agoric/babel-parser';
import babelGenerate from '@babel/generator';
import babelTraverse from '@babel/traverse';
import { makeTransform } from '@agoric/transform-eventual-send';
import { makeArchive } from '@agoric/compartment-mapper';
import { encodeBase64 } from '@agoric/base64';

import { SourceMapConsumer } from 'source-map';

const DEFAULT_MODULE_FORMAT = 'nestedEvaluate';
const DEFAULT_FILE_PREFIX = '/bundled-source';
const SUPPORTED_FORMATS = ['getExport', 'nestedEvaluate'];
const SUPPORTED_FORMATS = ['getExport', 'nestedEvaluate', 'endoZipBase64'];

const IMPORT_RE = new RegExp('\\b(import)(\\s*(?:\\(|/[/*]))', 'sg');
const HTML_COMMENT_START_RE = new RegExp(`${'<'}!--`, 'g');
const HTML_COMMENT_END_RE = new RegExp(`--${'>'}`, 'g');

const read = async location => fs.promises.readFile(new URL(location).pathname);

export function tildotPlugin() {
const transformer = makeTransform(babelParser, babelGenerate);
return {
Expand All @@ -37,6 +42,18 @@ export default async function bundleSource(
if (!SUPPORTED_FORMATS.includes(moduleFormat)) {
throw Error(`moduleFormat ${moduleFormat} is not implemented`);
}
if (moduleFormat === 'endoZipBase64') {
// TODO endoZipBase64 format does not yet support the tildot transform, as
// Compartment Mapper does not yet reveal a pre-archive transform facility.
// Such a facility might be better served by a transform specified in
// individual package.jsons and driven by the compartment mapper.
const base = new URL(`file://${process.cwd()}`).toString();
const entry = new URL(startFilename, base).toString();
const bytes = await makeArchive(read, entry);
const endoZipBase64 = encodeBase64(bytes);
return { endoZipBase64, moduleFormat };
}

const {
commonjsPlugin = commonjs0,
rollup = rollup0,
Expand Down
19 changes: 19 additions & 0 deletions packages/bundle-source/test/test-sanity.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* global Compartment */

import '@agoric/install-ses';
import { decodeBase64 } from '@agoric/base64';
import { parseArchive } from '@agoric/compartment-mapper';
import test from 'ava';
import bundleSource from '..';

Expand All @@ -9,6 +11,23 @@ function evaluate(src, endowments) {
return c.evaluate(src);
}

test('endoZipBase64', async t => {
const { endoZipBase64 } = await bundleSource(
`${__dirname}/../demo/dir1/encourage.js`,
'endoZipBase64',
);

const bytes = decodeBase64(endoZipBase64);
const archive = await parseArchive(bytes);
// Call import by property to bypass SES censoring for dynamic import.
// eslint-disable-next-line dot-notation
const { namespace } = await archive['import']('.');
const { message, encourage } = namespace;

t.is(message, `You're great!`);
t.is(encourage('you'), `Hey you! You're great!`);
});

test('nestedEvaluate', async t => {
const {
moduleFormat: mf1,
Expand Down
3 changes: 3 additions & 0 deletions packages/captp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/captp",
"version": "1.6.0",
"description": "Capability Transfer Protocol for distributed objects",
"parsers": {
"js": "mjs"
},
"keywords": [
"agoric",
"captp",
Expand Down
3 changes: 3 additions & 0 deletions packages/cosmic-swingset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/cosmic-swingset",
"version": "0.23.0",
"description": "Agoric's Cosmos blockchain integration",
"parsers": {
"js": "mjs"
},
"main": "lib/ag-solo/main.js",
"repository": "https://github.com/Agoric/agoric-sdk",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions packages/dapp-svelte-wallet/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "@agoric/dapp-svelte-wallet",
"version": "0.5.0",
"parsers": {
"js": "mjs"
},
"main": "index.js",
"license": "Apache-2.0",
"author": "Agoric",
Expand Down
2 changes: 1 addition & 1 deletion packages/dapp-svelte-wallet/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dev": "rollup -c -w",
"start": "sirv public",
"lint-check": "yarn lint",
"lint-fix": "yarn lint --fix",
"lint-fix": "exit 0",
"lint": "exit 0",
"test": "exit 0"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/deployment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/deployment",
"version": "1.23.0",
"description": "Set up Agoric public chain nodes",
"parsers": {
"js": "mjs"
},
"private": true,
"main": "main.js",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions packages/eventual-send/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/eventual-send",
"version": "0.12.0",
"description": "Extend a Promise class to implement the eventual-send API",
"parsers": {
"js": "mjs"
},
"main": "src/no-shim.js",
"types": "src/index.d.ts",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions packages/import-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@agoric/import-bundle",
"version": "0.1.0",
"description": "load modules created by @agoric/bundle-source",
"parsers": {
"js": "mjs"
},
"main": "src/index.js",
"module": "src/index.js",
"engines": {
Expand All @@ -14,6 +17,11 @@
"lint-fix": "eslint --fix '**/*.js'",
"lint-check": "eslint '**/*.js'"
},
"dependencies": {
"@agoric/base64": "^0.0.0+1-dev",
"@agoric/assert": "^0.1.0",
"@agoric/compartment-mapper": "^0.2.3"
},
"devDependencies": {
"@agoric/bundle-source": "^1.1.10",
"@agoric/install-ses": "^0.4.0",
Expand Down
57 changes: 42 additions & 15 deletions packages/import-bundle/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* global harden Compartment */
import { parseArchive } from '@agoric/compartment-mapper';
import { decodeBase64 } from '@agoric/base64';
import { wrapInescapableCompartment } from './compartment-wrapper';

// importBundle takes the output of bundle-source, and returns a namespace
Expand All @@ -8,13 +10,49 @@ export async function importBundle(bundle, options = {}) {
const {
filePrefix,
endowments: optEndowments = {},
globalLexicals = {},
// transforms are indeed __shimTransforms__, intended to apply to both
// evaluated programs and modules shimmed to programs.
transforms = [],
inescapableTransforms = [],
inescapableGlobalLexicals = {},
...compartmentOptions
} = options;
const endowments = { ...optEndowments };
const { source, sourceMap, moduleFormat } = bundle;
const endowments = {
TextEncoder,
Copy link
Member

Choose a reason for hiding this comment

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

@michaelfig will exposing these enable a metering break? If they're wrapped like all the other globals, probably not, but it's a new ability being handed to all Compartments so I wanted to double-check.

Copy link
Member

Choose a reason for hiding this comment

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

@kriskowal I'm guessing that TextEncoder and TextDecoder will be available in the immediate Compartments produced by import-bundle, but they won't automatically be in any child compartments created therein. Is that correct? I guess that won't prohibit nested import-bundles, like the kernel does to load vats.

Copy link
Member Author

Choose a reason for hiding this comment

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

encode and decode are O(string.length), so we may need to meter them.

Copy link
Member Author

Choose a reason for hiding this comment

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

cc @michaelfig I think I need your reaction to the question “do we need metering to attenuate Text{En,De}coder?”, before I land this change.

Copy link
Member

Choose a reason for hiding this comment

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

cc @michaelfig I think I need your reaction to the question “do we need metering to attenuate Text{En,De}coder?”, before I land this change.

Ideally, the TextEncoder and TextDecoder sources would be evaluated in a metered compartment (not endowed as a vetted shim), so that the metering transform would be applied to them as they are to user code.

Copy link
Member Author

@kriskowal kriskowal Nov 18, 2020

Choose a reason for hiding this comment

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

TextEncoder and TextDecoder are platform code.

sigh… We could get metering for free by slurping a UTF-8 dependency, but I’d wanted to avoid that because it’ll be unnecessary bloat on the web. The Text* utilities are available in both Node.js and Web.

Copy link
Member

Choose a reason for hiding this comment

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

TextEncoder and TextDecoder are platform code.

Oh, in that case, they're covered by the blanket metering performed by @agoric/install-metering-and-ses. No need to special-case (I had thought they were shims written by you).

TextDecoder,
...optEndowments,
};

let CompartmentToUse = Compartment;
if (
inescapableTransforms.length ||
Object.keys(inescapableGlobalLexicals).length
) {
CompartmentToUse = wrapInescapableCompartment(
Compartment,
inescapableTransforms,
inescapableGlobalLexicals,
);
}

const { moduleFormat } = bundle;
if (moduleFormat === 'endoZipBase64') {
const { endoZipBase64 } = bundle;
const bytes = decodeBase64(endoZipBase64);
const archive = await parseArchive(bytes);
// Call import by property to bypass SES censoring for dynamic import.
// eslint-disable-next-line dot-notation
const { namespace } = await archive['import']({
globals: endowments,
__shimTransforms__: transforms,
Compartment: CompartmentToUse,
});
// namespace.default has the default export
return namespace;
}

let c;
const { source, sourceMap } = bundle;
if (moduleFormat === 'getExport') {
// The 'getExport' format is a string which defines a wrapper function
// named `getExport()`. This function provides a `module` to the
Expand All @@ -40,18 +78,7 @@ export async function importBundle(bundle, options = {}) {
throw Error(`unrecognized moduleFormat '${moduleFormat}'`);
}

let CompartmentToUse = Compartment;
if (
inescapableTransforms.length ||
Object.keys(inescapableGlobalLexicals).length
) {
CompartmentToUse = wrapInescapableCompartment(
Compartment,
inescapableTransforms,
inescapableGlobalLexicals,
);
}
c = new CompartmentToUse(endowments, {}, compartmentOptions);
c = new CompartmentToUse(endowments, {}, { globalLexicals, transforms });
harden(c.globalThis);
const actualSource = `(${source})\n${sourceMap || ''}`;
const namespace = c.evaluate(actualSource)(filePrefix);
Expand Down
7 changes: 1 addition & 6 deletions packages/import-bundle/test/bundle1.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ export function f6ReadGlobal() {
return globalThis.sneakyChannel;
}

export function f7WriteGlobal(a) {
// this will throw TypeError
globalThis.sneakyChannel = a;
}

export function f8ReadGlobalSubmodule() {
export function f7ReadGlobalSubmodule() {
Copy link
Member

Choose a reason for hiding this comment

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

Hm, why remove this test? We should still check that globalThis cannot be used as a sneaky channel..

Copy link
Member Author

@kriskowal kriskowal Nov 18, 2020

Choose a reason for hiding this comment

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

The crux of the issue here is that freezing globalThis can be optional because freezing it only limits the vulnerability of programs in the same Compartment to one another. With the prior two bundle formats, there is only one compartment so it is necessary and sufficient to freeze a single globalThis to protect the application from its third-party dependencies. With this new bundle format, it is neither necessary nor sufficient to freeze the globalThis, and whether to freeze globalThis should be at the bundled application’s discretion, so that it can be informed by LavaMoat.

I could leave this test in place if I copied and modified the test for the new bundle format, but that seems like an unnecessary maintenance overhead. I could parameterize the test fixture so the constraint could vary. I could alternately go back into compartment mapper and thread a directive to freeze globalThis from package.json or a parallel policy JSON file. I had hoped to defer the last point until we had a better grasp of how LavaMoat will integrate.

return bundle2ReadGlobal();
}
28 changes: 21 additions & 7 deletions packages/import-bundle/test/test-import-bundle.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/* global harden */
import '@agoric/install-ses';
import { encodeBase64 } from '@agoric/base64';
import * as fs from 'fs';
import { makeArchive } from '@agoric/compartment-mapper';

import bundleSource from '@agoric/bundle-source';
import test from 'ava';
import { importBundle } from '../src/index.js';

const read = async location =>
fs.promises.readFile(new URL(location, 'file:///').pathname);

function transform1(src) {
return src
.replace('replaceme', 'substitution')
Expand Down Expand Up @@ -38,14 +44,8 @@ async function testBundle1(t, b1, mode, ew) {
const endowments4 = { sneakyChannel: 3, ...ew };
const ns4 = await importBundle(b1, { endowments: endowments4 });
t.is(ns4.f6ReadGlobal(), 3, `ns3.f6 ${mode} ok`);
t.is(ns4.f8ReadGlobalSubmodule(), 3, `ns3.f8 ${mode} ok`);
t.throws(
() => ns4.f7WriteGlobal(5),
{ message: /Cannot assign to read only property/ },
`ns4.f7 ${mode} ok`,
);
t.is(ns4.f6ReadGlobal(), 3, `ns4.f6 ${mode} ok`);
t.is(ns4.f8ReadGlobalSubmodule(), 3, `ns3.f8 ${mode} ok`);
t.is(ns4.f7ReadGlobalSubmodule(), 3, `ns3.f8 ${mode} ok`);
}

test('test import', async function testImport(t) {
Expand Down Expand Up @@ -75,6 +75,20 @@ test('test import', async function testImport(t) {
await testBundle1(t, b1NestedEvaluate, 'nestedEvaluate', endowments);
});

test('test import archive', async function testImportArchive(t) {
const endowments = { console };
const b1EndoZip = await makeArchive(
read,
`file://${require.resolve('./bundle1.js')}`,
);
const b1EndoZipBase64 = encodeBase64(b1EndoZip);
const b1EndoZipBase64Bundle = {
moduleFormat: 'endoZipBase64',
endoZipBase64: b1EndoZipBase64,
};
await testBundle1(t, b1EndoZipBase64Bundle, 'endoZipBase64', endowments);
});

test('test missing sourceMap', async function testImport(t) {
function req(what) {
console.log(`require(${what})`);
Expand Down
Loading