Property descriptor normalization (Was: General comments response (was Re: ES6 Rev13 Review: MOP-refactoring, symbols, proxies, Reflect module))

# Tom Van Cutsem (11 years ago)

2012/12/30 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 29, 2012, at 2:37 PM, Tom Van Cutsem wrote:

  • I'm a bit uncomfortable with the removal of property descriptor normalization in the getOwnPropertyDescriptor/defineProperty traps. Especially for getOwnPropertyDescriptor I think it's a breaking change w.r.t. ES5.1. [...]

This permits things like:

Object.defineOwnProperty(pObj1,"foo", {method: func}); //define property on a proxy-based object, that have "method" properties console.log (Object.getOwnPropertyDescriptor(pObj1,"foo").method); //we can retrieve the value of the method attribute (if the proxy supports it)

Object.defineOwnProperty(pObj2,"foo",Object.getOwnProperty(pObj1, "foo")); //copy a method properry from pObj1 to pObj2

If descriptor object with extended attributes is applied to an ordinary object, it is always first internally converted to a PD record. PD records only contain fields for the ordinary attributes, and any operations upon ordinary objects will have no visibility of the extended attributes.

Yes, I agree to all of this and I understand this is the intent of the new [[Origin]] field of internal property descriptors. I also agree there's no problem for normal objects, which continue to always cons a fresh property descriptor object.

The only breaking change (relative to ES 5.1) possibility I see must start with the assumption that ES5.1 property attributes are the final definition of property descriptor objects and that additional property attributes can never be added to the language (by the spec., not just via proxies) using any of the pre-existing ES5.1 APIs. That seems quite unreasonable and was certainly not the intent when we introduced the reflection API into ES5. [...]

This was not the breaking change I had in mind and I agree with you that adding new attributes is both useful and supported by the ES5.1 design.

Here's the breaking change I had in mind:

var propDescMap = {}; var proxy = Proxy({}, { defineProperty(target, name, desc) { propDescMap[name] = desc; return true; }, getOwnPropertyDescriptor(target, name) { return propDescMap[name]; } };

// client1 adds: var pd1 = { get configurable() { return Math.random() < 0.5; }, // return true or false, randomly custom: true }; Object.defineProperty(proxy, "foo", pd1);

// client2 queries: var pd2 = Object.getOwnPropertyDescriptor(proxy, "foo");

If I understand correctly, pd2 now refers to exactly the same object as pd1, preserving the custom attribute.

However, this semantics also implies that:

  1. pd2 is not a completed property descriptor, lacking a writable/enumerable attribute. Only the internal property descriptor was fully completed, not the [[Origin]] object.
  2. pd2.configurable is not a data property, but may change randomly
  3. since pd1/pd2 refers to a mutable object, changes made by client1 will be visible to client2 and vice-versa.

None of these behaviors are possible in ES5.1, so clients of Object.getOwnPropertyDescriptor currently don't (need to) guard against these.

The issue is that in 8.5.6 Proxy [[GetOwnProperty]], only the "resultDesc" is normalized, not the trapResultObj that is its [[Origin]]. The FromPropertyDescriptor operation blindly returns the [[Origin]], disregarding the normalized descriptor.

To rescue the [[Origin]] design, the most straightforward fix I can come up with is that FromPropertyDescriptor first normalizes the [[Origin]] object before returning it (i.e. verifying that it is complete, or making it complete, and ensuring the standard attributes are data properties). Even then so, it's messy that these side-effects are visible to end-user code (the pd1 object of client1 would get mutated-at-a-distance as a result of returning it from a getOwnPropertyDescriptor trap).

The only other alternative I see is to create normalized copies, as specified in the draft proxy spec on the wiki.

# Allen Wirfs-Brock (11 years ago)

On Dec 31, 2012, at 3:37 AM, Tom Van Cutsem wrote:

2012/12/30 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 29, 2012, at 2:37 PM, Tom Van Cutsem wrote:

  • I'm a bit uncomfortable with the removal of property descriptor normalization in the getOwnPropertyDescriptor/defineProperty traps. Especially for getOwnPropertyDescriptor I think it's a breaking change w.r.t. ES5.1. [...] This permits things like:

Object.defineOwnProperty(pObj1,"foo", {method: func}); //define property on a proxy-based object, that have "method" properties console.log (Object.getOwnPropertyDescriptor(pObj1,"foo").method); //we can retrieve the value of the method attribute (if the proxy supports it)

Object.defineOwnProperty(pObj2,"foo",Object.getOwnProperty(pObj1, "foo")); //copy a method properry from pObj1 to pObj2

If descriptor object with extended attributes is applied to an ordinary object, it is always first internally converted to a PD record. PD records only contain fields for the ordinary attributes, and any operations upon ordinary objects will have no visibility of the extended attributes.

Yes, I agree to all of this and I understand this is the intent of the new [[Origin]] field of internal property descriptors. I also agree there's no problem for normal objects, which continue to always cons a fresh property descriptor object.

The only breaking change (relative to ES 5.1) possibility I see must start with the assumption that ES5.1 property attributes are the final definition of property descriptor objects and that additional property attributes can never be added to the language (by the spec., not just via proxies) using any of the pre-existing ES5.1 APIs. That seems quite unreasonable and was certainly not the intent when we introduced the reflection API into ES5. [...]

This was not the breaking change I had in mind and I agree with you that adding new attributes is both useful and supported by the ES5.1 design.

Here's the breaking change I had in mind:

var propDescMap = {}; var proxy = Proxy({}, { defineProperty(target, name, desc) { propDescMap[name] = desc; return true; }, getOwnPropertyDescriptor(target, name) { return propDescMap[name]; } };

// client1 adds: var pd1 = { get configurable() { return Math.random() < 0.5; }, // return true or false, randomly custom: true }; Object.defineProperty(proxy, "foo", pd1);

// client2 queries: var pd2 = Object.getOwnPropertyDescriptor(proxy, "foo");

If I understand correctly, pd2 now refers to exactly the same object as pd1, preserving the custom attribute.

Note that you don't need to start with the defineProperty call using pd1 to get the following effects. The same observable behavior could occur just by defining the getOwnPropertyDescriptor trap to directly construct and return a similar value for pd2.

However, this semantics also implies that:

  1. pd2 is not a completed property descriptor, lacking a writable/enumerable attribute. Only the internal property descriptor was fully completed, not the [[Origin]] object.

We can't say what "complete" means for a property descriptor containing custom attributes. Just like the complete PD for an ordinary accessor property doesn't contain a "writable" property, perhaps a PD for an exotic "custom" property only have "configurable" and "custom" attributes. This is all up to the designer of the exotic property and something they should define when they describe the contract of their exotic object.

Yes, the internal PD record will be completed but it is only used for a sequence of pseudo-code algorithm steps. I don't currently see any place where the internal PD record is used in such a way that we have any problems. Of course, the proxy itself should provide consistent Get/Set/defineProperty/getOwnPropertyDescriptor behavior for such exotic properties. If it doesn't its a ES programmer level bug.

  1. pd2.configurable is not a data property, but may change randomly

So? That's the nature of accessor properties although it's not how they are normally used. Why are these values being unstable any worse than any others?

The one thingI can imagine is that a proxy author could use an "configurable" attribute accessor to sneak past the configurable invariant check in Proxy [[GetOwnProperty]]. I can imagine that we might want to normalize the "configurable" property (only for the situation where their is a corresponding target property??) of the descriptor object, but no other properties.

  1. since pd1/pd2 refers to a mutable object, changes made by client1 will be visible to client2 and vice-versa.

Again, this is the nature of programming with object references. If the Proxy wants to prevent this, it shouldn't store the descriptor objects -- just like the spec. doesn't store the PD record that is used to create a property. If client2 is worried about this and doesn't trust the Proxy (in which case it probably shouldn't be using it...) in can always do its own cloning/normalization. Regardless, the values of pd2 can't asynchronously change out from under client2. client2 is in control and presumably should be aware of the possible side-effects of anything it calls.

Basically, I'm saying I don't see why the "hazards" of an accessor are any greater here than in any other random ES code.

None of these behaviors are possible in ES5.1, so clients of Object.getOwnPropertyDescriptor currently don't (need to) guard against these.

The change is the possible occurrence of exotic properties. Regardless of any normalization of ES5 ordinary property attributes, existing code is not going to be prepared to handle exotic properties in any specific way. Even for new code where the client knows it could potential be accessing a proxy provided descriptor object, I don't know that it would be worth guarding against these things. Any object returned from any function may potentially be buggy and violate the assumed contract of the function. Most code doesn't and should actively defend itself against such potential bugs.

The issue is that in 8.5.6 Proxy [[GetOwnProperty]], only the "resultDesc" is normalized, not the trapResultObj that is its [[Origin]]. The FromPropertyDescriptor operation blindly returns the [[Origin]], disregarding the normalized descriptor.

Yes, because I don't know what it means to "normalize" an arbitrary exotic property descriptor. The primary purpose of such exotic descriptors is to convey from the Proxy handler to the client information about an exotic object and its exotic properties. It is also used to by the client to pass exotic property attributes back to a handler. Arbitrarily normalizing an exotic descriptor produced by a getOwnPropertyDescriptor trap might result in turning into something that would be treated as an invalid exotic descriptor when passed back to a corresponding defineProperty trap.

To rescue the [[Origin]] design, the most straightforward fix I can come up with is that FromPropertyDescriptor first normalizes the [[Origin]] object before returning it (i.e. verifying that it is complete, or making it complete, and ensuring the standard attributes are data properties). Even then so, it's messy that these side-effects are visible to end-user code (the pd1 object of client1 would get mutated-at-a-distance as a result of returning it from a getOwnPropertyDescriptor trap).

Here is a list of alternative, I can envision, ordered (from my perspective) in decreasing desirability:

  1. Provide a Reflect.normalizePD(desc) function that produces a new descriptor object via ToPropertyDescriptor(CompletePropertyDescriptor(FromPropertyDesriptor(desc),undefined)).

    Anybody who is paranoid about encountering exotic descriptors, malformed descriptors, or accessor properties can use it.

  2. Normalized only the "configurable" property. Make it an invariant that "configurable" is always present and is a data property. Either normalize it to that or throw as an invariant violation like for other proxy invariants.

    This seems like the only property attribute whose setting is important for the integrity use cases, so like elsewhere in the proxy design only enforce that single invariant.

  3. Make it an invariant that all own properties of the returned data property are data properties.

    This gets rid of the accessor hazard. However this puts additional extra checking into the proxy [[GetOwnProperty]] that is almost always going to yield a negative result. In other words, you a placing a performance tax on the usual case with little usual benefit. Unless there is real integrity hazard that can't be explicitly mitigated where it matters, we should just treat such situations like misuse of accessors as user level bugs.

# Tom Van Cutsem (11 years ago)

2013/1/3 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 31, 2012, at 3:37 AM, Tom Van Cutsem wrote:

[...]

However, this semantics also implies that:

  1. pd2 is not a completed property descriptor, lacking a writable/enumerable attribute. Only the internal property descriptor was fully completed, not the [[Origin]] object.

We can't say what "complete" means for a property descriptor containing custom attributes. Just like the complete PD for an ordinary accessor property doesn't contain a "writable" property, perhaps a PD for an exotic "custom" property only have "configurable" and "custom" attributes. This is all up to the designer of the exotic property and something they should define when they describe the contract of their exotic object.

I think it's perfectly obvious what it means for an exotic property to be complete: that it is at least a complete data or accessor property (i.e. it either has {value,writable,enumerable,configurable}, or it has {get,set,enumerable,configurable} properties). It may further specify any number of additional custom attributes.

More generally, I think it is backwards-compatible to extend property descriptors with extra attributes, but it's backwards-incompatible to allow proxies to remove some of the standard attributes from property descriptors. This breaks the assumptions of existing clients.

Yes, the internal PD record will be completed but it is only used for a sequence of pseudo-code algorithm steps. I don't currently see any place where the internal PD record is used in such a way that we have any problems.

The use of the internal PD record is fine. My problem lies only with Object.getOwnPropertyDescriptor(proxy,name) exposing a non-normalized descriptor object.

Of course, the proxy itself should provide consistent Get/Set/defineProperty/getOwnPropertyDescriptor behavior for such exotic properties. If it doesn't its a ES programmer level bug.

  1. pd2.configurable is not a data property, but may change randomly

So? That's the nature of accessor properties although it's not how they are normally used. Why are these values being unstable any worse than any others?

The one thingI can imagine is that a proxy author could use an "configurable" attribute accessor to sneak past the configurable invariant check in Proxy [[GetOwnProperty]].

It seems to me that alone would be reason enough to ban "accessor attributes".

I can imagine that we might want to normalize the "configurable" property (only for the situation where their is a corresponding target property??) of the descriptor object, but no other properties.

I'm uncomfortable with this. It's already exceedingly difficult (for me at least) to think about the correctness of the invariant checks. Having to take into account that property descriptors may be proxies with arbitrary behavior introduces further complexity.

  1. since pd1/pd2 refers to a mutable object, changes made by client1 will be visible to client2 and vice-versa.

Again, this is the nature of programming with object references. If the Proxy wants to prevent this, it shouldn't store the descriptor objects -- just like the spec. doesn't store the PD record that is used to create a property.

The scenario I'm worried about assumes that the Proxy is actively trying to confuse a client, so trusting the Proxy to do the right thing is not an option.

If client2 is worried about this and doesn't trust the Proxy (in which case it probably shouldn't be using it...) in can always do its own cloning/normalization.

