On revoking access to the target of a proxy

# David Bruant (12 years ago)

I spent a couple of hours presenting proxies to a couple of folks at the IDRC (Inclusive Design Research Center) in Toronto and Colin Clark (IDRC Tech Lead) made me realize that we have lost something between the previous proxy design and the current one. This is related to the caretaker and membrane patterns and garbage collection.

 // With the previous proxy design:
 var handler = {
     get: function(){
         something(target)
     },
     set: function(){
         somethingElse(target)
     },
     target : {};
 }
 function revoke(){
     handler.target = null;
 }

On calling the revoke method, access to the target will cause error to be thrown when the proxy is touched. Additionally, if this was the only reference to the target, the target can be garbage collected. This scales also for a membrane where revoke can lead to the garbage collection of an arbitrary number of objects.

With the current design, there is no way to cut the access to the target and enable its garbage collection because the target is an internal property of the proxy. It means that malicious or buggy code keeping a reference to the proxy keeps a reference to the target.

As a matter of fact, I've recently had this problem of a library keeping references to a large files and ending up crashing the (node.js) process because of memory overflow. Having the ability to cut the reference would likely allow to garbage collect the content and solve the problem. I should mention also the "Huey fix" [1][2] which is a real-world use of the caretaker pattern used for the sake of memory consumption.

