Skip to content

Commit 8cf7a5e

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

6 files changed

+91
-30
lines changed

packages/ses/src/global-object.js

+4-2
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

+29-8
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,14 @@ 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+
// See https://github.com/facebook/hermes/issues/957
256+
warn(
257+
`SES initializing with an unoriginal intrinsic 'eval', not suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)`,
258+
);
259+
}
246260

247261
/**
248262
* Because of packagers and bundlers, etc, multiple invocations of lockdown
@@ -408,6 +422,12 @@ export const repairIntrinsics = (options = {}) => {
408422
markVirtualizedNativeFunction,
409423
});
410424

425+
if (legacyHermesTaming === 'unsafe') {
426+
globalThis.testCompartmentHooks = undefined;
427+
// @ts-ignore Compartment does exist on globalThis
428+
delete globalThis.Compartment;
429+
}
430+
411431
if (evalTaming === 'noEval') {
412432
setGlobalObjectEvaluators(
413433
globalThis,
@@ -420,6 +440,7 @@ export const repairIntrinsics = (options = {}) => {
420440
globalThis,
421441
safeEvaluate,
422442
markVirtualizedNativeFunction,
443+
legacyHermesTaming,
423444
);
424445
} else if (evalTaming === 'unsafeEval') {
425446
// Leave eval function and Function constructor of the initial compartment in-tact.

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

+16-4
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,15 @@ 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+
return evaluator(source);
2335
},
2436
}.eval;
2537

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

+4-3
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

+31-13
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

+7
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)