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

compartment-mapper: makeSecureBundle with bundled runtime and no-eval #1449

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
39e43f9
refactor(compartment-mapper): refactor archive tools for more composa…
kumavis Jan 25, 2023
00ce2a3
feat(compartment-mapper): introduce secure bundler
kumavis Jan 26, 2023
83d647b
feat(compartment-map): add makeSecureBundleFromArchive for creating a…
kumavis Feb 21, 2023
8349f5b
test(compartment-mapper): refactor vmEval for reuse
naugtur Mar 2, 2023
d5d9438
test(compartment-map): rename getVmEval to getVmEvalKit and expose co…
kumavis Mar 6, 2023
d6600b2
test(compartment-map): improve fixture package description
kumavis Mar 6, 2023
d13957e
test(compartment-map): improve test to show exposure of globals
kumavis Mar 6, 2023
205eaa0
fix(compartment-mapper): create secure bundle runtime from own fs rea…
kumavis Mar 6, 2023
e07f279
test(compartment-map): improve bundle test by comparing to non-secure…
kumavis Mar 6, 2023
57d5a3f
test(compartment-map): improve bundle test by checking for realm glob…
kumavis Mar 6, 2023
2b98c49
fix(compartment-map): correctly setup read powers for assembling bund…
kumavis Mar 6, 2023
fc8dc1e
fix(compartment-map): couple small readability fixes
kumavis Mar 6, 2023
316ec63
test(compartment-map): add failing test showing failure to validate a…
kumavis Mar 7, 2023
b4d2d81
fix(compartment-mapper): remove commented notes section from bundle r…
kumavis May 2, 2023
632bb38
chore(compartment-mapper): improve succinctness of serialization code
kumavis May 17, 2023
0752e13
chore(compartment-mapper): bundle explicitly based on the module parser
kumavis May 17, 2023
56d3159
fix(compartment-mapper): validate module sources pass mandatory trans…
kumavis May 17, 2023
8b17f6b
chore(compartment-mapper): breakout bundling into safe, unsafe, and u…
kumavis May 17, 2023
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/compartment-mapper/bundle.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { makeBundle, writeBundle } from './src/bundle.js';
export { makeBundle, writeBundle } from './src/bundle-unsafe.js';
6 changes: 5 additions & 1 deletion packages/compartment-mapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ export {
} from './src/import-archive.js';
export { search } from './src/search.js';
export { compartmentMapForNodeModules } from './src/node-modules.js';
export { makeBundle, writeBundle } from './src/bundle.js';
export { makeBundle, writeBundle } from './src/bundle-unsafe.js';
export {
makeSecureBundle,
makeSecureBundleFromArchive,
} from './src/bundle-safe.js';
31 changes: 21 additions & 10 deletions packages/compartment-mapper/src/archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,27 +207,38 @@ const renameSources = (sources, compartmentRenames) => {
};

