Membranes, unmediated access to objects through Object.getPrototypeOf
Another issue with membranes and incomplete trap coverage is mutations to [[proto]]. An implementation of this would be to automatically wrapIfDry whatever the result of getPrototype is which isn't possible under the current rules without using a dummy target.
I really don't think this is an "elephant in the room". We've encountered this before. The issue you point out is exactly the same issue with regard to the "get" trap having to return an identical value (as per [[SameValue]]) for non-configurable own properties of the target.
The solution for wrapping non-configurable own properties was to introduce a "dummy" target on which to store wrapped versions of the properties. The solution to wrapping [[Prototype]] is to similarly use a dummy target whose prototype is the wrapped prototype of the real target.
Indeed, one of the other reasons for introducing a "getPrototypeOf" trap was so that membranes could properly "sync" the dummy's [[Prototype]] with the target's [[Prototype]] in the face of mutable prototypes.
Your proposed alternative involving the target-chain is the solution employed by Racket chaperones [1]. Mark and I have been hesitant to use this solution as it violates invariants pertaining to object-identity in Javascript. For example, using the current proxy spec and assuming no mutable proto (e.g. Object.prototype.proto was deleted), you're guaranteed that Object.getPrototypeOf(obj) always returns the same (as per "===") object. The chaperone solution voids that guarantee.
Finally, note that a "naively implemented" membrane that just always wraps the prototype, without using a dummy target, would still not have leaked any references: this code would instead trip on the assertions that protect the "getPrototypeOf" trap as it tries to return a wrapped prototype.
Cheers, Tom
[1] www.cs.utah.edu/plt/publications/oopsla12-stff.pdf
2012/10/8 David Bruant <bruant.d at gmail.com>
Just to be sure I coded up your example and tested it with my harmony-reflect shim and a "naive" membrane. The code throws as expected: < gist.github.com/3854020>
[I re-ordered a bit your message before answering]
2012/10/8 Tom Van Cutsem <tomvc.be at gmail.com>
Hi David,
I really don't think this is an "elephant in the room".
It's reassuring, I was somewhat shocked by the thought something like this could have been overlooked :-p
Finally, note that a "naively implemented" membrane that just always wraps the prototype, without using a dummy target, would still not have leaked any references: this code would instead trip on the assertions that protect the "getPrototypeOf" trap as it tries to return a wrapped prototype.
True, I forgot this second half about throwing, but the overall point
remains: Object.getPrototypeOf cannot be used within a membrane. The two choices are leak or throw, neither being practical. Within a membrane, the following object-identity equailty is broken: Object.getPrototypeOf(new wrappedC) !== wrappedC.prototype.constructor (either because the operation throws or the returned object is a wrapped version) My point is that by enforcing some identity invariants, we're breaking some others. Likewise, for frozen properties, membraned code either can't make use of them (because they throw when wrapped values are presented) or the unwrapped values leak. Once again, "throw or leak" is not a practical choice.
We've encountered this before. The issue you point out is exactly the same
issue with regard to the "get" trap having to return an identical value (as per [[SameValue]]) for non-configurable own properties of the target.
The solution for wrapping non-configurable own properties was to introduce a "dummy" target on which to store wrapped versions of the properties. The solution to wrapping [[Prototype]] is to similarly use a dummy target whose prototype is the wrapped prototype of the real target.
...and well, creating an entire new subgraph of proxies over dummy targets doesn't look very practical either. Basically, for a fully frozen graph, you need twice as much memory to membrane it (once for the original targets, once for the dummy targets).
Your proposed alternative involving the target-chain is the solution
employed by Racket chaperones [1]. Mark and I have been hesitant to use this solution as it violates invariants pertaining to object-identity in Javascript. For example, using the current proxy spec and assuming no mutable proto (e.g. Object.prototype.proto was deleted), you're guaranteed that Object.getPrototypeOf(obj) always returns the same (as per "===") object. The chaperone solution voids that guarantee.
Why do we need this guarantee anyway? From inside a membrane, objects you know about are wrapped, you're not able to differentiate the target.[[Prototype]] from the one that's been returned because you never have access to the original target.[[Prototype]]. Even if you did acquire access to this object by another mean, having a different identity is actually a feature, especially when it comes to solving some problems where identity is the only element that can enable differenciation [1] ;-)
Just to clarify, when I ask "Why do we need this guarantee anyway?", I don't ask to be able to return any arbitrary object, just a proxy which has the expected object in its target chain, which sounds like a reasonable way to bend the identity equality invariant.
David
[+markm]
2012/10/9 David Bruant <bruant.d at gmail.com>
Finally, note that a "naively implemented" membrane that just always wraps the prototype, without using a dummy target, would still not have leaked any references: this code would instead trip on the assertions that protect the "getPrototypeOf" trap as it tries to return a wrapped prototype.
True, I forgot this second half about throwing, but the overall point remains: Object.getPrototypeOf cannot be used within a membrane. The two choices are leak or throw, neither being practical.
No, there's a third choice: use a dummy target and return its wrapped prototype.
Within a membrane, the following object-identity equailty is broken: Object.getPrototypeOf(new wrappedC) !== wrappedC.prototype.constructor (either because the operation throws or the returned object is a wrapped version)
Sorry, I don't get it. I coded up this example using a membrane that uses a dummy target and the invariant is preserved across membranes: < gist.github.com/3857930> (the code prints "true" twice)
My point is that by enforcing some identity invariants, we're breaking some others. Likewise, for frozen properties, membraned code either can't make use of them (because they throw when wrapped values are presented) or the unwrapped values leak. Once again, "throw or leak" is not a practical choice.
It's throw, leak or return a wrapper backed by a dummy target.
We've encountered this before. The issue you point out is exactly the same
issue with regard to the "get" trap having to return an identical value (as per [[SameValue]]) for non-configurable own properties of the target.
The solution for wrapping non-configurable own properties was to introduce a "dummy" target on which to store wrapped versions of the properties. The solution to wrapping [[Prototype]] is to similarly use a dummy target whose prototype is the wrapped prototype of the real target.
...and well, creating an entire new subgraph of proxies over dummy targets doesn't look very practical either. Basically, for a fully frozen graph, you need twice as much memory to membrane it (once for the original targets, once for the dummy targets).
True, the extra object allocation sucks. But let's not naively state that you always need "twice as much memory":
- first, the graph is membraned lazily: only objects that cross the membrane actually require a proxy. Wrapping a large graph in a membrane does not imply immediately copying the whole graph.
- second, the dummy target stores wrapped versions of the original frozen values. It doesn't need to copy the frozen values themselves. If the target has a frozen property pointing to a large string value or huge array, the dummy points to a wrapper to the same large string value or huge array.
Your proposed alternative involving the target-chain is the solution
employed by Racket chaperones [1]. Mark and I have been hesitant to use this solution as it violates invariants pertaining to object-identity in Javascript. For example, using the current proxy spec and assuming no mutable proto (e.g. Object.prototype.proto was deleted), you're guaranteed that Object.getPrototypeOf(obj) always returns the same (as per "===") object. The chaperone solution voids that guarantee.
Why do we need this guarantee anyway? From inside a membrane, objects you know about are wrapped, you're not able to differentiate the target.[[Prototype]] from the one that's been returned because you never have access to the original target.[[Prototype]]. Even if you did acquire access to this object by another mean, having a different identity is actually a feature, especially when it comes to solving some problems where identity is the only element that can enable differenciation [1] ;-)
I don't see how this strengthens your case of voiding the identity guarantee. Everything you say above is true for the way membranes currently work.
Just to clarify, when I ask "Why do we need this guarantee anyway?", I don't ask to be able to return any arbitrary object, just a proxy which has the expected object in its target chain, which sounds like a reasonable way to bend the identity equality invariant.
I agree this is a (probably the only) reasonable way to relax the current invariant. Chaperones in Racket show that this is workable. I'd still like Mark to weigh in though. IIRC he had good reasons for not wanting to break the identity-invariants related to frozen properties. I think the grant matcher puzzle actually strengthens the case for not weakening the identity guarantees provided by Javascript today.
2012/10/9 Tom Van Cutsem <tomvc.be at gmail.com>
[+markm]
2012/10/9 David Bruant <bruant.d at gmail.com>
Within a membrane, the following object-identity equailty is broken: Object.getPrototypeOf(new wrappedC) !== wrappedC.prototype.constructor (either because the operation throws or the returned object is a wrapped version)
Sorry, I don't get it. I coded up this example using a membrane that uses a dummy target and the invariant is preserved across membranes: < gist.github.com/3857930> (the code prints "true" twice)
True, my bad.
My point is that by enforcing some identity invariants, we're breaking some others. Likewise, for frozen properties, membraned code either can't make use of them (because they throw when wrapped values are presented) or the unwrapped values leak. Once again, "throw or leak" is not a practical choice.
It's throw, leak or return a wrapper backed by a dummy target.
Indeed.
We've encountered this before. The issue you point out is exactly the
same issue with regard to the "get" trap having to return an identical value (as per [[SameValue]]) for non-configurable own properties of the target.
The solution for wrapping non-configurable own properties was to introduce a "dummy" target on which to store wrapped versions of the properties. The solution to wrapping [[Prototype]] is to similarly use a dummy target whose prototype is the wrapped prototype of the real target.
...and well, creating an entire new subgraph of proxies over dummy targets doesn't look very practical either. Basically, for a fully frozen graph, you need twice as much memory to membrane it (once for the original targets, once for the dummy targets).
True, the extra object allocation sucks. But let's not naively state that you always need "twice as much memory":
- first, the graph is membraned lazily: only objects that cross the membrane actually require a proxy. Wrapping a large graph in a membrane does not imply immediately copying the whole graph.
- second, the dummy target stores wrapped versions of the original frozen values. It doesn't need to copy the frozen values themselves. If the target has a frozen property pointing to a large string value or huge array, the dummy points to a wrapper to the same large string value or huge array.
True. I admit the situation isn't as bad as I described it.
Your proposed alternative involving the target-chain is the solution
employed by Racket chaperones [1]. Mark and I have been hesitant to use this solution as it violates invariants pertaining to object-identity in Javascript. For example, using the current proxy spec and assuming no mutable proto (e.g. Object.prototype.proto was deleted), you're guaranteed that Object.getPrototypeOf(obj) always returns the same (as per "===") object. The chaperone solution voids that guarantee.
Why do we need this guarantee anyway? From inside a membrane, objects you know about are wrapped, you're not able to differentiate the target.[[Prototype]] from the one that's been returned because you never have access to the original target.[[Prototype]]. Even if you did acquire access to this object by another mean, having a different identity is actually a feature, especially when it comes to solving some problems where identity is the only element that can enable differenciation [1] ;-)
I don't see how this strengthens your case of voiding the identity guarantee. Everything you say above is true for the way membranes currently work.
Just to clarify, when I ask "Why do we need this guarantee anyway?", I don't ask to be able to return any arbitrary object, just a proxy which has the expected object in its target chain, which sounds like a reasonable way to bend the identity equality invariant.
I agree this is a (probably the only) reasonable way to relax the current invariant. Chaperones in Racket show that this is workable. I'd still like Mark to weigh in though. IIRC he had good reasons for not wanting to break the identity-invariants related to frozen properties. I think the grant matcher puzzle actually strengthens the case for not weakening the identity guarantees provided by Javascript today.
For a target, if a proxy wrapping target.[[Prototype]] can be returned by the getPrototypeOf trap, then, someone who knows target.[[Prototype]] can differentiate the wrapped version from the actual one thanks to identity. Being able to differentiate is what protects from being confused by 2 objects which are presented to be the same (like in the grant-matcher problem). My point is that if the trap returns something else than the original object, a defender who knows which object is expected can know that either the object is a proxy or the genuine one, so it's not that big of a deal to allow a different object. If you don't (and won't) know the original object, what difference does it make to be lied to if you can't ever know the truth? As the dummy target example shows, actual identities don't make much of a difference.
In a way, what I'm asking is to make dummy target "official" and more restricted, by not allowing arbitrary objects, but only proxies with the actual target in their target chain.
On Tue, Oct 9, 2012 at 6:25 AM, David Bruant <bruant.d at gmail.com> wrote:
[...]
I agree this is a (probably the only) reasonable way to relax the current
invariant. Chaperones in Racket show that this is workable. I'd still like Mark to weigh in though. IIRC he had good reasons for not wanting to break the identity-invariants related to frozen properties. I think the grant matcher puzzle actually strengthens the case for not weakening the identity guarantees provided by Javascript today.
For a target, if a proxy wrapping target.[[Prototype]] can be returned by the getPrototypeOf trap, then, someone who knows target.[[Prototype]] can differentiate the wrapped version from the actual one thanks to identity. Being able to differentiate is what protects from being confused by 2 objects which are presented to be the same (like in the grant-matcher problem).
That is correct.
My point is that if the trap returns something else than the original object, a defender who knows which object is expected can know that either the object is a proxy or the genuine one, so it's not that big of a deal to allow a different object.
The typical use of a membrane is to separate two subgraphs that are not otherwise connected, except through the membrane and perhaps a small number of objects created and trusted by the spawner of the membrane. In my examples, Alice spawns a membrane so that she can give Bob mediated access to Carol, typically under the assumption that Bob and Carol are not otherwise connected. If this membrane maintains identity on each side as corresponding to identity on the other side, if maintains the illusion. Since neither Bob nor Carol are ever on both sides of the membrane, neither can do the comparison you suggest. This is as much a transparency issue as it is a security issue. Think of FF's use of membranes to separate frames. If the illusion broke, so would much software.
If you don't (and won't) know the original object, what difference does it make to be lied to if you can't ever know the truth? As the dummy target example shows, actual identities don't make much of a difference.
I didn't follow that at all. It is a stability invariant which is being maintained. Much software in JS counts on object identity: indexOf, switch, WeakMaps, etc. If (Racket-like) state were maintained but identity were not, all these would break when a membrane is interposed.
In a way, what I'm asking is to make dummy target "official" and more restricted, by not allowing arbitrary objects, but only proxies with the actual target in their target chain.
Making them more official and restricted sounds promising. But if I understand the rest of your sentence, what you're suggesting is not more restricted, it's differently restricted. And the way it is less restricted is, IMO, bad.
2012/10/9 Mark S. Miller <erights at google.com>
On Tue, Oct 9, 2012 at 6:25 AM, David Bruant <bruant.d at gmail.com> wrote:
In a way, what I'm asking is to make dummy target "official" and more restricted, by not allowing arbitrary objects, but only proxies with the actual target in their target chain.
Making them more official and restricted sounds promising. But if I understand the rest of your sentence, what you're suggesting is not more restricted, it's differently restricted. And the way it is less restricted is, IMO, bad.
To make matters more concrete, one way in which the chaperone-approach would differ visibly from the current state has to do with caching. As Mark said, the invariants related to getPrototypeOf and frozen properties have more to do with stability than with identity.
Valid approach today:
var desc = Object.getOwnPropertyDescriptor(obj, "foo"); // assume "foo" is a data property if (!desc.configurable && !desc.writable) { var cached = obj.foo; /* from now on, can reliably use cached instead of obj.foo */ }
If we would relax the frozen property get invariant, if obj is a proxy, obj.foo could evaluate to a different proxy (admittedly to the same target) even though "foo" is non-configurable, non-writable. This can lead to observably different behavior.
(getPrototypeOf is analogous)
That said, you're right to question these invariants and the complexity of the dummy-target approach. These are some of the deepest aspects of the Proxy API and there's a real tradeoff here between the expressiveness of the Proxy API (how far can we take virtualization?) versus the invariants of the language (how far do we want to relax existing invariants?)
Le 09/10/2012 19:52, Mark S. Miller a écrit :
On Tue, Oct 9, 2012 at 6:25 AM, David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>> wrote:
My point is that if the trap returns something else than the original object, a defender who knows which object is expected can know that either the object is a proxy or the genuine one, so it's not that big of a deal to allow a different object.
The typical use of a membrane is to separate two subgraphs that are not otherwise connected, except through the membrane and perhaps a small number of objects created and trusted by the spawner of the membrane. In my examples, Alice spawns a membrane so that she can give Bob mediated access to Carol, typically under the assumption that Bob and Carol are not otherwise connected. If this membrane maintains identity on each side as corresponding to identity on the other side, if maintains the illusion. Since neither Bob nor Carol are ever on both sides of the membrane, neither can do the comparison you suggest. This is as much a transparency issue as it is a security issue. Think of FF's use of membranes to separate frames. If the illusion broke, so would much software.
To summarize, to implement a proper membrane, we need to maintain the illusion of the wrapped graph stability. This can be achieved today with dummy proxies (with arguable downsides) as demonstrated by Tom. The use of dummy targets is necessary to generate a graph isomorphic to the original targets graph and respect the current identity-based invariants.
If you don't (and won't) know the original object, what difference does it make to be lied to if you can't ever know the truth? As the dummy target example shows, actual identities don't make much of a difference.
I didn't follow that at all. It is a stability invariant which is being maintained. Much software in JS counts on object identity: indexOf, switch, WeakMaps, etc. If (Racket-like) state were maintained but identity were not, all these would break when a membrane is interposed.
If I (the attacker) first return some object o1 as prototype of o, then o2 to Alice (defender). Of course, indexOf, switch, weakmaps, etc. don't work "as expected", but that's a good thing. In making believe Bob that Object.getPrototypeOf(o) is o2, I have been forced by the language to create a new object with a different identity than o1, so Bob can know i'm trying to lie to him or be inconsistent. The good property the language provides here is that a malicious attacker is forced to tell when it's attacking (by creating an object with a different identity which can be compared with what's already known thue revealing the attempt to fool). It seems that additionally to this good property, what's wanted from the language is to enforce what's returned in some cases and I question whether it's a worthwhile addition to the current property (only for the case of differenciating objects which have the same end-target).
In a way, what I'm asking is to make dummy target "official" and more restricted, by not allowing arbitrary objects, but only proxies with the actual target in their target chain.
Making them more official and restricted sounds promising. But if I understand the rest of your sentence, what you're suggesting is not more restricted, it's differently restricted. And the way it is less restricted is, IMO, bad.
I see your point. I realize that what I've written above about isomorphic graphs only makes sense if we have a notion of equility. Does end-target equality define a relevant isomorphism? Naively, I would say yes, because comparing identity (through === or indirectly through indexOf/WeakMap/Sets, etc.) can help detecting any attempt to deviate from how the graph is supposed to be shaped, but it really isn't clear.
About making the restriction for membranes "official" could be done by introducing "branded proxies" which output values (through get/getOwnPropertyDescriptor/getPrototypeOf... traps) would also be "branded proxies" (with the same brand). Brand validation would be performed to ensure graph isomorphism. It ensures that you deviate from the exact target, but only with a given proxy and only because you're coming from a branded proxy (and not for no reason). But as I describe it, I realize I'm describing somewhat native support for membranes... so.. yeah... Maybe too early to suggest this.
2012/10/10 David Bruant <bruant.d at gmail.com>
About making the restriction for membranes "official" could be done by introducing "branded proxies" which output values (through get/getOwnPropertyDescriptor/getPrototypeOf... traps) would also be "branded proxies" (with the same brand). Brand validation would be performed to ensure graph isomorphism. It ensures that you deviate from the exact target, but only with a given proxy and only because you're coming from a branded proxy (and not for no reason).
I have given more thoughts to this idea.
Proxy.branded(brand, target, handler) // brand is a symbol
The trap invariants would not rely on SameValue for object equality anymore, but if an object is returned from a trap, it has to be the unique proxy created of off the (brand, target) pair instead of what happens currently where it has to be the target (which is the unique object which identity is equal to the target's identity). The invariant can be implemented with a weakmap of weaksets and each test is at worst 2 lookups (one on the weakmap, the other on the weakset), so O(log(NumberOfTargets*NumberOfBrands)). Arguably heavy-weight, but I don't think it can be made better.
Having this uniqueness invariant is what enforces graph isomorphism. In a nutshell, unlike the current proposal, traps can still return proxies, but it has to be the exact proxy that can be returned is decided in advanced, pretty much like the current invariants where there is no choice on the object to be returned. What I'm proposing, just shifts which the set of objects from the targets set to a particular set of proxies.
Proxy.branded.bind(someBrand) would create a new Proxy constructor somehow specialized to create a membrane.
2012/10/10 David Bruant <bruant.d at gmail.com>
Having this uniqueness invariant is what enforces graph isomorphism. In a nutshell, unlike the current proposal, traps can still return proxies, but it has to be the exact proxy that can be returned is decided in advanced, pretty much like the current invariants where there is no choice on the object to be returned. What I'm proposing, just shifts which the set of objects from the targets set to a particular set of proxies.
Sorry, you lost me. I don't understand the benefit of this alternative approach.
We should also be wary of adding even more Proxy constructors, as we'll otherwise end up with a combinatorial explosion (revocable branded proxies, branded proxies with a symbol whitelist, etc.)
2012/10/10 Tom Van Cutsem <tomvc.be at gmail.com>
2012/10/10 David Bruant <bruant.d at gmail.com>
Having this uniqueness invariant is what enforces graph isomorphism. In a nutshell, unlike the current proposal, traps can still return proxies, but it has to be the exact proxy that can be returned is decided in advanced, pretty much like the current invariants where there is no choice on the object to be returned. What I'm proposing, just shifts which the set of objects from the targets set to a particular set of proxies.
Sorry, you lost me. I don't understand the benefit of this alternative approach.
I forked the thread a bit to explore an idea. I don't know whether this idea is worthwhile or even a true improvement over what we currently have. Sorry, I should have made that more clear.
Tom Van Cutsem wrote:
We should also be wary of adding even more Proxy constructors, as we'll otherwise end up with a combinatorial explosion (revocable branded proxies, branded proxies with a symbol whitelist, etc.)
But didn't David find a way to avoid Proxy.revocable, namely make revocation swap in an all-throwing-traps handler?
Just trying to keep up here (and hoping we indeed avoided a revocable proxy constructor).
2012/10/10 Brendan Eich <brendan at mozilla.org>
Tom Van Cutsem wrote:
We should also be wary of adding even more Proxy constructors, as we'll otherwise end up with a combinatorial explosion (revocable branded proxies, branded proxies with a symbol whitelist, etc.)
But didn't David find a way to avoid Proxy.revocable, namely make revocation swap in an all-throwing-traps handler?
Unfortunately no. I agree with Tom concern of combinatorial explosion, because we will have revocable, branded, and what's next? However, the symbol whitelist isn't part of it. The whitelist doesn't make a new type of proxies.
Just trying to keep up here (and hoping we indeed avoided a revocable proxy constructor).
Even with the idea of native support for membrane as I presented it, it doesn't change that anyone holding a reference to a proxy indirectly keeps a reference to the target, thus causing the GC to not be able to collect the target.
We need platform support for revocation, because it cannot be implemented by JS code.
All in all, membranes can be implemented today. I find the idea of using dummy targets a bit awkward, but it works. If after some experience using proxies, the cost turns out to be prohibited, There is still room for improving proxies with what proposed or whatever else in a later version of the spec I think. The issue I'm pointing is not as urgent as the revocation issue was.
David Bruant wrote:
2012/10/10 Brendan Eich <brendan at mozilla.org <mailto:brendan at mozilla.org>>
Tom Van Cutsem wrote: We should also be wary of adding even more Proxy constructors, as we'll otherwise end up with a combinatorial explosion (revocable branded proxies, branded proxies with a symbol whitelist, etc.) But didn't David find a way to avoid Proxy.revocable, namely make revocation swap in an all-throwing-traps handler?
Unfortunately no. I agree with Tom concern of combinatorial explosion, because we will have revocable, branded, and what's next? However, the symbol whitelist isn't part of it. The whitelist doesn't make a new type of proxies.
Yes, I agree with all that (except the revocable constructor, see below), including general apprehension of combinatorial explosion.
Just trying to keep up here (and hoping we indeed avoided a revocable proxy constructor).
Even with the idea of native support for membrane as I presented it, it doesn't change that anyone holding a reference to a proxy indirectly keeps a reference to the target, thus causing the GC to not be able to collect the target.
Why would not the target be nulled and the handler be replaced with a handler full of throwing traps? Sorry if I missed a message on this.
We need platform support for revocation, because it cannot be implemented by JS code.
I quite agree, but this does not mean a Proxy.revocable constructor, unless you want a revoke method to work only on proxies distinguished by being created via an alternative constructor.
IIRC (and I hope I do!) we were considering RevokableProxy or Proxy.revocable only because non-revocable proxies cannot tolerate overhead in dispatching their traps to check for a default-false revoked internal property. But your idea of nulling target and replacing handler with a throwing handler seemed to avoid any overhead.
So is the issue the revoke API and a desire to "narrow" it to apply only to proxies distinguished from birth as revocable? If so, why?
2012/10/11 Brendan Eich <brendan at mozilla.org>
Why would not the target be nulled and the handler be replaced with a handler full of throwing traps? Sorry if I missed a message on this.
Yes, that's the general idea. It's just that you need some built-in function to do this for you.
We need platform support for revocation, because it cannot be implemented
by JS code.
I quite agree, but this does not mean a Proxy.revocable constructor, unless you want a revoke method to work only on proxies distinguished by being created via an alternative constructor.
IIRC (and I hope I do!) we were considering RevokableProxy or Proxy.revocable only because non-revocable proxies cannot tolerate overhead in dispatching their traps to check for a default-false revoked internal property. But your idea of nulling target and replacing handler with a throwing handler seemed to avoid any overhead.
So is the issue the revoke API and a desire to "narrow" it to apply only to proxies distinguished from birth as revocable? If so, why?
tl;dr:
- revocable proxies and non-revocable proxies are exactly the same internally (this is good)
- only revocable proxies require an associated |revoke()| function. Thus having two constructors makes sense as it avoids the (object allocation and API ergonomics) overhead of the revoke() function for non-revocable proxies.
You're right: thanks to David's insight of just replacing a proxy's [[Handler]] with a special handler that always throws, revocable proxies are no different from non-revocable proxies except for the fact that there's a user-visible function to actually perform this replacement.
So indeed, the currently specced Proxy.revocable is there only to expose the additional |revoke()| function acting as that proxy's off-switch. I think the current API strikes a pretty good balance:
- proxies that don't need an off-switch are not bothered by the additional return value. For these cases, one can continue to write just |proxy = new Proxy(target, handler)|.
- Proxy.revocable, contrary to just the Proxy constructor, has to allocate the unique |revoke()| function for the new proxy. The Proxy constructor avoids that overhead if you never need to revoke.
- Proxy.revocable returns a tuple {proxy, revoke}. While more cumbersome to work with (especially in pre-ES6 code without destructuring), this API gets the authority to revoke a proxy exactly right: at proxy birth, only the creator of the proxy holds the right to revoke it. This is infinitely better than a global Proxy.revoke(proxy) method that would allow arbitrary objects to revoke any proxy.
Tom Van Cutsem wrote:
- Proxy.revocable returns a tuple {proxy, revoke}. While more cumbersome to work with (especially in pre-ES6 code without destructuring), this API gets the authority to revoke a proxy exactly right: at proxy birth, only the creator of the proxy holds the right to revoke it. This is infinitely better than a global Proxy.revoke(proxy) method that would allow arbitrary objects to revoke any proxy.
Ok, thanks for this recap. It makes sense, the ocap treatments are working ;-).
On 11 October 2012 09:32, Brendan Eich <brendan at mozilla.org> wrote:
Tom Van Cutsem wrote:
- Proxy.revocable returns a tuple {proxy, revoke}. While more cumbersome to work with (especially in pre-ES6 code without destructuring), this API gets the authority to revoke a proxy exactly right: at proxy birth, only the creator of the proxy holds the right to revoke it. This is infinitely better than a global Proxy.revoke(proxy) method that would allow arbitrary objects to revoke any proxy.
Ok, thanks for this recap. It makes sense, the ocap treatments are working ;-).
Even then I don't think the additional creation API is needed. The handler itself can be mutable, right? So why not have a function Proxy.revoke that takes a handler (not a proxy) and replaces all its trap methods by poisoned traps? This is still perfectly ocap (because only the creator has access to the handler), but requires no extra API for creating revocable proxies -- just make sure your handler is mutable.
On Thu, Oct 11, 2012 at 4:25 AM, Andreas Rossberg <rossberg at google.com>wrote:
On 11 October 2012 09:32, Brendan Eich <brendan at mozilla.org> wrote:
Tom Van Cutsem wrote:
- Proxy.revocable returns a tuple {proxy, revoke}. While more cumbersome to work with (especially in pre-ES6 code without destructuring), this API
gets the authority to revoke a proxy exactly right: at proxy birth, only the
creator of the proxy holds the right to revoke it. This is infinitely better
than a global Proxy.revoke(proxy) method that would allow arbitrary objects
to revoke any proxy.
Ok, thanks for this recap. It makes sense, the ocap treatments are working ;-).
Even then I don't think the additional creation API is needed. The handler itself can be mutable, right? So why not have a function Proxy.revoke that takes a handler (not a proxy) and replaces all its trap methods by poisoned traps? This is still perfectly ocap (because only the creator has access to the handler), but requires no extra API for creating revocable proxies -- just make sure your handler is mutable.
How does the target get dropped? Remember, this all started with David's observation that without some additional magic, we have an unsolvable GC problem. This is still true.
On 11 October 2012 13:41, Mark S. Miller <erights at google.com> wrote:
On Thu, Oct 11, 2012 at 4:25 AM, Andreas Rossberg <rossberg at google.com> wrote:
On 11 October 2012 09:32, Brendan Eich <brendan at mozilla.org> wrote:
Tom Van Cutsem wrote:
- Proxy.revocable returns a tuple {proxy, revoke}. While more cumbersome to work with (especially in pre-ES6 code without destructuring), this API gets the authority to revoke a proxy exactly right: at proxy birth, only the creator of the proxy holds the right to revoke it. This is infinitely better than a global Proxy.revoke(proxy) method that would allow arbitrary objects to revoke any proxy.
Ok, thanks for this recap. It makes sense, the ocap treatments are working ;-).
Even then I don't think the additional creation API is needed. The handler itself can be mutable, right? So why not have a function Proxy.revoke that takes a handler (not a proxy) and replaces all its trap methods by poisoned traps? This is still perfectly ocap (because only the creator has access to the handler), but requires no extra API for creating revocable proxies -- just make sure your handler is mutable.
How does the target get dropped? Remember, this all started with David's observation that without some additional magic, we have an unsolvable GC problem. This is still true.
Ah, right. If revoke also froze the handler object, then it could delete the target, because it will never be observable again. Would that be too magic?
2012/10/11 Andreas Rossberg <rossberg at google.com>
On 11 October 2012 09:32, Brendan Eich <brendan at mozilla.org> wrote:
Tom Van Cutsem wrote:
- Proxy.revocable returns a tuple {proxy, revoke}. While more cumbersome to work with (especially in pre-ES6 code without destructuring), this API
gets the authority to revoke a proxy exactly right: at proxy birth, only the
creator of the proxy holds the right to revoke it. This is infinitely better
than a global Proxy.revoke(proxy) method that would allow arbitrary objects
to revoke any proxy.
Ok, thanks for this recap. It makes sense, the ocap treatments are working ;-).
Even then I don't think the additional creation API is needed. The handler itself can be mutable, right?
Yes, but "can" doesn't mean "must".
So why not have a function Proxy.revoke that takes a handler (not a proxy) and replaces all its trap methods by poisoned traps?
What about cases where several proxies shared the same handler? Here is an example of membrane implementation where all proxies of the membrane share the exact same handler: gist.github.com/3852761
Also, as suggested above with "can" and "must", what do you do with frozen handlers?
2012/10/11 Andreas Rossberg <rossberg at google.com>
On 11 October 2012 13:41, Mark S. Miller <erights at google.com> wrote:
How does the target get dropped? Remember, this all started with David's observation that without some additional magic, we have an unsolvable GC problem. This is still true.
Ah, right. If revoke also froze the handler object, then it could delete the target, because it will never be observable again. Would that be too magic?
You're assuming that from the handler you can access the proxy with which it's associated, but there is no such explicit link. You might keep track of the link purely at the implementation level, but then what to do with one-to-many relationships as David pointed out.
I think the additional Proxy.revocable constructor is fine. It doesn't introduce the combinatorial explosion problem I mentioned earlier for branded proxies since Proxy.revocable takes exactly the same arguments as the Proxy constructor. Only the return value is different.
On Thu, Oct 11, 2012 at 10:26 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
2012/10/11 Andreas Rossberg <rossberg at google.com>
On 11 October 2012 13:41, Mark S. Miller <erights at google.com> wrote:
How does the target get dropped? Remember, this all started with David's observation that without some additional magic, we have an unsolvable GC problem. This is still true.
Ah, right. If revoke also froze the handler object, then it could delete the target, because it will never be observable again. Would that be too magic?
You're assuming that from the handler you can access the proxy with which it's associated, but there is no such explicit link. You might keep track of the link purely at the implementation level, but then what to do with one-to-many relationships as David pointed out.
I think the additional Proxy.revocable constructor is fine. It doesn't introduce the combinatorial explosion problem I mentioned earlier for branded proxies since Proxy.revocable takes exactly the same arguments as the Proxy constructor. Only the return value is different.
Agreed on everything but a terminology nit. Proxy.revocable is a function, not a constructor. The record it returns is not an instanceof Proxy.revocable.
Proxies enable the implementation of membranes [1]. It's one motivating use case of proxies. Useful membranes mediate access to every object which was reach from another object in the membrane. I realize now that Object.getPrototypeOf violate this principle in harmful ways:
Object.getPrototypeOf(cInstance)
wrappedC !! // It's still possible to delete C.prototype.constructor for this case, but // the least undeleted .constructor can yield unsecure code...
Basically, the getPrototypeOf invariant provides unmediated access to prototype objects which sounds like an abusive amount of authority.
I suggest to revise the getPrototypeOf invariant from "the trap result must me target.[[Prototype]]" to: "the trap result must be either target.[[Prototype]] or a proxy which has target.[[Prototype]] in its target chain"
For the definition of "target chain" (or "proxy chain"): esdiscuss/2012-September/025135
This new definition would enable wrapping Object.getPrototypeOf calls and allow preserving the following equality:
because as the proxy proposal is, this equality cannot be true any longer.
This flaw was here since the original proxy design in which Object.getPrototypeof was untrapped and I think justifies in itself the addition of the getPrototypeOf trap regardless of the whole proto story.
This flaw gives me an impression of "elephant in the room" that we've missed. Over time, I've gained a certain level of intimacy with the proxy spec and I can't help having this feeling of "what else are we missing?". I don't have the answer, i'm still searching, but if we've missed this elephant the whole time, I can't help thinking that the proxy spec needs more attention.
David
[1] soft.vub.ac.be/~tvcutsem/invokedynamic/js