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

Allen Wirfs-Brock allen at wirfs-brock.com
Thu Jan 3 12:10:22 PST 2013


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.
> 
>> 2) 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.

>> 3) 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.

>  
> 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. 
> 
> 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.

>  
> 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.
> 
> 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.

Allen

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20130103/87e28f80/attachment-0001.html>


More information about the es-discuss mailing list