Skip to content

Commit e607055

Browse files
guybedfordtargos
authored andcommitted
bootstrap: --frozen-intrinsics override problem workaround
PR-URL: #28254 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 64f8530 commit e607055

File tree

3 files changed

+225
-73
lines changed

3 files changed

+225
-73
lines changed

doc/api/cli.md

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -209,44 +209,7 @@ Enable experimental frozen intrinsics like `Array` and `Object`.
209209

210210
Support is currently only provided for the root context and no guarantees are
211211
currently provided that `global.Array` is indeed the default intrinsic
212-
reference.
213-
214-
**Code breakage is highly likely with this flag**, since redefining any
215-
builtin properties on a subclass will throw in strict mode due to the ECMA-262
216-
issue https://github.com/tc39/ecma262/pull/1307. This flag may still change
217-
or be removed in the future.
218-
219-
To avoid these cases, any builtin function overrides should be defined upfront:
220-
221-
```js
222-
const o = {};
223-
// THROWS: Cannot assign read only property 'toString' of object
224-
o.toString = () => 'string';
225-
226-
class X {
227-
constructor() {
228-
this.toString = () => 'string';
229-
}
230-
}
231-
// THROWS: Cannot assign read only property 'toString' of object
232-
new X();
233-
```
234-
235-
```js
236-
// OK
237-
const o = { toString: () => 'string' };
238-
239-
class X {
240-
toString = undefined;
241-
constructor() {
242-
this.toString = () => 'string';
243-
}
244-
}
245-
// OK
246-
new X();
247-
```
248-
249-
212+
reference. Code may break under this flag.
250213

251214
### `--heapsnapshot-signal=signal`
252215
<!-- YAML

lib/internal/freeze_intrinsics.js

Lines changed: 203 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,29 @@
1212
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
15-
// SPDX-License-Identifier: MIT
15+
// SPDX-License-Identifier: Apache-2.0
1616

1717
// Based upon:
1818
// https://github.com/google/caja/blob/master/src/com/google/caja/ses/startSES.js
1919
// https://github.com/google/caja/blob/master/src/com/google/caja/ses/repairES5.js
20-
// https://github.com/tc39/proposal-frozen-realms/blob/91ac390e3451da92b5c27e354b39e52b7636a437/shim/src/deep-freeze.js
20+
// https://github.com/tc39/proposal-ses/blob/e5271cc42a257a05dcae2fd94713ed2f46c08620/shim/src/freeze.js
2121

22-
/* global WebAssembly, SharedArrayBuffer */
22+
/* global WebAssembly, SharedArrayBuffer, console */
2323
/* eslint-disable no-restricted-globals */
2424
'use strict';
2525

