Skip to content

Commit

Permalink
fix(ses): handle properties that are already override protected (#1969)
Browse files Browse the repository at this point in the history
  • Loading branch information
erights authored Jan 17, 2024
1 parent 1add88a commit 5792949
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 8 deletions.
18 changes: 17 additions & 1 deletion packages/ses/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
User-visible changes in SES:
# next

- The [iterators-helpers](https://github.com/tc39/proposal-iterator-helpers)
proposal includes two accessor properties whose purpose is to emulate
a data property, but without the override mistake problem. The ses-shim
creates many such properties, but was unprepared for them to already be
present in the JS platform it starts with. A Chrome Canary and Node 22
both implement the iterators-helper proposal, triggering this bug, preventing
the ses-shim from initializing. The ses-shim
[now copes safely](https://github.com/endojs/endo/pull/1969) with an
enumerated set of such properties, starting with these two properties from
the iterators-helpers proposal.
- The ses-shim now permits the new methods from the
[set-methods](https://github.com/tc39/proposal-set-methods) proposal,
enabling these methods to be used on platforms where they are implemented,
which are currently a Chrome Canary and a Node 22.

# v0.18.8 (2023-09-11)

Expand Down Expand Up @@ -27,7 +43,7 @@ User-visible changes in SES:

# v0.18.6 (2023-08-07)

- Censors the pattern `{...import(specifier)`}.
- Censors the pattern `{...import(specifier)}`.
We previously censored `import(specifier)` and expressly allowed
`object.import(specifier)`.
The relaxation for the latter form in version 0.13.0 inadvertently allowed
Expand Down
10 changes: 3 additions & 7 deletions packages/ses/src/enable-property-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
defineProperty,
getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
getOwnPropertyNames,
isObject,
objectHasOwnProperty,
ownKeys,
Expand Down Expand Up @@ -156,23 +155,20 @@ export default function enablePropertyOverrides(
}
// TypeScript does not allow symbols to be used as indexes because it
// cannot recokon types of symbolized properties.
// @ts-ignore
arrayForEach(ownKeys(descs), prop => enable(path, obj, prop, descs[prop]));
}

function enableProperties(path, obj, plan) {
for (const prop of getOwnPropertyNames(plan)) {
for (const prop of ownKeys(plan)) {
const desc = getOwnPropertyDescriptor(obj, prop);
if (!desc || desc.get || desc.set) {
// No not a value property, nothing to do.
// eslint-disable-next-line no-continue
continue;
}

// Plan has no symbol keys and we use getOwnPropertyNames()
// so `prop` cannot only be a string, not a symbol. We coerce it in place
// with `String(..)` anyway just as good hygiene, since these paths are just
// for diagnostic purposes.
// In case `prop` is a symbol, we first coerce it with `String`,
// purely for diagnostic purposes.
const subPath = `${path}.${String(prop)}`;
const subPlan = plan[prop];

Expand Down
13 changes: 13 additions & 0 deletions packages/ses/src/enablements.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toStringTagSymbol } from './commons.js';

/**
* @file Exports {@code enablements}, a recursively defined
* JSON record defining the optimum set of intrinsics properties
Expand Down Expand Up @@ -74,6 +76,13 @@ export const minEnablements = {
'%ErrorPrototype%': {
name: true, // set by "precond", "ava", "node-fetch"
},
'%IteratorPrototype%': {
toString: true,
// https://github.com/tc39/proposal-iterator-helpers
constructor: true,
// https://github.com/tc39/proposal-iterator-helpers
[toStringTagSymbol]: true,
},
};

/**
Expand Down Expand Up @@ -152,6 +161,10 @@ export const moderateEnablements = {

'%IteratorPrototype%': {
toString: true,
// https://github.com/tc39/proposal-iterator-helpers
constructor: true,
// https://github.com/tc39/proposal-iterator-helpers
[toStringTagSymbol]: true,
},
};

Expand Down
3 changes: 3 additions & 0 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { getAnonymousIntrinsics } from './get-anonymous-intrinsics.js';
import { makeCompartmentConstructor } from './compartment.js';
import { tameHarden } from './tame-harden.js';
import { tameSymbolConstructor } from './tame-symbol-constructor.js';
import { tameFauxDataProperties } from './tame-faux-data-properties.js';

/** @typedef {import('../types.js').LockdownOptions} LockdownOptions */

Expand Down Expand Up @@ -335,6 +336,8 @@ export const repairIntrinsics = (options = {}) => {
// Replace *Locale* methods with their non-locale equivalents
tameLocaleMethods(intrinsics, localeTaming);

tameFauxDataProperties(intrinsics);

/**
* 2. WHITELIST to standardize the environment.
*/
Expand Down
214 changes: 214 additions & 0 deletions packages/ses/src/tame-faux-data-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import {
getOwnPropertyDescriptor,
apply,
defineProperty,
toStringTagSymbol,
} from './commons.js';

const throws = thunk => {
try {
thunk();
} catch (er) {
return true;
}
return false;
};

/**
* Exported for convenience of unit testing. Harmless, but not expected
* to be useful by itself.
*
* @param {any} obj
* @param {string|symbol} prop
* @param {any} expectedValue
* @returns {boolean}
* Returns whether `tameFauxDataProperty` turned the property in question
* from an apparent faux data property into the actual data property it
* seemed to emulate.
* If it returns `false`, then we hope no effects happened. However, the whole
* point of the validation tests here are to sniff out if an accessor
* property seems to be a faux data property. To do we, it invokes the
* getter and setter functions that might possibly have side effects.
* `tameFauxDataProperty` is not in a position to tell.
*/
export const tameFauxDataProperty = (obj, prop, expectedValue) => {
if (obj === undefined) {
// The object does not exist in this version of the platform
return false;
}
const desc = getOwnPropertyDescriptor(obj, prop);
if (!desc || 'value' in desc) {
// The property either doesn't exist, or is already an actual data property.
return false;
}
const { get, set } = desc;
if (typeof get !== 'function') {
// A faux data property has both a getter and a setter
return false;
}
if (typeof set !== 'function') {
return false;
}
if (get() !== expectedValue) {
// The getter called by itself should produce the expectedValue
return false;
}
if (apply(get, obj, []) !== expectedValue) {
// The getter called with `this === obj` should also return the
// expectedValue.
return false;
}
const testValue = 'Seems to be a setter';
const subject1 = { __proto__: null };
apply(set, subject1, [testValue]);
if (subject1[prop] !== testValue) {
// The setter called with an unrelated object as `this` should
// set the property on the object.
return false;
}
const subject2 = { __proto__: obj };
apply(set, subject2, [testValue]);
if (subject2[prop] !== testValue) {
// The setter called on an object that inherits from `obj` should
// override the property from `obj` as if by assignment.
return false;
}
if (!throws(() => apply(set, obj, [expectedValue]))) {
// The setter called with `this === obj` should throw without having
// caused any effect.
// This is the test that has the greatest danger of leaving behind some
// persistent side effect. The most obvious one is to emulate a
// successful assignment to the property. That's why this test
// uses `expectedValue`, so that case is likely not to actually
// change anything.
return false;
}
if ('originalValue' in get) {
// The ses-shim uniquely, as far as we know, puts an `originalValue`
// property on the getter, so that reflect property tranversal algorithms,
// like `harden`, will traverse into the enulated value without
// calling the getter. That does not happen until `permits-intrinsics.js`
// which is much later. So if we see one this early, we should
// not assume we understand what's going on.
//
return false;
}

// We assume that this code runs before any untrusted code runs, so
// we do not need to worry about the above conditions passing because of
// malicious intent. In fact, it runs even before vetted shims are supposed
// to run, between repair and hardening. Given that, after all these tests
// pass, we have adequately validated that the property in question is
// an accessor function whose purpose is suppressing the override mistake,
// i.e., enabling a non-writable property to be overridden by assignment.
// In that case, here we *temporarily* turn it into the data property
// it seems to emulate, but writable so that it does not trigger the
// override mistake while in this temporary state.

// For those properties that are also listed in `enablements.js`,
// that phase will re-enable override for these properties, but
// via accessor functions that ses controls, so we know what they are
// doing. In addition, the getter functions installed by
// `enable-property-overrides.js` have an `originalValue` field
// enabling meta-traversal code like harden to visit the original value
// without calling the getter.

if (desc.configurable === false) {
// Even though it seems to be a faux data property, we're unable to fix it.
return false;
}

// Many of the `return false;` cases above plausibly should be turned into
// errors, or an least generate warnings. However, for those, the checks
// following this phase are likely to signal an error anyway.

// At this point, we have passed all our sniff checks for validating that
// it seems to be a faux data property with the expected value. Turn
// it into the actual data property it emulates, but writable so there is
// not yet an override mistake problem.

defineProperty(obj, prop, {
value: expectedValue,
writable: true,
enumerable: desc.enumerable,
configurable: true,
});

return true;
};

/**
* In JavaScript, the so-called "override mistake" is the inability to
* override an inherited non-writable data property by assignment. A common
* workaround is to instead define an accessor property that acts like
* a non-writable data property, except that it allows an object that
* inherits this property to override it by assignment. Let's call
* an access property that acts this way a "faux data property". In this
* ses-shim, `enable-property-overrides.js` makes the properties listed in
* `enablements.js` into faux data properties.
*
* But the ses-shim is not the only one to use this trick. Starting with the
* [iterators-helpers proposal](https://github.com/tc39/proposal-iterator-helpers),
* some properties are defined as (what we call) faux data properties.
* Some of these are new properties (`Interator.prototype.constructor`) and
* some are old data properties converted to accessor properties
* (`Iterator.prototype[String.toStringTag]`). So the ses shim needs to be
* prepared for some enumerated set of properties to already be faux data
* properties in the platform prior to the ses-shim initialization.
*
* For these possible faux data properties, it is important that
* `permits.js` describe these as if they are a data property, so that
* it can further constrain the apparent value (the value
* that allegedly would be returned by the getter) according to its own permits.
*
* However, at the time of this writing, the precise behavior specified
* by the iterators-helpers proposal for these faux data properties is
* novel. We should not be too confident that all further such platform
* additions do what we would now expect. So, for each of these possible
* faux data properties, we do some sniffing to see if it behaves as we
* currently expect a faux data property to act. If not, then
* `tameFauxDataProperties` tries not to modify it, leaving it to later
* checks, especially `permits-intrinsics.js`, to error when it sees an
* accessor is expected.
*
* If one of these enumerated accessor properties does seem to be
* a faithful faux data property, then `tameFauxDataProperties` itself
* *tempoarily* turns it into the actual data property that it seems to emulate.
* This data property starts with writable, so that in this state it will
* not trigger the override mistake, i.e., assignment to an object inheriting
* this property is allowed to succeed at overriding this property.
*
* For those properties that should be a faux data property rather than an
* actual one, such as those from the iterators-helpers proposal,
* they should be listed as such in `enablements.js`, so
* `enable-property-overrides.js` will turn it back into a faux data property.
* But one controlled by the ses-shim, whose behavior we understand.
*
* `tameFauxDataProperties`, which turns these into actual data properties,
* happens during the `repairIntrinsics` phase
* of `lockdown`, before even vetted shim are supposed to run.
* `enable-property-overrides.js` runs after vetted shims, turning the
* appropriate ones back into faux data properties. Thus vetted shims
* can observe the possibly non-conforming state where these are temporarily
* actual data properties, rather than faux data properties.
*
* Coordinate the property enumeration here
* with `enablements.js`, so the appropriate properties are
* turned back to faux data properties.
*
* @param {Record<any,any>} intrinsics
*/
export const tameFauxDataProperties = intrinsics => {
// https://github.com/tc39/proposal-iterator-helpers
tameFauxDataProperty(
intrinsics['%IteratorPrototype%'],
'constructor',
intrinsics.Iterator,
);
// https://github.com/tc39/proposal-iterator-helpers
tameFauxDataProperty(
intrinsics['%IteratorPrototype%'],
toStringTagSymbol,
'Iterator',
);
};
Loading

0 comments on commit 5792949

Please sign in to comment.