-
Notifications
You must be signed in to change notification settings - Fork 80
wip: Function constructor evaluator #3000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() | ||
|
|
@@ -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, []); | ||
| return apply( | ||
| FERAL_FUNCTION( | ||
| `{${SCOPE_TERMINATOR_KEYS}}`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For new keys, 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is currently no way to hook into scope binding access (without
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Or within a single evaluation too. If
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. |
||
| `{${MODULE_LEXICAL_KEYS}}`, | ||
| `return (function () { | ||
| 'use strict'; | ||
| ${code} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means that I think
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that a concern on a dummy empty function? Was
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I think because of our direct eval in a function scope, |
||
| }).bind(this)();`, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. simply prepending
|
||
| ), | ||
| this, | ||
| [context.globalObject, context.moduleLexicals], | ||
| ); | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.