Skip to content
Draft
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
140 changes: 51 additions & 89 deletions packages/ses/src/make-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
// @ts-check

import { FERAL_FUNCTION, arrayJoin, apply } from './commons.js';
import { getScopeConstants } from './scope-constants.js';
import {
FERAL_FUNCTION,
apply,
getOwnPropertyNames,
// preventExtensions,
arrayJoin,
Set,
globalThis,
} from './commons.js';

/**
* buildOptimizer()
* Given an array of identifiers, the optimizer returns a `const` declaration
* destructuring `this.${name}`.
*
* @param {Array<string>} constants
* @param {string} name
*/
function buildOptimizer(constants, name) {
// No need to build an optimizer when there are no constants.
if (constants.length === 0) return '';
// Use 'this' to avoid going through the scope proxy, which is unnecessary
// since the optimizer only needs references to the safe global.
// Destructure the constants from the target scope object.
return `const {${arrayJoin(constants, ',')}} = this.${name};`;
}
// // This should be necessary for sealing the environment, but for now it fails tests
// let tried = false;
// const attemptMakingGlobalThisNonExtensible = () => {
// if (tried) {
// return;
// }
// try {
// preventExtensions(globalThis);
// } catch (e) {
// /* empty */
// }
// tried = true;
// };

/**
* makeEvaluate()
Expand All @@ -31,80 +35,38 @@ function buildOptimizer(constants, name) {
* @param {object} context.scopeTerminator
*/
export const makeEvaluate = context => {
const { globalObjectConstants, moduleLexicalConstants } = getScopeConstants(
context.globalObject,
context.moduleLexicals,
);
const globalObjectOptimizer = buildOptimizer(
globalObjectConstants,
'globalObject',
'use strict';

const SCOPE_TERMINATOR_KEYS = arrayJoin(
[
...new Set([
...getOwnPropertyNames(globalThis),
...getOwnPropertyNames(context.globalObject),
]),
],
',',
);
const moduleLexicalOptimizer = buildOptimizer(
moduleLexicalConstants,
'moduleLexicals',
const MODULE_LEXICAL_KEYS = arrayJoin(
getOwnPropertyNames(context.moduleLexicals),
',',
);

// Create a function in sloppy mode, so that we can use 'with'. It returns
// a function in strict mode that evaluates the provided code using direct
// eval, and thus in strict mode in the same scope. We must be very careful
// to not create new names in this scope

// 1: we use multiple nested 'with' to catch all free variable names. The
// `this` value of the outer sloppy function holds the different scope
// layers, from inner to outer:
// a) `evalScope` which must release the `FERAL_EVAL` as 'eval' once for
// every invocation of the inner `evaluate` function in order to
// trigger direct eval. The direct eval semantics is what allows the
// evaluated code to lookup free variable names on the other scope
// objects and not in global scope.
// b) `moduleLexicals` which provide a way to introduce free variables
// that are not available on the globalObject.
// c) `globalObject` is the global scope object of the evaluator, aka the
// Compartment's `globalThis`.
// d) `scopeTerminator` is a proxy object which prevents free variable
// lookups to escape to the start compartment's global object.
// 2: `optimizer`s catch constant variable names for speed.
// 3: The inner strict `evaluate` function should be passed two parameters:
// a) its arguments[0] is the source to be directly evaluated.
// b) its 'this' is the this binding seen by the code being
// directly evaluated (the globalObject).

// Notes:
// - The `optimizer` strings only lookup values on the `globalObject` and
// `moduleLexicals` objects by construct. Keywords like 'function' are
// reserved and cannot be used as a variable, so they are excluded from the
// optimizer. Furthermore to prevent shadowing 'eval', while a valid
// identifier, that name is also explicitly excluded.
// - when 'eval' is looked up in the `evalScope`, the powerful unsafe eval
// intrinsic is returned after automatically removing it from the
// `evalScope`. Any further reference to 'eval' in the evaluate string will
// get the tamed evaluator from the `globalObject`, if any.
return function evaluate(code) {
'use strict';

// TODO https://github.com/endojs/endo/issues/816
// The optimizer currently runs under sloppy mode, and although we doubt that
// there is any vulnerability introduced just by running the optimizer
// sloppy, we are much more confident in the semantics of strict mode.
// The `evaluate` function can be and is reused across multiple evaluations.
// Since the optimizer should not be re-evaluated every time, it cannot be
// inside the `evaluate` closure. While we could potentially nest an
// intermediate layer of `() => {'use strict'; ${optimizers}; ...`, it
// doesn't seem worth the overhead and complexity.
const evaluateFactory = FERAL_FUNCTION(`
with (this.scopeTerminator) {
with (this.globalObject) {
with (this.moduleLexicals) {
with (this.evalScope) {
${globalObjectOptimizer}
${moduleLexicalOptimizer}
return function() {
'use strict';
return eval(arguments[0]);
};
}
}
}
}
`);
context.evalScope.eval; // required by make-safe-evaluator.

return apply(evaluateFactory, context, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we haven't used the evaluate factory as a standalone thing in a while. I guess this removes the concept completely.

Copy link
Member Author

Choose a reason for hiding this comment

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

that's not a problem. We can split into steps by adding some complexity if need be.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not a concern, just an observation. I added re-used of evaluator functions a while ago, so re-use of factory is pretty much not needed anymore.

return apply(
FERAL_FUNCTION(
`{${SCOPE_TERMINATOR_KEYS}}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused, how does it work for changing keys of the global object? Either the code changing a pre-existing global binding foo = 42, or someone changing an existing property of the global object and expecting the global binding to be updated. globalThis.bar = 42; console.log(bar).

Copy link
Member Author

Choose a reason for hiding this comment

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

For new keys, globalThis is intended to be made non-extensible (but it was breaking tests even further so I commented it out)

as for updating existing fields... I forgot about that issue, but I have a vague memory of a solution for that existing. Did I hallucinate that?

Copy link
Contributor

@mhofman mhofman Oct 30, 2025

Choose a reason for hiding this comment

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

There is currently no way to hook into scope binding access (without this). Someday we may have let decorators that would do that. The refs proposal might also be able to do something here. Both of those would still require a non-extensible global object, but at least the bindings could be mutable (mutated either by the evaluated code, or reflect mutation of the source)

Copy link
Member Author

Choose a reason for hiding this comment

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

We're talking about propagating updates between multiple evaluations within one compartment here I assume. So if we only ever had one, that would not be a problem. It'd be hard to pull off, but theoretically possible with a bundler.

Otherwise, manual propagation of individual globals seems to be the only option.
A code snippet that reads specified local variables and passes them on to a callback on demand could work.

Copy link
Contributor

Choose a reason for hiding this comment

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

We're talking about propagating updates between multiple evaluations within one compartment here I assume.

Or within a single evaluation too. If bar exist on the global object and is writable/configurable, code would expect that doing globalThis.bar = 42 is immediately visible in calls to console.log(bar).

It'd be hard to pull off, but theoretically possible with a bundler.

If we're considering code rewrites, I assume the easiest is to transform all binding references that do not resolve to a surrounding scope into some kind of call access. E.g. console.log(bar) -> console.log(__getGlobal__('bar')) and bar = 42; -> __setGlobal__('bar', 42);.

`{${MODULE_LEXICAL_KEYS}}`,
`return (function () {
'use strict';
${code}
Copy link
Contributor

Choose a reason for hiding this comment

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

This means that code be allowed to utter new.target (undefined), which may be an acceptable fidelity concern.

I think super would still be illegal?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is that a concern on a dummy empty function?

Was eval the thing that prevented these issues before?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I think because of our direct eval in a function scope, new.target being allowed is an existing fidelity concern, however undocumented I believe.

}).bind(this)();`,
Copy link
Member Author

Choose a reason for hiding this comment

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

simply prepending 'use strict'; to code sadly doesn't work because of the destructuring of arguments

SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list

),
this,
[context.globalObject, context.moduleLexicals],
);
};
};
21 changes: 21 additions & 0 deletions packages/ses/test/make-evaluate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ const makeObservingProxy = target => {
return [proxy, ops];
};


test('makeEvaluate - evaluates something', t => {
t.plan(1);

const globalObject = Object.create(null);
const moduleLexicals = Object.create(null);

globalObject.pass = t.pass;

const scopeTerminator = strictScopeTerminator;
const evalScopeKit = makeEvalScopeKit();
const { evalScope } = evalScopeKit;

const evaluate = makeEvaluate(
freeze({ scopeTerminator, globalObject, moduleLexicals, evalScope }),
);

evalScopeKit.allowNextEvalToBeUnsafe();
evaluate('pass()');
});

test('makeEvaluate - optimizer', t => {
t.plan(5);

Expand Down
Loading