Skip to content

Commit

Permalink
feat: add regenerator runtime taming (#2383)
Browse files Browse the repository at this point in the history
Closes: #621
Refs: #1950

## Description

regenerator-runtime is a widely used package in the ecosystem. It is
used to support generators and async functions transpiled to ES5.

This PR adds an option `legacyRegeneratorRuntimeTaming` to fix
`regenerator-runtime` from 0.10.5 to 0.13.7. Although the newer version
of the regenerator runtime package is compatible with lockdown, some
libraries bundle old (hence "legacy") regenerator runtime in their code
and it's not practical to get them all to upgrade.

- `legacyRegeneratorRuntimeTaming: 'safe'` do nothing.
- `legacyRegeneratorRuntimeTaming: 'unsafe-ignore'` turns
`Iterator.prototype[@@iterator]` to a funky accessor that drops all
assignments to it.

Note: `regenerator-runtime` is doing this:

```js
Gp[iteratorSymbol] = function () {
    return this;
}
```
which is effectively
```js
IteratorPrototype[Symbol.iterator] = function () {
    return this;
}
```

### Security Considerations

The replacement function from legacy regenerator runtime is the same as
the native code, so it is "safe" to drop this assignment, in the sense
that it does not cause any bad effects. However, this option drops the
assignment by dropping any assignment to
`IteratorPrototype[Symbol.iterator]`, since we have no practical way to
ensure that the assignment it drops is exactly the one above. Thus, this
option is not actual safe since it causes any other such assignment to
be ignored silently. This echoes the unsafety of ES3 and of sloppy mode,
where failed assignments were silently ignored. Such behavior is unsafe
because it allows control flow to proceed into code that assumes the
assignment succeeded. That's why ES5 strict mode changed failed
assignments to throw.

To emphasize the hazard, we have named this setting of the option
`'unsafe-ignore'`.

### Scaling Considerations

Nothing

### Documentation Considerations

If you're hitting problems with an old version of regenerator-runtime
(or any package that bundles it), you might need this.

### Testing Considerations

Tests added for the specific effect of this PR. However, to avoid
introducing even a devDependency on a legacy version of regenerator
runtime, no automated test has been added to test that compatibility.
Instead, this PR can be tested like this:

```js
import './lockdown.umd.js'
lockdown({ legacyRegeneratorRuntimeTaming: 'unsafe-ignore', errorTaming: 'unsafe', consoleTaming: 'unsafe' })
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.js'
document.head.appendChild(script)
```

### Compatibility Considerations

Note: Some version of `regenerator-runtime` requires to be run in the
sloppy mode. Thus, these are incompat with the ses-shim independent of
this option.

### Upgrade Considerations

No

> Update `NEWS.md` for user-facing changes.

TODO.
  • Loading branch information
Jack-Works authored Aug 13, 2024
1 parent 59b756e commit 6ae7995
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/ses/src/commons.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export const { prototype: generatorPrototype } = getPrototypeOf(
// eslint-disable-next-line no-empty-function, func-names
function* () {},
);
export const iteratorPrototype = getPrototypeOf(
// eslint-disable-next-line @endo/no-polymorphic-call
getPrototypeOf(arrayPrototype.values()),
);

export const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype);

Expand Down
12 changes: 12 additions & 0 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ 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';
import { tameRegeneratorRuntime } from './tame-regenerator-runtime.js';

/** @import {LockdownOptions} from '../types.js' */

Expand Down Expand Up @@ -180,12 +181,20 @@ export const repairIntrinsics = (options = {}) => {
/** @param {string} debugName */
debugName => debugName !== '',
),
legacyRegeneratorRuntimeTaming = getenv(
'LOCKDOWN_LEGACY_REGENERATOR_RUNTIME_TAMING',
'safe',
),
__hardenTaming__ = getenv('LOCKDOWN_HARDEN_TAMING', 'safe'),
dateTaming = 'safe', // deprecated
mathTaming = 'safe', // deprecated
...extraOptions
} = options;