Two observations:

  1. We've been careful to make proxies transparent, and we consider it an anti-pattern for clients to test whether they're using a Proxy.
  2. Yes, the client defending itself by normalizing the descriptor explicitly would help, but my point is that there is extent ES5.1 code that does not currently do this because it's currently not necessary.

Regardless, the values of pd2 can't asynchronously change out from under client2. client2 is in control and presumably should be aware of the possible side-effects of anything it calls.

Basically, I'm saying I don't see why the "hazards" of an accessor are any greater here than in any other random ES code.

Because existing code can assume that "desc.configurable", "desc.writable", etc. are stable values, and changing these into accessors breaks those assumptions.

I think of Object.getOwnPropertyDescriptor as the high-integrity, high-fidelity part of the reflection API. This is the API that security-conscious code should use when it manipulates untrusted objects. It can only be a high-integrity reflection API if the return value can be reliably depended upon.

None of these behaviors are possible in ES5.1, so clients of Object.getOwnPropertyDescriptor currently don't (need to) guard against these.

The change is the possible occurrence of exotic properties. Regardless of any normalization of ES5 ordinary property attributes, existing code is not going to be prepared to handle exotic properties in any specific way. Even for new code where the client knows it could potential be accessing a proxy provided descriptor object, I don't know that it would be worth guarding against these things. Any object returned from any function may potentially be buggy and violate the assumed contract of the function. Most code doesn't and should actively defend itself against such potential bugs.

