Object.getOwnPropertyDescriptor can return just about anything

# Jason Orendorff (10 years ago)

The [[Origin]] field of Property Descriptor Records is not yet implemented in Firefox. Eric Faust is looking at implementing it. We noticed two interesting cases:

  1. Suppose handler.getOwnPropertyDescriptor returns ({value: 0}). Then 9.5.5 Proxy.[[GetOwnProperty]] calls 6.2.4.6 CompletePropertyDescriptor, and all the fields of the Property Descriptor Record are populated. But the object itself is not populated. This means Object.getOwnPropertyDescriptor will return an object that is missing most of the fields the caller wants to know about. This seems strange.

  2. The object returned by the handler can have getters. It can answer ToPropertyDescriptor's queries one way, and then say something else afterwards, making it look like language invariants have been broken.

Come to think of it, [[GetOwnProperty]] is a weird API. It computes two results: a set of Property Descriptor fields, and an [[Origin]] object. The ES language itself relies exclusively on the former. Scripts are only allowed to see the latter. That seems really weird to me.

What is an example of a concrete use case for this [[Origin]] feature? Is it to avoid allocating a new object here?

# Andrea Giammarchi (10 years ago)

I think Eric Faust is right over there when he says: "It is easily fixed if we do the conversion to PropSpec in the proxy api"

my 2 cents

# Allen Wirfs-Brock (10 years ago)

On Apr 30, 2014, at 8:43 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

The [[Origin]] field of Property Descriptor Records is not yet implemented in Firefox. Eric Faust is looking at implementing it.[1] We noticed two interesting cases:

  1. Suppose handler.getOwnPropertyDescriptor returns ({value: 0}). Then 9.5.5 Proxy.[[GetOwnProperty]] calls 6.2.4.6 CompletePropertyDescriptor, and all the fields of the Property Descriptor Record are populated. But the object itself is not populated. This means Object.getOwnPropertyDescriptor will return an object that is missing most of the fields the caller wants to know about. This seems strange.

The idea is that a Proxy can define its own property descriptor formats that it exposes to and accepts form user code. vis Object.defineProperty/Object.getOwnPropertyDescriptors. This includes the possibility of new property attributes and censoring built-in attributes.

For example, you might define a Proxy that exposes something like a typed array but which did not have any attributes on indexed properties. Such a Proxy might decided to return only {value: 0} as the descriptor for such a zero-valued property.

  1. The object returned by the handler can have getters. It can answer ToPropertyDescriptor's queries one way, and then say something else afterwards, making it look like language invariants have been broken.

Mostly, such custom descriptors are only returned to user code. I don’t believe (it’s been while since I looked at this part of the spec, but once upon a time I traced all these logic paths) there are any paths within actual specified operations where a property descriptor is accessed in this manner and that descriptor is immediately used to define another property.

There aren’t any internal invariant sensitivities that I could find. Once such a non-standard descriptor is never directly used by any of the ordinary object MOP operations

Come to think of it, [[GetOwnProperty]] is a weird API. It computes two results: a set of Property Descriptor fields, and an [[Origin]] object. The ES language itself relies exclusively on the former. Scripts are only allowed to see the latter. That seems really weird to me.

Yes, that is the design. The set of fields are for internal use of specified operations. The [[Origin]] field is there so a handler can produce a non-standard descriptor and pass it back to user code. It is necessary, because [[GetOwnDescriptor]] returns an internal record, not an object.

What is an example of a concrete use case for this [[Origin]] feature? Is it to avoid allocating a new object here?

So a Proxy can define the property descriptors it produces and consumes. For example, you might have a property descriptor like: {method: func, referencesSuper: boolean}

# Mark S. Miller (10 years ago)

I'm surprised and alarmed by this, and it seems wrong. It is also not what I think I remember. What about, for example, the invariant that an object cannot both claim that a property is non-configurable but then later change its alleged configuration? The issue isn't just the internal perception by other internal procedures,but obviously also the observations by user code.

What I think I remember is that a proxy can add other fields to a property descriptor object, but that it cannot change the behavior of any of the ES6-defined fields (the usual .value, .writable, .set, .get, .enumerable, .configurable), not even to censor them.