legacyRegeneratorRuntimeTaming === 'safe' ||
legacyRegeneratorRuntimeTaming === 'unsafe-ignore' ||
Fail`lockdown(): non supported option legacyRegeneratorRuntimeTaming: ${q(legacyRegeneratorRuntimeTaming)}`;

evalTaming === 'unsafeEval' ||
evalTaming === 'safeEval' ||
evalTaming === 'noEval' ||
Expand Down Expand Up @@ -412,6 +421,9 @@ export const repairIntrinsics = (options = {}) => {
// clear yet which is better.
// @ts-ignore enablePropertyOverrides does its own input validation
enablePropertyOverrides(intrinsics, overrideTaming, overrideDebug);
if (legacyRegeneratorRuntimeTaming === 'unsafe-ignore') {
tameRegeneratorRuntime();
}

// Finally register and optionally freeze all the intrinsics. This
// must be the operation that modifies the intrinsics.
Expand Down
29 changes: 29 additions & 0 deletions packages/ses/src/tame-regenerator-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
defineProperty,
iteratorPrototype,
iteratorSymbol,
objectHasOwnProperty,
} from './commons.js';

export const tameRegeneratorRuntime = () => {
const iter = iteratorPrototype[iteratorSymbol];
defineProperty(iteratorPrototype, iteratorSymbol, {
configurable: true,
get() {
return iter;
},
set(value) {
// ignore the assignment on IteratorPrototype
if (this === iteratorPrototype) return;
if (objectHasOwnProperty(this, iteratorSymbol)) {
this[iteratorSymbol] = value;
}
defineProperty(this, iteratorSymbol, {
value,
writable: true,
enumerable: true,
configurable: true,
});
},
});
};
45 changes: 45 additions & 0 deletions packages/ses/test/tame-legacy-regenerator-helper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import test from 'ava';
import '../index.js';

lockdown({ legacyRegeneratorRuntimeTaming: 'unsafe-ignore' });

test('lockdown Iterator.prototype[@@iterator] is tamed', t => {
const IteratorProto = Object.getPrototypeOf(
Object.getPrototypeOf([].values()),
);
const desc = Object.getOwnPropertyDescriptor(IteratorProto, Symbol.iterator);
if (!desc || !desc.get || !desc.set) throw new Error('unreachable');
t.is(desc.configurable || desc.enumerable, false);
t.is(desc.value, undefined);

const { get } = desc;

const child = Object.create(IteratorProto);
child[Symbol.iterator] = 'foo'; // override test
t.is(child[Symbol.iterator], 'foo');

const native = get();
IteratorProto[Symbol.iterator] = function f() {
return this;
};
t.is(get(), native);
t.is(
Function.prototype.toString.call(native),
'function [Symbol.iterator]() { [native code] }',
);
});

test('lockdown Iterator.prototype[@@iterator] is tamed in Compartments', t => {
const c = new Compartment();
const compartmentIteratorProto = Object.getPrototypeOf(
Object.getPrototypeOf(c.globalThis.Array().values()),
);
t.is(
compartmentIteratorProto,
Object.getPrototypeOf(Object.getPrototypeOf([].values())),
);
const parentFunction = /** @type {any} */ (
Object.getOwnPropertyDescriptor(compartmentIteratorProto, Symbol.iterator)
).get.constructor;
t.throws(() => Reflect.construct(parentFunction, ['return globalThis']));
});
6 changes: 6 additions & 0 deletions packages/ses/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export interface RepairOptions {
overrideTaming?: 'moderate' | 'min' | 'severe';
overrideDebug?: Array<string>;
domainTaming?: 'safe' | 'unsafe';
/**
* safe (default): do nothing.
*
* unsafe-ignore: make %IteratorPrototype%[@@iterator] to a funky accessor which ignores all assignments.
*/
legacyRegeneratorRuntimeTaming?: 'safe' | 'unsafe-ignore';
__hardenTaming__?: 'safe' | 'unsafe';
}

Expand Down

0 comments on commit 6ae7995

Please sign in to comment.