feat(ses): Harden typed arrays
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
User-visible changes in SES:
+# Next release
+
+- Harden now gives special treatment to typed arrays.
+ Instead of freezing a typed array, harden will seal it and make all of its
+ non-integer properties non-writable.
+ All of their integer properties will remain writable and non-configurable.
+ TypedArrays are exceedingly unusual because their integer properties are
+ writable and neither freeze nor defineProperty can convert them from writable
+ to non-writable.
+
# v0.15.3 (2022-01-21)
- Fixes the type definition for assert.error so that the final options bag,

MODIFY
packages/ses/src/commons.js
packages/ses/src/commons.js
@@ -62,6 +62,7 @@ export const {
keys,
prototype: objectPrototype,
seal,
+ preventExtensions,
setPrototypeOf,
values,
} = Object;
@@ -73,6 +74,8 @@ export const {
matchAll: matchAllSymbol,
} = Symbol;
+export const { isInteger } = Number;
+
export const { stringify: stringifyJson } = JSON;
// At time of this writing, we still support Node 10 which doesn't have
@@ -137,6 +140,8 @@ export const { prototype: weaksetPrototype } = WeakSet;
export const { prototype: functionPrototype } = Function;
export const { prototype: promisePrototype } = Promise;
+export const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype);
+
/**
* uncurryThis()
* This form of uncurry uses Reflect.apply()
@@ -174,6 +179,7 @@ export const mapHas = uncurryThis(mapPrototype.has);
export const iterateMap = uncurryThis(mapPrototype[iteratorSymbol]);
//
export const setAdd = uncurryThis(setPrototype.add);
+export const setDelete = uncurryThis(setPrototype.delete);
export const setForEach = uncurryThis(setPrototype.forEach);
export const setHas = uncurryThis(setPrototype.has);
export const iterateSet = uncurryThis(setPrototype[iteratorSymbol]);

MODIFY
packages/ses/src/make-hardener.js
packages/ses/src/make-hardener.js
@@ -27,26 +27,88 @@ import {
TypeError,
WeakMap,
WeakSet,
+ apply,
arrayForEach,
+ defineProperty,
freeze,
+ getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
getPrototypeOf,
+ isInteger,
isObject,
objectHasOwnProperty,
ownKeys,
+ preventExtensions,
setAdd,
setForEach,
setHas,
+ toStringTagSymbol,
+ typedArrayPrototype,
weakmapGet,
weakmapSet,
weaksetAdd,
weaksetHas,
} from './commons.js';
+import { assert } from './error/assert.js';
/**
* @typedef {import('../index.js').Harden} Harden
*/
+// 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.
+/** @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;
+};
+
+/**
+ * @template T
+ * @param {ArrayLike<T>} array
+ */
+const freezeTypedArray = array => {
+ const descs = getOwnPropertyDescriptors(array);
+
+ preventExtensions(array);
+
+ // Downgrade writable expandos to readonly, even if non-configurable.
+ arrayForEach(ownKeys(descs), (/** @type {string | symbol} */ name) => {
+ const desc = descs[/** @type {string} */ (name)];
+ // The numbered properties are writable and non-configurable,
+ // and cannot be made non-writable by defineProperty.
+ // This is a strange behavior intrinsic to TypedArrays, but no more harmful
+ // than the mutability of properties of a hardened Map or Set,
+ // so we carve out this exceptional behavior.
+ //
+ // TypedArrays are integer-indexed exotic objects, so indexed properties
+ // outside the range of 0 to the typed array's length are disallowed.
+ // Assignment to these indexes silently fails and defining an indexed
+ // property throws an error.
+ // So, we only need to make non-index properties non-writable and
+ // non-configurable.
+ // https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects
+ const number = +String(name);
+ if (!isInteger(number)) {
+ defineProperty(array, name, {
+ ...desc,
+ writable: false,
+ configurable: false,
+ });
+ }
+ });
+};
+
/**
* Create a `harden` function.
*
@@ -101,7 +163,12 @@ export const makeHardener = () => {
// 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

ADD
packages/ses/test/test-is-typed-array.js
+/* global BigInt64Array, BigUint64Array */
+// @ts-check
+
+import test from 'ava';
+import '../index.js';
+
+import { isTypedArray } from '../src/make-hardener.js';
+
+// Tests borrowed from https://github.com/inspect-js/is-typed-array
+// https://github.com/inspect-js/is-typed-array/blob/c1ad8eae617870e3b3700dda0ff736a267b7e990/LICENSE
+// MIT license.
+
+test('isTypedArray positive cases', t => {
+ t.assert(isTypedArray(new Int8Array()));
+ t.assert(isTypedArray(new Uint8Array()));
+ t.assert(isTypedArray(new Uint8ClampedArray()));
+ t.assert(isTypedArray(new Int16Array()));
+ t.assert(isTypedArray(new Uint16Array()));
+ t.assert(isTypedArray(new Int32Array()));
+ t.assert(isTypedArray(new Uint32Array()));
+ t.assert(isTypedArray(new Float32Array()));
+ t.assert(isTypedArray(new Float64Array()));
+ t.assert(isTypedArray(new BigInt64Array()));
+ t.assert(isTypedArray(new BigUint64Array()));
+});
+
+test('isTypedArray negative cases', t => {
+ t.assert(!isTypedArray(undefined));
+ t.assert(!isTypedArray(null));
+ t.assert(!isTypedArray(false));
+ t.assert(!isTypedArray(true));
+ t.assert(!isTypedArray([]));
+ t.assert(!isTypedArray({}));
+ t.assert(!isTypedArray(/a/g));
+ t.assert(!isTypedArray(new RegExp('a', 'g')));
+ t.assert(!isTypedArray(new Date()));
+ t.assert(!isTypedArray(42));
+ t.assert(!isTypedArray(NaN));
+ t.assert(!isTypedArray(Infinity));
+ // eslint-disable-next-line no-new-wrappers
+ t.assert(!isTypedArray(new Number(42)));
+ t.assert(!isTypedArray('foo'));
+ t.assert(!isTypedArray(Object('foo')));
+ t.assert(!isTypedArray(function f() {}));
+ t.assert(
+ !isTypedArray(function* g() {
+ yield;
+ }),
+ );
+ t.assert(!isTypedArray(() => {}));
+ t.assert(!isTypedArray([]));
+ t.assert(!isTypedArray(new ArrayBuffer(1)));
+});

