Private symbols auto-unwrapping proxies (was: Security Demands Simplicity (was: Private Slots))
Kevin Smith pointed out something I hadn't though about before but is obvious in retrospect. The hazard that proxies bring to the language in general: proxies make it possible to break the target object by inconsistently handling forwarding during the operation of methods.
class Counter {
constructor(){
this.value = 0;
}
increment(){
this.state = "working";
this.value++;
this.state = "idle";
return this.value;
}
}
const counter = new Counter;
const counterfeit = new Proxy(counter, {
get(target, key, receiver){
if (key === 'value') {
throw 'break target';
}
return Reflect.get(target, key, receiver);
}
});
While this may not be common, and the example is contrived, it seems like it may be less uncommon when there's a trap that exists who's sole purpose is to throw or not.
It is my opinion that the primary use case for private symbols is for properties that proxies expressly shouldn't be given a chance, in any manner, to corrupt or modify. They are likely used for sensitive internal state that will only be accessed by methods or friend classes created in service of the target.
A membrane becomes less valuable if breaking the target is an easily
accomplished accidental side effect. This is already visible in practice
today when you attempt to use WeakMaps to create private state for objects
and they are proxied, since the private state will be keyed on this
in
the constructor which won't match this
in methods invoked on the proxy.
On Jan 22, 2013, at 9:06 AM, Brandon Benvie wrote:
Kevin Smith pointed out something I hadn't though about before but is obvious in retrospect. The hazard that proxies bring to the language in general: proxies make it possible to break the target object by inconsistently handling forwarding during the operation of methods.
class Counter { constructor(){ this.value = 0; } increment(){ this.state = "working"; this.value++; this.state = "idle"; return this.value; } } const counter = new Counter; const counterfeit = new Proxy(counter, { get(target, key, receiver){ if (key === 'value') { throw 'break target'; } return Reflect.get(target, key, receiver); } });
While this may not be common, and the example is contrived, it seems like it may be less uncommon when there's a trap that exists who's sole purpose is to throw or not.
It is my opinion that the primary use case for private symbols is for properties that proxies expressly shouldn't be given a chance, in any manner, to corrupt or modify. They are likely used for sensitive internal state that will only be accessed by methods or friend classes created in service of the target.
A membrane becomes less valuable if breaking the target is an easily accomplished accidental side effect. This is already visible in practice today when you attempt to use WeakMaps to create private state for objects and they are proxied, since the private state will be keyed on
this
in the constructor which won't matchthis
in methods invoked on the proxy.
Right, more concretely:
let fooRegistry = new WeakMapl class Foo { constructor() { fooRegistry.set(this,true); //should really be in a @@create method to prevent counterfeiting } op() { if (!fooRegisty.has (this)) throw Error("Foo called on non-Foo object"); } }
let f = new Foo; let pf = new Proxy(f, {});
f.op(); //no problem pf.op(); //throws
This problem has nothing to do with private Symbols and is the same problem Tom and I talked about in esdiscuss/2012-December/027277
As that thread discussed, the root cause is that the lookup that retrieved a method is a separate MOP level operation from the call of the method and there currently is no mechanism to coordinate the this value between those MOP operations.
On way to fix this would be to introduce a new MOP operation [[CallProperty]] that combines [[Get]]/[[Call]]. I know Brendan has opposed this when it has come up in the past (bad E4X experiences) but I'm not talking about any new user level semantics (other than making Proxy work reasonably). The ordinary object definition of [[CallProperty]] would have exactly the same semantics as occurs for the [[Get]]/[[Call]] sequence that evaluates for expr.key()
Another way to address this issue avoids introducing a new MOP level operation. Instead we would introduce a new kind of Reference value that is used for the result of Proxy [[Get]] operations. The [[Call]] op would know about these special Proxy References and adjust the this value accordingly before invoking the function. Something similar to that is currently in the spec. to support super based property accesses.
I think, [[CallProperty]] actually has less hair, but either one could be used to make default Proxy behavior be consistent forwarding to the target with the target value always substituted for this position proxy values. We would get rid of the confusing and error prone combination of forwarding and delegation that is now the default for Proxy.
Le 22/01/2013 16:13, Tom Van Cutsem a écrit :
2013/1/22 Allen Wirfs-Brock <allen at wirfs-brock.com <mailto:allen at wirfs-brock.com>>
We can probably fix the built-ins with some ad hoc language about them automatically resolving proxies to the target as the this value. Or perhaps we could expand the internal MOP api to include a resolve proxy to target operation. Using private symbols for all of these cases, including the built-ins also seems like an alternative that may work.
Let me try to summarize:
The proposal: private symbol access auto-unwraps proxies.
In code:
var s = new PrivateSymbol(); var t = {}; var p = Proxy(t, {...}); t[s] = "foo" p[s] // doesn't trap, returns "foo" p[s] = "bar" // doesn't trap, sets t[s] = "bar"
Pro:
- would solve the issue of wrapping class instances with private state stored via private symbols
- would solve the issue of how to proxy built-ins, like Date, if they are specified to use private symbols to access internal state
- would get rid of the unknownPrivateSymbol trap in Proxies
- could maybe even get rid of the private symbol whitelist in the Proxy constructor, which would making proxies entirely oblivious to private names
Remaining issue: private symbols can pierce membranes.
This issue is resolved if:
- (base case) there are no built-in private symbols in a standard JS environment (i.e. all the built-in symbols are unique)
- (inductive case) a membrane takes care to detect and wrap any private symbols that cross the membrane, and keeps a 1-to-1 mapping to maintain the identity of the symbols across both sides of the membrane.
Just realizing now, but how does the membrane do the symbol unwrapping if private symbols pierces it? 2 contexts A and B share a symbol, the symbol initially has to go through a public channel (get trap with a string name for instance) and if A created a symbol a, the membrane can provide a symbol b to the B context, but when A does "someObject[a] = 2" and B does "someObject[b]", both accesses pierce proxies, so the membrane can't do its unwrapping job.
Also, in some cases, the membrane can't switch a value. // in context A var s = new PrivateSymbol() var o = Object.freeze({s:s}); // send o across the membrane
In B, invariants checks make that the membrane can't answer anything else than the original symbol when b retrieves the "s" property
2013/1/22 David Bruant <bruant.d at gmail.com>
Just realizing now, but how does the membrane do the symbol unwrapping if private symbols pierces it? 2 contexts A and B share a symbol, the symbol initially has to go through a public channel (get trap with a string name for instance) and if A created a symbol a, the membrane can provide a symbol b to the B context, but when A does "someObject[a] = 2" and B does "someObject[b]", both accesses pierce proxies, so the membrane can't do its unwrapping job.
The membrane doesn't need to unwrap. Evaluating "someObject[a]" in A, and "someObject[b]" in B will result in different values.
In the context of membranes, "someObject" is actually a "Harvey Two-Face" type of object (its split across two worlds, one in A, and a proxy representation in B). Symbol-keyed indexing on the A face is distinct from symbol-keyed indexing on the B face. But that's OK: it's the job of the membrane to separate the A and the B face in the first place.
Also, in some cases, the membrane can't switch a value. // in context A
var s = new PrivateSymbol() var o = Object.freeze({s:s}); // send o across the membrane
In B, invariants checks make that the membrane can't answer anything else than the original symbol when b retrieves the "s" property
This is independent of private symbols. The same issue occurs if s were a String. That's what requires the shadow-target work-around in membrane proxies in general.
Le 22/01/2013 20:05, Tom Van Cutsem a écrit :
2013/1/22 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
Just realizing now, but how does the membrane do the symbol unwrapping if private symbols pierces it? 2 contexts A and B share a symbol, the symbol initially has to go through a public channel (get trap with a string name for instance) and if A created a symbol a, the membrane can provide a symbol b to the B context, but when A does "someObject[a] = 2" and B does "someObject[b]", both accesses pierce proxies, so the membrane can't do its unwrapping job.
The membrane doesn't need to unwrap. Evaluating "someObject[a]" in A, and "someObject[b]" in B will result in different values.
In the context of membranes, "someObject" is actually a "Harvey Two-Face" type of object (its split across two worlds, one in A, and a proxy representation in B).
Indeed, sorry, using "someObject" in both cases was a confusing shortcut.
Symbol-keyed indexing on the A face is distinct from symbol-keyed indexing on the B face. But that's OK: it's the job of the membrane to separate the A and the B face in the first place.
I don't think that's ok. A goal of the proxy mediation is to gives A and B the impression they communicate with one another like if there was no mediation (but keeping the right to revoke all communications when necessary). That's why the membrane faithfully forwards primitive values and preserve object identity equalities in other cases than private symbols. If you created A and B and started to make them communicate, it's because you wanted them to collaborate to achieve something for you. If A and B share a private symbol, it's in order to communicate using it. If the membrane changes the symbol, then A and B don't communicate as if there was no mediation anymore. It's even possible that they won't be able to work together if their mutual collaboration relied on communication via the private symbol they expected to share.
Also, in some cases, the membrane can't switch a value. // in context A var s = new PrivateSymbol() var o = Object.freeze({s:s}); // send o across the membrane In B, invariants checks make that the membrane can't answer anything else than the original symbol when b retrieves the "s" property
This is independent of private symbols. The same issue occurs if s were a String. That's what requires the shadow-target work-around in membrane proxies in general.
Good point.
Brandon Benvie wrote:
It is my opinion that the primary use case for private symbols is for properties that proxies expressly shouldn't be given a chance, in any manner, to corrupt or modify. They are likely used for sensitive internal state that will only be accessed by methods or friend classes created in service of the target.
A membrane becomes less valuable if breaking the target is an easily accomplished accidental side effect. This is already visible in practice today when you attempt to use WeakMaps to create private state for objects and they are proxied, since the private state will be keyed on
this
in the constructor which won't matchthis
in methods invoked on the proxy.
ding ding ding ding ding ding....
2013/1/23 Brendan Eich <brendan at mozilla.com>
Brandon Benvie wrote:
It is my opinion that the primary use case for private symbols is for properties that proxies expressly shouldn't be given a chance, in any manner, to corrupt or modify. They are likely used for sensitive internal state that will only be accessed by methods or friend classes created in service of the target.
A membrane becomes less valuable if breaking the target is an easily accomplished accidental side effect. This is already visible in practice today when you attempt to use WeakMaps to create private state for objects and they are proxied, since the private state will be keyed on
this
in the constructor which won't matchthis
in methods invoked on the proxy.ding ding ding ding ding ding....
Let's try to separate the issues here:
- proxies and private symbols: Brandon strengthens the case that proxies should be oblivious to private symbols.
- any sort of private state (regardless of whether it's stored using private symbols or weakmaps) is always keyed off of the identity of the target.
- because of JS's "invoke = get + apply" semantics, by default a proxy always leaves the |this| value pointing at the proxy.
The combination of 2) and 3) is problematic (as in: not transparent).
Looking only at 3), sometimes this is what you want, and sometimes it isn't. In the case of membranes I agree the more natural thing to do is to rebind |this| to the real target, not a membraned proxy for it. I think membranes can do that already without added cost: the "get" trap already needs to return a wrapper for the actual function object anyway. That wrapper, when called, can call the real function with the real target as the |this| value.
Looking back at the code for an identity-preserving membrane < harmony:proxies#an_identity-preserving_membrane>
I think this membrane actually properly unwraps the |this| value before forwarding. I haven't yet tried expressing this code using Direct proxies, but off the top of my head I don't see how the change from proxies to direct proxies would impact this particular aspect of membranes.
So 3) may be an issue in general, but membranes should be able to avoid it.
2013/1/23 Tom Van Cutsem <tomvc.be at gmail.com>
Looking back at the code for an identity-preserving membrane < harmony:proxies#an_identity-preserving_membrane> I think this membrane actually properly unwraps the |this| value before forwarding.
The relevant line of code here is (starting at line 30):
const callTrap(...dryArgs) { return asDry(wet.apply(asWet(this), dryArgs.map(asWet))); }
Note the "asWet(this)" conversion.
(there's similar code a couple of lines below that for the reverse direction)
Tom Van Cutsem wrote:
2013/1/23 Brendan Eich <brendan at mozilla.com <mailto:brendan at mozilla.com>>
Brandon Benvie wrote: It is my opinion that the primary use case for private symbols is for properties that proxies expressly shouldn't be given a chance, in any manner, to corrupt or modify. They are likely used for sensitive internal state that will only be accessed by methods or friend classes created in service of the target. A membrane becomes less valuable if breaking the target is an easily accomplished accidental side effect. This is already visible in practice today when you attempt to use WeakMaps to create private state for objects and they are proxied, since the private state will be keyed on `this` in the constructor which won't match `this` in methods invoked on the proxy. ding ding ding ding ding ding....
Let's try to separate the issues here:
Thanks, did not mean to ring Pavlov's bell there ;-).
- proxies and private symbols: Brandon strengthens the case that proxies should be oblivious to private symbols.
- any sort of private state (regardless of whether it's stored using private symbols or weakmaps) is always keyed off of the identity of the target.
- because of JS's "invoke = get + apply" semantics, by default a proxy always leaves the |this| value pointing at the proxy.
The combination of 2) and 3) is problematic (as in: not transparent).
This is an aspect of JS's dynamic-by-default |this| binding, it seems to me.
Looking only at 3), sometimes this is what you want, and sometimes it isn't.
Very true. For HTML5, given function f written in JS, loaded in window referenced from another frame by w, referencing a WindowProxy, w.f() binds |this| in f to the WindowProxy. We do not want to expose the Window on the scope chain directly -- it is too easy to leak that capability and bypass the same-origin security membraning done by the WindowProxy.
For a built-in function written in C++, implementations I know of all auto-unwrap to get the Window. One might then want to self-host such legacy C++ built-ins, and want auto-unwrapping. The burden of not leaking the capability still exists, but the self-hosted built-in is part of the TCB, same as the C++ counterpart.
So the issue is situational, depending on trusted computing base membership among other things.
In the case of membranes I agree the more natural thing to do is to rebind |this| to the real target, not a membraned proxy for it. I think membranes can do that already without added cost: the "get" trap already needs to return a wrapper for the actual function object anyway. That wrapper, when called, can call the real function with the real target as the |this| value.
Looking back at the code for an identity-preserving membrane harmony:proxies#an_identity-preserving_membrane I think this membrane actually properly unwraps the |this| value before forwarding. I haven't yet tried expressing this code using Direct proxies, but off the top of my head I don't see how the change from proxies to direct proxies would impact this particular aspect of membranes.
So 3) may be an issue in general, but membranes should be able to avoid it.
Exactly.
Le 23/01/2013 09:38, Tom Van Cutsem a écrit :
- because of JS's "invoke = get + apply" semantics, by default a proxy always leaves the |this| value pointing at the proxy.
Looking only at 3), sometimes this is what you want, and sometimes it isn't.
In which case would it be what you want? The example Brandon (and Kevin before him) provided showed something very intrusive about proxies related to your 3). That proxies mediate the access to the public method is one thing, that they pretend to be the object acted on inside the method opens a entire world.
Even with fixes suggested by Allen, the hazard can still exist if someone does: Counter.prototype.increment.call(new Proxy(counter, maliciousHandler))
I have no idea how this can be mitigated in general without creating a mechanism that can be abused to unwrap proxies. For classes specifically, maybe an option can make that classes keep track of generated objects and throw if non-instance is passed in a method as |this| (...which is exactly the kind of things DOM Node tree manipulation methods will need)
2013/1/23 David Bruant <bruant.d at gmail.com>
Le 23/01/2013 09:38, Tom Van Cutsem a écrit :
- because of JS's "invoke = get + apply" semantics, by default a proxy always leaves the |this| value pointing at the proxy.
Looking only at 3), sometimes this is what you want, and sometimes it isn't.
In which case would it be what you want?
See the example by Brendan just upstream in this thread.
The example Brandon (and Kevin before him) provided showed something very intrusive about proxies related to your 3). That proxies mediate the access to the public method is one thing, that they pretend to be the object acted on inside the method opens a entire world.
Even with fixes suggested by Allen, the hazard can still exist if someone does: Counter.prototype.increment.**call(new Proxy(counter, maliciousHandler))
I don't understand why this is a hazard. Even without proxies, |this| is never reliable, unless you use .bind().
I have no idea how this can be mitigated in general without creating a mechanism that can be abused to unwrap proxies. For classes specifically, maybe an option can make that classes keep track of generated objects and throw if non-instance is passed in a method as |this| (...which is exactly the kind of things DOM Node tree manipulation methods will need)
Recall that it was a goal for classes to be a form of sugar over the existing object model. That means the use of |this| within a method specified using class syntax should really be no different from using |this| outside of classes. Let's try to avoid making up special rules for class instances.
The OP was about a proposal that auto-unwraps proxies specifically for private symbol access. This decouples proxies and private symbols, but in a potentially dangerous way (cf. the need to change membranes). Auto-unwrapping a proxy without consulting the handler inherently breaks abstraction boundaries.
Another alternative that crossed my mind, which may be as simple, and less "dangerous":
We could make proxies have their own private state, such that proxy[privateSymbol] just accesses the proxy's private state (without trapping!). That means there is no a priori relationship between proxy[privateSymbol] and target[privateSymbol]. Basically this is also how WeakMaps interact with proxies today (different identities, different mappings).
Under this proposal:
- private symbols are never leaked to proxies
- private symbol access is "reliable" (it never throws)
- private symbols don't pierce membranes
- proxies don't need facilities to interact with private symbols (the whitelist and unknownPrivateSymbol trap)
What you lose is the ability to call a method that expects an object with certain built-in state, and that will "just work" when passed a proxy for an object with such state. Maybe this was a bad idea anyway.
Going back to the big discussion thread about proxying DOM objects, I maintain that it's a bad idea to try to make existing APIs (that expect objects of a very specific type) work with any random proxy, either by interacting with it or by unwrapping it. The cleaner thing to do would be to replace/wrap the API with one that also recognizes and accepts certain proxies (still not just anyone's proxies).
Le 24/01/2013 09:52, Tom Van Cutsem a écrit :
2013/1/23 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>
Le 23/01/2013 09:38, Tom Van Cutsem a écrit : 3) because of JS's "invoke = get + apply" semantics, by default a proxy always leaves the |this| value pointing at the proxy. Looking only at 3), sometimes this is what you want, and sometimes it isn't. In which case would it be what you want?
See the example by Brendan just upstream in this thread.
True, I had read this post too quickly.
The example Brandon (and Kevin before him) provided showed something very intrusive about proxies related to your 3). That proxies mediate the access to the public method is one thing, that they pretend to be the object acted on inside the method opens a entire world. Even with fixes suggested by Allen, the hazard can still exist if someone does: Counter.prototype.increment.call(new Proxy(counter, maliciousHandler))
I don't understand why this is a hazard. Even without proxies, |this| is never reliable, unless you use .bind().
I'm not worried about the |this|-reliability for the method, but rather that the target instance can be left in an inconsistent state because of a malicious handler. The important part in the above expression isn't the .call, but that an actual Counter instance is the proxy target.
I have no idea how this can be mitigated in general without creating a mechanism that can be abused to unwrap proxies. For classes specifically, maybe an option can make that classes keep track of generated objects and throw if non-instance is passed in a method as |this| (...which is exactly the kind of things DOM Node tree manipulation methods will need)
Recall that it was a goal for classes to be a form of sugar over the existing object model. That means the use of |this| within a method specified using class syntax should really be no different from using |this| outside of classes. Let's try to avoid making up special rules for class instances.
I agree with you, I suggested to add an option, not to change the default semantics. Because of the too-dynamic |this| and everyone being used to it, protecting yourself from malicious proxies from attacks like the one above ("method.call(new Proxy(legitObject, maliciousProxy))") has to be an opt-in. Basically, methods make sure their |this| is an object that came out of the class constructor. It would be nice if this opt-in could be made as simple as an optional keyword in the class syntax. This option would just desugar differently (put all objects created by the constructor in a WeakSet, add a prolog to each method verifying |this| is part of the weakset, continue if yes, throw if not).
Going back to the big discussion thread about proxying DOM objects, I maintain that it's a bad idea to try to make existing APIs (that expect objects of a very specific type) work with any random proxy, either by interacting with it or by unwrapping it. The cleaner thing to do would be to replace/wrap the API with one that also recognizes and accepts certain proxies (still not just anyone's proxies).
I agree. The selector matching use case convinced me there is no chance to put proxies or weird objects in a DOM tree.
Just to clarify, the example I gave of WeakMap usage breaking with proxies was just to illustrate how a type of private data currently in use alongside proxies can break the target object purely by accident. While I do think [[CallMethod]] would go a long way to ameliorating the problem, I still believe allowing private symbol keyed properties to have a special immunity to proxies is a useful tool. The advice would be: use private symbols for properties that hold sensitive state to prevent proxies from accidentally breaking instances of your classes.
I just wrote up a strawman on the wiki to summarize the recent debates about the interaction between proxies and private symbols:
strawman:proxy_symbol_decoupled
The page actually lists two proposals, out of which I prefer the second one.
If I forgot some benefits/drawbacks of either approach, please speak up. Thanks.
Thanks for this summary. About the first proposal and getting rid of the whitelist, indeed, the whitelist was here to tell about known symbols to avoid leakage. If private symbols pierce proxies, the whitelist has no purpose any longer.
I don't understand why problem B isn't solved with the first proposal. Since the proxy is pierced, access to private symbol'ed properties don't trap and the proxy can't throw on access (since it's not trapped). If I'm misunderstanding the proposal, could you show an example, under the unconditional forwarding proposal in which the problem B can be reproduced.
The second proposal reminds me a bit of an evolution of the first proxy design in which each proxy would have its own storage for invariant checks. Since a forwarding proxy was the main use case and already had a storage area (the target), this led to the second design "direct proxy" in which both storages would be merged in the target. By analogy, the natural evolution of the second proposal would be the first proposal.
David
Le 28/01/2013 19:45, Tom Van Cutsem a écrit :
as you and I discussed in chat, "(base case) there are no built-in private symbols in a standard JS environment (i.e. all the built-in symbols are unique)" is a bad misunderstanding of the utility of membranes. Membranes (and membrane-like patterns) are useful and needed at many finer-grains than realms. It is not safe to assume that no private symbols exist on both sides of any membrane. I think proposal #1 is fatally insecure. I'm glad you like #2.
Btw, there's a terminology problem, assuming you were referring to Joe-E's distinctions: In Joe-E terminology, private symbols are immutable but not powerless. (In E terms, private symbols are DeepFrozen but not * DeepPassByCopy* or Data.)
On Mon, Jan 28, 2013 at 11:10 AM, David Bruant <bruant.d at gmail.com> wrote:
Hi Tom,
Thanks for this summary. About the first proposal and getting rid of the whitelist, indeed, the whitelist was here to tell about known symbols to avoid leakage. If private symbols pierce proxies, the whitelist has no purpose any longer.
I don't understand why problem B isn't solved with the first proposal. Since the proxy is pierced, access to private symbol'ed properties don't trap and the proxy can't throw on access (since it's not trapped). If I'm misunderstanding the proposal, could you show an example, under the unconditional forwarding proposal in which the problem B can be reproduced.
The second proposal reminds me a bit of an evolution of the first proxy design in which each proxy would have its own storage for invariant checks. Since a forwarding proxy was the main use case and already had a storage area (the target), this led to the second design "direct proxy" in which both storages would be merged in the target. By analogy, the natural evolution of the second proposal would be the first proposal.
The natural evolution of the second proposal is to use WeakMaps where we would otherwise use private symbols, and get rid of the notion of private symbol per se.
Modulo naming, Kevin's unified proposal is equivalent to this. To illustrate
function KevinsSymbol() { var result = WeakMap(); result.proto = null; // no own properties and inherits no properties return Object.freeze(result); }
(Given) var call = Function.prototype.call;
var kevinsGet = call.bind(WeakMap.prototype.get); var kevinsSet = call.bind(WeakMap.prototype.set);
On 28 January 2013 19:45, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
I just wrote up a strawman on the wiki to summarize the recent debates about the interaction between proxies and private symbols:
strawman:proxy_symbol_decoupled
The page actually lists two proposals, out of which I prefer the second one.
If I forgot some benefits/drawbacks of either approach, please speak up.
Under the second approach, how can you transparently proxy an object with private properties at all? It seems like you can't, even when you have access to its private names. In other words, what do you mean by "inherit the private state of the target", when the target is still aliased and accessed?
On Mon, Jan 28, 2013 at 11:52 AM, Andreas Rossberg <rossberg at google.com>wrote:
On 28 January 2013 19:45, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
I just wrote up a strawman on the wiki to summarize the recent debates about the interaction between proxies and private symbols:
strawman:proxy_symbol_decoupled
The page actually lists two proposals, out of which I prefer the second one.
If I forgot some benefits/drawbacks of either approach, please speak up.
Under the second approach, how can you transparently proxy an object with private properties at all? It seems like you can't, even when you have access to its private names. In other words, what do you mean by "inherit the private state of the target", when the target is still aliased and accessed?
There is an interesting difference between weakmaps and Tom's second proposal.
Consider, in proposal #2:
// All on the wet side of the membrane. No interesting difference yet
var wetRecord = { symbol: PrivateSymbol(), thing: whatever() }; var wetValue = whatever() wetRecord.thing[wetRecord.symbol] = wetValue;
// transition to dry side
var { wrapper: dryRecord, gate } = makeMembrane(wetRecord);
var dryValue = dryRecord.thing[dryRecord.symbol];
// Under proposal #2, dryValue is undefined
Now let's do the "same" thing with WeakMaps:
// All on the wet side of the membrane. No interesting difference yet
var wetRecord = { symbol: WeakMap(), thing: whatever() }; var wetValue = whatever() wetRecord.symbol.set(wetRecord.thing, wetValue);
// transition to dry side
var { wrapper: dryRecord, gate } = makeMembrane(wetRecord);
var dryValue = dryRecord.symbol.get(dryRecord.thing);
// dryValue is a proxy for wetValue
Conclusion: Although both WeakMaps and Private Symbols have "issues" when virtualized through membranes, WeakMaps virtualize usefully under more scenarios than do Private Symbols under proposal #2.
Tor this[symbol] = value or equivalent on one side of a membrane, followed by this[symbol] or equivalent on the other side of the membrane, we have the following eight cases.
For all of these, o1 is the original non-proxy thing associated with the private properties. p1 is the proxy result of passing o1 through a membrane o2 is a PrivateSymbol or WeakMap p2 is the result of passing o2 through a membrane. When o2 is a WeakMap, p2 is a proxy for the WeakMap. When o2 is a PrivateSymbol, p2 is a PrivateSymbol that the membrane considers to correspond to o2 o3 is the original non-proxy value to be stored in this private property. p3 is the proxy result of passing o3 through the membrane.
o1[o2] = o3; p1[p2] // undefined !== p3 o2.set(o1, o3); p2.get(p1) // p3
o1[o2] = p3; p1[p2] // undefined !== o3 o2.set(o1, p3); p2.get(p1) // o3
o1[p2] = o3; p1[o2] // undefined !== p3 p2.set(o1, o3); o2.get(p1) // p3
The remaining 5 cases are left as an exercise for the reader ;).
The reason they all work for WeakMaps is that the WeakMap itself is on one side of the membrane and both gets and sets go there. The weakmap only sees the thing and value on its side of the membrane. The requestor only sees the thing and value on their side of the membrane.
The reason none of these work under proposal #2 is that the thing[privateSymbol] operations on one side of the membrane don't make it to the other side.
The reason none of these work under proposal #1 is that the thing[privateSymbol] operations don't trap, and so the membrane doesn't get to wrap or unwrap the stored value.
The assumption that my conclusion on auto-unwrapping rests on is that the situation shouldn't arise where a wet value is set as a dry object's private property. The reasoning is that the private key is presumed to be something closely guarded and that won't be shared in such a way that this happens. This assumption is the underpinning of the whole thing, so it's the real point of contention.
So the corresponding WeakMap situation would be one where the WeakMap o2 is never passed through the membrane, so there is no p2 on the other side of the membrane. In that scenario, AFAICT PrivateSymbol proposal #1, #2, and WeakMaps are all equivalent. Not so?
On Mon, Jan 28, 2013 at 3:59 PM, Mark Miller <erights at gmail.com> wrote:
Tor this[symbol] = value or equivalent on one side of a membrane, followed by this[symbol] or equivalent on the other side of the membrane, we have the following eight cases.
That should be "For thing[symbol] = value or equivalent...by thing[symbol] or equivalent..."
I realized the discrepancy between my thinking and what you said. In the case of a membrane, you are correct. A membrane will always unwrap everything, so you will only ever end up with dry method/dry this. That makes all the different forms of private equivalent. The difference arises when you're not dealing with a membrane, rather just a one shot proxy. In this case, you often do end up with (to borrow membrane the membrane terminology) dry method/wet this. this is the specific circumstance (and I believe the likely most commonly encountered one) in which auto-unwrapped private symbols do the right thing when the other private forms fail to work correctly.
2013/1/28 David Bruant <bruant.d at gmail.com>
I don't understand why problem B isn't solved with the first proposal. Since the proxy is pierced, access to private symbol'ed properties don't trap and the proxy can't throw on access (since it's not trapped). If I'm misunderstanding the proposal, could you show an example, under the unconditional forwarding proposal in which the problem B can be reproduced.
As noted on the wiki page: "It’s worth noting that revocable proxies (those created using Proxy.revocable) can still stop forwarding private symbol access when revoked. Hence, under this proposal, we solve problem A, but private symbol access remains “unreliable” (we don’t solve problem B)."
i.e. the problem only exists for revocable proxies. When revoked, these set their [[Target]] to null. This implies that transparently forwarding private symbol lookups to the target will fail when revoked.
2013/1/28 Mark S. Miller <erights at google.com>
Hi Tom, as you and I discussed in chat, "(base case) there are no built-in private symbols in a standard JS environment (i.e. all the built-in symbols are unique)" is a bad misunderstanding of the utility of membranes. Membranes (and membrane-like patterns) are useful and needed at many finer-grains than realms. It is not safe to assume that no private symbols exist on both sides of any membrane. I think proposal #1 is fatally insecure. I'm glad you like #2.
Ok, so the base case is unsound. Good to know. Proves that it's always important to explicitly state your base assumptions ;-)
Btw, there's a terminology problem, assuming you were referring to Joe-E's distinctions: In Joe-E terminology, private symbols are immutable but not powerless. (In E terms, private symbols are DeepFrozen but not * DeepPassByCopy* or Data.)
Thanks for the clarification. Will update the wiki.
2013/1/28 Andreas Rossberg <rossberg at google.com>
On 28 January 2013 19:45, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
I just wrote up a strawman on the wiki to summarize the recent debates about the interaction between proxies and private symbols:
strawman:proxy_symbol_decoupled
The page actually lists two proposals, out of which I prefer the second one.
If I forgot some benefits/drawbacks of either approach, please speak up.
Under the second approach, how can you transparently proxy an object with private properties at all? It seems like you can't, even when you have access to its private names. In other words, what do you mean by "inherit the private state of the target", when the target is still aliased and accessed?
What I had in mind was something along the lines of:
// wrap target in a proxy, "inheriting" the private state identified by privateSymbols function wrap(target, handler, privateSymbols) { var p = Proxy(target, handler); privateSymbols.forEach( symbol => { Object.defineProperty(p, symbol, { get: function() { return target[symbol]; }, set: function(val) { target[symbol] = val; } }); }); return p; }
Having written this out, I realize I further assumed that Object.defineProperty would work for private symbols, and would similarly not trap on a proxy. I think these assumptions are valid.
I further realize that under the above approach, we lose the ability for the proxy to virtualize the attributes of properties keyed by private symbols (i.e. target[symbol] may be a data property, while proxy[symbol] will be an accessor). My assumption is that this is not that big a deal, but we should be wary of this limitation.
2013/1/29 Brandon Benvie <brandon at brandonbenvie.com>
I realized the discrepancy between my thinking and what you said. In the case of a membrane, you are correct. A membrane will always unwrap everything, so you will only ever end up with dry method/dry this. That makes all the different forms of private equivalent. The difference arises when you're not dealing with a membrane, rather just a one shot proxy. In this case, you often do end up with (to borrow membrane the membrane terminology) dry method/wet this. this is the specific circumstance (and I believe the likely most commonly encountered one) in which auto-unwrapped private symbols do the right thing when the other private forms fail to work correctly.
Indeed. To summarize, the "dry method / wet this" case is easy to achieve considering the current default behavior of proxies:
- proxies by default forward property |get| to the target, so the returned method is unwrapped ("dry")
- proxies by default leave |this| bound to the proxy for method invocations, so the |this| value is wrapped ("wet")
As previously noted, this pattern breaks down for methods that expect |this| to:
- have particular private state (keyed by private symbols)
- have a particular identity (e.g. the object may be a key in a WeakMap)
- be of a particular built-in or exotic type (Date, NodeList, ...)
What I argued for in defense of proposal #2 is that the auto-unwrapping solution (proposal #1), while it seemingly makes things "just work", also inherently breaks the abstraction boundary of the proxy. The proxy's target could be a closely held object that should not be exposed to the dry method.
Instead, what I feel is the right way to deal with this dry method / wet this problem is that it's the proxy's responsibility to explicitly bind |this| to the right target upon forwarding.
The problem with this "right way" is that binding |this| is expensive (requires an allocation) in the absence of an invoke() trap. Membranes can cope because they have to return a wrapped function anyway, but in general binding |this| costs.
In conclusion, I don't feel this dry method / wet this problem should stand in the way of proposal #2. The problem touches upon a deeper issue unrelated to symbols per se. Instead, we should probably (separately) discuss alternative mechanisms by which a proxy can choose to either leave |this| bound to the proxy, or rebind it to another target object upon forwarding, with minimal overhead.
I would then suggest, as awb has, the creation of CallProperty (or Invoke) which fixes the problem in most circumstances. With the existence of WeakMaps and private symbols, the dry method/wet this problem is going to be an extremely common one.
On Mon, Jan 28, 2013 at 11:27 PM, Brandon Benvie <brandon at brandonbenvie.com>wrote:
I realized the discrepancy between my thinking and what you said. In the case of a membrane, you are correct. A membrane will always unwrap everything, so you will only ever end up with dry method/dry this. That makes all the different forms of private equivalent.
Hi Brandon, just to be clear, so this statement of "equivalence" is not misunderstood later: They are "equivalent" only in the limited scenario you outline, where the private-symbol/weakmap does not cross the membrane boundary. When it does, for all eight cases that arise, the weakmap maintains transparency. The private-symbol does not.
Yes, apologies. I meant equivalent in delivering the "final product", or the desired result of the operation as a whole from the the standpoint of the original architect. They are not equivalent on all the other ways discussed at length otherwise.
To elaborate, I've realized that my main disagreement with your position (really my care at all with the private discussion) is much better solved by introducing [[CallProperty]]. That is: Proxies currently handle |this| binding of method calls very poorly. I'm all for either removing private or making it really private, but Proxies are basically worthless in the private middleground which causes them to break half the time in ES6 code. But [[CallProperty]] fixes the issue in a way that seems acceptable to all parties.
Brandon Benvie wrote:
To elaborate, I've realized that my main disagreement with your position (really my care at all with the private discussion) is much better solved by introducing [[CallProperty]]. That is: Proxies currently
... and complicates object model. My main problem with [[CallProperty]] is the discrepancy between [[Get]] (used in [[Get]]+[[Call]]) and [[CallProperty]] (my assumption that by [[CallProperty]] you mean operation that replaces [[Get]]+[[Call]]).
I'd solve it otherwise. Let's say that foo.bar(baz) is split to three operations: [[Get]]+[[OopBind]]+[[Call]], where [[OopBind]] resolves this. Then, proxies should only be able to trap [[OopBind]] (but it will still be the same combination, involving trappable [[Get]] at the beginning and ending with (direct) [[Call]]).
2013/1/28 Tom Van Cutsem <tomvc.be at gmail.com>
I just wrote up a strawman on the wiki to summarize the recent debates about the interaction between proxies and private symbols:
strawman:proxy_symbol_decoupled
The page actually lists two proposals, out of which I prefer the second one.
I just updated the above wiki page with a brief summary of follow-up discussions on-list and off-list.
There are pros and cons either way. We'll have to make compromises. There's also the third alternative: stick with the current design (whitelist to trap known private symbols + unknownPrivateSymbol trap to implement a forwarding policy for unknown private symbols).
2013/1/22 Allen Wirfs-Brock <allen at wirfs-brock.com>
Let me try to summarize:
The proposal: private symbol access auto-unwraps proxies.
In code:
var s = new PrivateSymbol(); var t = {}; var p = Proxy(t, {...}); t[s] = "foo" p[s] // doesn't trap, returns "foo" p[s] = "bar" // doesn't trap, sets t[s] = "bar"
Pro:
Remaining issue: private symbols can pierce membranes.
This issue is resolved if:
It's worth noting that revocable proxies (those created using Proxy.revocable) would also stop forwarding private symbol access when revoked.