Proposals (first shot at least):

  • A Proxy.revokeTarget(proxy) & corresponding revokeTarget trap (returns a boolean to decide whether to procede or not). After the access to the target has been revoked, any attempt to touch the proxy throws an exception without trapping (very much like transferable objects when they've been transfered IIRC). If no revokeTarget trap is provided, the target is not revoked (return false by default). Called on a non-proxy object, Proxy.revokeTarget does nothing. Of course, if someone else holds a reference to the target, it is not garbage-collected no matter how many proxies have been revoked access to it.
  • Alternatively, the proxy constructor returns a pair so that only the proxy creator has access to the revoke method (removes the need for the trap). But it induces boilerplate when you don't care about revokability.
  • Alternatively, having 2 constructors, Proxy and RevokableProxy. The former is the one for current direct proxies, the latter returns a pair as described in the second proposal.

David

[1] blog.kylehuey.com/post/21892343371/fixing-the-memory-leak [2] blog.mozilla.org/nnethercote/2012/05/15/additional-update-on-leaky-add-ons

# Sam Tobin-Hochstadt (12 years ago)

On Fri, Aug 3, 2012 at 5:03 PM, David Bruant <bruant.d at gmail.com> wrote:

With the current design, there is no way to cut the access to the target and enable its garbage collection because the target is an internal property of the proxy. It means that malicious or buggy code keeping a reference to the proxy keeps a reference to the target.

Can't this be implemented using Tom & Mark's shadow target technique, plus explicit nulling of portions of the shadow target (since that is kept alive by the proxy)?

# David Bruant (12 years ago)

Le 03/08/2012 17:08, Sam Tobin-Hochstadt a écrit :

On Fri, Aug 3, 2012 at 5:03 PM, David Bruant <bruant.d at gmail.com> wrote:

With the current design, there is no way to cut the access to the target and enable its garbage collection because the target is an internal property of the proxy. It means that malicious or buggy code keeping a reference to the proxy keeps a reference to the target. Can't this be implemented using Tom & Mark's shadow target technique,

What do you call the shadow target technique? Can it be implemented with direct proxies?

plus explicit nulling of portions of the shadow target (since that is kept alive by the proxy)?

nulling is not possible on frozen objects. So partially yes, it depends on the cases for that part.

# Sam Tobin-Hochstadt (12 years ago)

On Fri, Aug 3, 2012 at 5:12 PM, David Bruant <bruant.d at gmail.com> wrote:

Le 03/08/2012 17:08, Sam Tobin-Hochstadt a écrit :

On Fri, Aug 3, 2012 at 5:03 PM, David Bruant <bruant.d at gmail.com> wrote:

With the current design, there is no way to cut the access to the target and enable its garbage collection because the target is an internal property of the proxy. It means that malicious or buggy code keeping a reference to the proxy keeps a reference to the target.

Can't this be implemented using Tom & Mark's shadow target technique,

What do you call the shadow target technique? Can it be implemented with direct proxies?

Yes. See the discussion of dummyTarget in Tom and Mark's recent draft: soft.vub.ac.be/Publications/2012/vub-soft-tr-12-03.pdf

# David Bruant (12 years ago)

Le 03/08/2012 17:38, Sam Tobin-Hochstadt a écrit :

On Fri, Aug 3, 2012 at 5:12 PM, David Bruant <bruant.d at gmail.com> wrote:

Le 03/08/2012 17:08, Sam Tobin-Hochstadt a écrit :

On Fri, Aug 3, 2012 at 5:03 PM, David Bruant <bruant.d at gmail.com> wrote:

With the current design, there is no way to cut the access to the target and enable its garbage collection because the target is an internal property of the proxy. It means that malicious or buggy code keeping a reference to the proxy keeps a reference to the target. Can't this be implemented using Tom & Mark's shadow target technique, What do you call the shadow target technique? Can it be implemented with direct proxies? Yes. See the discussion of dummyTarget in Tom and Mark's recent draft: soft.vub.ac.be/Publications/2012/vub-soft-tr-12-03.pdf

Quoting relevant section (tell me if I'm missing another relevant section): "6.1. Virtual Objects To create a virtual object using our Proxy API, it suffices to create a proxy with a dummy (perhaps empty) target object, and to have the handler ignore that target object. There are a couple of caveats though: (1) The invariant enforcement mechanism discussed in Section 5.2 will not allow the handler to expose non-configurable properties or emulate a non-extensible object, unless the dummy object stores the exposed non-configurable properties or is itself non-extensible."

So your actual target can be GC'ed, but the dummyTarget has to keep observed non-configurable (and non-writable) properties. Or you can give up non-configurability. I don't find any of these solutions satisfactory.

# Brendan Eich (12 years ago)

David Bruant wrote:

So your actual target can be GC'ed, but the dummyTarget has to keep observed non-configurable (and non-writable) properties. Or you can give up non-configurability. I don't find any of these solutions satisfactory.

Please see Tom's post-meeting notes on the wiki:

harmony:direct_proxies#discussed_during_tc39_july_2012_meeting_microsoft_redmond

specifically

*trapping isSealed and friends

Unless I've misunderstood, this addresses your concern.

# David Bruant (12 years ago)

Le 03/08/2012 19:54, Brendan Eich a écrit :

David Bruant wrote:

So your actual target can be GC'ed, but the dummyTarget has to keep observed non-configurable (and non-writable) properties. Or you can give up non-configurability. I don't find any of these solutions satisfactory.

Please see Tom's post-meeting notes on the wiki:

harmony:direct_proxies#discussed_during_tc39_july_2012_meeting_microsoft_redmond

specifically

*trapping isSealed and friends

Unless I've misunderstood, this addresses your concern.

I don't see how it does. This part is mostly about non-extensibility while I'm worried about non-configurability of some properties of the dummy target (not all properties have to be).

Some code to describe my concern:

 var target = {};
 // create a proxy with a dummyTarget instead of the actual target
 var {ref, revoke} = makeCareTaker(target);
 Object.defineProperty(ref, 'a', {value: hugeString, conf: false, 

writ:false}; ref.b = 2; // no need to copy to the dummyTarget, because there is no invariant to check. revoke(); ref.a; // throws thanks to revokation, but the dummyTarget can't be GC'ed

The problem is that the handler.defineProperty call either has to throw (forbid creation of non-conf/non-writ props) or accept in which case, it has to copy the descriptor on the dummy target first (including the hugeString), because that's how invariant enforcement works. After the revokation, the target is not accessible which is what we want, but the dummyTarget will stay in memory as long as the proxy does.

In the end, revokability can be implemented, but the nice GC property that should come along can only be implemented if the revokable proxy does not support invariant checking. Regardless, it still requires to delete all configurable properties of the dummy target on revokation, which may take some time.

As we've seen with the Hueyfix, having enabling GC thanks to revokation sometimes yields tremendously excellent results (whether the properties of the GC'ed objects are configurable or not, this should not matter). I think it justifies language-level revokability.

# Mark Miller (12 years ago)

On Aug 3, 2012, at 5:58 PM, David Bruant <bruant.d at gmail.com> wrote: [...]

In the end, revokability can be implemented, but the nice GC property that should come along can only be implemented if the revokable proxy does not support invariant checking. Regardless, it still requires to delete all configurable properties of the dummy target on revokation, which may take some time.

As we've seen with the Hueyfix, having enabling GC thanks to revokation sometimes yields tremendously excellent results (whether the properties of the GC'ed objects are configurable or not, this should not matter). I think it justifies language-level revokability.

I see you point. This gc issue surprises me. I agree it seems serious. I hate to complexify the proxy design yet further, but I don't see how a library could workaround this restiction otherwise. Other than the additional complexity, which is a significant argument against, what other arguments are there against enabling a proxy to drop its target?

# David Bruant (12 years ago)

Le 04/08/2012 16:32, Mark Miller a écrit :

On Aug 3, 2012, at 5:58 PM, David Bruant <bruant.d at gmail.com> wrote: [...]

In the end, revokability can be implemented, but the nice GC property that should come along can only be implemented if the revokable proxy does not support invariant checking. Regardless, it still requires to delete all configurable properties of the dummy target on revokation, which may take some time.

As we've seen with the Hueyfix, having enabling GC thanks to revokation sometimes yields tremendously excellent results (whether the properties of the GC'ed objects are configurable or not, this should not matter). I think it justifies language-level revokability. I see you point. This gc issue surprises me.