I don't see how existing ES5.1 code manipulating a property descriptor with extra custom attributes would break, if it only ever touches the standard attributes (which should be most of the code manipulating descriptors out there).

In the draft Proxy spec on the wiki, custom attributes are simply copied onto the normalized descriptor as part of the normalization process. It's worth noting that they are always copied as data properties. I think this makes for a good guideline: property descriptors are objects with just data properties.

If ECMAScript would have had an actual record value type, I'm confident property descriptors would have been reified as such. Since we only have objects, descriptors were reified as objects, but that brings with it features (mutability, behavior, ...) that seem to get in the way here.

The issue is that in 8.5.6 Proxy [[GetOwnProperty]], only the "resultDesc" is normalized, not the trapResultObj that is its [[Origin]]. The FromPropertyDescriptor operation blindly returns the [[Origin]], disregarding the normalized descriptor.

Yes, because I don't know what it means to "normalize" an arbitrary exotic property descriptor. The primary purpose of such exotic descriptors is to convey from the Proxy handler to the client information about an exotic object and its exotic properties. It is also used to by the client to pass exotic property attributes back to a handler. Arbitrarily normalizing an exotic descriptor produced by a getOwnPropertyDescriptor trap might result in turning into something that would be treated as an invalid exotic descriptor when passed back to a corresponding defineProperty trap.

If we make it clear that property descriptors are records (i.e. contain only data properties), and normalization entails:

  1. ensure all standard attributes are present and refer to values of the appropriate type
  2. copy any custom properties

Then I think the contract is clear and custom descriptors will round-trip fine.

To rescue the [[Origin]] design, the most straightforward fix I can come up with is that FromPropertyDescriptor first normalizes the [[Origin]] object before returning it (i.e. verifying that it is complete, or making it complete, and ensuring the standard attributes are data properties). Even then so, it's messy that these side-effects are visible to end-user code (the pd1 object of client1 would get mutated-at-a-distance as a result of returning it from a getOwnPropertyDescriptor trap).

Here is a list of alternative, I can envision, ordered (from my perspective) in decreasing desirability:

  1. Provide a Reflect.normalizePD(desc) function that produces a new descriptor object via ToPropertyDescriptor(CompletePropertyDescriptor(FromPropertyDesriptor(desc),undefined)).

    Anybody who is paranoid about encountering exotic descriptors, malformed descriptors, or accessor properties can use it.

I would have agreed to this if it weren't for the fact that there is probably ES5.1 code out there that already tries to be correct in the presence of untrusted code, and doesn't know about or use this primitive. I hope MarkM can point at some relevant sources.

  1. Normalized only the "configurable" property. Make it an invariant that "configurable" is always present and is a data property. Either normalize it to that or throw as an invariant violation like for other proxy invariants.

    This seems like the only property attribute whose setting is important for the integrity use cases, so like elsewhere in the proxy design only enforce that single invariant.

This still violates the ES5.1 assumption that all descriptors returned by getOwnPropertyDescriptor are guaranteed to be complete.

  1. Make it an invariant that all own properties of the returned data property are data properties.

    This gets rid of the accessor hazard. However this puts additional extra checking into the proxy [[GetOwnProperty]] that is almost always going to yield a negative result. In other words, you a placing a performance tax on the usual case with little usual benefit. Unless there is real integrity hazard that can't be explicitly mitigated where it matters, we should just treat such situations like misuse of accessors as user level bugs.

I won't deny there's a performance tax here, either by verifying that the descriptor is really a "record" (i.e. has only data properties), or by making an explicit copy. Like all other invariant checks, I agree they will be wasteful for "benign" proxies.

Then again, let's place things in perspective: when will this become a perf issue? When querying a large number of proxies for their own property descriptors. But then any heap containing a large number of proxies probably already has other perf issues to worry about?

# Allen Wirfs-Brock (11 years ago)

On Jan 3, 2013, at 12:18 AM, Tom Van Cutsem wrote:

2013/1/3 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 31, 2012, at 3:37 AM, Tom Van Cutsem wrote:

[...]

However, this semantics also implies that:

  1. pd2 is not a completed property descriptor, lacking a writable/enumerable attribute. Only the internal property descriptor was fully completed, not the [[Origin]] object.

We can't say what "complete" means for a property descriptor containing custom attributes. Just like the complete PD for an ordinary accessor property doesn't contain a "writable" property, perhaps a PD for an exotic "custom" property only have "configurable" and "custom" attributes. This is all up to the designer of the exotic property and something they should define when they describe the contract of their exotic object.

I think it's perfectly obvious what it means for an exotic property to be complete: that it is at least a complete data or accessor property (i.e. it either has {value,writable,enumerable,configurable}, or it has {get,set,enumerable,configurable} properties). It may further specify any number of additional custom attributes.

You seem to be assuming that all exotic properties must be an incremental variation of a data property or an accessor property. However, historically this hasn't been the case. When accessor attributes were added to ES they weren't just a incremental extension to the attributes of a data property. If any new property forms are added to ES in the future, they probably also won't be simply extended variations of data or accessor properties. I would hope that proxies could be used to prototype any such future extensions.

