Skip to content

Instantly share code, notes, and snippets.

@Jamesernator
Last active June 16, 2022 06:28
Show Gist options
  • Save Jamesernator/dc9831f9c13d17ac9a75d1fe25a2e015 to your computer and use it in GitHub Desktop.
Save Jamesernator/dc9831f9c13d17ac9a75d1fe25a2e015 to your computer and use it in GitHub Desktop.
Harden

Harden - Fixing the override mistake

The override mistake

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.

Solution: Overridable writable 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);

Object.harden

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?

Hazard: Existing code relying on descriptors

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.

@Jamesernator
Copy link
Author

Jamesernator commented Jun 16, 2022

Why have an extra slot for [[OverridableProperties]] instead of adding an 'overridable' property to the descriptor of the prototype? Object.freeze could leave the overridable property to false (triggering current behavior) and "harden" or whatever could set it as true.

That's essentially the proposal I suggested in the initial post, but this changes all objects not just ones with [[OverridableProperties]]. As mentioned by @erights :

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.

And the answer to the query given there, is actually yes, the current object model already permits "override" behaviour as can proved by creating a proxy that does so:

const frozenProto = Object.freeze({
    toString() {
        return "[Object object]";
    }
});

const obj = {
    __proto__: frozenProto,
};

const override = new Proxy(obj, {
    // This is overly simplified as it doesn't actually invoke
    // getters/setters on the prototype, but the above proposal
    // is a bit more specific with the behaviour here
    set() {
        return true;
    },
});

// Works
override.toString = () => "override";
// Throws an error, this is the "override mistake"
obj.toString = () => "obj";

Hence, we can fix the override mistake without changing the object model, we just need to change the behaviour of [[Set]] for "Ordinary objects" to allow setting the property (under appropriate conditions).

And this is most easily accomplished by having an [[OverridableProperties]] internal slot or something vaguely similar. Hence the second suggested proposal I made above.

@Jamesernator
Copy link
Author

I should point out one reason something like [[OveridableProperties]] is needed on the prototype rather than just unconditionally returning true in cases where there's an unwritable property on the prototype is that this changes the behaviour of existing code (and that exact solution has been previously proposed and rejected).

Rather by having Object.harden or whatever as an opt-in, existing code will be unchanged, but new code can still freely opt-in to overridable properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment