ses
Version:
Hardened JavaScript for Fearless Cooperation
276 lines (249 loc) • 9.04 kB
JavaScript
// Adapted from SES/Caja - Copyright (C) 2011 Google Inc.
// Copyright (C) 2018 Agoric
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// based upon:
// https://github.com/google/caja/blob/master/src/com/google/caja/ses/startSES.js
// https://github.com/google/caja/blob/master/src/com/google/caja/ses/repairES5.js
// then copied from proposal-frozen-realms deep-freeze.js
// then copied from SES/src/bundle/deepFreeze.js
// @ts-check
import {
Set,
String,
TypeError,
WeakSet,
globalThis,
apply,
arrayForEach,
defineProperty,
freeze,
getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
getPrototypeOf,
isInteger,
isObject,
objectHasOwnProperty,
ownKeys,
preventExtensions,
setAdd,
setForEach,
setHas,
toStringTagSymbol,
typedArrayPrototype,
weaksetAdd,
weaksetHas,
FERAL_STACK_GETTER,
FERAL_STACK_SETTER,
isError,
} from './commons.js';
import { assert } from './error/assert.js';
/**
* @import {Harden} from '../types.js'
*/
// Obtain the string tag accessor of of TypedArray so we can indirectly use the
// TypedArray brand check it employs.
const typedArrayToStringTag = getOwnPropertyDescriptor(
typedArrayPrototype,
toStringTagSymbol,
);
assert(typedArrayToStringTag);
const getTypedArrayToStringTag = typedArrayToStringTag.get;
assert(getTypedArrayToStringTag);
// Exported for tests.
/**
* Duplicates packages/marshal/src/helpers/passStyle-helpers.js to avoid a dependency.
*
* @param {unknown} object
*/
export const isTypedArray = object => {
// The object must pass a brand check or toStringTag will return undefined.
const tag = apply(getTypedArrayToStringTag, object, []);
return tag !== undefined;
};
/**
* Tests if a property key is an integer-valued canonical numeric index.
* https://tc39.es/ecma262/#sec-canonicalnumericindexstring
*
* @param {string | symbol} propertyKey
*/
const isCanonicalIntegerIndexString = propertyKey => {
const n = +String(propertyKey);
return isInteger(n) && String(n) === propertyKey;
};
/**
* @template T
* @param {ArrayLike<T>} array
*/
const freezeTypedArray = array => {
preventExtensions(array);
// Downgrade writable expandos to readonly, even if non-configurable.
// We get each descriptor individually rather than using
// getOwnPropertyDescriptors in order to fail safe when encountering
// an obscure GraalJS issue where getOwnPropertyDescriptor returns
// undefined for a property that does exist.
arrayForEach(ownKeys(array), (/** @type {string | symbol} */ name) => {
const desc = getOwnPropertyDescriptor(array, name);
assert(desc);
// TypedArrays are integer-indexed exotic objects, which define special
// treatment for property names in canonical numeric form:
// integers in range are permanently writable and non-configurable.
// https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects
//
// This is analogous to the data of a hardened Map or Set,
// so we carve out this exceptional behavior but make all other
// properties non-configurable.
if (!isCanonicalIntegerIndexString(name)) {
defineProperty(array, name, {
...desc,
writable: false,
configurable: false,
});
}
});
};
/**
* Create a `harden` function.
*
* @returns {Harden}
*/
export const makeHardener = () => {
// Use a native hardener if possible.
if (typeof globalThis.harden === 'function') {
const safeHarden = globalThis.harden;
return safeHarden;
}
const hardened = new WeakSet();
const { harden } = {
/**
* @template T
* @param {T} root
* @returns {T}
*/
harden(root) {
const toFreeze = new Set();
// If val is something we should be freezing but aren't yet,
// add it to toFreeze.
/**
* @param {any} val
*/
function enqueue(val) {
if (!isObject(val)) {
// ignore primitives
return;
}
const type = typeof val;
if (type !== 'object' && type !== 'function') {
// future proof: break until someone figures out what it should do
throw TypeError(`Unexpected typeof: ${type}`);
}
if (weaksetHas(hardened, val) || setHas(toFreeze, val)) {
// Ignore if this is an exit, or we've already visited it
return;
}
// console.warn(`adding ${val} to toFreeze`, val);
setAdd(toFreeze, val);
}
/**
* @param {any} obj
*/
const baseFreezeAndTraverse = obj => {
// Now freeze the object to ensure reactive
// objects such as proxies won't add properties
// during traversal, before they get frozen.
// Object are verified before being enqueued,
// therefore this is a valid candidate.
// Throws if this fails (strict mode).
// Also throws if the object is an ArrayBuffer or any TypedArray.
if (isTypedArray(obj)) {
freezeTypedArray(obj);
} else {
freeze(obj);
}
// we rely upon certain commitments of Object.freeze and proxies here
// get stable/immutable outbound links before a Proxy has a chance to do
// something sneaky.
const descs = getOwnPropertyDescriptors(obj);
const proto = getPrototypeOf(obj);
enqueue(proto);
arrayForEach(ownKeys(descs), (/** @type {string | symbol} */ name) => {
// The 'name' may be a symbol, and TypeScript doesn't like us to
// index arbitrary symbols on objects, so we pretend they're just
// strings.
const desc = descs[/** @type {string} */ (name)];
// getOwnPropertyDescriptors is guaranteed to return well-formed
// descriptors, but they still inherit from Object.prototype. If
// someone has poisoned Object.prototype to add 'value' or 'get'
// properties, then a simple 'if ("value" in desc)' or 'desc.value'
// test could be confused. We use hasOwnProperty to be sure about
// whether 'value' is present or not, which tells us for sure that
// this is a data property.
if (objectHasOwnProperty(desc, 'value')) {
enqueue(desc.value);
} else {
enqueue(desc.get);
enqueue(desc.set);
}
});
};
const freezeAndTraverse =
FERAL_STACK_GETTER === undefined && FERAL_STACK_SETTER === undefined
? // On platforms without v8's error own stack accessor problem,
// don't pay for any extra overhead.
baseFreezeAndTraverse
: obj => {
if (isError(obj)) {
// Only pay the overhead if it first passes this cheap isError
// check. Otherwise, it will be unrepaired, but won't be judged
// to be a passable error anyway, so will not be unsafe.
const stackDesc = getOwnPropertyDescriptor(obj, 'stack');
if (
stackDesc &&
stackDesc.get === FERAL_STACK_GETTER &&
stackDesc.configurable
) {
// Can only repair if it is configurable. Otherwise, leave
// unrepaired, in which case it will not be judged passable,
// avoiding a safety problem.
defineProperty(obj, 'stack', {
// NOTE: Calls getter during harden, which seems dangerous.
// But we're only calling the problematic getter whose
// hazards we think we understand.
// @ts-expect-error TS should know FERAL_STACK_GETTER
// cannot be `undefined` here.
// See https://github.com/endojs/endo/pull/2232#discussion_r1575179471
value: apply(FERAL_STACK_GETTER, obj, []),
});
}
}
return baseFreezeAndTraverse(obj);
};
const dequeue = () => {
// New values added before forEach() has finished will be visited.
setForEach(toFreeze, freezeAndTraverse);
};
/** @param {any} value */
const markHardened = value => {
weaksetAdd(hardened, value);
};
const commit = () => {
setForEach(toFreeze, markHardened);
};
enqueue(root);
dequeue();
// console.warn("toFreeze set:", toFreeze);
commit();
return root;
},
};
return harden;
};