Am I misunderstanding something? If not, this seems like a huge disconnect for which we need to stop the train.

# Allen Wirfs-Brock (10 years ago)

On Apr 30, 2014, at 11:08 AM, Mark S. Miller <erights at google.com> wrote:

I'm surprised and alarmed by this, and it seems wrong. It is also not what I think I remember. What about, for example, the invariant that an object cannot both claim that a property is non-configurable but then later change its alleged configuration? The issue isn't just the internal perception by other internal procedures,but obviously also the observations by user code.

Don’t read too much into quick email responses. This has been hashed over before, and we should dig out the old discussions and nothing has really changed with this part of the spec. for a long time. Unfortunately I’m traveling this week and can’t spend much time on it.

Note that invariants such as you describe (for Proxies, which is what this is all about) apply only to non-configurable properties (which, by definition must exist on the target). It is possible that there needs to be an additional check in that case requiring that for such properties that the descriptor object returned from the handler must only use data properties for the attribures.

What I think I remember is that a proxy can add other fields to a property descriptor object, but that it cannot change the behavior of any of the ES6-defined fields (the usual .value, .writable, .set, .get, .enumerable, .configurable), not even to censor them.

For configurable properties, it can do anything it wants. How would we prevent?

Am I misunderstanding something? If not, this seems like a huge disconnect for which we need to stop the train.\

Again, there is nothing new here. At the very least you should look at the old discussion about this.

# Tom Van Cutsem (10 years ago)

2014-04-30 3:29 GMT+02:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

Again, there is nothing new here. At the very least you should look at the old discussion about this.

Pointers to the old discussions:

I first commented on [[Origin]] as part of my review of the first ES6 draft Proxy spec: esdiscuss/2012-December/027623

The thread then forked: esdiscuss/2012-December/027708, esdiscuss/2013-January/027788

Unfortunately, the conversation was a ping-pong between just me and Allen, with me pushing back on the idea and Allen eventually proposing a middle-ground alternative that I felt somewhat uncomfortable with, but seemed to be addressing the major concerns.

Reading the conversation again, both Allen and me failed to convince each other, we ran out of arguments ammo, and so ended up with a compromise. It would be good if others chimed in.

# Jason Orendorff (10 years ago)

On Tue, Apr 29, 2014 at 7:49 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

The idea is that a Proxy can define its own property descriptor formats that it exposes to and accepts form user code. vis Object.defineProperty/Object.getOwnPropertyDescriptors. This includes the possibility of new property attributes and censoring built-in attributes.

New property attributes will be useful in fancy metaprogramming, such as glue layers binding JS to other languages with different object models. Proxies already provide plenty of rope for those users without [[Origin]]. They will of course wish to store property metadata in a form other than complete descriptor objects, and they can do that internally. They can even easily provide custom reflection APIs to expose their custom metadata. But [[GetOwnProperty]]'s job is to make those special properties intelligible to the rest of JS, and Object.getOwnPropertyDescriptor's job is to reflect [[GetOwnProperty]].

For example, you might define a Proxy that exposes something like a typed array but which did not have any attributes on indexed properties.

Well---all properties have attributes. [[GetOwnProperty]] always returns a complete Property Descriptor Record, or undefined. The language relies on it.

In that light, you're saying: "you might define a Proxy that exposes something like a typed array but for which the reflection APIs don't work". You might, but why?

# Jason Orendorff (10 years ago)

On Tue, Apr 29, 2014 at 8:08 PM, Mark S. Miller <erights at google.com> wrote:

I'm surprised and alarmed by this, and it seems wrong. It is also not what I think I remember. What about, for example, the invariant that an object cannot both claim that a property is non-configurable but then later change its alleged configuration?

As specified, proxies can do this:

  js> Object.isFrozen(proxy)
  true
  js> Object.getOwnPropertyDescriptor(proxy).configurable
  true

Of course the property is not really configurable. The extent of the issue is that Object.getOwnPropertyDescriptor is not a reliable reflection API.

# Mark Miller (10 years ago)

This is a stop-the-train scale disaster. ES6 cannot ship in this state. We need to fix this asap.

My apologies for not tracking this issue better earlier. I thought we had all agreed on the constraints, so I did not pay sufficient attention to what I thought was merely the ironing out of minor kinks.

