-
Notifications
You must be signed in to change notification settings - Fork 75
/
Copy pathenable-property-overrides.js
204 lines (189 loc) · 6.69 KB
/
enable-property-overrides.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// Adapted from SES/Caja
// Copyright (C) 2011 Google Inc.
// @ts-check
import {
Set,
String,
TypeError,
arrayForEach,
defineProperty,
getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
getOwnPropertyNames,
isObject,
objectHasOwnProperty,
ownKeys,
setHas,
} from './commons.js';
import {
minEnablements,
moderateEnablements,
severeEnablements,
} from './enablements.js';
/**
* For a special set of properties defined in the `enablement` whitelist,
* `enablePropertyOverrides` ensures that the effect of freezing does not
* suppress the ability to override these properties on derived objects by
* simple assignment.
*
* Because of lack of sufficient foresight at the time, ES5 unfortunately
* specified that a simple assignment to a non-existent property must fail if
* it would override an non-writable data property of the same name in the
* shadow of the prototype chain. In retrospect, this was a mistake, the
* so-called "override mistake". But it is now too late and we must live with
* the consequences.
*
* As a result, simply freezing an object to make it tamper proof has the
* unfortunate side effect of breaking previously correct code that is
* considered to have followed JS best practices, if this previous code used
* assignment to override.
*
* For the enabled properties, `enablePropertyOverrides` effectively shims what
* the assignment behavior would have been in the absence of the override
* mistake. However, the shim produces an imperfect emulation. It shims the
* behavior by turning these data properties into accessor properties, where
* the accessor's getter and setter provide the desired behavior. For
* non-reflective operations, the illusion is perfect. However, reflective
* operations like `getOwnPropertyDescriptor` see the descriptor of an accessor
* property rather than the descriptor of a data property. At the time of this
* writing, this is the best we know how to do.
*
* To the getter of the accessor we add a property named
* `'originalValue'` whose value is, as it says, the value that the
* data property had before being converted to an accessor property. We add
* this extra property to the getter for two reason:
*
* The harden algorithm walks the own properties reflectively, i.e., with
* `getOwnPropertyDescriptor` semantics, rather than `[[Get]]` semantics. When
* it sees an accessor property, it does not invoke the getter. Rather, it
* proceeds to walk both the getter and setter as part of its transitive
* traversal. Without this extra property, `enablePropertyOverrides` would have
* hidden the original data property value from `harden`, which would be bad.
* Instead, by exposing that value in an own data property on the getter,
* `harden` finds and walks it anyway.
*
* We enable a form of cooperative emulation, giving reflective code an
* opportunity to cooperate in upholding the illusion. When such cooperative
* reflective code sees an accessor property, where the accessor's getter
* has an `originalValue` property, it knows that the getter is
* alleging that it is the result of the `enablePropertyOverrides` conversion
* pattern, so it can decide to cooperatively "pretend" that it sees a data
* property with that value.
*
* @param {Record<string, any>} intrinsics
* @param {'min' | 'moderate' | 'severe'} overrideTaming
* @param {Iterable<string | symbol>} [overrideDebug]
*/
export default function enablePropertyOverrides(
intrinsics,
overrideTaming,
overrideDebug = [],
) {
const debugProperties = new Set(overrideDebug);
function enable(path, obj, prop, desc) {
if ('value' in desc && desc.configurable) {
const { value } = desc;
function getter() {
return value;
}
defineProperty(getter, 'originalValue', {
value,
writable: false,
enumerable: false,
configurable: false,
});
const isDebug = setHas(debugProperties, prop);
function setter(newValue) {
if (obj === this) {
throw new TypeError(
`Cannot assign to read only property '${String(
prop,
)}' of '${path}'`,
);
}
if (objectHasOwnProperty(this, prop)) {
this[prop] = newValue;
} else {
if (isDebug) {
// eslint-disable-next-line @endo/no-polymorphic-call
console.error(new TypeError(`Override property ${prop}`));
}
defineProperty(this, prop, {
value: newValue,
writable: true,
enumerable: true,
configurable: true,
});
}
}
defineProperty(obj, prop, {
get: getter,
set: setter,
enumerable: desc.enumerable,
configurable: desc.configurable,
});
}
}
function enableProperty(path, obj, prop) {
const desc = getOwnPropertyDescriptor(obj, prop);
if (!desc) {
return;
}
enable(path, obj, prop, desc);
}
function enableAllProperties(path, obj) {
const descs = getOwnPropertyDescriptors(obj);
if (!descs) {
return;
}
// TypeScript does not allow symbols to be used as indexes because it
// cannot recokon types of symbolized properties.
// @ts-ignore
arrayForEach(ownKeys(descs), prop => enable(path, obj, prop, descs[prop]));
}
function enableProperties(path, obj, plan) {
for (const prop of getOwnPropertyNames(plan)) {
const desc = getOwnPropertyDescriptor(obj, prop);
if (!desc || desc.get || desc.set) {
// No not a value property, nothing to do.
// eslint-disable-next-line no-continue
continue;
}
// Plan has no symbol keys and we use getOwnPropertyNames()
// so `prop` cannot only be a string, not a symbol. We coerce it in place
// with `String(..)` anyway just as good hygiene, since these paths are just
// for diagnostic purposes.
const subPath = `${path}.${String(prop)}`;
const subPlan = plan[prop];
if (subPlan === true) {
enableProperty(subPath, obj, prop);
} else if (subPlan === '*') {
enableAllProperties(subPath, desc.value);
} else if (isObject(subPlan)) {
enableProperties(subPath, desc.value, subPlan);
} else {
throw new TypeError(`Unexpected override enablement plan ${subPath}`);
}
}
}
let plan;
switch (overrideTaming) {
case 'min': {
plan = minEnablements;
break;
}
case 'moderate': {
plan = moderateEnablements;
break;
}
case 'severe': {
plan = severeEnablements;
break;
}
default: {
throw new TypeError(`unrecognized overrideTaming ${overrideTaming}`);
}
}
// Do the repair.
enableProperties('root', intrinsics, plan);
}