As part of a general extension mechanism, I don't think we should unnecessarily limit how a proxy handler chooses to define the interpretation of its property descriptors. It's clear to me that such limitations restrict the utility of proxies. What benefit to we get from those limitations.

More generally, I think it is backwards-compatible to extend property descriptors with extra attributes, but it's backwards-incompatible to allow proxies to remove some of the standard attributes from property descriptors. This breaks the assumptions of existing clients.

Since we never fully defined the invariants of Object.getOwnPropertyDescriptor it isn't clear what assumptions existing clients should be making. I think the most basic assumption that is justified is that the result of gOPD can be used with Object.defineProperty to define a similar property on a different object that is the same kind of object as the original.

Sure, a simple forwarding proxy to an ordinary object might break existing code if it didn't return complete property descriptors. That's a user level bug in the definition of the Proxy and can be found and corrected using user level testing and debugging techniques. I don't think we should be adding additional runtime time overhead to every proxy [[GetOwnProperty]] call to protect against such bugs. Such checks penalizes correct code and the restrictions they impose limits the utility of the proxies.

I believe we should only be dynamically validating invariants that are essential for the low level integrity and robustness or the execution environment. Everything else should be treated as as a programmer bugs.

Yes, the internal PD record will be completed but it is only used for a sequence of pseudo-code algorithm steps. I don't currently see any place where the internal PD record is used in such a way that we have any problems.

The use of the internal PD record is fine. My problem lies only with Object.getOwnPropertyDescriptor(proxy,name) exposing a non-normalized descriptor object.

Of course, the proxy itself should provide consistent Get/Set/defineProperty/getOwnPropertyDescriptor behavior for such exotic properties. If it doesn't its a ES programmer level bug.

  1. pd2.configurable is not a data property, but may change randomly

So? That's the nature of accessor properties although it's not how they are normally used. Why are these values being unstable any worse than any others?

The one thingI can imagine is that a proxy author could use an "configurable" attribute accessor to sneak past the configurable invariant check in Proxy [[GetOwnProperty]].

It seems to me that alone would be reason enough to ban "accessor attributes".

Or for mitigating just this specific integrity hole, as I described below.

I can imagine that we might want to normalize the "configurable" property (only for the situation where their is a corresponding target property??) of the descriptor object, but no other properties.

I'm uncomfortable with this. It's already exceedingly difficult (for me at least) to think about the correctness of the invariant checks. Having to take into account that property descriptors may be proxies with arbitrary behavior introduces further complexity.

Layers of abstraction...

defineProperty and its internal helpers already have to deal with the fact the descriptor objects may contain inherited attribute properties or that they may be accessor properties. So, while we might restrict what is returned by Proxy [[GetOwnPropety]] we still couldn't restrict what is passed to Proxy [[DefineOwnProperty]].

While I think it is unnecessary (and restrictive) I could probably live with alternative 3 that I proposed in my last message even with the addition that the object produced by the handler must be an ordinary object.

  1. since pd1/pd2 refers to a mutable object, changes made by client1 will be visible to client2 and vice-versa.

Again, this is the nature of programming with object references. If the Proxy wants to prevent this, it shouldn't store the descriptor objects -- just like the spec. doesn't store the PD record that is used to create a property.

The scenario I'm worried about assumes that the Proxy is actively trying to confuse a client, so trusting the Proxy to do the right thing is not an option.

If client2 is worried about this and doesn't trust the Proxy (in which case it probably shouldn't be using it...) in can always do its own cloning/normalization.

Two observations:

  1. We've been careful to make proxies transparent, and we consider it an anti-pattern for clients to test whether they're using a Proxy.
  2. Yes, the client defending itself by normalizing the descriptor explicitly would help, but my point is that there is extent ES5.1 code that does not currently do this because it's currently not necessary.

Except that transparency probably isn't appropriate in situations where the client is highly defensive. Do we currently have a isProxy test? It seems that it would be essential in such situations.

Does any such code actually exist. Does it depend upon anything other than the "configurable" property? Does such code, if it exists, work correctly with DOM objects in all implementations?

Regardless, the values of pd2 can't asynchronously change out from under client2. client2 is in control and presumably should be aware of the possible side-effects of anything it calls.

Basically, I'm saying I don't see why the "hazards" of an accessor are any greater here than in any other random ES code.

Because existing code can assume that "desc.configurable", "desc.writable", etc. are stable values, and changing these into accessors breaks those assumptions.

I think of Object.getOwnPropertyDescriptor as the high-integrity, high-fidelity part of the reflection API. This is the API that security-conscious code should use when it manipulates untrusted objects. It can only be a high-integrity reflection API if the return value can be reliably depended upon.

Consider me as representing the programmers whose primarily interest in Proxies is for extending ES via metaprogramming. From that perspective I think of O.gOPD (along with Object.defineProperty) as the primary user facing APIs that allow regular programmers to configure a wide range of exotic objects that I might invent. I don't want to be restricted to making all my properties look like some variant of existing data properties or accessor properties. Nor do I want to arbitrary transformations made to property descriptor I may choose to define. I'm perfectly happy for place into the hands of security-conscious coder the tools they need to detect and even reject my exotic objects. If my uses are incompatible with your high integrity code, I'll just run my code somewhere else. I want to be sure that you are enabled to create what ever short of high integrity environment you want but please don't place restrictions on what I can do when I'm not in your environment