I am trying to arrange a hangout with Tom and Allen to discuss this further.

# Tom Van Cutsem (10 years ago)

2014-04-30 18:55 GMT+02:00 Mark Miller <erights at gmail.com>:

This is a stop-the-train scale disaster. ES6 cannot ship in this state. We need to fix this asap.

I agree we need to revisit and iron out the details asap, but keep your calm. I'm sure we'll be able to fix this well in time before the ES6 spec is frozen. This is a very localized spec issue.

# Allen Wirfs-Brock (10 years ago)

On May 1, 2014, at 2:50 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

As specified, proxies can do this:

 js> Object.isFrozen(proxy)
 true
 js> Object.getOwnPropertyDescriptor(proxy).configurable
 true

No, that is not the intent. However, Object.isFrozen depends upon the reliability of [[OwnPropertyKeys]] and one of its checked variants (and this is not yet in the spec. because Tom, Mark, and I just worked out the final details last week) is that [[OwnPropertyKeys]] must return a complete and accurate list of all non-configurable property names.

Also note that Object.isFrozen is specified to operates at the level of the MOP operations and property descriptor records, not at the Object.getOwnPropertyDescriptor/descriptor object level. It never looks at the object that is passed through [[Origin]].

It isn’t clear exactly what you intend by the above snippet (does proxy have a ‘getOwnPropertyDescriptorHandler’? did you really mean to use undefined as the property key?). In either case, if the object passes the criteria for isFrozen then then the con configurable attribute of the resulting descriptor (if the property exists) must be false and consistent with the target. Steps 11-21 of 9.9.5 (proxy [[GetOwnProperty]] are intended to guarantees that. Maybe there are bugs in those steps, I can review them in detail right now, but the design intent is a frozen object can never expose a ;configurable property.

Of course the property is not really configurable. The extent of the issue is that Object.getOwnPropertyDescriptor is not a reliable reflection API.

It’s supposed to be for frozen objects, so if you see bugs in that regard I’m obvious interested.

# André Bargull (10 years ago)

I think the following example should cover Jason's concerns w.r.t reliability of Object.getOwnPropertyDescriptor.

Output:

Object.isFrozen(target) = true
Object.isFrozen(proxy) = true
Object.getOwnPropertyDescriptor(proxy, propertyName).configurable = true
{
   let propertyName = "propertyName";
   let target = Object.freeze({[propertyName]: 0});
   let beStupid = false;

   let proxy = new Proxy(target, {
     getOwnPropertyDescriptor(t, pk) {
       if (!beStupid) {
         return Reflect.getOwnPropertyDescriptor(t, pk);
       }
       return {
         value: 0, writable: false, enumerable: true,
         get configurable() {
           delete this.configurable;
           this.configurable = true;
           return false;
         }
       };
     }
   });

   print(`Object.isFrozen(target) = ${Object.isFrozen(target)}`);
   print(`Object.isFrozen(proxy) = ${Object.isFrozen(proxy)}`);

   beStupid = true;

   let desc = Object.getOwnPropertyDescriptor(proxy, propertyName);
   print(`Object.getOwnPropertyDescriptor(proxy, 
propertyName).configurable = ${desc.configurable}`);
}
# Jason Orendorff (10 years ago)

On Wed, Apr 30, 2014 at 4:06 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

No, that is not the intent.

André got it right. Here is the proxy I had in mind:

var proxy = new Proxy(Object.freeze({prop: undefined}), {
    ownKeys: function () {
        return ["prop"];
    },
    getOwnPropertyDescriptor: function name() {
        let n = 0;
        return {get configurable() { return n++ > 0; }, enumerable: true};
    }
});

Currently in SpiderMonkey:

js> Object.isFrozen(proxy)
true
js> Object.getOwnPropertyDescriptor(proxy, "prop").configurable
false  // but only because we don't implement [[Origin]]

Also note that Object.isFrozen is specified to operates at the level of the MOP operations and property descriptor records, not at the Object.getOwnPropertyDescriptor/descriptor object level. It never looks at the object that is passed through [[Origin]].

Agreed.

It isn’t clear exactly what you intend by the above snippet (does proxy have a ‘getOwnPropertyDescriptorHandler’? did you really mean to use undefined as the property key?).

Oh. No to the latter. Sorry for the typo!

In either case, if the object passes the criteria for isFrozen then then the con configurable attribute of the resulting descriptor (if the property exists) must be false and consistent with the target.

The only inconsistency here is that Object.getOwnPropertyDescriptor is returning an [[Origin]] object which is then tricking the end user.

# Tom Van Cutsem (10 years ago)

Allen, Mark and I discussed the [[Origin]] issue and came to the following consensus:

We remove [[Origin]] and revert to the originally specified behavior (< harmony:proxies_spec>) where the

descriptor returned by the proxy is coerced into a fresh, normalized, completed, ordinary descriptor object.

This ensures complete backward-compatibility with the ES5 behavior (i.e. Object.getOwnPropertyDescriptor will always return a fresh, complete data or accessor descriptor), and doesn't allow a proxy to play tricks with descriptor objects.

Allen's remaining concern is that this disallows proxies (or new exotic objects) from inventing new types of descriptors, next to data and accessor descriptors. Due to backwards-compat. constraints, we're basically stuck with these two types of property descriptors forever.

The originally specified Proxy behavior also included copying any non-standard attributes provided by the proxy onto the fresh descriptor object. However, if we're serious about keeping Object.getOwnPropertyDescriptor backwards-compatible with existing ES5 code, we may be better off by not augmenting descriptor objects with non-standard attributes, even if this is unlikely to break existing code. As Jason mentioned, if proxies want to introduce new per-property attributes, they can provide other means of getting at that meta-data rather than abusing the standard reflection API.

So, the current proposal is to spec [[GetOwnProperty]] for Proxies such that the descriptor returned by the trap is coerced into a fresh, normalized, complete descriptor object, without copying custom attributes.

Relevant bug seems to already have been filed by Andre: < ecmascript#2382>

# Rick Waldron (10 years ago)

Forgive me if this has already been discussed elsewhere, but the "Notes" section of [[GetOwnProperty]](P) lists several invariants that are similar in nature to the following (which I've just made up):

  • A property cannot be reported as configurable, if it does not exists as an own property of the target object or if it exists as a non-configurable own property of the target object.

  • A property cannot be reported as writable, if it does not exists as an own property of the target object or if it exists as a non-writable own property of the target object.

  • A property cannot be reported as enumerable, if it does not exists as an own property of the target object or if it exists as a non-enumerable own property of the target object.

Then descriptors would allow user-invented descriptor properties, while still upholding the target's integrity.

# Tom Van Cutsem (10 years ago)

It's true that allowing user-invented custom attributes will not break any important existing invariants (except perhaps that all existing descriptors can be assumed not to have any own properties besides the standard attributes. Existing code may depend on that, although it feels highly unlikely).

# David Bruant (10 years ago)

Just to try to assess the unlikelihood and understand the cases where a ES5 code expectations aren't met:

The only case where ES6 and ES5 may diverge is for Object.getOwnPropertyDescriptor where a Proxy may return something that cannot be expected from any ES5 object. The after-trap completes the property descriptor (and when completing picks specifically only data or accessor property), so code that expects a complete property descriptor cannot be broken. However, a divergence may only occur if, for instance, the code loops over the property descriptor properties or expects exactly 4 properties.

Is that correct or am I missing cases?

# Tom Van Cutsem (10 years ago)

Another possibility would be that existing client code itself extends property descriptors with new attributes (since descriptors returned by gOPD are always extensible), and can then get confused when proxies pre-populate these descriptors with their own custom attributes. But again, this is highly unlikely. I think allowing proxies to accept/return custom attributes remains feasible.

# Mark S. Miller (10 years ago)

By the same token, it is feasible to postpone these custom attributes till ES7. My concern is that since the [[Origin]]-oriented proxy proposal that has been in the draft spec was a distracting dead end[1], we should be conservative in returning to the design that preserves the crucial invariants. The custom attributes seem safe enough, but we have too much on our plate and it is safer yet to postpone them. Nothing else depends on the presence of these custom attributes being in ES6.

[1] My apologies once again for not paying enough attention to catch this early.