MODIFY
packages/ses/test/test-make-hardener.js
packages/ses/test/test-make-hardener.js
+/* global BigInt64Array, BigUint64Array */
+// @ts-check
+
import test from 'ava';
import { makeHardener } from '../src/make-hardener.js';
@@ -49,7 +52,7 @@ test('harden up prototype chain', t => {
t.truthy(Object.isFrozen(a));
});
-test('harden() tolerates objects with null prototypes', t => {
+test('harden tolerates objects with null prototypes', t => {
const h = makeHardener();
const o = { a: 1 };
Object.setPrototypeOf(o, null);
@@ -57,3 +60,160 @@ test('harden() tolerates objects with null prototypes', t => {
t.truthy(Object.isFrozen(o));
t.truthy(Object.isFrozen(o.a));
});
+
+test('harden typed arrays', t => {
+ const typedArrayConstructors = [
+ BigInt64Array,
+ BigUint64Array,
+ Float32Array,
+ Float64Array,
+ Int16Array,
+ Int32Array,
+ Int8Array,
+ Uint16Array,
+ Uint32Array,
+ Uint8Array,
+ Uint8ClampedArray,
+ ];
+
+ for (const TypedArray of typedArrayConstructors) {
+ const h = makeHardener();
+ const a = new TypedArray(1);
+
+ t.is(h(a), a, `harden ${TypedArray}`);
+ t.truthy(Object.isSealed(a));
+ t.deepEqual(
+ {
+ value: a[0],
+ writable: true, // Note that indexes of typed arrays are exceptionally writable for hardened objects.
+ configurable: false,
+ enumerable: true,
+ },
+ Object.getOwnPropertyDescriptor(a, '0'),
+ );
+ }
+});
+
+test('harden typed arrays and their expandos', t => {
+ const h = makeHardener();
+ const a = new Uint8Array(1);
+ // @ts-ignore
+ a.x = { y: { z: 10 } };
+
+ t.is(h(a), a);
+ t.truthy(Object.isSealed(a));
+
+ t.deepEqual(
+ {
+ value: 0,
+ writable: true, // Note that indexes of typed arrays are exceptionally writable for hardened objects.
+ configurable: false,
+ enumerable: true,
+ },
+ Object.getOwnPropertyDescriptor(a, '0'),
+ );
+
+ t.deepEqual(
+ {
+ // @ts-ignore
+ value: a.x,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ Object.getOwnPropertyDescriptor(a, 'x'),
+ );
+
+ // @ts-ignore
+ t.truthy(Object.isFrozen(a.x));
+ // @ts-ignore
+ t.truthy(Object.isFrozen(a.x.y));
+ // @ts-ignore
+ t.truthy(Object.isFrozen(a.x.y.z));
+});
+
+test('hardening makes writable properties readonly even if non-configurable', t => {
+ const h = makeHardener();
+ const o = {};
+ Object.defineProperty(o, 'x', {
+ value: 10,
+ writable: true,
+ configurable: false,
+ enumerable: false,
+ });
+ h(o);
+ t.deepEqual(
+ {
+ value: 10,
+ writable: false,
+ configurable: false,
+ enumerable: false,
+ },
+ Object.getOwnPropertyDescriptor(o, 'x'),
+ );
+});
+
+test('harden a typed array with a writable non-configurable expando', t => {
+ const h = makeHardener();
+ const a = new Uint8Array(1);
+ Object.defineProperty(a, 'x', {
+ value: 'A',
+ writable: true,
+ configurable: false,
+ enumerable: false,
+ });
+
+ t.is(h(a), a);
+ t.truthy(Object.isSealed(a));
+
+ t.deepEqual(
+ {
+ value: 'A',
+ writable: false,
+ configurable: false,
+ enumerable: false,
+ },
+ Object.getOwnPropertyDescriptor(a, 'x'),
+ );
+});
+
+test('harden a typed array subclass', t => {
+ const h = makeHardener();
+
+ class Ooint8Array extends Uint8Array {
+ oo = 'ghosts';
+ }
+
+ const a = new Ooint8Array(1);
+ t.is(h(a), a);
+
+ t.deepEqual(
+ {
+ value: 'ghosts',
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ Object.getOwnPropertyDescriptor(a, 'oo'),
+ );
+ t.truthy(Object.isFrozen(Ooint8Array.prototype));
+ t.truthy(Object.isFrozen(Object.getPrototypeOf(Ooint8Array.prototype)));
+});
+
+test('harden depends on invariant: typed arrays have no storage for integer indexes beyond length', t => {
+ const a = new Uint8Array(1);
+ a[1] = 1;
+ t.is(a[1], undefined);
+});
+
+test('harden depends on invariant: typed arrays cannot have integer expandos', t => {
+ const a = new Uint8Array(1);
+ t.throws(() => {
+ Object.defineProperty(a, '1', {
+ value: 'A',
+ writable: true,
+ configurable: false,
+ enumerable: false,
+ });
+ });
+});

feat(ses): Harden typed arrays (#1032)