Skip to content

Commit 063ec33

Browse files
committed
feat(ses): Hermes eval and compartment taming
1 parent fd7ad98 commit 063ec33

File tree

6 files changed

+105
-30
lines changed

6 files changed

+105
-30
lines changed

packages/ses/src/global-object.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { constantProperties, universalPropertyNames } from './permits.js';
1919
* guest programs, we cannot emulate the proper behavior.
2020
* With this shim, assigning Symbol.unscopables causes the given lexical
2121
* names to fall through to the terminal scope proxy.
22-
* But, we can install this setter to prevent a program from proceding on
22+
* But, we can install this setter to prevent a program from proceeding on
2323
* this false assumption.
2424
*
2525
* @param {object} globalObject
@@ -146,14 +146,16 @@ export const setGlobalObjectMutableProperties = (
146146
* @param {object} globalObject
147147
* @param {Function} evaluator
148148
* @param {(object) => void} markVirtualizedNativeFunction
149+
* @param {string} [legacyHermesTaming]
149150
*/
150151
export const setGlobalObjectEvaluators = (
151152
globalObject,
152153
evaluator,
153154
markVirtualizedNativeFunction,
155+
legacyHermesTaming,
154156
) => {
155157
{
156-
const f = freeze(makeEvalFunction(evaluator));
158+
const f = freeze(makeEvalFunction(evaluator, legacyHermesTaming));
157159
markVirtualizedNativeFunction(f);
158160
defineProperty(globalObject, 'eval', {
159161
value: f,

packages/ses/src/lockdown.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const safeHarden = makeHardener();
102102

103103
const assertDirectEvalAvailable = () => {
104104
let allowed = false;
105+
let evaluatorsBlocked = false;
105106
try {
106107
allowed = FERAL_FUNCTION(
107108
'eval',
@@ -122,12 +123,13 @@ const assertDirectEvalAvailable = () => {
122123
// We reach here if eval is outright forbidden by a Content Security Policy.
123124
// We allow this for SES usage that delegates the responsibility to isolate
124125
// guest code to production code generation.
125-
allowed = true;
126+
evaluatorsBlocked = true;
126127
}
127-
if (!allowed) {
128+
if (!allowed && !evaluatorsBlocked) {
128129
// See https://github.com/endojs/endo/blob/master/packages/ses/error-codes/SES_DIRECT_EVAL.md
129130
throw TypeError(
130-
`SES cannot initialize unless 'eval' is the original intrinsic 'eval', suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)`,
131+
`SES cannot initialize unless 'eval' is the original intrinsic 'eval', suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)
132+
Did you mean legacyHermesTaming: 'unsafe'?`,
131133
);
132134
}
133135
};
@@ -152,11 +154,11 @@ export const repairIntrinsics = (options = {}) => {
152154
// The `stackFiltering` is not a safety issue. Rather it is a tradeoff
153155
// between relevance and completeness of the stack frames shown on the
154156
// console. Setting`stackFiltering` to `'verbose'` applies no filters, providing
155-
// the raw stack frames that can be quite versbose. Setting
157+
// the raw stack frames that can be quite verbose. Setting
156158
// `stackFrameFiltering` to`'concise'` limits the display to the stack frame
157159
// information most likely to be relevant, eliminating distracting frames
158160
// such as those from the infrastructure. However, the bug you're trying to
159-
// track down might be in the infrastrure, in which case the `'verbose'` setting
161+
// track down might be in the infrastructure, in which case the `'verbose'` setting
160162
// is useful. See
161163
// [`stackFiltering` options](https://github.com/Agoric/SES-shim/blob/master/packages/ses/docs/lockdown.md#stackfiltering-options)
162164
// for an explanation.
@@ -189,6 +191,9 @@ export const repairIntrinsics = (options = {}) => {
189191
/** @param {string} debugName */
190192
debugName => debugName !== '',
191193
),
194+
legacyHermesTaming = /** @type { 'safe' | 'unsafe' } */ (
195+
getenv('LOCKDOWN_LEGACY_HERMES_TAMING', 'safe')
196+
),
192197
legacyRegeneratorRuntimeTaming = getenv(
193198
'LOCKDOWN_LEGACY_REGENERATOR_RUNTIME_TAMING',
194199
'safe',
@@ -199,6 +204,10 @@ export const repairIntrinsics = (options = {}) => {
199204
...extraOptions
200205
} = options;
201206

207+
legacyHermesTaming === 'safe' ||
208+
legacyHermesTaming === 'unsafe' ||
209+
Fail`lockdown(): non supported option legacyHermesTaming: ${q(legacyHermesTaming)}`;
210+
202211
legacyRegeneratorRuntimeTaming === 'safe' ||
203212
legacyRegeneratorRuntimeTaming === 'unsafe-ignore' ||
204213
Fail`lockdown(): non supported option legacyRegeneratorRuntimeTaming: ${q(legacyRegeneratorRuntimeTaming)}`;
@@ -218,13 +227,11 @@ export const repairIntrinsics = (options = {}) => {
218227
const { warn } = reporter;
219228

220229
if (dateTaming !== undefined) {
221-
// eslint-disable-next-line no-console
222230
warn(
223231
`SES The 'dateTaming' option is deprecated and does nothing. In the future specifying it will be an error.`,
224232
);
225233
}
226234
if (mathTaming !== undefined) {
227-
// eslint-disable-next-line no-console
228235
warn(
229236
`SES The 'mathTaming' option is deprecated and does nothing. In the future specifying it will be an error.`,
230237
);
@@ -242,7 +249,13 @@ export const repairIntrinsics = (options = {}) => {
242249
// trace retained:
243250
priorRepairIntrinsics.stack;
244251

245-
assertDirectEvalAvailable();
252+
if (legacyHermesTaming === 'safe') {
253+
assertDirectEvalAvailable();
254+
} else if (legacyHermesTaming === 'unsafe') {
255+
warn(
256+
`SES initializing with an unoriginal intrinsic 'eval', not suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)`,
257+
);
258+
}
246259

247260
/**
248261
* Because of packagers and bundlers, etc, multiple invocations of lockdown
@@ -408,6 +421,12 @@ export const repairIntrinsics = (options = {}) => {
408421
markVirtualizedNativeFunction,
409422
});
410423

424+
if (legacyHermesTaming === 'unsafe') {
425+
globalThis.testCompartmentHooks = undefined;
426+
// @ts-ignore Compartment does exist on globalThis
427+
delete globalThis.Compartment;
428+
}
429+
411430
if (evalTaming === 'noEval') {
412431
setGlobalObjectEvaluators(
413432
globalThis,
@@ -420,6 +439,7 @@ export const repairIntrinsics = (options = {}) => {
420439
globalThis,
421440
safeEvaluate,
422441
markVirtualizedNativeFunction,
442+
legacyHermesTaming,
423443
);
424444
} else if (evalTaming === 'unsafeEval') {
425445
// Leave eval function and Function constructor of the initial compartment in-tact.

packages/ses/src/make-eval-function.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { TypeError } from './commons.js';
2+
13
/**
24
* makeEvalFunction()
35
* A safe version of the native eval function which relies on
4-
* the safety of safeEvaluate for confinement.
6+
* the safety of safeEvaluate for confinement, unless noEval
7+
* is specified (then a TypeError is thrown).
58
*
6-
* @param {Function} safeEvaluate
9+
* @param {Function} evaluator
10+
* @param legacyHermesTaming
711
*/
8-
export const makeEvalFunction = safeEvaluate => {
12+
export const makeEvalFunction = (evaluator, legacyHermesTaming) => {
913
// We use the concise method syntax to create an eval without a
1014
// [[Construct]] behavior (such that the invocation "new eval()" throws
1115
// TypeError: eval is not a constructor"), but which still accepts a
@@ -19,7 +23,30 @@ export const makeEvalFunction = safeEvaluate => {
1923
// rule. Track.
2024
return source;
2125
}
22-
return safeEvaluate(source);
26+
if (legacyHermesTaming === 'unsafe') {
27+
throw TypeError(
28+
`Legacy Hermes unsupported eval() called with string arguments cannot be tamed safe under legacyHermesTaming ${legacyHermesTaming}
29+
See: https://github.com/facebook/hermes/issues/1056
30+
See: https://github.com/endojs/endo/issues/1561
31+
Did you mean evalTaming: 'unsafeEval'?`,
32+
);
33+
}
34+
// refactoring to try/catch...
35+
// - error output still 'Uncaught'
36+
// - SES_NO_EVAL no longer encountered first
37+
// try {
38+
// safeEvaluate(source);
39+
// } catch (e) {
40+
// // throw Error(e); // Uncaught Error: SyntaxError: 2:5:invalid statement encountered.
41+
// throw TypeError(
42+
// `legacy Hermes unsupported eval() called with string arguments cannot be tamed safe under legacyHermesTaming ${legacyHermesTaming}
43+
// see: https://github.com/facebook/hermes/issues/1056
44+
// see: https://github.com/endojs/endo/issues/1561
45+
// did you mean evalTaming: 'unsafeEval'?`,
46+
// );
47+
// }
48+
// Disabling safeEvaluate is not enough, since returning the source string is not evaluating it.
49+
return evaluator(source);
2350
},
2451
}.eval;
2552

packages/ses/src/make-function-constructor.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ const { Fail } = assert;
1212
/*
1313
* makeFunctionConstructor()
1414
* A safe version of the native Function which relies on
15-
* the safety of safeEvaluate for confinement.
15+
* the safety of safeEvaluate for confinement, unless noEval
16+
* is specified (then a TypeError is thrown).
1617
*/
17-
export const makeFunctionConstructor = safeEvaluate => {
18+
export const makeFunctionConstructor = evaluator => {
1819
// Define an unused parameter to ensure Function.length === 1
1920
const newFunction = function Function(_body) {
2021
// Sanitize all parameters at the entry point.
@@ -54,7 +55,7 @@ export const makeFunctionConstructor = safeEvaluate => {
5455
// TODO: since we create an anonymous function, the 'this' value
5556
// isn't bound to the global object as per specs, but set as undefined.
5657
const src = `(function anonymous(${parameters}\n) {\n${bodyText}\n})`;
57-
return safeEvaluate(src);
58+
return evaluator(src);
5859
};
5960

6061
defineProperties(newFunction, {

packages/ses/src/permits.js

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
/* eslint-disable no-restricted-globals */
22
/* eslint max-lines: 0 */
33

4-
import { arrayPush, arrayForEach } from './commons.js';
4+
import {
5+
arrayPush,
6+
arrayForEach,
7+
getOwnPropertyDescriptor,
8+
} from './commons.js';
59

610
/** @import {GenericErrorConstructor} from '../types.js' */
711

@@ -316,23 +320,37 @@ const accessor = {
316320
set: fn,
317321
};
318322

323+
// TODO Remove this once we no longer support Hermes.
324+
// While all engines have a ThrowTypeError accessor for fields not permitted in strict mode,
325+
// some (Hermes 0.12) put that accessor in unexpected places.
326+
// We can't clean them up because they're non-configurable.
327+
// Therefore we're checking for identity with specCompliantThrowTypeError and dynamically adding permits for those.
328+
319329
// eslint-disable-next-line func-names
320-
const strict = function () {
330+
const specCompliantThrowTypeError = (function () {
321331
'use strict';
322-
};
323332

324-
// TODO Remove this once we no longer support the Hermes that needed this.
325-
arrayForEach(['caller', 'arguments'], prop => {
326-
try {
327-
strict[prop];
328-
} catch (e) {
329-
// https://github.com/facebook/hermes/blob/main/test/hermes/function-non-strict.js
330-
if (e.message === 'Restricted in strict mode') {
331-
// Fixed in Static Hermes: https://github.com/facebook/hermes/issues/1582
333+
// eslint-disable-next-line prefer-rest-params
334+
const desc = getOwnPropertyDescriptor(arguments, 'callee');
335+
return desc && desc.get;
336+
})();
337+
if (specCompliantThrowTypeError) {
338+
// eslint-disable-next-line func-names
339+
const strict = function () {
340+
'use strict';
341+
};
342+
arrayForEach(['caller', 'arguments'], prop => {
343+
const desc = getOwnPropertyDescriptor(strict, prop);
344+
if (
345+
desc &&
346+
desc.configurable === false &&
347+
desc.get &&
348+
desc.get === specCompliantThrowTypeError
349+
) {
332350
FunctionInstance[prop] = accessor;
333351
}
334-
}
335-
});
352+
});
353+
}
336354

337355
export const isAccessorPermit = permit => {
338356
return permit === getter || permit === accessor;

packages/ses/types.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ export interface RepairOptions {
4040
overrideTaming?: 'moderate' | 'min' | 'severe';
4141
overrideDebug?: Array<string>;
4242
domainTaming?: 'safe' | 'unsafe';
43+
/**
44+
* safe (default): do nothing.
45+
*
46+
* unsafe: skips direct-eval check and compartment.
47+
*
48+
*/
49+
legacyHermesTaming?: 'safe' | 'unsafe';
4350
/**
4451
* safe (default): do nothing.
4552
*

0 commit comments

Comments
 (0)