Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/ses/src/commons.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const {
set: reflectSet,
} = Reflect;

export const { isArray, prototype: arrayPrototype } = Array;
export const { isArray, prototype: arrayPrototype, from: ArrayFrom } = Array;
export const { prototype: arrayBufferPrototype } = ArrayBuffer;
export const { prototype: mapPrototype } = Map;
export const { revocable: proxyRevocable } = Proxy;
Expand Down
32 changes: 28 additions & 4 deletions packages/ses/src/eval-scope.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { FERAL_EVAL, create, defineProperties, freeze } from './commons.js';
import {
FERAL_EVAL,
apply,
create,
defineProperties,
freeze,
} from './commons.js';

import { assert } from './error/assert.js';

Expand Down Expand Up @@ -69,17 +75,35 @@ export const makeEvalScopeKit = () => {
},
});

const allowEvalProperties = freeze({
eval: {
value: props => {
const { eval: evalProp } = props;
delete evalScope.eval;
defineProperties(evalScope, { eval: evalProp });
},
Comment on lines +78 to +84
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took me a good 10 minutes of staring at this to figure out why it works.
I suggest we add a lengthy comment to explain it 😅
Something like this maybe:

Suggested change
const allowEvalProperties = freeze({
eval: {
value: props => {
const { eval: evalProp } = props;
delete evalScope.eval;
defineProperties(evalScope, { eval: evalProp });
},
// This conforms to the shape of evalScope properties, but instead of evaluating what arrives as first
// argument, it accepts a new shape to replace eval with.
// As a result, when `apply(evaluate, globalObject, [oneTimeEvalProperties]);` is called below,
// the same exact stack depth is reached and the same optimizations are applied to it before the
// FERAL_EVAL is put in place, making it impossible for a stack overflow error to occur in the process
// of getting eval for the main call to the evaluator if it didn't occur in revealing eval.
const allowEvalProperties = freeze({
eval: {
value: props => {
const { eval: evalProp } = props;
delete evalScope.eval;
defineProperties(evalScope, { eval: evalProp });
},

enumerable: false,
configurable: true,
},
});

const evalScopeKit = {
evalScope,
allowNextEvalToBeUnsafe() {
allowNextEvalToBeUnsafe(evaluate, globalObject) {
const { revoked } = evalScopeKit;
if (revoked !== null) {
Fail`a handler did not reset allowNextEvalToBeUnsafe ${revoked.err}`;
}
// Allow next reference to eval produce the unsafe FERAL_EVAL.
// Use the evaluator and evalScope to reveal the unsafe FERAL_EVAL
// We avoid defineProperty because it consumes an extra stack frame taming
// its return value.
defineProperties(evalScope, oneTimeEvalProperties);
defineProperties(evalScope, allowEvalProperties);
try {
apply(evaluate, globalObject, [oneTimeEvalProperties]);
} catch (err) {
// We didn't manage to reveal eval, so clean up the powerless revealer
delete evalScope.eval;
}
},
/** @type {null | { err: any }} */
revoked: null,
Expand Down
5 changes: 2 additions & 3 deletions packages/ses/src/make-safe-evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const makeSafeEvaluator = ({
? createSloppyGlobalsScopeTerminator(globalObject)
: strictScopeTerminator;
const evalScopeKit = makeEvalScopeKit();
const { evalScope } = evalScopeKit;
const { evalScope, allowNextEvalToBeUnsafe } = evalScopeKit;

const evaluateContext = freeze({
evalScope,
Expand Down Expand Up @@ -73,8 +73,7 @@ export const makeSafeEvaluator = ({
let err;
try {
// Allow next reference to eval produce the unsafe FERAL_EVAL.
// eslint-disable-next-line @endo/no-polymorphic-call
evalScopeKit.allowNextEvalToBeUnsafe();
allowNextEvalToBeUnsafe(evaluate, globalObject);

// Ensure that "this" resolves to the safe global.
return apply(evaluate, globalObject, [source]);
Expand Down
6 changes: 4 additions & 2 deletions packages/ses/test/eval-scope.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ test('evalScope - is not created with eval exposed', t => {

const evalScopeKit = makeEvalScopeKit();
const { evalScope } = evalScopeKit;
const evaluateForAllower = props => evalScope.eval(props);

t.is(Reflect.has(evalScope, 'eval'), false);

evalScopeKit.allowNextEvalToBeUnsafe();
evalScopeKit.allowNextEvalToBeUnsafe(evaluateForAllower);

t.is(Reflect.has(evalScope, 'eval'), true);
});
Expand All @@ -25,10 +26,11 @@ test('evalScope - getting eval removes it from evalScope', t => {

const evalScopeKit = makeEvalScopeKit();
const { evalScope } = evalScopeKit;
const evaluateForAllower = props => evalScope.eval(props);

t.is(Reflect.has(evalScope, 'eval'), false);

evalScopeKit.allowNextEvalToBeUnsafe();
evalScopeKit.allowNextEvalToBeUnsafe(evaluateForAllower);

t.is(Reflect.has(evalScope, 'eval'), true);

Expand Down
6 changes: 3 additions & 3 deletions packages/ses/test/make-evaluate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test('makeEvaluate - optimizer', t => {
globalObjectOps.length = 0;
moduleLexicalsOps.length = 0;

evalScopeKit.allowNextEvalToBeUnsafe();
evalScopeKit.allowNextEvalToBeUnsafe(evaluate, globalObject);

const result = apply(evaluate, globalObject, [`!foo && bar && baz`]);

Expand All @@ -70,9 +70,9 @@ test('makeEvaluate - strict-mode', t => {
freeze({ scopeTerminator, globalObject, moduleLexicals, evalScope }),
);

evalScopeKit.allowNextEvalToBeUnsafe();
evalScopeKit.allowNextEvalToBeUnsafe(evaluate, globalObject);
t.throws(() => apply(evaluate, globalObject, [`foo = 42`]));

evalScopeKit.allowNextEvalToBeUnsafe();
evalScopeKit.allowNextEvalToBeUnsafe(evaluate, globalObject);
t.throws(() => apply(evaluate, globalObject, [`with({}) {}`]));
});
27 changes: 27 additions & 0 deletions packages/ses/test/make-safe-evaluator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,30 @@ test('safeEvaluate - transforms - rewrite source', t => {
'localTransforms rewrite source first',
);
});

test.failing(`safeEvaluate - stack overflow doesn't leak feral eval`, t => {
t.plan(3);
const globalObject = Object.create(null);
const moduleLexicals = Object.create(null);

const { safeEvaluate: evaluate } = makeSafeEvaluator({
globalObject,
moduleLexicals,
});

let depth = 0;
function stackBuildUp() {
depth += 1;
evaluate(`1+1`);
// didn't fail yet? keep digging.
return stackBuildUp();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend avoiding tail recursion here as that is an explicitly allowed optional optimization.

}

t.throws(stackBuildUp, {
instanceOf: RangeError,
message: /Maximum call stack size exceeded/,
});
t.log('stack depth', depth);
t.is(depth > 1, true, 'stack overflow occurred after many recursions');
t.is(evaluate('1+1'), 2, `stack overflow didn't compromise evaluate scope`);
});