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 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
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.
Copy link
Member

Choose a reason for hiding this comment

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

I do not have the details paged into active brain right now, but this looks like it might be relevant to a mitigation @leotm needs for Hermes support.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@leotm , care to comment? Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#2563 (and #2334 ?), right?

*
* 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;
}
};
8 changes: 7 additions & 1 deletion packages/ses/src/compartment-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { globalThis } from './commons.js';
import { makeCompartmentConstructor } from './compartment.js';
import { tameFunctionToString } from './tame-function-tostring.js';
import { getGlobalIntrinsics } from './intrinsics.js';
import { chooseReporter } from './reporting.js';

const markVirtualizedNativeFunction = tameFunctionToString();

const muteReporter = chooseReporter('none');

// @ts-ignore Compartment is definitely on globalThis.
globalThis.Compartment = makeCompartmentConstructor(
makeCompartmentConstructor,
getGlobalIntrinsics(globalThis),
// Any reporting that would need to be done should have already been done
// during `lockdown()`.
// See https://github.com/endojs/endo/pull/2624#discussion_r1840979770
getGlobalIntrinsics(globalThis, muteReporter),
markVirtualizedNativeFunction,
);
26 changes: 22 additions & 4 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
*/
export const makeIntrinsicsCollector = reporter => {
/** @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 @@ -164,9 +180,11 @@ export const makeIntrinsicsCollector = () => {
* *original* unsafe (feral, untamed) bindings of these global variables.
*
* @param {object} globalObject
* @param {Reporter} reporter
*/
export const getGlobalIntrinsics = globalObject => {
const { addIntrinsics, finalIntrinsics } = makeIntrinsicsCollector();
export const getGlobalIntrinsics = (globalObject, reporter) => {
// TODO pass a proper reporter to `makeIntrinsicsCollector`
erights marked this conversation as resolved.
Show resolved Hide resolved
const { addIntrinsics, finalIntrinsics } = makeIntrinsicsCollector(reporter);

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
33 changes: 3 additions & 30 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 All @@ -77,7 +78,7 @@ import {
export default function removeUnpermittedIntrinsics(
intrinsics,
markVirtualizedNativeFunction,
{ warn, error },
reporter,
) {
// These primitives are allowed for permits.
const primitives = ['undefined', 'boolean', 'number', 'string', 'symbol'];
Expand Down Expand Up @@ -279,35 +280,7 @@ 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, reporter);
}
}
}
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