2626
module.exports = function() {
27+
const {
28+
defineProperty,
29+
freeze,
30+
getOwnPropertyDescriptor,
31+
getOwnPropertyDescriptors,
32+
getOwnPropertyNames,
33+
getOwnPropertySymbols,
34+
getPrototypeOf
35+
} = Object;
36+
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
37+
const { ownKeys } = Reflect;
2738
const {
2839
clearImmediate,
2940
clearInterval,
@@ -33,30 +44,112 @@ module.exports = function() {
3344
setTimeout
3445
} = require('timers');
3546

47+
const intrinsicPrototypes = [
48+
// Anonymous Intrinsics
49+
// IteratorPrototype
50+
getPrototypeOf(
51+
getPrototypeOf(new Array()[Symbol.iterator]())
52+
),
53+
// ArrayIteratorPrototype
54+
getPrototypeOf(new Array()[Symbol.iterator]()),
55+
// StringIteratorPrototype
56+
getPrototypeOf(new String()[Symbol.iterator]()),
57+
// MapIteratorPrototype
58+
getPrototypeOf(new Map()[Symbol.iterator]()),
59+
// SetIteratorPrototype
60+
getPrototypeOf(new Set()[Symbol.iterator]()),
61+
// GeneratorFunction
62+
getPrototypeOf(function* () {}),
63+
// AsyncFunction
64+
getPrototypeOf(async function() {}),
65+
// AsyncGeneratorFunction
66+
getPrototypeOf(async function* () {}),
67+
// TypedArray
68+
getPrototypeOf(Uint8Array),
69+
70+
// 19 Fundamental Objects
71+
Object.prototype, // 19.1
72+
Function.prototype, // 19.2
73+
Boolean.prototype, // 19.3
74+
75+
Error.prototype, // 19.5
76+
EvalError.prototype,
77+
RangeError.prototype,
78+
ReferenceError.prototype,
79+
SyntaxError.prototype,
80+
TypeError.prototype,
81+
URIError.prototype,
82+
83+
// 20 Numbers and Dates
84+
Number.prototype, // 20.1
85+
Date.prototype, // 20.3
86+
87+
// 21 Text Processing
88+
String.prototype, // 21.1
89+
RegExp.prototype, // 21.2
90+
91+
// 22 Indexed Collections
92+
Array.prototype, // 22.1
93+
94+
Int8Array.prototype,
95+
Uint8Array.prototype,
96+
Uint8ClampedArray.prototype,
97+
Int16Array.prototype,
98+
Uint16Array.prototype,
99+
Int32Array.prototype,
100+
Uint32Array.prototype,
101+
Float32Array.prototype,
102+
Float64Array.prototype,
103+
BigInt64Array.prototype,
104+
BigUint64Array.prototype,
105+
106+
// 23 Keyed Collections
107+
Map.prototype, // 23.1
108+
Set.prototype, // 23.2
109+
WeakMap.prototype, // 23.3
110+
WeakSet.prototype, // 23.4
111+
112+
// 24 Structured Data
113+
ArrayBuffer.prototype, // 24.1
114+
DataView.prototype, // 24.3
115+
Promise.prototype, // 25.4
116+
117+
// Other APIs / Web Compatibility
118+
console.Console.prototype,
119+
BigInt.prototype,
120+
WebAssembly.Module.prototype,
121+
WebAssembly.Instance.prototype,
122+
WebAssembly.Table.prototype,
123+
WebAssembly.Memory.prototype,
124+
WebAssembly.CompileError.prototype,
125+
WebAssembly.LinkError.prototype,
126+
WebAssembly.RuntimeError.prototype,
127+
SharedArrayBuffer.prototype
128+
];
36129
const intrinsics = [
37130
// Anonymous Intrinsics
38131
// ThrowTypeError
39-
Object.getOwnPropertyDescriptor(Function.prototype, 'caller').get,
132+
getOwnPropertyDescriptor(Function.prototype, 'caller').get,
40133
// IteratorPrototype
41-
Object.getPrototypeOf(
42-
Object.getPrototypeOf(new Array()[Symbol.iterator]())
134+
getPrototypeOf(
135+
getPrototypeOf(new Array()[Symbol.iterator]())
43136
),
44137
// ArrayIteratorPrototype
45-
Object.getPrototypeOf(new Array()[Symbol.iterator]()),
138+
getPrototypeOf(new Array()[Symbol.iterator]()),
46139
// StringIteratorPrototype
47-
Object.getPrototypeOf(new String()[Symbol.iterator]()),
140+
getPrototypeOf(new String()[Symbol.iterator]()),
48141
// MapIteratorPrototype
49-
Object.getPrototypeOf(new Map()[Symbol.iterator]()),
142+
getPrototypeOf(new Map()[Symbol.iterator]()),
50143
// SetIteratorPrototype
51-
Object.getPrototypeOf(new Set()[Symbol.iterator]()),
144+
getPrototypeOf(new Set()[Symbol.iterator]()),
52145
// GeneratorFunction
53-
Object.getPrototypeOf(function* () {}),
146+
getPrototypeOf(function* () {}),
54147
// AsyncFunction
55-
Object.getPrototypeOf(async function() {}),
148+
getPrototypeOf(async function() {}),
56149
// AsyncGeneratorFunction
57-
Object.getPrototypeOf(async function* () {}),
150+
getPrototypeOf(async function* () {}),
58151
// TypedArray
59-
Object.getPrototypeOf(Uint8Array),
152+
getPrototypeOf(Uint8Array),
60153

61154
// 18 The Global Object
62155
eval,
@@ -75,14 +168,13 @@ module.exports = function() {
75168
Boolean, // 19.3
76169
Symbol, // 19.4
77170

78-
// Disabled pending stack trace mutation handling
79-
// Error, // 19.5
80-
// EvalError,
81-
// RangeError,
82-
// ReferenceError,
83-
// SyntaxError,
84-
// TypeError,
85-
// URIError,
171+
Error, // 19.5
172+
EvalError,
173+
RangeError,
174+
ReferenceError,
175+
SyntaxError,
176+
TypeError,
177+
URIError,
86178

87179
// 20 Numbers and Dates
88180
Number, // 20.1
@@ -128,36 +220,37 @@ module.exports = function() {
128220
escape,
129221
unescape,
130222

131-
// Web compatibility
223+
// Other APIs / Web Compatibility
132224
clearImmediate,
133225
clearInterval,
134226
clearTimeout,
135227
setImmediate,
136228
setInterval,
137229
setTimeout,
138-
139-
// Other APIs
230+
console,
140231
BigInt,
141232
Atomics,
142233
WebAssembly,
143234
SharedArrayBuffer
144235
];
145236

146-
if (typeof Intl !== 'undefined')
237+
if (typeof Intl !== 'undefined') {
238+
intrinsicPrototypes.push(Intl.Collator.prototype);
239+
intrinsicPrototypes.push(Intl.DateTimeFormat.prototype);
240+
intrinsicPrototypes.push(Intl.ListFormat.prototype);
241+
intrinsicPrototypes.push(Intl.NumberFormat.prototype);
242+
intrinsicPrototypes.push(Intl.PluralRules.prototype);
243+
intrinsicPrototypes.push(Intl.RelativeTimeFormat.prototype);
147244
intrinsics.push(Intl);
245+
}
246+
247+
intrinsicPrototypes.forEach(enableDerivedOverrides);
148248

249+
const frozenSet = new WeakSet();
149250
intrinsics.forEach(deepFreeze);
150251

252+
// Objects that are deeply frozen.
151253
function deepFreeze(root) {
152-
153-
const { freeze, getOwnPropertyDescriptors, getPrototypeOf } = Object;
154-
const { ownKeys } = Reflect;
155-
156-
// Objects that are deeply frozen.
157-
// It turns out that Error is reachable from WebAssembly so it is
158-
// explicitly added here to ensure it is not frozen
159-
const frozenSet = new WeakSet([Error, Error.prototype]);
160-
161254
/**
162255
* "innerDeepFreeze()" acts like "Object.freeze()", except that:
163256
*
@@ -246,4 +339,79 @@ module.exports = function() {
246339
innerDeepFreeze(root);
247340
return root;
248341
}
342+
343+
/**
344+
* For a special set of properties (defined below), it ensures that the
345+
* effect of freezing does not suppress the ability to override these
346+
* properties on derived objects by simple assignment.
347+
*
348+
* Because of lack of sufficient foresight at the time, ES5 unfortunately
349+
* specified that a simple assignment to a non-existent property must fail if
350+
* it would override a non-writable data property of the same name. (In
351+
* retrospect, this was a mistake, but it is now too late and we must live
352+
* with the consequences.) As a result, simply freezing an object to make it
353+
* tamper proof has the unfortunate side effect of breaking previously correct
354+
* code that is considered to have followed JS best practices, if this
355+
* previous code used assignment to override.
356+
*
357+
* To work around this mistake, deepFreeze(), prior to freezing, replaces
358+
* selected configurable own data properties with accessor properties which
359+
* simulate what we should have specified -- that assignments to derived
360+
* objects succeed if otherwise possible.
361+
*/
362+
function enableDerivedOverride(obj, prop, desc) {
363+
if ('value' in desc && desc.configurable) {
364+
const value = desc.value;
365+
366+
function getter() {
367+
return value;
368+
}
369+
370+
// Re-attach the data property on the object so
371+
// it can be found by the deep-freeze traversal process.
372+
getter.value = value;
373+
374+
function setter(newValue) {
375+
if (obj === this) {
376+
// eslint-disable-next-line no-restricted-syntax
377+
throw new TypeError(
378+
`Cannot assign to read only property '${prop}' of object '${obj}'`
379+
);
380+
}
381+
if (objectHasOwnProperty.call(this, prop)) {
382+
this[prop] = newValue;
383+
} else {
384+
defineProperty(this, prop, {
385+
value: newValue,
386+
writable: true,
387+
enumerable: desc.enumerable,
388+
configurable: desc.configurable
389+
});
390+
}
391+
}
392+
393+
defineProperty(obj, prop, {
394+
get: getter,
395+
set: setter,
396+
enumerable: desc.enumerable,
397+
configurable: desc.configurable
398+
});
399+
}
400+
}
401+
402+
function enableDerivedOverrides(obj) {
403+
if (!obj) {
404+
return;
405+
}
406+
const descs = getOwnPropertyDescriptors(obj);
407+
if (!descs) {
408+
return;
409+
}
410+
getOwnPropertyNames(obj).forEach((prop) => {
411+
return enableDerivedOverride(obj, prop, descs[prop]);
412+
});
413+
getOwnPropertySymbols(obj).forEach((prop) => {
414+
return enableDerivedOverride(obj, prop, descs[prop]);
415+
});
416+
}
249417
};

test/parallel/test-freeze-intrinsics.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,24 @@ assert.throws(
77
() => Object.defineProperty = 'asdf',
88
TypeError
99
);
10+
11+
// Ensure we can extend Console
12+
{
13+
class ExtendedConsole extends console.Console {}
14+
15+
const s = new ExtendedConsole(process.stdout);
16+
const logs = [];
17+
s.log = (msg) => logs.push(msg);
18+
s.log('custom');
19+
s.log = undefined;
20+
assert.strictEqual(s.log, undefined);
21+
assert.strictEqual(logs.length, 1);
22+
assert.strictEqual(logs[0], 'custom');
23+
}
24+
25+
// Ensure we can write override Object prototype properties on instances
26+
{
27+
const o = {};
28+
o.toString = () => 'Custom toString';
29+
assert.strictEqual(o + 'asdf', 'Custom toStringasdf');
30+
}

0 commit comments

Comments
 (0)