Skip to content
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

fix(ses): fix #2598 with cauterizeProperty reuse #2624

Merged
merged 7 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
69 changes: 69 additions & 0 deletions packages/ses/src/cauterize-property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { objectHasOwnProperty } from './commons.js';

/**
* @import {Reporter} from './reporting-types.js'
*/

/**
* Delete `obj[prop]` or at least make it harmless.
leotm marked this conversation as resolved.
Show resolved Hide resolved
*
* If the property was not expected, then emit a reporter-dependent warning
* to bring attention to this case, so someone can determine what to do with it.
*
* If the property to be deleted is a function's `.prototype` property, this
* will normally be because the function was supposed to be a
* - builtin method or non-constructor function
* - arrow function
* - concise method
*
* all of whom are not supposed to have a `.prototype` property. Nevertheless,
* on some platforms (like older versions of Hermes), or as a result of
* some shim-based mods to the primordials (like core-js?), some of these
* functions may accidentally be more like `function` functions with
* an undeletable `.prototype` property. In these cases, if we can
* set the value of that bogus `.prototype` property to `undefined`,
* we do so, issuing a warning, rather than failing to initialize ses.
*
* @param {object} obj
* @param {PropertyKey} prop
* @param {boolean} known If deletion is expected, don't warn
* @param {string} subPath Used for warning messages
* @param {Reporter} reporter Where to issue warning or error.
* @returns {void}
*/
export const cauterizeProperty = (
obj,
prop,
known,
subPath,
{ warn, error },
) => {
// Either the object lacks a permit or the object doesn't match the
// permit.
// If the permit is specifically false, not merely undefined,
// this is a property we expect to see because we know it exists in
// some environments and we have expressly decided to exclude it.
// Any other disallowed property is one we have not audited and we log
// that we are removing it so we know to look into it, as happens when
// the language evolves new features to existing intrinsics.
if (!known) {
warn(`Removing ${subPath}`);
}
try {
delete obj[prop];
} catch (err) {
if (objectHasOwnProperty(obj, prop)) {
if (typeof obj === 'function' && prop === 'prototype') {
obj.prototype = undefined;
erights marked this conversation as resolved.
Show resolved Hide resolved
if (obj.prototype === undefined) {
warn(`Tolerating undeletable ${subPath} === undefined`);
return;
}
}
error(`failed to delete ${subPath}`, err);
} else {
error(`deleting ${subPath} threw`, err);
}
throw err;
}
};
1 change: 1 addition & 0 deletions packages/ses/src/compartment-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const markVirtualizedNativeFunction = tameFunctionToString();
// @ts-ignore Compartment is definitely on globalThis.
globalThis.Compartment = makeCompartmentConstructor(
makeCompartmentConstructor,
// TODO pass a proper reporter as second arg to `getGlobalIntrinsics`?
getGlobalIntrinsics(globalThis),
markVirtualizedNativeFunction,
);
21 changes: 19 additions & 2 deletions packages/ses/src/intrinsics.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cauterizeProperty } from './cauterize-property.js';
import {
TypeError,
WeakSet,
Expand All @@ -23,6 +24,10 @@ import {
permitted,
} from './permits.js';

/**
* @import {Reporter} from './reporting-types.js'
*/

const isFunction = obj => typeof obj === 'function';

// Like defineProperty, but throws if it would modify an existing property.
Expand Down Expand Up @@ -71,7 +76,10 @@ function sampleGlobals(globalObject, newPropertyNames) {
return newIntrinsics;
}

export const makeIntrinsicsCollector = () => {
/**
* @param {Reporter} [reporter] defaults to `console`
*/
export const makeIntrinsicsCollector = (reporter = console) => {
/** @type {Record<any, any>} */
const intrinsics = create(null);
let pseudoNatives;
Expand Down Expand Up @@ -100,7 +108,15 @@ export const makeIntrinsicsCollector = () => {
}
const namePrototype = permit.prototype;
if (!namePrototype) {
throw TypeError(`${name}.prototype property not permitted`);
cauterizeProperty(
intrinsic,
'prototype',
false,
`${name}.prototype`,
reporter,
);
// eslint-disable-next-line no-continue
continue;
}
if (
typeof namePrototype !== 'string' ||
Expand Down Expand Up @@ -166,6 +182,7 @@ export const makeIntrinsicsCollector = () => {
* @param {object} globalObject
*/
export const getGlobalIntrinsics = globalObject => {
// TODO pass a proper reporter to `makeIntrinsicsCollector`
erights marked this conversation as resolved.
Show resolved Hide resolved
const { addIntrinsics, finalIntrinsics } = makeIntrinsicsCollector();

addIntrinsics(sampleGlobals(globalObject, sharedGlobalPropertyNames));
Expand Down
2 changes: 1 addition & 1 deletion packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const repairIntrinsics = (options = {}) => {
const markVirtualizedNativeFunction = tameFunctionToString();

const { addIntrinsics, completePrototypes, finalIntrinsics } =
makeIntrinsicsCollector();
makeIntrinsicsCollector(reporter);

// @ts-expect-error __hardenTaming__ could be any string
const tamedHarden = tameHarden(safeHarden, __hardenTaming__);
Expand Down
34 changes: 5 additions & 29 deletions packages/ses/src/permits-intrinsics.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
ownKeys,
symbolKeyFor,
} from './commons.js';
import { cauterizeProperty } from './cauterize-property.js';

/**
* @import {Reporter} from './reporting-types.js'
Expand Down Expand Up @@ -279,35 +280,10 @@ export default function removeUnpermittedIntrinsics(
const subPermit = getSubPermit(obj, permit, propString);

if (!subPermit || !isAllowedProperty(subPath, obj, prop, subPermit)) {
// Either the object lacks a permit or the object doesn't match the
// permit.
// If the permit is specifically false, not merely undefined,
// this is a property we expect to see because we know it exists in
// some environments and we have expressly decided to exclude it.
// Any other disallowed property is one we have not audited and we log
// that we are removing it so we know to look into it, as happens when
// the language evolves new features to existing intrinsics.
if (subPermit !== false) {
warn(`Removing ${subPath}`);
}
try {
delete obj[prop];
} catch (err) {
if (prop in obj) {
if (typeof obj === 'function' && prop === 'prototype') {
obj.prototype = undefined;
if (obj.prototype === undefined) {
warn(`Tolerating undeletable ${subPath} === undefined`);
// eslint-disable-next-line no-continue
continue;
}
}
error(`failed to delete ${subPath}`, err);
} else {
error(`deleting ${subPath} threw`, err);
}
throw err;
}
cauterizeProperty(obj, prop, subPermit === false, subPath, {
warn,
error,
});
erights marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions packages/ses/test/tolerate-empty-prototype-toplevel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* global globalThis */
import test from 'ava';
import '../index.js';

// See https://github.com/zloirock/core-js/issues/1092
// See https://github.com/endojs/endo/issues/2598
const originalEscape = globalThis.escape;
globalThis.escape = function escape(...args) {
return Reflect.apply(originalEscape, this, args);
};

lockdown();

test('tolerate empty escape.prototype', t => {
t.is(globalThis.escape, escape);
t.assert('prototype' in escape);
t.is(escape.prototype, undefined);
t.deepEqual(Object.getOwnPropertyDescriptor(escape, 'prototype'), {
value: undefined,
writable: !!harden.isFake,
enumerable: false,
configurable: false,
});
});
3 changes: 3 additions & 0 deletions packages/ses/test/tolerate-empty-prototype.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import test from 'ava';
import '../index.js';

// See https://github.com/zloirock/core-js/issues/1092
// Does not detect https://github.com/endojs/endo/issues/2598 because
// `push` is not toplevel.
// See tolerate-empty-prototype-toplevel.test.js
const originalPush = Array.prototype.push;
// eslint-disable-next-line no-extend-native
Array.prototype.push = function push(...args) {
Expand Down
Loading