Function identity of non-configurable accessors
On Sat, Dec 15, 2012 at 9:27 AM, David Bruant <bruant.d at gmail.com> wrote:
The way things are going, WindowProxy [Unforgeable] properties will be non-configurable getters. If, upon underlying window change, the WindowProxy is expected to keep the exact same getter function, then, it may result in capability leaks (attach something in a getter in one iframe and get it back when the iframe at src has changed).
This is only a capability leak if these getters are, in fact, a capability. That's an implementation issue that browsers will have to face, not something that needs to be fixed on the ES side.
One solution could be to make these getters frozen; a better solution would be to not "same getter identity" as an invariant for proxies reflecting non-configurable accessors.
This sounds like giving up on invariant checking entirely. If I
create a non-configurable property with a getter that I define (such
as () => 3
), I know that accessing the property will always produce
a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation.
Le 15/12/2012 15:49, Sam Tobin-Hochstadt a écrit :
On Sat, Dec 15, 2012 at 9:27 AM, David Bruant <bruant.d at gmail.com> wrote:
The way things are going, WindowProxy [Unforgeable] properties will be non-configurable getters. If, upon underlying window change, the WindowProxy is expected to keep the exact same getter function, then, it may result in capability leaks (attach something in a getter in one iframe and get it back when the iframe at src has changed). This is only a capability leak if these getters are, in fact, a capability.
These getters are objects and can be added properties to, so they could be used as a communication channel for capabilities across browsing contexts.
That's an implementation issue that browsers will have to face, not something that needs to be fixed on the ES side.
Assuming that having different functions for non-configurable accessors is a requirement for secure WindowProxy and if emulating the DOM (including WindowProxy) is a use case for ES6 Proxies, then making sure ES6 Proxies aren't in the way of WIndowProxy requirements is an ES-side issue.
One solution could be to make these getters frozen; a better solution would be to not "same getter identity" as an invariant for proxies reflecting non-configurable accessors. This sounds like giving up on invariant checking entirely.
Not entirely. The invariant that non-configurable accessor keep being accessors (and don't change to data property) remains.
If I create a non-configurable property with a getter that I define (such as
() => 3
), I know that accessing the property will always produce a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation.
Indeed. Note that it's true currently true with ES5 host objects. The frozen getter/setter could be a working solution as well.
Davis
David Bruant wrote:
If I create a non-configurable property with a getter that I define (such as
() => 3
), I know that accessing the property will always produce a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation. Indeed. Note that it's true currently true with ES5 host objects. The frozen getter/setter could be a working solution as well.
Frozen accessors would be best if we can get away with the incompatibility. Another proposal for public-script-coord or better.
Le 15/12/2012 19:11, Brendan Eich a écrit :
David Bruant wrote:
If I create a non-configurable property with a getter that I define (such as
() => 3
), I know that accessing the property will always produce a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation. Indeed. Note that it's true currently true with ES5 host objects. The frozen getter/setter could be a working solution as well.Frozen accessors would be best if we can get away with the incompatibility.
I've given more thought. Frozen accessors can't be a solution. Only deeply frozen would be. If the accessor isn't deeply frozen, then, it means that any non-frozen object that can be reached through the accessor can be used as a communication channel between 2 browsing contexts that were in the same iframe. Among other things, "deeply frozen" means to freeze the accessor's [[Prototype]] which is Function.prototype which is probably not workable.
Or there are probably other things like comparing iframes contentWindow.Function.prototype objects over time (when changing src) that wouldn't be compatible.
Back to the idea of reflecting different getter/setters for non-configurable accessors, I guess.
David Bruant wrote:
Le 15/12/2012 19:11, Brendan Eich a écrit :
David Bruant wrote:
If I create a non-configurable property with a getter that I define (such as
() => 3
), I know that accessing the property will always produce a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation. Indeed. Note that it's true currently true with ES5 host objects. The frozen getter/setter could be a working solution as well.Frozen accessors would be best if we can get away with the incompatibility. I've given more thought. Frozen accessors can't be a solution. Only deeply frozen would be.
Oh sure -- that goes without saying (or so I thought when I wrote it :-P).
If the accessor isn't deeply frozen, then, it means that any non-frozen object that can be reached through the accessor can be used as a communication channel between 2 browsing contexts that were in the same iframe. Among other things, "deeply frozen" means to freeze the accessor's [[Prototype]] which is Function.prototype which is probably not workable.
Yes, so to avoid the "Ice-9" problem, these would have to bottom out in a "null realm" where everything is frozen: Function.prototype, Object.prototype, anything else required (is anything else required for function objects to be deeply frozen?).
Or there are probably other things like comparing iframes contentWindow.Function.prototype objects over time (when changing src) that wouldn't be compatible.
If you buy the "null realm" idea, the only breaking change would be if someone could extract the get or set function via lookupGetter/lookupSetter or the ES5 standard forms, inspect them and find the current window's realm built-ins. Or monkey-patch the current realm's built-in Function.prototype or Object.prototype and expect those properties to be inherited by the get and set functions.
But this seems like something we could get away with breaking, maybe.
Back to the idea of reflecting different getter/setters for non-configurable accessors, I guess.
Let's talk about deep-freezing and the null realm more.
Is there any leak if you set the accessor functions to have a null [[Prototype]] and to have no non-primitive properties?
Le 15/12/2012 22:20, Brendan Eich a écrit :
David Bruant wrote:
Le 15/12/2012 19:11, Brendan Eich a écrit :
Frozen accessors would be best if we can get away with the incompatibility. I've given more thought. Frozen accessors can't be a solution. Only deeply frozen would be.
Oh sure -- that goes without saying (or so I thought when I wrote it :-P). :-)
If the accessor isn't deeply frozen, then, it means that any non-frozen object that can be reached through the accessor can be used as a communication channel between 2 browsing contexts that were in the same iframe. Among other things, "deeply frozen" means to freeze the accessor's [[Prototype]] which is Function.prototype which is probably not workable.
Yes, so to avoid the "Ice-9" problem, these would have to bottom out in a "null realm" where everything is frozen: Function.prototype, Object.prototype, anything else required (is anything else required for function objects to be deeply frozen?).
A frozen Object.prototype means a non-deletable proto, but it could be omitted, I guess (it's of no use anyway). 'arguments' and 'caller' probably need to be poisoned regardless of strictness in "null realm" (maybe it's already the case in WebIDL?) The rest of the deeply freezing involves a bunch of stateless functions.
Or there are probably other things like comparing iframes contentWindow.Function.prototype objects over time (when changing src) that wouldn't be compatible.
If you buy the "null realm" idea, the only breaking change would be if someone could extract the get or set function via lookupGetter/lookupSetter or the ES5 standard forms, inspect them and find the current window's realm built-ins. Or monkey-patch the current realm's built-in Function.prototype or Object.prototype and expect those properties to be inherited by the get and set functions.
Related to your last point, instanceof Function and instanceof Object do not work as people would expect (unless they're aware of "null realm"). Not a huge deal, bur worth mentioning.
But this seems like something we could get away with breaking, maybe.
My least favorite solution because it creates a weird special case, but it seems workable indeed.
Le 15/12/2012 22:26, Brandon Benvie a écrit :
Is there any leak if you set the accessor functions to have a null [[Prototype]] and to have no non-primitive properties?
There isn't. That would be the super-minimal "null realm" solution.
Le 15/12/2012 16:14, David Bruant a écrit :
Le 15/12/2012 15:49, Sam Tobin-Hochstadt a écrit :
If I create a non-configurable property with a getter that I define (such as
() => 3
), I know that accessing the property will always produce a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation. Indeed. Note that it's true currently true with ES5 host objects.
I'd like to add that you can defend yourself by checking if the reflected function is the one you put in there. As far as expecting some behavior on [[Get]] or [[Set]], nothing prevents a proxy from either not calling the getter/setter you passed or do call them but do/return whatever inconsistent with the accessor. Or why not call the setter on [[Get]] and the getter on [[Set]]. That might be one reason why there was no invariant regarding accessors in ES5.
[Reposted at David's request.]
---------- Forwarded message ---------- From: Mark S. Miller <erights at google.com>
Date: Tue, Dec 18, 2012 at 8:19 AM Subject: Re: Function identity of non-configurable accessors To: David Bruant <bruant.d at gmail.com>
On Tue, Dec 18, 2012 at 8:08 AM, David Bruant <bruant.d at gmail.com> wrote:
[off-list]
Hi Mark,
I have an email with the conclusions on the whole WindowProxy thing and ramifications to be cross-posted to es-discuss and public-script-coord. There are one remaining pending issues about function identity of non-configurable accessors. There are 2 main ideas:
- Allow non-configurable accessors to change the getter/setter functions
That is unacceptable. That breaks the intended invariants. That this invariant isn't specified is an oversight.
- Don't allow to change the functions and for WindowProxy, define functions to have a special deeply frozen Function.prototype and Object.prototype ("null realm" solution championed by Brendan).
That could work, but because of its complexity, I'm leaning back towards the "configurable data property that refuses to be configured" approach. Is there a problem with that? It self-hosts fine.
Mark S. Miller wrote:
That could work, but because of its complexity, I'm leaning back towards the "configurable data property that refuses to be configured" approach. Is there a problem with that? It self-hosts fine.
Certainly this is the simplest solution. It has a slight smell, but no worse than the others'!
On Dec 15, 2012, at 2:51 PM, David Bruant wrote:
Le 15/12/2012 16:14, David Bruant a écrit :
Le 15/12/2012 15:49, Sam Tobin-Hochstadt a écrit :
If I create a non-configurable property with a getter that I define (such as
() => 3
), I know that accessing the property will always produce a known value. Relaxing this restriction means that proxies could produce whatever they wanted in this situation. Indeed. Note that it's true currently true with ES5 host objects. I'd like to add that you can defend yourself by checking if the reflected function is the one you put in there. As far as expecting some behavior on [[Get]] or [[Set]], nothing prevents a proxy from either not calling the getter/setter you passed or do call them but do/return whatever inconsistent with the accessor. Or why not call the setter on [[Get]] and the getter on [[Set]]. That might be one reason why there was no invariant regarding accessors in ES5.
The whole whole idea of such invariants was a late addition to ES5, and not without some controversy. I don't think anyone believed that ES5 had a complete set of invariants or even what that might be.
Regarding the identify of stored/retrieved get/set functions, for "ordinary objects" that is fairly explicit in the ES5 spec. [[DefineOwnProperty]] sets a properties internal attributes to the provided values and [[GetOwnProperty]] retrieves the values of those attributes. The only specified way to modify the identify of such a stored get/set attribute would be by an intervening [[DefineOwnProperty]] call. Hence, we have identify preservation across setting/retrieving of get/set accessor functions. I've thought about making this even more explicit, but the ES5 language seems clear enough.
For "exotic objects", as is usual for most such hypothetical invariants, anything goes in ES5. So, a host object could change the identity of a get/set accessor function. That doesn't bother me, "exotic objects" are an escape mechanism for unanticipated new semantics. But, the provider of such an object really needs to fully document its behavior. Otherwise, its not going to be very useful. But if the documentation says that the identify of get/set functions are not preserved, then that seems like sufficient warning.
On Dec 18, 2012, at 9:08 AM, Brendan Eich wrote:
Mark S. Miller wrote:
That could work, but because of its complexity, I'm leaning back towards the "configurable data property that refuses to be configured" approach. Is there a problem with that? It self-hosts fine.
Certainly this is the simplest solution. It has a slight smell, but no worse than the others'!
I'm really bothered by this concept. It seems to be saying:
For my important use cases, I must be able to depend upon the meaning of [[Configurable]]: false. So implementation must take all measures necessary to ensure that meaning. However, I don't care about other use cases that might depend upon the meaning of [[Configurable]]: true. I think its fine that an implementation lies and says that an object is configurable when it really isn't.
This really seems like a double standard. Integrity issue are important, and in some cases may need to be given special consideration, but we need to consider the full spectrum of use cases.
Regarding simplicity. I don't really see it.
For example, we presumably still want global object bindings introduced via var to not be deletable via delete (unless they were introduced using eval). How do we express the link between the semantic of var and delete if we can't use [[DefineOwnProperty]], [[GetOwnProperty]] and the [[Configurable]] attribute . Instead we have to define some new mechanism/state to establish this relationship [1].
More generally, if we forbid certain host object from having [[Configurable]]: false properties yet we also need to have some properties be non-deletable then we will need a new mechanism that allows that to be expressed and specified.
If we are only talking about the Global Object, we can probably accommodate almost anything by defining it as a special kind of exotic object. We already have special case handling for declarations and identifier resolution involving it. But once we're in the space of arbitrary exotic objects I think we should be very careful about trying to identify and enforce additional invariants.
Allen
On Tue, Dec 18, 2012 at 9:38 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
The whole whole idea of such invariants was a late addition to ES5, and not without some controversy. I don't think anyone believed that ES5 had a complete set of invariants or even what that might be.
As part of the proxy work, Tom and I have started a more complete list of invariants. Yes, it would be good to make more progress on this.
Regarding the identify of stored/retrieved get/set functions, for "ordinary objects" that is fairly explicit in the ES5 spec. [[DefineOwnProperty]] sets a properties internal attributes to the provided values and [[GetOwnProperty]] retrieves the values of those attributes. The only specified way to modify the identify of such a stored get/set attribute would be by an intervening [[DefineOwnProperty]] call. Hence, we have identify preservation across setting/retrieving of get/set accessor functions. I've thought about making this even more explicit, but the ES5 language seems clear enough.
Agreed so far.
For "exotic objects", as is usual for most such hypothetical invariants, anything goes in ES5. So, a host object could change the identity of a get/set accessor function. That doesn't bother me, "exotic objects" are an escape mechanism for unanticipated new semantics. But, the provider of such an object really needs to fully document its behavior. Otherwise, its not going to be very useful. But if the documentation says that the identify of get/set functions are not preserved, then that seems like sufficient warning.
This seems nonsensical to me. At es3.1:attribute_states is my
summary of the point of some of these invariants -- that no state transitions beyond those shown in this diagram are possible. When David pointed out the getter/setter identity stability invariant was missing, this surprised me. It is clearly an oversight. It never occurred to me when drawing this diagram that the lack of getter/setter change applies only to ordinary objects.
JavaScript is a very dynamically typed language. Static reasoning proceeds safely often without knowledge of the object types of the values involved, nor (as usual) of potential aliasing. These invariants enable useful static reasoning precisely because they are universal (and so not type dependent) and monotonic (and so aliasing independent). Further, as I've mentioned several times, direct proxies can leverage the presence of a single invariant-violating exotic object to create any number of other invariant violating objects.
On Tue, Dec 18, 2012 at 10:23 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
If we are only talking about the Global Object, we can probably accommodate almost anything by defining it as a special kind of exotic object.
AFAICT, we are only talking about the global object, in order to deal with the navigation behavior embodied by WindowProxy.
We already have special case handling for declarations and identifier resolution involving it. But once we're in the space of arbitrary exotic objects I think we should be very careful about trying to identify and enforce additional invariants.
By "invariant" here, I only mean those that are universal. NONE of the invariants we have been discussing here have applied only to ordinary objects. We are not talking about adding any additional invariants. But we should certainly flesh out the list of invariants we've got so they form a coherent set. The getter/setter-identity issue is the only such that I've seen in this thread which we had previously failed to identify, though no doubt there are others. But this doesn't seem to be the invariant you're arguing with. The more general "non-configurable implies stability" invariant isn't new, and has always applied to exotics.
On Dec 18, 2012, at 10:34 AM, Mark S. Miller wrote:
On Tue, Dec 18, 2012 at 9:38 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
The whole whole idea of such invariants was a late addition to ES5, and not without some controversy. I don't think anyone believed that ES5 had a complete set of invariants or even what that might be.
As part of the proxy work, Tom and I have started a more complete list of invariants. Yes, it would be good to make more progress on this.
Regarding the identify of stored/retrieved get/set functions, for "ordinary objects" that is fairly explicit in the ES5 spec. [[DefineOwnProperty]] sets a properties internal attributes to the provided values and [[GetOwnProperty]] retrieves the values of those attributes. The only specified way to modify the identify of such a stored get/set attribute would be by an intervening [[DefineOwnProperty]] call. Hence, we have identify preservation across setting/retrieving of get/set accessor functions. I've thought about making this even more explicit, but the ES5 language seems clear enough.
Agreed so far.
For "exotic objects", as is usual for most such hypothetical invariants, anything goes in ES5. So, a host object could change the identity of a get/set accessor function. That doesn't bother me, "exotic objects" are an escape mechanism for unanticipated new semantics. But, the provider of such an object really needs to fully document its behavior. Otherwise, its not going to be very useful. But if the documentation says that the identify of get/set functions are not preserved, then that seems like sufficient warning.
This seems nonsensical to me. At es3.1:attribute_states is my summary of the point of some of these invariants -- that no state transitions beyond those shown in this diagram are possible. When David pointed out the getter/setter identity stability invariant was missing, this surprised me. It is clearly an oversight. It never occurred to me when drawing this diagram that the lack of getter/setter change applies only to ordinary objects.
Similarly, then a value: identify (actually value) stability invariant is also missing...
This diagram is based upon [[DefineOwnProperty]] for ordinary objects. The fact it is specified as an internal method is a way of explicitly saying that exotic objects might apply different rules. That has pretty much always been the ES definition of "host object", an object that doesn't follow the ordinary rules.
JavaScript is a very dynamically typed language. Static reasoning proceeds safely often without knowledge of the object types of the values involved, nor (as usual) of potential aliasing. These invariants enable useful static reasoning precisely because they are universal (and so not type dependent) and monotonic (and so aliasing independent).
I think you and I differ on what "useful static reasoning" means. To me, wearing the hat of an application developer who is predominately dealing with my own or other trusted code, it means that I can reason about the expect behavior of my program using the ordinary semantics plus any exceptional semantics that I am explicitly expecting to deal with. Anything else that might occur corresponds to a "bug". I look for and test for bugs, but realistically I never expect a program to be totally bug free.
However, in your description above you use the term "safely" which I understand that for you goes hand-in-hand with "useful". For high integrity code, static reasoning is presumably only useful if it is also safe from an integrity perspective. Basically, you need perfect code. You can't tolerate bugs in your code, your reasoning, or violations of the invariants that are the axioms of you reasoning.
This seems like the crux of matter WRT whether OCAP within a general purpose OO language can be useful as the sole high integrity encapsulation mechanism. To you, as a builder of high integrity sandboxes, enforcement of certain invariants are essential. To me, as a application programmer or even a library programmer, enforcement of these invariants are generally unnecessary. If enforcement impacts performance or expressibility they have a negative impact on my ability to get my job done.
I wrote about some related issues here: www.wirfs-brock.com/allen/posts/379 Personally, I think we need the sort of kernel/framework separation I wrote about there. OCAP probably isn't the sole answer.
Further, as I've mentioned several times, direct proxies can leverage the presence of a single invariant-violating exotic object to create any number of other invariant violating objects.
I buy this and I'm sure you've probably described a specific scenario already. Can you provide a link to the specific exploit you have in mind here.
2012/12/15 David Bruant <bruant.d at gmail.com>
Hi,
ES5 invariants are silent when it comes to function identity of non-configurable accessors. That is, for a given object o, Object.**getOwnPropertyDescriptor(o, 'a').get === Object.**getOwnPropertyDescriptor(o, 'a').get (two seperate calls) is not guaranteed.
The built-in [[DefineOwnProperty]] (ES5.1 - 8.12.9) prevents from changing of getter and setter functions when the property is not configurable (step 11), but that's not an expected invariant for self-hosted objects.
I think that the current definition of non-compatible descriptors (based on Object.**getOwnPropertyDescriptor and Object.defineProperty) used in invariant checks makes impossible to use different functions and that may be a problem.
Indeed, proxies enforce this invariant. I don't see this as a problem.
The way things are going, WindowProxy [Unforgeable] properties will be non-configurable getters. If, upon underlying window change, the WindowProxy is expected to keep the exact same getter function, then, it may result in capability leaks (attach something in a getter in one iframe and get it back when the iframe at src has changed). One solution could be to make these getters frozen; a better solution would be to not "same getter identity" as an invariant for proxies reflecting non-configurable accessors.
It seems you are arguing to weaken probably the most important ES5 invariant ("non-configurability implies stability", as Mark put it succinctly) to support the emulation of a pretty exotic object.
Re. the potential capability leak: even if the get/set attribute of a non-configurable property could be updated, how does that prevent the leak? Code that could previously access the old value of the getter via a WindowProxy can presumably as easily access the new value of the getter via the same WindowProxy.
To me, the issue does not seem to be related to non-configurable accessors, but rather to the fact that when a WindowProxy changes location, you expect "old" clients of the WindowProxy to no longer be able to access "new" target state. That seems impossible if both old and new clients share the same proxy.
2012/12/18 Allen Wirfs-Brock <allen at wirfs-brock.com>
On Dec 18, 2012, at 9:08 AM, Brendan Eich wrote:
Mark S. Miller wrote:
That could work, but because of its complexity, I'm leaning back towards the "configurable data property that refuses to be configured" approach. Is there a problem with that? It self-hosts fine.
Certainly this is the simplest solution. It has a slight smell, but no worse than the others'!
I'm really bothered by this concept. It seems to be saying:
For my important use cases, I must be able to depend upon the meaning of [[Configurable]]: false. So implementation must take all measures necessary to ensure that meaning. However, I don't care about other use cases that might depend upon the meaning of [[Configurable]]: true. I think its fine that an implementation lies and says that an object is configurable when it really isn't.
This really seems like a double standard. Integrity issue are important, and in some cases may need to be given special consideration, but we need to consider the full spectrum of use cases.
Regarding simplicity. I don't really see it.
For example, we presumably still want global object bindings introduced via var to not be deletable via delete (unless they were introduced using eval). How do we express the link between the semantic of var and delete if we can't use [[DefineOwnProperty]], [[GetOwnProperty]] and the [[Configurable]] attribute . Instead we have to define some new mechanism/state to establish this relationship [1].
I too find the solution of non-configurable data properties masquerading as configurable data properties bothersome. We have a mechanism in place for describing the behavior of ES5 properties, yet here is a case where we can't be honest about a property's real behavior. That smells.
The issue seems to be that there are two "access rights" to this data property: external clients are only allowed to read, but not write to the property (that is why it should really be reflected to clients as non-configurable). Some privileged code, however, is allowed to write to the property (that is why we cannot really make it non-configurable).
Implementing these access rights is easy to achieve using an accessor property, so it seems logical to turn this property into an accessor.
Why can't we make it a non-configurable accessor property? I don't understand the leak described by David in the OP.
On 18 December 2012 20:24, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
To me, as a application programmer or even a library programmer, enforcement of these invariants are generally unnecessary. If enforcement impacts performance or expressibility they have a negative impact on my ability to get my job done.
I take issues with the dichotomy you build up here. It's important to note that enforcement is a form of expressiveness! Unfortunately, one that is too often overlooked.
Expressiveness is defined by what a piece of code can do, as well as, dually, what a piece of code can enforce its context not to do. If I cannot prevent things from happening then I generally have to work around that by defending against them manually and potentially everywhere.
Moreover, as an application or library programmer I constantly need to enforce certain things, especially against myself. Any smart programmer knows that his own stupidity is most likely to outsmart him.
Le 19/12/2012 14:38, Tom Van Cutsem a écrit :
I too find the solution of non-configurable data properties masquerading as configurable data properties bothersome. We have a mechanism in place for describing the behavior of ES5 properties, yet here is a case where we can't be honest about a property's real behavior. That smells.
That smell is partially a consequence of configurable being overloaded. At the same times, configurable:false means "the property can't disappear" and "applying the 'delete' operator fails" as a corrolary. We're in a case (WindowProxy) where we want the property to disappear, but 'delete' to fail.
The issue seems to be that there are two "access rights" to this data property: external clients are only allowed to read, but not write to the property (that is why it should really be reflected to clients as non-configurable). Some privileged code, however, is allowed to write to the property (that is why we cannot really make it non-configurable).
Implementing these access rights is easy to achieve using an accessor property, so it seems logical to turn this property into an accessor.
Why can't we make it a non-configurable accessor property? I don't understand the leak described by David in the OP.
The leak is very specific to the WindowProxy case
// # Inside an iframe
var locationPropDesc = Object.getOwnPropertyDescriptor(window,
'location'); assert(locationPropDesc.configurable === false); // 'false' is the current WebIDL definition of [Unforgeable] properties // but consensus seems to be that WindowProxy [Unforgeables] would be 'true'
var locationGetter = locationPropDesc.get;
locationGetter.cap = whatever;
// # Outside the iframe
var win = iframe.contentWindow;
var cap = Object.getOwnPropertyDescriptor(win, 'location').get.cap;
iframe.src = "somewhere else";
// # inside the iframe *after* the navigation
var locationPropDesc = Object.getOwnPropertyDescriptor(window,
'location'); assert(locationPropDesc.configurable === false);
var locationGetterInsideIframe = locationPropDesc.get;
The part where it gets tricky is here. If window.location is indeed non-configurable and getter identity is preserved (observable from outside the iframe), then locationGetterInsideIframe has a property "cap" which value comes from the previous browsing context in the iframe. That's the leak. We have 2 browsing contexts able to exchange capabilities why they probably shouldn't. I don't know if it's a bad or dangerous leak, but it looks very fishy.
That's easily solved by making the accessor have a null prototype and frozen though.
Le 19/12/2012 16:34, Brandon Benvie a écrit :
That's easily solved by making the accessor have a null prototype and frozen though.
True, but it makes that you can't re-apply these getters and setters with .call. Since the getter doesn't inherit from Function.prototype any longer, you have to go through
Function.prototype.call.call(webIDLGetter, context)
or
var call = Function.prototype.call.bind(Function.prototype.call);
call(webIDLGetter, context);
Since recontextualizing getter/setter is probably one of their main use case, I wish this wasn't the chosen solution. I'd like to point out that recently on Twitter, Eddy Bruel, (who among other things implemented direct proxies on SpiderMonkey) shared his trouble understanding the Function.prototype.call.bind trick [1] If he has trouble, I'm sure thousands (millions?) of less educated JS devs will do too.
I prefer the configurable:true solution so far.
David
It only has to be a handful of accessors, no? I was thinking basically of
just the few things on the global object that need this treatment.
location, document, window, navigator, maybe a few others? And location is
the only one with a setter, so we're mostly just talking about getters on
the global object. Since it's the global object, you could just call the
getter directly and this
will implicitly be the correct receiver when
called inside the context. So the remaining situations in which it's
difficult are in the parent frame when you want to do this one of the
handful of properties that have this treatment.
An option that might have uses in other places as well is a kind of primitive version of call, bind, and apply that have the null prototype, frozen treatment. They could be singletons over the entire runtime, across realms, and be attached to any function in any realm (including these accessors) without leaking anything.
Also note that none of these properties are currently accessors so no one has come to rely on being able to extract their getters. So introducing them with neutered accessors would not have an impact on existing code.
Le 19/12/2012 17:34, Brandon Benvie a écrit :
Also note that none of these properties are currently accessors so no one has come to rely on being able to extract their getters. So introducing them with neutered accessors would not have an impact on existing code.
I fully agree. I wasn't really worried about backward compat.
On Wednesday, December 19, 2012, Brandon Benvie wrote:
It only has to be a handful of accessors, no? I was thinking basically of just the few things on the global object that need this treatment. location, document, window, navigator, maybe a few others?
Very good point. We're not redefining what [Unforgeable] means, but only what it means for WindowProxy objects, so that's very few properties in the end.
And location is the only one with a setter, so we're mostly just talking about getters on the global object. Since it's the global object, you could just call the getter directly and `this` will implicitly be the correct receiver when called inside the context.
I always use strict mode, so it wouldn't work for me. But since these getter/setter are special, I wouldn't be shocked if they were bound functions.
So the remaining situations in which it's difficult are in the parent frame when you want to do this one of the handful of properties that have this treatment. An option that might have uses in other places as well is a kind of primitive version of call, bind, and apply that have the null prototype, frozen treatment. They could be singletons over the entire runtime, across realms, and be attached to any function in any realm (including these accessors) without leaking anything.
I think that was what Brendan called "null realm". It has the problem that if someone adds new things to Function.prototype or Object.prototype, your "null realm" getters/setters wouldn't benefit from it.
2012/12/19 David Bruant <bruant.d at gmail.com>
Le 19/12/2012 17:34, Brandon Benvie a écrit :
So the remaining situations in which it's difficult are in the parent
frame when you want to do this one of the handful of properties that have this treatment.
An option that might have uses in other places as well is a kind of primitive version of call, bind, and apply that have the null prototype, frozen treatment. They could be singletons over the entire runtime, across realms, and be attached to any function in any realm (including these accessors) without leaking anything.
I think that was what Brendan called "null realm". It has the problem that if someone adds new things to Function.prototype or Object.prototype, your "null realm" getters/setters wouldn't benefit from it.
Is that really a problem though? As mentioned, these properties aren't accessors today. So no code could depend on those accessors having extra properties. I think I'm leaning towards non-configurable accessors with deep-frozen (null-realm) getter/setter functions.
Le 20/12/2012 09:02, Tom Van Cutsem a écrit :
2012/12/19 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
Le 19/12/2012 17:34, Brandon Benvie a écrit :
So the remaining situations in which it's difficult are in the parent frame when you want to do this one of the handful of properties that have this treatment. An option that might have uses in other places as well is a kind of primitive version of call, bind, and apply that have the null prototype, frozen treatment. They could be singletons over the entire runtime, across realms, and be attached to any function in any realm (including these accessors) without leaking anything.
I think that was what Brendan called "null realm". It has the problem that if someone adds new things to Function.prototype or Object.prototype, your "null realm" getters/setters wouldn't benefit from it.
Is that really a problem though?
I don't know. I was just repeating the main argument against this idea.
Historically, the ECMAScript representation of browser APIs was probably the least interoperable part of the web platform and certainly still is. Open davidbruant.github.com/ObjectViz and click anywhere, it should visualize a click event object. The result is very different in the latest Chrome, Firefox, Opera and IE (I haven't tested Safari but I assume it's the same than Chrome). There are good reasons for this (lack of spec initially then software architecture issues). Web devs have written at length about the fact that extending the DOM was a bad idea [1].
But let's project ourselves in a world of WebIDL compliant browsers and ES5+symbols. Suddenly, extending the ECMAScript environment is safe (collision-free) with unique symbols and extending the DOM is both safe and reliable (thanks to WebIDL-compliance). Will special-casing WindowProxy [Unforgeable] getters/setters undermine the possibilities of this world? I cannot tell. I'm too used to the current world where extending the language is socially unacceptable.
I'm not worried about backward-compat (because as you and Brandon said, it's not a problem). I'm rather worried about potential forward-compat issues. configurable:true seems to be more future-friendly.
I think I'm leaning towards non-configurable accessors with deep-frozen (null-realm) getter/setter functions.
Brandon's point about the fact that it really is the matter of a few properties made me reconsider my position. I'm between deeply-frozen and configurable:true and can't make up my mind :-s
David
2012/12/20 David Bruant <bruant.d at gmail.com>
Le 20/12/2012 09:02, Tom Van Cutsem a écrit :
I think I'm leaning towards non-configurable accessors with deep-frozen (null-realm) getter/setter functions.
Brandon's point about the fact that it really is the matter of a few properties made me reconsider my position. I'm between deeply-frozen and configurable:true and can't make up my mind :-s
I think the general issue (of which we are discussing a tiny specific case) is the following:
Mark's position is that configurable:true implies no formal contract on the part of the advertising object. A property advertised as configurable may or may not behave as configurable. On the other hand, configurable:false implies a formal contract, and one for which we go through great pains so that even proxies can't violate the contract.
Mark supports this view to enable reasoning about code of the form:
if ( ! Object.getOwnPropertyDescriptor(obj, "foo").configurable) { // I can now safely assume that obj.foo will never change }
Allen's position is that this is a double standard. I think he wants to support reasoning about code of the form:
if (Object.getOwnPropertyDescriptor(obj, "foo").configurable) { // I can now safely assume that updating obj.foo will actually update the property }
If I understand correctly, the current reality is that there exist DOM properties that violate the configurable:true contract, but there are not (yet?) DOM properties that violate the configurable:false contract.
If that is true, then making the accessors on WindowProxy be configurable:true would continue the current practice of violating the configurable:true contract. Not saying this is good or bad, just an observation.
Le 20/12/2012 13:18, Tom Van Cutsem a écrit :
2012/12/20 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
Le 20/12/2012 09:02, Tom Van Cutsem a écrit :
I think I'm leaning towards non-configurable accessors with deep-frozen (null-realm) getter/setter functions.
Brandon's point about the fact that it really is the matter of a few properties made me reconsider my position. I'm between deeply-frozen and configurable:true and can't make up my mind :-s
I think the general issue (of which we are discussing a tiny specific case) is the following:
Mark's position is that configurable:true implies no formal contract on the part of the advertising object. A property advertised as configurable may or may not behave as configurable. On the other hand, configurable:false implies a formal contract, and one for which we go through great pains so that even proxies can't violate the contract.
Mark supports this view to enable reasoning about code of the form:
if ( ! Object.getOwnPropertyDescriptor(obj, "foo").configurable) {
host objects (or proxies) could omit .configurable or make it any other falsy value. The test is rather: if (Object.getOwnPropertyDescriptor(obj, "foo").configurable === false) { It's a nit, but an important one.
// I can now safely assume that obj.foo will never change }
Allen's position is that this is a double standard. I think he wants to support reasoning about code of the form:
if (Object.getOwnPropertyDescriptor(obj, "foo").configurable) { // I can now safely assume that updating obj.foo will actually update the property }
If I understand correctly, the current reality is that there exist DOM properties that violate the configurable:true contract
Not that I am aware of. But Mark repeated suggestion was to go in that direction and I agree because of the next point:
but there are not (yet?) DOM properties that violate the configurable:false contract.
Quite the opposite, there are 3 of them just for WindowProxy!
- davidbruant.github.com/iframeProxyIssueDemo View source and open the console to see the configurable:false. I see the violation in Firefox and Chrome and this violation is what the current WebIDL spec expects. (side note: I have the proxy invariants enforcement rules hardwired in my brain because I have a weird feeling each time I open the page and see the violation; a little voice telling me "something is not right in this code and what's logged")
- var/function global declarations. See: esdiscuss/2012-December/027180 & esdiscuss/2012-December/027183
- WebIDL requires [Unforgeable] to be non-configurable meaning that getter/setter identity has to be preserves which can lead to a capability leak. WebIDL currently requires web browsers to leak capabilities or violate the getter/setter identity requirement.
configurable:false violation are the ones I have been chasing, because they go against the ES5 invariants and consequently can't be emulated with proxies.
WindowProxy would violate both contracts when reflecting var/function globals and [Unforgeable]. These properties can't be configurable:false because they may disappear and they don't respect "Allen's configurable:true contract", because they can't be removed using 'delete'. The WindowProxy de-facto standard does not correspond to the configurable true or false behavior, so one contract has to be violated. Because of the importance of invariants, it has to be configurable:true. But this contract is not a big deal because it's more a convention that a contract. Neither ES5 host objects nor proxies are expected to anything regarding configurable:true. All objects defined in the ES spec are expected to follow the contract defined by the semantics of its internal methods.
The current solutions are:
- throw on Object.defineProperty with configurable:false
- configurable:true
- Still in debate 3.1) configurable:true 3.2) configurable:false with deeply frozen "null realm" getters/setters
If that is true, then making the accessors on WindowProxy be configurable:true would continue the current practice of violating the configurable:true contract.
It would actually be the first configurable:true convention "violation".
In Chrome (WebKit in general maybe?) there are already plenty of examples of violating the convention "configurable: true means deletable". For example, attempting to delete any of the hundreds DOM interfaces that are present on the WindowProxy does nothing. Using defineProperty to overwrite them works but as soon as you delete that newly defined version the original definition comes back.
2012/12/20 David Bruant <bruant.d at gmail.com>
Le 20/12/2012 13:18, Tom Van Cutsem a écrit :
Mark supports this view to enable reasoning about code of the form:
if ( ! Object.getOwnPropertyDescriptor(obj, "foo").configurable) {
host objects (or proxies) could omit .configurable or make it any other falsy value. The test is rather: if (Object.getOwnPropertyDescriptor(obj, "foo").configurable === false) { It's a nit, but an important one.
We've taken care that Object.getOwnPropertyDescriptor actually normalizes and completes the standard attributes, so proxies at least cannot do that.
If I understand correctly, the current reality is that there exist DOM properties that violate the configurable:true contract
Not that I am aware of. But Mark repeated suggestion was to go in that direction and I agree because of the next point:
By the configurable:true contract, I also meant the "deletable" part, which as Brandon points out, is violated in practice.
but there are not (yet?) DOM properties that violate the configurable:false contract.
Quite the opposite, there are 3 of them just for WindowProxy!
- davidbruant.github.com/iframeProxyIssueDemo View source and open the console to see the configurable:false. I see the violation in Firefox and Chrome and this violation is what the current WebIDL spec expects. (side note: I have the proxy invariants enforcement rules hardwired in my brain because I have a weird feeling each time I open the page and see the violation; a little voice telling me "something is not right in this code and what's logged")
:-)
WindowProxy would violate both contracts when reflecting var/function globals and [Unforgeable]. These properties can't be configurable:false because they may disappear and they don't respect "Allen's configurable:true contract", because they can't be removed using 'delete'. The WindowProxy de-facto standard does not correspond to the configurable true or false behavior, so one contract has to be violated. Because of the importance of invariants, it has to be configurable:true. But this contract is not a big deal because it's more a convention that a contract. Neither ES5 host objects nor proxies are expected to anything regarding configurable:true. All objects defined in the ES spec are expected to follow the contract defined by the semantics of its internal methods.
Generalizing from this specific example again, we have the following facts:
- proxies are restricted so that they can only emulate objects that uphold ES5 invariants
- DOM objects are weird objects that don't always act like ES5 objects.
- Some DOM objects don't uphold ES5 invariants.
- an important use case for proxies is to be able to self-host DOM objects.
Putting these together, the inevitable conclusion is that there are some DOM objects that can't be emulated by proxies.
What to conclude from that? a) lift the restrictions on proxies. This is the same as saying: ES6 has no universal invariants (it has some invariants that apply to normal objects, but none that apply to normal and exotic objects alike) One could introduce an Object.isExotic predicate that answers true on host and proxy objects, allowing code to "protect" itself from these objects. One issue is that extant ES5 code does not protect itself in this way. b) make the consensus that in a proxy-emulated DOM, it's OK for the emulation to be imperfect when it comes to invariants. One could argue that having the emulation be imperfect for these cases is not that big a deal, especially since the incompatibilities involve corner cases with little cross-browser compatibility in the first place.
Any other options?
My intuition, I now realize just assumption, was that most/all invariant guarantees were one way. That is: { configurable: true } specifically does not guarantee anything (although I guess that's a kind of guarantee). Either way, it is intuitive for me that something that is configurable says nothing about whether it's deletable except that it might be. The guarantees come in when configurable is false. There's no conflict if this policy rings true.
Le 20/12/2012 18:51, Tom Van Cutsem a écrit :
2012/12/20 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
Le 20/12/2012 13:18, Tom Van Cutsem a écrit :
Mark supports this view to enable reasoning about code of the form: if ( ! Object.getOwnPropertyDescriptor(obj, "foo").configurable) {
host objects (or proxies) could omit .configurable or make it any other falsy value. The test is rather: if (Object.getOwnPropertyDescriptor(obj, "foo").configurable === false) { It's a nit, but an important one.
We've taken care that Object.getOwnPropertyDescriptor actually normalizes and completes the standard attributes, so proxies at least cannot do that.
Good point. I had forgotten that part.
WindowProxy would violate both contracts when reflecting var/function globals and [Unforgeable]. These properties can't be configurable:false because they may disappear and they don't respect "Allen's configurable:true contract", because they can't be removed using 'delete'. The WindowProxy de-facto standard does not correspond to the configurable true or false behavior, so one contract has to be violated. Because of the importance of invariants, it has to be configurable:true. But this contract is not a big deal because it's more a convention that a contract. Neither ES5 host objects nor proxies are expected to anything regarding configurable:true. All objects defined in the ES spec are expected to follow the contract defined by the semantics of its internal methods.
Generalizing from this specific example again, we have the following facts:
- proxies are restricted so that they can only emulate objects that uphold ES5 invariants
- DOM objects are weird objects that don't always act like ES5 objects.
- Some DOM objects don't uphold ES5 invariants.
If I had to guess, I would say that the reason is that the invariants are a not well-known part of the ES5 spec and that before I raised the concerned, no one had thought about what happens to the WindowProxy object of an iframe when navigation occurs. Since WebIDL is not widely deployed (IE9 and probably 10 conform well, Firefox's woking on it. I don't know for the rest), it's still easy to change the spec and implementations.
- an important use case for proxies is to be able to self-host DOM objects.
Putting these together, the inevitable conclusion is that there are some DOM objects that can't be emulated by proxies.
Wasn't it a goal of the Proxy API? It seems that we're only a couple of booleans away from achieving it (besides document.all being falsy).
What to conclude from that? a) lift the restrictions on proxies. This is the same as saying: ES6 has no universal invariants (it has some invariants that apply to normal objects, but none that apply to normal and exotic objects alike) One could introduce an Object.isExotic predicate that answers true on host and proxy objects, allowing code to "protect" itself from these objects. One issue is that extant ES5 code does not protect itself in this way. b) make the consensus that in a proxy-emulated DOM, it's OK for the emulation to be imperfect when it comes to invariants. One could argue that having the emulation be imperfect for these cases is not that big a deal, especially since the incompatibilities involve corner cases with little cross-browser compatibility in the first place.
In both cases, you're going to make Mark angry ;-) If any of these 2 ideas were considered, I would go further and add an Object.doesNotConformToInvariants sort of test so that only one test can determine whether an object conforms to invariants or not. Bouncing on Mark's point about building new invariant-violating objects when you have access to existing ones, if a proxy has a target returning false for this test, so would the proxy.
I have to admit, I like the idea of eternal invariants that apply to every single ECMAScript object and the set we currently have is small enough to be not that intrusive.
Any other options?
Change WebIDL/WindowProxy to conform to the ES5 invariants, which they should have from the start and still can because WebIDL has a low adoption.
Given that there are plenty of cases where the configurable:true "contract" is violated (i.e. non-deletable configurable properties), and that it is still possible to fix the 3 violations of the configurable:false contract, I think I'm swayed to simply go for configurable:true getters/setters that refuse to be updated.
Also, with proxies, it becomes easy for user-defined abstractions to violate the configurable:true contract as well. So there's little point in pretending that configurable:true implies any universal invariants.
Le 21/12/2012 20:19, Tom Van Cutsem a écrit :
Given that there are plenty of cases where the configurable:true "contract" is violated (i.e. non-deletable configurable properties), and that it is still possible to fix the 3 violations of the configurable:false contract, I think I'm swayed to simply go for configurable:true getters/setters that refuse to be updated.
Also, with proxies, it becomes easy for user-defined abstractions to violate the configurable:true contract as well. So there's little point in pretending that configurable:true implies any universal invariants.
That's exactly how I think about it. I've failed to express it as concisely, but I fully agree with this view.
From this point on, if there is value for an object to be assigned configurable non-deletable properties or reflect existing properties as such, ideas like deletable:false (see discussion starting at [1]) can still be considered.
David
ES5 invariants are silent when it comes to function identity of non-configurable accessors. That is, for a given object o, Object.getOwnPropertyDescriptor(o, 'a').get === Object.getOwnPropertyDescriptor(o, 'a').get (two seperate calls) is not guaranteed.
The built-in [[DefineOwnProperty]] (ES5.1 - 8.12.9) prevents from changing of getter and setter functions when the property is not configurable (step 11), but that's not an expected invariant for self-hosted objects.
I think that the current definition of non-compatible descriptors (based on Object.getOwnPropertyDescriptor and Object.defineProperty) used in invariant checks makes impossible to use different functions and that may be a problem.
The way things are going, WindowProxy [Unforgeable] properties will be non-configurable getters. If, upon underlying window change, the WindowProxy is expected to keep the exact same getter function, then, it may result in capability leaks (attach something in a getter in one iframe and get it back when the iframe at src has changed). One solution could be to make these getters frozen; a better solution would be to not "same getter identity" as an invariant for proxies reflecting non-configurable accessors.