/**
* @param {ArchiveWriter} archive
* @param {Sources} sources
*/
const addSourcesToArchive = async (archive, sources) => {
export const locationsForSources = function* locationsForSources(sources) {
for (const compartment of keys(sources).sort()) {
const modules = sources[compartment];
const compartmentLocation = resolveLocation(`${compartment}/`, 'file:///');
for (const specifier of keys(modules).sort()) {
const { bytes, location } = modules[specifier];
const module = modules[specifier];
const { location } = module;
if (location !== undefined) {
const moduleLocation = resolveLocation(location, compartmentLocation);
const path = new URL(moduleLocation).pathname.slice(1); // elide initial "/"
if (bytes !== undefined) {
// eslint-disable-next-line no-await-in-loop
await archive.write(path, bytes);
}
const path = new URL(moduleLocation).pathname.slice(1); // skip initial "/"
yield { path, module, compartment };
}
}
}
};

/**
* @param {ArchiveWriter} archive
* @param {Sources} sources
*/
export const addSourcesToArchive = async (archive, sources) => {
for (const { path, module } of locationsForSources(sources)) {
const { bytes } = module;
if (bytes !== undefined) {
// eslint-disable-next-line no-await-in-loop
await archive.write(path, bytes);
}
}
};

/**
* @param {Sources} sources
* @param {CaptureSourceLocationHook} captureSourceLocation
Expand All @@ -247,7 +258,7 @@ const captureSourceLocations = async (sources, captureSourceLocation) => {
/**
* @param {CompartmentMapDescriptor} compartmentMap
* @param {Sources} sources
* @returns {{archiveCompartmentMap: CompartmentMapDescriptor, archiveSources: Sources}}
* @returns {{archiveCompartmentMap: CompartmentMapDescriptor, archiveSources: Sources, compartmentRenames: Record<string, string>}}
*/
export const makeArchiveCompartmentMap = (compartmentMap, sources) => {
const {
Expand Down Expand Up @@ -279,7 +290,7 @@ export const makeArchiveCompartmentMap = (compartmentMap, sources) => {
// accept all valid compartment maps.
assertCompartmentMap(archiveCompartmentMap);

return { archiveCompartmentMap, archiveSources };
return { archiveCompartmentMap, archiveSources, compartmentRenames };
};

/**
Expand Down
16 changes: 13 additions & 3 deletions packages/compartment-mapper/src/bundle-cjs.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/** quotes strings */
const q = JSON.stringify;

const maybeAppendDefaultValueForExport = exportName =>
exportName === 'default' ? `, {}` : '';

const exportsCellRecord = exportsList =>
''.concat(
...exportsList.map(
exportName => `\
${exportName}: cell(${q(exportName)}),
${exportName}: cell(${q(exportName)}${maybeAppendDefaultValueForExport(
exportName,
)}),
`,
),
);
Expand All @@ -15,7 +20,12 @@ const runtime = function wrapCjsFunctor(num) {
/* eslint-disable no-undef */
return ({ imports = {} }) => {
const cModule = Object.freeze(
Object.defineProperty({}, 'exports', cells[num].default),
Object.defineProperties(
{},
{
exports: cells[num].default,
},
),
);
// TODO: specifier not found handling
const requireImpl = specifier => cells[imports[specifier]].default.get();
Expand All @@ -39,7 +49,7 @@ export default {
return {
getFunctor: () => `\
// === functors[${index}] ===
${cjsFunctor},
${cjsFunctor}
`,
getCells: () => `\
{
Expand Down
2 changes: 1 addition & 1 deletion packages/compartment-mapper/src/bundle-mjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default {
return {
getFunctor: () => `\
// === functors[${index}] ===
${__syncModuleProgram__},
${__syncModuleProgram__}
`,
getCells: () => `\
{
Expand Down
103 changes: 103 additions & 0 deletions packages/compartment-mapper/src/bundle-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-check
/** @typedef {import('ses').RedirectStaticModuleInterface} RedirectStaticModuleInterface */
/** @typedef {import('./types.js').ExecuteFn} ExecuteFn */

/* globals globalThis */
import { scopeTerminators } from 'ses/tools.js';
import { link } from './link.js';
import { makeArchiveImportHookMaker } from './import-archive.js';

const { strictScopeTerminator } = scopeTerminators;
const textEncoder = new TextEncoder();

export function loadApplication(
compartmentMap,
moduleRegistry,
loadModuleFunctors,
archiveLocation,
) {
const lookupModule = moduleLocation =>
textEncoder.encode(JSON.stringify(moduleRegistry[moduleLocation]));

const {
compartments: compartmentDescriptors,
entry: { module: entrySpecifier },
} = compartmentMap;

const archiveMakeImportHook = makeArchiveImportHookMaker(
lookupModule, // <-- this is our get function
compartmentDescriptors,
archiveLocation,
);

// see getEvalKitForCompartment definition for note on hoisting
/* eslint-disable-next-line no-use-before-define */
const moduleFunctors = loadModuleFunctors(getEvalKitForCompartment);

const makeImportHook = (packageLocation, packageName) => {
const archiveImportHook = archiveMakeImportHook(
packageLocation,
packageName,
);
const { modules: moduleDescriptors } =
compartmentDescriptors[packageLocation];
const importHook = async moduleSpecifier => {
const staticModuleRecord = await archiveImportHook(moduleSpecifier);
// archiveImportHook always setups on an alias record
// loadRecord will read the alias so use that
const aliasModuleRecord = /** @type {RedirectStaticModuleInterface} */ (
staticModuleRecord
).record;
// put module functor on the staticModuleRecord
const moduleDescriptor = moduleDescriptors[moduleSpecifier];
const moduleLocation = `${packageLocation}/${moduleDescriptor.location}`;
const makeModuleFunctor = moduleFunctors[moduleLocation];
/* eslint-disable-next-line no-underscore-dangle */
/** @type {any} */ (aliasModuleRecord).__syncModuleFunctor__ =
makeModuleFunctor();
return staticModuleRecord;
};
return importHook;
};

/*
wiring would be cleaner if `link()` could take a compartments collection

reference cycle:
- `getEvalKitForCompartment` needs `compartments`
- `compartments` is created by `link()`
- `link()` needs `makeImportHook`
- `makeImportHook` needs `getEvalKitForCompartment`
*/
const { compartment: entryCompartment, compartments } = link(compartmentMap, {
makeImportHook,
globals: globalThis,
// transforms,
});

function getCompartmentByName(name) {
let compartment = compartments[name];
if (compartment === undefined) {
compartment = new Compartment();
compartments[name] = compartment;
}
return compartment;
}

// this relies on hoisting to close a reference triangle
// as noted above, this could be fixed if we could pass `compartments` into `link()`
function getEvalKitForCompartment(compartmentName) {
const compartment = getCompartmentByName(compartmentName);
const scopeTerminator = strictScopeTerminator;
const { globalThis } = compartment;
return { globalThis, scopeTerminator };
}

/** @type {ExecuteFn} */
const execute = () => {
// eslint-disable-next-line dot-notation
return entryCompartment['import'](entrySpecifier);
};

return { execute, compartments };
}
Loading