None of these behaviors are possible in ES5.1, so clients of Object.getOwnPropertyDescriptor currently don't (need to) guard against these.

The change is the possible occurrence of exotic properties. Regardless of any normalization of ES5 ordinary property attributes, existing code is not going to be prepared to handle exotic properties in any specific way. Even for new code where the client knows it could potential be accessing a proxy provided descriptor object, I don't know that it would be worth guarding against these things. Any object returned from any function may potentially be buggy and violate the assumed contract of the function. Most code doesn't and should actively defend itself against such potential bugs.

I don't see how existing ES5.1 code manipulating a property descriptor with extra custom attributes would break, if it only ever touches the standard attributes (which should be most of the code manipulating descriptors out there).

Code that for-ins and switch dispatches over a property descriptor will see extra attributes.

In the draft Proxy spec on the wiki, custom attributes are simply copied onto the normalized descriptor as part of the normalization process. It's worth noting that they are always copied as data properties. I think this makes for a good guideline: property descriptors are objects with just data properties.

I'm concerned about two things:

  1. copying is expensive and will almost always be unnecessary
  2. You are assuming that you can meaningfully turn my exotic properties into either data descriptors or accessor descriptors and that when you hand them back to me I won't care. Here's an example where I would care. I add a kind of exotic method property whose descriptor has a "method" property. Methods are never enumerable or writable. I want to throw an error with somebody codes: Object.defineProperty(pObj, "myMethod", {enumerable: true, writable: true,method:func, configurable: true}); //this throws because enumerable or writable methods aren't allowed Instead they should have said: Object.defineProperty(pObj, "myMethod", {method:func, configurable: true});

If you normalize as described in the wiki, then this will fail: Object.defineProperty(pObj2,"myMethod", Object.getOwnPropertyDescriptor(pObj, "myMethod"));

If ECMAScript would have had an actual record value type, I'm confident property descriptors would have been reified as such. Since we only have objects, descriptors were reified as objects, but that brings with it features (mutability, behavior, ...) that seem to get in the way here.

Perhaps, but I fine with using objects. For example, some people have made reasonable use of property descriptor objects that are populated via inheritance.

The issue is that in 8.5.6 Proxy [[GetOwnProperty]], only the "resultDesc" is normalized, not the trapResultObj that is its [[Origin]]. The FromPropertyDescriptor operation blindly returns the [[Origin]], disregarding the normalized descriptor.

Yes, because I don't know what it means to "normalize" an arbitrary exotic property descriptor. The primary purpose of such exotic descriptors is to convey from the Proxy handler to the client information about an exotic object and its exotic properties. It is also used to by the client to pass exotic property attributes back to a handler. Arbitrarily normalizing an exotic descriptor produced by a getOwnPropertyDescriptor trap might result in turning into something that would be treated as an invalid exotic descriptor when passed back to a corresponding defineProperty trap.

If we make it clear that property descriptors are records (i.e. contain only data properties), and normalization entails:

  1. ensure all standard attributes are present and refer to values of the appropriate type
  2. copy any custom properties

Then I think the contract is clear and custom descriptors will round-trip fine.

see above

To rescue the [[Origin]] design, the most straightforward fix I can come up with is that FromPropertyDescriptor first normalizes the [[Origin]] object before returning it (i.e. verifying that it is complete, or making it complete, and ensuring the standard attributes are data properties). Even then so, it's messy that these side-effects are visible to end-user code (the pd1 object of client1 would get mutated-at-a-distance as a result of returning it from a getOwnPropertyDescriptor trap).

Here is a list of alternative, I can envision, ordered (from my perspective) in decreasing desirability:

  1. Provide a Reflect.normalizePD(desc) function that produces a new descriptor object via ToPropertyDescriptor(CompletePropertyDescriptor(FromPropertyDesriptor(desc),undefined)).

    Anybody who is paranoid about encountering exotic descriptors, malformed descriptors, or accessor properties can use it.

I would have agreed to this if it weren't for the fact that there is probably ES5.1 code out there that already tries to be correct in the presence of untrusted code, and doesn't know about or use this primitive. I hope MarkM can point at some relevant sources.

I'd guess that there probably isn't much or any such code in the wild. If we can determine that then perhaps we don't really have a significant issue here.

  1. Normalized only the "configurable" property. Make it an invariant that "configurable" is always present and is a data property. Either normalize it to that or throw as an invariant violation like for other proxy invariants.

    This seems like the only property attribute whose setting is important for the integrity use cases, so like elsewhere in the proxy design only enforce that single invariant.

This still violates the ES5.1 assumption that all descriptors returned by getOwnPropertyDescriptor are guaranteed to be complete.