It really was a non-obvious loss in the switch from the previous design to direct proxies.

I agree it seems serious. I hate to complexify the proxy design yet further, but I don't see how a library could workaround this restiction otherwise.Other than the additional complexity, which is a significant argument against, what other arguments are there against enabling a proxy to drop its target?

I only have an additional argument in favor. Transferables (and private names?) will require the implementation of objects which will throw when touched. Also, Hueyfix included DeadObjectProxies [1]. So it seems JS impls that the implementation cost of this feature is mostly already paid.

David

[1] hg.mozilla.org/mozilla-central/rev/cc5254f9825f#l7.13

# David Bruant (12 years ago)

It's been a couple of days that this thread has had no update and specifically no answer to Mark Miller's question "Other than the additional complexity, which is a significant argument against, what other arguments are there against enabling a proxy to drop its target?" So I guess we can assume agreement (until proven otherwise) on the necessity of a language construct for revoking target access and can consider moving forward on the "how".

Here are 3 proposals I made:

  • A Proxy.revokeTarget(proxy) & corresponding revokeTarget trap (returns a boolean to decide whether to procede or not). After the access to the target has been revoked, any attempt to touch the proxy throws an exception without trapping (very much like transferable objects when they've been transfered IIRC). If no revokeTarget trap is provided, the target is not revoked (return false by default). Called on a non-proxy object, Proxy.revokeTarget does nothing. Of course, if someone else holds a reference to the target, it is not garbage-collected no matter how many proxies have been revoked access to it.
  • Alternatively, the proxy constructor returns a pair so that only the proxy creator has access to the revoke method (removes the need for the trap). But it induces boilerplate when you don't care about revokability.
  • Alternatively, having 2 constructors, Proxy and RevokableProxy. The former is the one for current direct proxies, the latter returns a pair as described in the second proposal.

I tend to favor the third proposal because revokability is not everyone's use case and having "new Proxy()" returning a proxy seems more intuitive.

Are there other proposals? other preferences?

# Brendan Eich (12 years ago)

You made a good point, one I thought we had touched on at the last TC39 meeting, but apparently not.

Tom is

# Tom Van Cutsem (12 years ago)

(sorry for the slow reply, catching up on e-mail)

So the old fix() trap is back with a vengeance, it seems!

To answer Mark's question of what other downsides there are to dropping the target:

  1. it would require an additional check on every trapped operation to verify whether the proxy is revoked or not, throwing an exception if it is. Nothing critical since we're already on the slow path, but this "revocation" check is new overhead.

  2. it reintroduces a subtle issue from the old design that had to do with fixing a proxy while it still has pending trap activations on the runtime stack: an unrevoked proxy may call one of its traps and be revoked by the time the trap returns. For invariant enforcement to work correctly, the trap's result must be verified against the original target. So, the proxy must ensure that it holds on to the original target before calling the trap, and use that stored pointer after the trap returns. Again, not a strong argument against, but it's an important new implementation detail.

(by the way: because we functionally pass the target as first argument to each trap invocation, revoking a proxy would not affect active trap activations at all: they just keep on referring to the original target parameter until they complete. This is Good.)

David's proposals are all very much consistent with the current Proxy design. I would prefer option 3 (having a separate RevokableProxy constructor that returns a {proxy, revoke} pair), since it somewhat isolates the additional complexity in a dedicated abstraction. Also, while I suspect that for spec economy, we would want to spec Proxy instances simply as RevokableProxy instances that are never revoked, for implementors it makes for an easy distinction, allowing them to elide the revocation check for Proxy instances).

# David Bruant (12 years ago)

Le 21/08/2012 17:41, Tom Van Cutsem a écrit :

(sorry for the slow reply, catching up on e-mail)

So the old fix() trap is back with a vengeance, it seems!

It's a very different form, but I see the analogy.

To answer Mark's question of what other downsides there are to dropping the target:

  1. it would require an additional check on every trapped operation to verify whether the proxy is revoked or not, throwing an exception if it is. Nothing critical since we're already on the slow path, but this "revocation" check is new overhead.

For the sake of the spec maybe, but in-memory, at revokation time, the proxy object can be swapped with an "unconditonnally-throw-when-touched" object. In that situation, there is no need for a "revocated" boolean and related check on pre-trap.

  1. it reintroduces a subtle issue from the old design that had to do with fixing a proxy while it still has pending trap activations on the runtime stack: an unrevoked proxy may call one of its traps and be revoked by the time the trap returns.

As a complement to what you're saying, in this case, the proxy user revokes the proxy (intentionally or not) in the middle of a normal object operation (get/set/has/etc.). After this operation, the proxy is revoked. We can say "revoked internally" since it has not been does by an external proxy user, but a trap.

For invariant enforcement to work correctly, the trap's result must be verified against the original target.

Or should it just throw, considering that no invariant holds for internally revoked proxy? Returning a last result while the proxy has been internally revoked might be considered as a leak.

 var revoke;
 var handler = {
     get: function(){
         revoke();
         return 1;
     }
 }

 var {revoke, p: proxy} = new RevokableProxy({}, handler);

 var a = p.prop; // should "a" be 1 or should this operation throw 

on post-get trap? var b = 'yo' in p; // throws anyhow without even calling the has trap because p has been revoked

While I said above that a before-trap check can be removed, I think a post-trap check is necessary for this idea.

I have no strong opinion as for what is best between doing a "last successful operation" or throwing for internally revoked proxies, I just wanted to share this option for the sake of completeness.

# Tom Van Cutsem (12 years ago)

2012/8/22 David Bruant <bruant.d at gmail.com>

Le 21/08/2012 17:41, Tom Van Cutsem a écrit :

  1. it reintroduces a subtle issue from the old design that had to do with fixing a proxy while it still has pending trap activations on the runtime stack: an unrevoked proxy may call one of its traps and be revoked by the time the trap returns.

As a complement to what you're saying, in this case, the proxy user revokes the proxy (intentionally or not) in the middle of a normal object operation (get/set/has/etc.). After this operation, the proxy is revoked. We can say "revoked internally" since it has not been does by an external proxy user, but a trap.

Not necessarily: the trap, while executing, may call on some external code which then does the revocation, so there's no reason to call it "revoked internally" (i.e. the revocation may happen arbitrarily deep in the call stack, it may not occur lexically in the scope of the trap).

For invariant enforcement to work correctly, the trap's result must be

verified against the original target.

Or should it just throw, considering that no invariant holds for internally revoked proxy? Returning a last result while the proxy has been internally revoked might be considered as a leak.

[example snipped]

I have no strong opinion as for what is best between doing a "last successful operation" or throwing for internally revoked proxies, I just wanted to share this option for the sake of completeness.

The way proxies are currently specced (see < harmony:proxies_spec#proxy_internal_methods>),

the [[Target]] of a proxy is always read into a local variable before calling the trap, and in the post-trap assertion checks, this local variable is then used. Thus, it doesn't matter whether the proxy was revoked in the mean time or not.

I don't see the harm in still allowing pending trap activations to return gracefully upon revocation.

# Tom Van Cutsem (12 years ago)

FYI, the ideas expressed in this thread are now written up as a strawman: strawman:revokable_proxies