ses
Version:
Hardened JavaScript for Fearless Cooperation
344 lines (307 loc) • 11.9 kB
JavaScript
import {
WeakMap,
WeakSet,
apply,
arrayFilter,
arrayJoin,
arrayMap,
arraySlice,
create,
defineProperties,
fromEntries,
reflectSet,
regexpExec,
regexpTest,
weakmapGet,
weakmapSet,
weaksetAdd,
weaksetHas,
TypeError,
} from '../commons.js';
// Permit names from https://v8.dev/docs/stack-trace-api
// Permiting only the names used by error-stack-shim/src/v8StackFrames
// callSiteToFrame to shim the error stack proposal.
const safeV8CallSiteMethodNames = [
// suppress 'getThis' definitely
'getTypeName',
// suppress 'getFunction' definitely
'getFunctionName',
'getMethodName',
'getFileName',
'getLineNumber',
'getColumnNumber',
'getEvalOrigin',
'isToplevel',
'isEval',
'isNative',
'isConstructor',
'isAsync',
// suppress 'isPromiseAll' for now
// suppress 'getPromiseIndex' for now
// Additional names found by experiment, absent from
// https://v8.dev/docs/stack-trace-api
'getPosition',
'getScriptNameOrSourceURL',
'toString', // TODO replace to use only permitted info
];
// TODO this is a ridiculously expensive way to attenuate callsites.
// Before that matters, we should switch to a reasonable representation.
const safeV8CallSiteFacet = callSite => {
const methodEntry = name => {
const method = callSite[name];
return [name, () => apply(method, callSite, [])];
};
const o = fromEntries(arrayMap(safeV8CallSiteMethodNames, methodEntry));
return create(o, {});
};
const safeV8SST = sst => arrayMap(sst, safeV8CallSiteFacet);
// If it has `/node_modules/` anywhere in it, on Node it is likely
// to be a dependent package of the current package, and so to
// be an infrastructure frame to be dropped from concise stack traces.
const FILENAME_NODE_DEPENDENTS_CENSOR = /\/node_modules\//;
// If it begins with `internal/` or `node:internal` then it is likely
// part of the node infrustructre itself, to be dropped from concise
// stack traces.
const FILENAME_NODE_INTERNALS_CENSOR = /^(?:node:)?internal\//;
// Frames within the `assert.js` package should be dropped from
// concise stack traces, as these are just steps towards creating the
// error object in question.
const FILENAME_ASSERT_CENSOR = /\/packages\/ses\/src\/error\/assert.js$/;
// Frames within the `eventual-send` shim should be dropped so that concise
// deep stacks omit the internals of the eventual-sending mechanism causing
// asynchronous messages to be sent.
// Note that the eventual-send package will move from agoric-sdk to
// Endo, so this rule will be of general interest.
const FILENAME_EVENTUAL_SEND_CENSOR = /\/packages\/eventual-send\/src\//;
// Any stack frame whose `fileName` matches any of these censor patterns
// will be omitted from concise stacks.
// TODO Enable users to configure FILENAME_CENSORS via `lockdown` options.
const FILENAME_CENSORS = [
FILENAME_NODE_DEPENDENTS_CENSOR,
FILENAME_NODE_INTERNALS_CENSOR,
FILENAME_ASSERT_CENSOR,
FILENAME_EVENTUAL_SEND_CENSOR,
];
// Should a stack frame with this as its fileName be included in a concise
// stack trace?
// Exported only so it can be unit tested.
// TODO Move so that it applies not just to v8.
export const filterFileName = fileName => {
if (!fileName) {
// Stack frames with no fileName should appear in concise stack traces.
return true;
}
for (const filter of FILENAME_CENSORS) {
if (regexpTest(filter, fileName)) {
return false;
}
}
return true;
};
// The ad-hoc rule of the current pattern is that any likely-file-path or
// likely url-path prefix, ending in a `/.../` should get dropped.
// Anything to the left of the likely path text is kept.
// Everything to the right of `/.../` is kept. Thus
// `'Object.bar (/vat-v1/.../eventual-send/test/test-deep-send.js:13:21)'`
// simplifies to
// `'Object.bar (eventual-send/test/test-deep-send.js:13:21)'`.
//
// See thread starting at
// https://github.com/Agoric/agoric-sdk/issues/2326#issuecomment-773020389
const CALLSITE_ELLIPSES_PATTERN = /^((?:.*[( ])?)[:/\w_-]*\/\.\.\.\/(.+)$/;
// The ad-hoc rule of the current pattern is that any likely-file-path or
// likely url-path prefix, ending in a `/` and prior to `package/` should get
// dropped.
// Anything to the left of the likely path prefix text is kept. `package/` and
// everything to its right is kept. Thus
// `'Object.bar (/Users/markmiller/src/ongithub/agoric/agoric-sdk/packages/eventual-send/test/test-deep-send.js:13:21)'`
// simplifies to
// `'Object.bar (packages/eventual-send/test/test-deep-send.js:13:21)'`.
// Note that `/packages/` is a convention for monorepos encouraged by
// lerna.
const CALLSITE_PACKAGES_PATTERN = /^((?:.*[( ])?)[:/\w_-]*\/(packages\/.+)$/;
// The use of these callSite patterns below assumes that any match will bind
// capture groups containing the parts of the original string we want
// to keep. The parts outside those capture groups will be dropped from concise
// stacks.
// TODO Enable users to configure CALLSITE_PATTERNS via `lockdown` options.
const CALLSITE_PATTERNS = [
CALLSITE_ELLIPSES_PATTERN,
CALLSITE_PACKAGES_PATTERN,
];
// For a stack frame that should be included in a concise stack trace, if
// `callSiteString` is the original stringified stack frame, return the
// possibly-shorter stringified stack frame that should be shown instead.
// Exported only so it can be unit tested.
// TODO Move so that it applies not just to v8.
export const shortenCallSiteString = callSiteString => {
for (const filter of CALLSITE_PATTERNS) {
const match = regexpExec(filter, callSiteString);
if (match) {
return arrayJoin(arraySlice(match, 1), '');
}
}
return callSiteString;
};
export const tameV8ErrorConstructor = (
OriginalError,
InitialError,
errorTaming,
stackFiltering,
) => {
if (errorTaming === 'unsafe-debug') {
throw TypeError(
'internal: v8+unsafe-debug special case should already be done',
);
}
// TODO: Proper CallSite types
/** @typedef {{}} CallSite */
const originalCaptureStackTrace = OriginalError.captureStackTrace;
// const callSiteFilter = _callSite => true;
const callSiteFilter = callSite => {
if (stackFiltering === 'verbose') {
return true;
}
// eslint-disable-next-line @endo/no-polymorphic-call
return filterFileName(callSite.getFileName());
};
const callSiteStringifier = callSite => {
let callSiteString = `${callSite}`;
if (stackFiltering === 'concise') {
callSiteString = shortenCallSiteString(callSiteString);
}
return `\n at ${callSiteString}`;
};
const stackStringFromSST = (_error, sst) =>
arrayJoin(
arrayMap(arrayFilter(sst, callSiteFilter), callSiteStringifier),
'',
);
/**
* @typedef {object} StructuredStackInfo
* @property {CallSite[]} callSites
* @property {undefined} [stackString]
*/
/**
* @typedef {object} ParsedStackInfo
* @property {undefined} [callSites]
* @property {string} stackString
*/
// Mapping from error instance to the stack for that instance.
// The stack info is either the structured stack trace
// or the generated tamed stack string
/** @type {WeakMap<Error, ParsedStackInfo | StructuredStackInfo>} */
const stackInfos = new WeakMap();
// Use concise methods to obtain named functions without constructors.
const tamedMethods = {
// The optional `optFn` argument is for cutting off the bottom of
// the stack --- for capturing the stack only above the topmost
// call to that function. Since this isn't the "real" captureStackTrace
// but instead calls the real one, if no other cutoff is provided,
// we cut this one off.
captureStackTrace(error, optFn = tamedMethods.captureStackTrace) {
if (typeof originalCaptureStackTrace === 'function') {
// OriginalError.captureStackTrace is only on v8
apply(originalCaptureStackTrace, OriginalError, [error, optFn]);
return;
}
reflectSet(error, 'stack', '');
},
// Shim of proposed special power, to reside by default only
// in the start compartment, for getting the stack traceback
// string associated with an error.
// See https://tc39.es/proposal-error-stacks/
getStackString(error) {
let stackInfo = weakmapGet(stackInfos, error);
if (stackInfo === undefined) {
// The following will call `prepareStackTrace()` synchronously
// which will populate stackInfos
// eslint-disable-next-line no-void
void error.stack;
stackInfo = weakmapGet(stackInfos, error);
if (!stackInfo) {
stackInfo = { stackString: '' };
weakmapSet(stackInfos, error, stackInfo);
}
}
// prepareStackTrace() may generate the stackString
// if errorTaming === 'unsafe'
if (stackInfo.stackString !== undefined) {
return stackInfo.stackString;
}
const stackString = stackStringFromSST(error, stackInfo.callSites);
weakmapSet(stackInfos, error, { stackString });
return stackString;
},
prepareStackTrace(error, sst) {
if (errorTaming === 'unsafe') {
const stackString = stackStringFromSST(error, sst);
weakmapSet(stackInfos, error, { stackString });
return `${error}${stackString}`;
} else {
weakmapSet(stackInfos, error, { callSites: sst });
return '';
}
},
};
// A prepareFn is a prepareStackTrace function.
// An sst is a `structuredStackTrace`, which is an array of
// callsites.
// A user prepareFn is a prepareFn defined by a client of this API,
// and provided by assigning to `Error.prepareStackTrace`.
// A user prepareFn should only receive an attenuated sst, which
// is an array of attenuated callsites.
// A system prepareFn is the prepareFn created by this module to
// be installed on the real `Error` constructor, to receive
// an original sst, i.e., an array of unattenuated callsites.
// An input prepareFn is a function the user assigns to
// `Error.prepareStackTrace`, which might be a user prepareFn or
// a system prepareFn previously obtained by reading
// `Error.prepareStackTrace`.
const defaultPrepareFn = tamedMethods.prepareStackTrace;
OriginalError.prepareStackTrace = defaultPrepareFn;
// A weakset branding some functions as system prepareFns, all of which
// must be defined by this module, since they can receive an
// unattenuated sst.
const systemPrepareFnSet = new WeakSet([defaultPrepareFn]);
const systemPrepareFnFor = inputPrepareFn => {
if (weaksetHas(systemPrepareFnSet, inputPrepareFn)) {
return inputPrepareFn;
}
// Use concise methods to obtain named functions without constructors.
const systemMethods = {
prepareStackTrace(error, sst) {
weakmapSet(stackInfos, error, { callSites: sst });
return inputPrepareFn(error, safeV8SST(sst));
},
};
weaksetAdd(systemPrepareFnSet, systemMethods.prepareStackTrace);
return systemMethods.prepareStackTrace;
};
// Note `stackTraceLimit` accessor already defined by
// tame-error-constructor.js
defineProperties(InitialError, {
captureStackTrace: {
value: tamedMethods.captureStackTrace,
writable: true,
enumerable: false,
configurable: true,
},
prepareStackTrace: {
get() {
return OriginalError.prepareStackTrace;
},
set(inputPrepareStackTraceFn) {
if (typeof inputPrepareStackTraceFn === 'function') {
const systemPrepareFn = systemPrepareFnFor(inputPrepareStackTraceFn);
OriginalError.prepareStackTrace = systemPrepareFn;
} else {
OriginalError.prepareStackTrace = defaultPrepareFn;
}
},
enumerable: false,
configurable: true,
},
});
return tamedMethods.getStackString;
};