-
Notifications
You must be signed in to change notification settings - Fork 71
/
tame-v8-error-constructor.js
337 lines (301 loc) · 11.4 KB
/
tame-v8-error-constructor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import {
WeakMap,
WeakSet,
apply,
arrayFilter,
arrayJoin,
arrayMap,
arraySlice,
create,
defineProperties,
fromEntries,
reflectSet,
regexpExec,
regexpTest,
weakmapGet,
weakmapSet,
weaksetAdd,
weaksetHas,
} from '../commons.js';
// Whitelist names from https://v8.dev/docs/stack-trace-api
// Whitelisting 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 whitelisted 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,
) => {
// 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;
};