See this comment for a revised version.
The override mistake is a problem introduced with ES5's ability to mark properties as non-writable. When a property on the prototype of an object is marked non-writable, it makes the property unaddable on the object.
A notable example of this is if we freeze (which marks all properties as non-writable) on Object.prototype
.
Object.freeze(Object.prototype);
When we do this, it becomes impossible to set toString()
on any objects that don't already have a .toString()
.
e.g. the following fails:
"use strict";
Object.freeze(Object.prototype);
const newObject = {};
newObject.toString = () => "Some string"; // TypeError
This is of particular concern to the SES proposal, which needs to freeze all builtins in order to guarantee security properties.
In this proposal we propose a new descriptor member called overridable
,
this member may only be present when writable
is set to false
.
When overridable
is present, non-writable properties on the prototype will be ignored when attempting to set them.
For example the following will print 10
:
"use strict";
const proto = {};
Object.defineProperty(proto, 'example', { value: 5, writable: false, overridable: true });
const object = { __proto__: proto };
object.example = 10;
console.log(object.example);
For SES, we need to be able to able to freeze entire objects, but without creating the problems of the override mistake.
As such a new function will be provided called Object.harden
, this method acts just like Object.freeze
,
except it sets overridable: true
on all descriptors (that are configurable).
For example, the following will just work now:
"use strict";
Object.harden(Object.prototype);
const object = {};
object.toString = () => "Hello world!";
console.log(object.toString()); // Hello world!
QUESTION: Should this simply be an option to freeze
rather than a new method?
If we expose overridable: boolean
as a new property on descriptors returned from Object.getOwnPropertyDescriptor(s)
,
will existing code break?
If so, should we add a new method to get overridable
to Object
(e.g. Object.getIsOverridable(object, name)
), or should we expose overridable
only when it is set to true
.
I like the general idea. However, some specifics need adjustment. Some thoughts.
We are not proposing
harden
be added toObject
. Rather we're proposing that it be global and enabled bylockdown()
. Once you start usingharden
you tend to call it from so many places that it needs to be said as short as possible. (If there were a word as clear with fewer letters, I'd be tempted to use that instead.) We also useharden
to transitively freeze the primordials, which would work well with this proposal. Currently, we only repair the override mistake for the properties on the whitelist at https://github.com/Agoric/SES-shim/blob/master/packages/ses/src/enablements.js . With this proposal, all properties of all primordials would be override-mistake-safe in this manner, removing the need for the ad hoc whitelist.Be aware that we're about to change the semantics of
harden
in ways that should not affect this proposal. Currently,harden
transitively walks own properties, which it would continue to do. But it only does a correctness check on[[Prototype]]
--- that after this harden completes, all hardened objects would inherit only from hardened objects. This complexity was motivated when we wantedharden
to be useful outside of SES, which turned out to be a waste of time and effort, and was hard to explain. Withharden
only being enabled bylockdown
we're going to return to the simpler semantics that it also transitively walks the[[Prototype]]
chain upwards, hardening all the objects that hardened objects would inherit from, rather than just checking that they would be hardened.Adding this protection against the override mistake to the semantics of a
lockdown
/harden
proposal makes a lot of sense. However, we would normally not shim the pervasive protection from the override mistake, as that turns out to be expensive. The shim would likely continue the whitelist approach we're currently using. It works extremely well.I am not in favor of adding a new descriptor member. Regarding whether we need to adjust the object property model at all, I gotta ask: Is it within the exotic behaviors allowed under the object invariants for an object to represent that it has a non-writable, non-configurable data property
foo
while still allowing an object that inherits from that exotic object to override it by assignment. We could test this by trying to write a proxy that does exactly that. If so, thenharden
should cause that change to the behaviors of objects that it freezes --- but only objects that it freezes. Any objects that are already frozen should not have this behavior changed byharden
.Whether and how to provide any reflective access to this, such as
getIsOverridable
, depends on the answer to the prior question. But some such reflective query is plausible.