Again, I don't see why this is an issue. It is either a case of the Proxy handler implementor is intentionally redefining what "complete" means for their exotic properties or it is a run of the milll bug in the handler. I don't see why such bugs is of much concern to us since missing attributes default to their "high integrity" state when used to define a new ordinary object property.

  1. Make it an invariant that all own properties of the returned data property are data properties.

    This gets rid of the accessor hazard. However this puts additional extra checking into the proxy [[GetOwnProperty]] that is almost always going to yield a negative result. In other words, you a placing a performance tax on the usual case with little usual benefit. Unless there is real integrity hazard that can't be explicitly mitigated where it matters, we should just treat such situations like misuse of accessors as user level bugs.

I won't deny there's a performance tax here, either by verifying that the descriptor is really a "record" (i.e. has only data properties), or by making an explicit copy. Like all other invariant checks, I agree they will be wasteful for "benign" proxies.

Then again, let's place things in perspective: when will this become a perf issue? When querying a large number of proxies for their own property descriptors. But then any heap containing a large number of proxies probably already has other perf issues to worry about?

My experience as a language runtime implementor is that all non-essential validity check at low levels of a runtime are undesirable. You simply never know which language features are going to be used by someone in some performance critical situation so it is good practice to push any non-essential checks to higher levels where they can be avoid.

I wouldn't want to be in the position where some part of the DOM or other interesting native library can't, in practice, be self hosted because of performance reasons that trace to unnecessary copying of descriptor objects. I don't know that it will occur, but my experience suggests that it could.

To wrap up, I think the record-like invariant check would be acceptable as we are already inspecting the descriptor object to build the corresponding PD record. I'm strongly \ opposed to any mandated object copying or normalization of exotic descriptor objects to include missing data/accessor property attributes.

# Tom Van Cutsem (11 years ago)

2013/1/3 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Jan 3, 2013, at 12:18 AM, Tom Van Cutsem wrote:

I think it's perfectly obvious what it means for an exotic property to be complete: that it is at least a complete data or accessor property (i.e. it either has {value,writable,enumerable,configurable}, or it has {get,set,enumerable,configurable} properties). It may further specify any number of additional custom attributes.

You seem to be assuming that all exotic properties must be an incremental variation of a data property or an accessor property. However, historically this hasn't been the case. When accessor attributes were added to ES they weren't just a incremental extension to the attributes of a data property. If any new property forms are added to ES in the future, they probably also won't be simply extended variations of data or accessor properties. I would hope that proxies could be used to prototype any such future extensions.

As part of a general extension mechanism, I don't think we should unnecessarily limit how a proxy handler chooses to define the interpretation of its property descriptors. It's clear to me that such limitations restrict the utility of proxies. What benefit to we get from those limitations.

More generally, I think it is backwards-compatible to extend property descriptors with extra attributes, but it's backwards-incompatible to allow proxies to remove some of the standard attributes from property descriptors. This breaks the assumptions of existing clients.

Since we never fully defined the invariants of Object.getOwnPropertyDescriptor it isn't clear what assumptions existing clients should be making. I think the most basic assumption that is justified is that the result of gOPD can be used with Object.defineProperty to define a similar property on a different object that is the same kindof object as the original.

Thanks, I feel this conversation has brought us to a higher-level point that I hope will allow others to join in.

Your position is that ES5.1's property descriptors are really open-ended sets of attributes, and ES5.1 clients should not assume that the result of Object.getOwnPropertyDescriptor is either a data or an accessor property. It may return some third type of non-standard property.

My position is that ES5.1, by codifying only two kinds of property descriptors, effectively does allow clients of Object.getOwnPropertyDescriptor to assume that the result is either a data or an accessor property. And my position is also that there most likely exists ES5.1 code that builds on this assumption. I also don't know of any DOM or other host objects that return a third type of property.

From your point of view, it's only natural that proxies would be an ES6

programmer's tool to define and experiment with entirely new types of properties.

From my point of view, it's problematic (as in: not backwards-compatible)

that ES6+ would expose any new type of property via existing APIs at all (with proxies or even just directly in the spec itself).

We will need some agreement on this higher-level point before we can make good decisions at the level of the Proxy API.

What do others think?

# Allen Wirfs-Brock (11 years ago)

On Jan 4, 2013, at 12:05 AM, Tom Van Cutsem wrote:

From your point of view, it's only natural that proxies would be an ES6 programmer's tool to define and experiment with entirely new types of properties.

From my point of view, it's problematic (as in: not backwards-compatible) that ES6+ would expose any new type of property via existing APIs at all (with proxies or even just directly in the spec itself).

We will need some agreement on this higher-level point before we can make good decisions at the level of the Proxy API.

But are orthogonal issues that I think we an make some progress on.

You're concerned about the nature of the descriptor object returned from the getOwnProertyDescriptor trap. In particular, you want to have something stable enough that you can reason about it and reliably enforce whatever attribute invariants we may have.

I want to avoid cloning, extensive validation, or normalization to data/accessor properties of the object returned by the trap.

As a middle ground that we can build upon I suggest we start with the following enforced invariants for the gOPD trap:

  1. The object returned must be an ordinary object
  2. If the object has value,get,set, writable, enumerable, or configurable properties, then those property must be data properties.

In addition, handlers should adhere to the following unenforced rules:

a) properties of the descriptor object should be configurable and writable (if a data property) b) a property descriptor should contain all information that would be necessary to create the property it describes, if that property did not already exist. c) If a property descriptor object contains either a "value" or "writable" property it should not also contain a "get" or "set" property.

Failure to adhere to the unenforced rules may lead to unexpected client code behavior but does will not threaten the integrity or stability of the ES execution environment.

What do you think? Can we start with the above as a baseline?

# Tom Van Cutsem (11 years ago)

2013/1/4 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Jan 4, 2013, at 12:05 AM, Tom Van Cutsem wrote:

From your point of view, it's only natural that proxies would be an ES6 programmer's tool to define and experiment with entirely new types of properties.

From my point of view, it's problematic (as in: not backwards-compatible) that ES6+ would expose any new type of property via existing APIs at all (with proxies or even just directly in the spec itself).

We will need some agreement on this higher-level point before we can make good decisions at the level of the Proxy API.

But are orthogonal issues that I think we an make some progress on.

You're concerned about the nature of the descriptor object returned from the getOwnProertyDescriptor trap. In particular, you want to have something stable enough that you can reason about it and reliably enforce whatever attribute invariants we may have.

I want to avoid cloning, extensive validation, or normalization to data/accessor properties of the object returned by the trap.

As a middle ground that we can build upon I suggest we start with the following enforced invariants for the gOPD trap:

  1. The object returned must be an ordinary object
  2. If the object has value,get,set, writable, enumerable, or configurable properties, then those property must be data properties.

These rules indeed take away most of my fear of breaking existing code by guaranteeing the stability of "desc.configurable" etc.

I wonder though how easy it is to verify part 2) since descriptor objects may inherit these properties. Since there is no [[GetProperty]] anymore, one would need to explicitly walk the proto-chain in search for the descriptor. This makes the verification more expensive again. It probably also means that property descriptors may not inherit from proxies.

In addition, handlers should adhere to the following unenforced rules:

a) properties of the descriptor object should be configurable and writable (if a data property) b) a property descriptor should contain all information that would be necessary to create the property it describes, if that property did not already exist. c) If a property descriptor object contains either a "value" or "writable" property it should not also contain a "get" or "set" property.

Failure to adhere to the unenforced rules may lead to unexpected client code behavior but does will not threaten the integrity or stability of the ES execution environment.

What do you think? Can we start with the above as a baseline?

It's a start, but I'm still uncomfortable because of potential aliasing issues: if the returned descriptor object is not a copy, then a proxy handler may hold onto a reference to the returned descriptor object. This means it can still update the (mutable) descriptor after having returned it from the trap. The aliasing in combination with the mutability may still trip up clients unexpectedly.

# Allen Wirfs-Brock (11 years ago)

On Jan 5, 2013, at 12:24 AM, Tom Van Cutsem wrote:

2013/1/4 Allen Wirfs-Brock <allen at wirfs-brock.com>

...

You're concerned about the nature of the descriptor object returned from the getOwnProertyDescriptor trap. In particular, you want to have something stable enough that you can reason about it and reliably enforce whatever attribute invariants we may have.

I want to avoid cloning, extensive validation, or normalization to data/accessor properties of the object returned by the trap.

As a middle ground that we can build upon I suggest we start with the following enforced invariants for the gOPD trap:

  1. The object returned must be an ordinary object
  2. If the object has value,get,set, writable, enumerable, or configurable properties, then those property must be data properties.

These rules indeed take away most of my fear of breaking existing code by guaranteeing the stability of "desc.configurable" etc.

I wonder though how easy it is to verify part 2) since descriptor objects may inherit these properties. Since there is no [[GetProperty]] anymore, one would need to explicitly walk the proto-chain in search for the descriptor. This makes the verification more expensive again. It probably also means that property descriptors may not inherit from proxies.

If you think it is important, I'd be willing to go a step further and say that such properties must be own properties. It takes away some flexibility from handlers but I'm willing to give it up. For reasoning purposes you probably you wouldn't want to allow exotic objects (including Proxies) on the prototype chain anyway.

Because Proxy [[GetOwnProperty]] normalized to a PD record it already has to look at these properties so verifying #2 for own properties would probably add very little extra cost.

In addition, handlers should adhere to the following unenforced rules:

a) properties of the descriptor object should be configurable and writable (if a data property) b) a property descriptor should contain all information that would be necessary to create the property it describes, if that property did not already exist. c) If a property descriptor object contains either a "value" or "writable" property it should not also contain a "get" or "set" property.

Failure to adhere to the unenforced rules may lead to unexpected client code behavior but does will not threaten the integrity or stability of the ES execution environment.

What do you think? Can we start with the above as a baseline?

It's a start, but I'm still uncomfortable because of potential aliasing issues: if the returned descriptor object is not a copy, then a proxy handler may hold onto a reference to the returned descriptor object. This means it can still update the (mutable) descriptor after having returned it from the trap. The aliasing in combination with the mutability may still trip up clients unexpectedly.

Only if the client makes a call that gives control back to the handler (or its cohorts) and, in general, all bets are off once you call into untrusted code.