Calling toString on function proxy throws TypeError exception
Proxies without handlers are not indistinguishable from their targets. There are many examples, eg. let p = new Proxy(new Map(), {}); p.set(1, 1);
also throws.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Thomas Greiner Sent: Thursday, October 22, 2015 4:39 AM To: es-discuss <es-discuss at mozilla.org>
Subject: Calling toString on function proxy throws TypeError exception
According to developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toStringna01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FFunction%2FtoString&data=01|01|brian.terlson%40microsoft.com|17838fa9a81040efef5108d2dad58478|72f988bf86f141af91ab2d7cd011db47|1&sdata=GOvHpju8FRlkPatB05axAFp7P8G0iRA%2F%2FU8mj%2FqnCX4%3D Function.prototype.toString
is supposed to throw a TypeError
exception when used on a function proxy. That's also consistent with how it's defined at www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.tostringna01.safelinks.protection.outlook.com/?url=http%3A%2F%2Fwww.ecma-international.org%2Fecma-262%2F6.0%2F%23sec-function.prototype.tostring&data=01|01|brian.terlson%40microsoft.com|17838fa9a81040efef5108d2dad58478|72f988bf86f141af91ab2d7cd011db47|1&sdata=Epix453P5jAq8rLpp9sIHXY8AVXtCiP99%2BohNXmf0p0%3D but presumably only because the Proxy scenario is not explicitly mentioned.
The problem I see with that is that it makes proxies distinguishable from their targets even if they don't specify any traps. At least in cases where the mere existence of a proxy needs to be kept secret, this introduces a severe limitation.
That behavior also goes against the "transparent virtualization" principle as outlined by Axel Rauschmayer (see www.2ality.com/2014/12/es6-proxies.html#transparent_virtualization_and_handler_encapsulationna01.safelinks.protection.outlook.com/?url=http%3A%2F%2Fwww.2ality.com%2F2014%2F12%2Fes6-proxies.html%23transparent_virtualization_and_handler_encapsulation&data=01|01|brian.terlson%40microsoft.com|17838fa9a81040efef5108d2dad58478|72f988bf86f141af91ab2d7cd011db47|1&sdata=a4sAwSPF11X28Ck082Ds%2FkFWQH%2F6I%2BgnGO6%2BURwrOM4%3D):
Proxies are shielded in two ways:
- It is impossible to determine whether an object is a proxy or not (transparent virtualization).
- You can’t access a handler via its proxy (handler encapsulation).
Thomas
On Oct 22, 2015, at 11:23 AM, Mark S. Miller <erights at google.com> wrote:
Tom, this doesn't sound right to me. Didn't we intend Function.prototype.toString to be transparent thru function proxies?
cc'ing Allen and Brian
There is nothing unique to toString
going on there. This is just the problem that the default proxy handlers are not transparent across method calls in that they pass the proxy object rather than the target object as the this
value to function that they ultimately invoke. If that method needs to access internal slots of its this
value or has this
identify dependencies they don’t work. It turns out that most Function.prototype methods have such dependencies.
I know that's true in general. But we made a few exceptions, especially for functions and arrays. I thought F.p.toString was one, but maybe not. I just don't remember.
On Oct 22, 2015, at 11:43 AM, Mark S. Miller <erights at google.com> wrote:
I know that's true in general. But we made a few exceptions, especially for functions and arrays. I thought F.p.toString was one, but maybe not. I just don't remember.
There are no such special cases that I’m aware of, and I don’t know how they would work. See the proxy [[Call]] internal method ecma-international.org/ecma-262/6.0/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist, ecma-international.org/ecma-262/6.0/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist
This is basically a coordination issue between the access of a method via a proxy [[Get]] and the invocation of the retrieved function via a [[Call]. Essentially, the creation of a membrane is required to make it work. As I’m sure you will recall, we explored using an invoke trap to help deal with this sort of situation but eventually abandoned that. At the time we recognized that proxies that only used default handler would not be transparent in such situations and decide that it would be ok.
Ok, that all makes sense and is fine with me. Thanks for clarifying.
Tom, I'm still curious what you remember?
2015-10-22 21:03 GMT+02:00 Mark Miller <erights at gmail.com>:
Ok, that all makes sense and is fine with me. Thanks for clarifying.
Tom, I'm still curious what you remember?
What I remember is that the original direct proxies spec did the transparent forwarding. wiki.ecmascript.org still seems to be down, but an archived copy of late 2014 (thank you Wayback Machine) shows that the draft spec indeed specified forwarding:
< web.archive.org/web/20141214082226/http://wiki.ecmascript.org/doku.php?id=harmony:proxies_spec
*15.3.4.2 Function.prototype.toString ( O ) *
When called on a Proxy that is callable (i.e. whose target has a [[Call]] internal method), returns the result of applying the built-in Function.prototype.toString on the Proxy’s [[Target]]. Otherwise, throw the same TypeError that would be thrown if this function is applied to a non-function object.
For a revoked proxy, IsCallable is false so this operation throws a TypeError.
I cannot recall any specific discussions on why that behavior was changed to throwing a TypeError instead. The only reason I can come up with is that it could be deemed a violation of the proxy's encapsulation in that it should not leak such details about its target object.
I think it would be useful to at least reconsider the original forwarding behavior. I think that in practice, forwarding toString() to the target is not harmful, and the more places where code throws on a Proxy, the less useful Proxies become altogether.
I noticed there is a stage 1 proposal to revise the specification of F.p.toString() < michaelficarra/Function-prototype-toString-revision>,
including standardizing its behavior for host objects. Perhaps the behavior of F.p.toString() when applied to (function) proxies can become a part of this revision effort?
On Oct 22, 2015, at 1:38 PM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
... I cannot recall any specific discussions on why that behavior was changed to throwing a TypeError instead. The only reason I can come up with is that it could be deemed a violation of the proxy's encapsulation in that it should not leak such details about its target object.
I think it would be useful to at least reconsider the original forwarding behavior. I think that in practice, forwarding toString() to the target is not harmful, and the more places where code throws on a Proxy, the less useful Proxies become altogether.
I noticed there is a stage 1 proposal to revise the specification of F.p.toString() <michaelficarra/Function-prototype-toString-revision, michaelficarra/Function-prototype-toString-revision>, including standardizing its behavior for host objects. Perhaps the behavior of F.p.toString() when applied to (function) proxies can become a part of this revision effort?
Cheers, Tom
Why would it be reasonable to do this for toString
, but not also call
, apply
, and bind
? And ultimately this problem exists for all built-in methods that have have internal state dependencies. I just don’t see why we care so much specifically about toString
2015-10-22 23:09 GMT+02:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:
Why would it be reasonable to do this for
toString
, but not alsocall
,apply
, andbind
? And ultimately this problem exists for all built-in methods that have have internal state dependencies. I just don’t see why we care so much specifically abouttoString
Calling Function.prototype.{call,apply,bind} on a function proxy should obviously work (not throw a TypeError), as a function proxy has well-defined behavior in such cases. For toString(), the two obvious choices are to forward (show the function body of the target function), or to generate a custom toString() representation like other exotic objects do (e.g. "function () { [function proxy] }", or perhaps even show the toString() representation of the apply trap).
Almost every method found on Function.prototype
"works" (or, at least, does not throw a TypeError before trying to work) if and only if the target is callable, or if and only if typeof
applied to the target produces "function"
.
That should not be different for Function.prototype.toString()
. Even if "working" means producing a useless string à la "function () { [native code] }"
.
On Oct 23, 2015, at 6:43 AM, Claude Pache <claude.pache at gmail.com> wrote:
Almost every method found on
Function.prototype
"works" (or, at least, does not throw a TypeError before trying to work) if and only if the target is callable, or if and only iftypeof
applied to the target produces"function"
. That should not be different forFunction.prototype.toString()
. Even if "working" means producing a useless string à la"function () { [native code] }"
.—Claude
Right, I should have look closer at the actual spec for call, apply, and blind rather than depending upon memory. The reason they work on proxies is that they only depend upon the MOP API of their this object. In other words, they are generic across all object that have a [[Call]] internal method.
Function.prototype.toString is different in that it is dependent upon the actual internal structure of the object is is applied to rather than the object’s MOP API. This is necessary because F.p.toString must access information (the source code of the function or the ability to recreate it) that is not available through the MOP. As I’ve mentioned in earlier messages, F.p.toString is in this regard like all other methods that directly access private internal state.
F.p.toString could be respecified to explicitly recognize proxies and drill through them but it isn’t clear why F.p.toString should be singled out from all other private state accessing buiit-in methods in this regard.
But, let’s assume we did make F.p.toString drill through callable proxies. What are the implications.
First remember, that a callable proxy can only be created on a target object that already has a [[Call]] internal method. The target might itself be a callable proxy, but ultimately the chain of targets must reach an object that has a primitive [[Call]] behavior. This would normally be either an ECMAScript function object or a built-in function object.
So, while it is perfectly reasonable to define a callable proxy that, for example, in its [[Call]] handler runs a Lisp interpreter over a captured S-expression, such a proxy must either directly or indirectly have as its target an ECMAScript function. If you were defining such a proxy where the Lisp interpreter is embedded within its [[Call]] handler, you might use something like: function() {}
as the target. If you apply an enhanced proxy drill throughing F.p.toString to such a proxy you would get the source code for function() {}
. How is that useful?
Another way, to implement our Lisp interpreting proxy would be to use as the target an ECMAScript function that actually implements the Lisp interpreter. In that case, the [[Call]] handler would simply call the target function passing the captured S-expression to it as an argument. If you apply an enhanced proxy drill throughing F.p.toString to that sort of a proxy you would get back the source code for the Lisp interpreter. This seems potentially harmful. Revealing the source code of the Lisp interpreter might reveal exploitable secrets. Heck, the very fact that a Lisp interpreter is being used might be a secret.
So, why would we want to do this?
I like the idea a function proxy is more encapsulating of its implementation than a function is.
I also like the idea of treating a function proxy as a builtin callable, rather than a function written in JS, and return something like "function () { [function proxy] }" as Tom suggested or "function () { [native code] }" as Claude suggested. We need progress on the draft spec for F.p.toString reform, including the standardized pattern for the function sources that are not supposed to parse, e.g., "function () { [...stuff...] }".
On Oct 26, 2015, at 11:20 AM, Mark Miller <erights at gmail.com> wrote:
I like the idea a function proxy is more encapsulating of its implementation than a function is.
I also like the idea of treating a function proxy as a builtin callable, rather than a function written in JS, and return something like "function () { [function proxy] }" as Tom suggested or "function () { [native code] }" as Claude suggested. We need progress on the draft spec for F.p.toString reform, including the standardized pattern for the function sources that are not supposed to parse, e.g., "function () { [...stuff...] }”.
I guess I still don’t understand the use case for applying there built-in F.p.toString to any callable. If you are explicitly defining a callable proxy you may want to define a toString
method for it that does something that makes sense for the specific kind of callable you are creating. But when you expect to do:
evalableString = Function.prototype.toString.call(anyOldCallable);
with the expectation that the result you are going to get will be useful for anything?
Only because typeof f === 'function' divides the world into callables and non callables.
On Oct 26, 2015 3:20 PM, "Allen Wirfs-Brock" <allen at wirfs-brock.com> wrote:
On Oct 26, 2015, at 11:20 AM, Mark Miller <erights at gmail.com> wrote:
I like the idea a function proxy is more encapsulating of its
implementation than a function is.
I also like the idea of treating a function proxy as a builtin
callable, rather than a function written in JS, and return something like "function () { [function proxy] }" as Tom suggested or "function () { [native code] }" as Claude suggested. We need progress on the draft spec for F.p.toString reform, including the standardized pattern for the function sources that are not supposed to parse, e.g., "function () { [...stuff...] }”.
I guess I still don’t understand the use case for applying there built-in
F.p.toString to any callable. If you are explicitly defining a callable
proxy you may want to define a toString
method for it that does something
that makes sense for the specific kind of callable you are creating. But
when you expect to do:
evalableString = Function.prototype.toString.call(anyOldCallable);
with the expectation that the result you are going to get will be useful
for anything?
function () { [...] } is how callables I idiomatically give a response that is not useful for anything. That's all, but it is enough.
Le 26 oct. 2015 à 20:20, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
On Oct 26, 2015, at 11:20 AM, Mark Miller <erights at gmail.com> wrote:
I like the idea a function proxy is more encapsulating of its implementation than a function is.
I also like the idea of treating a function proxy as a builtin callable, rather than a function written in JS, and return something like "function () { [function proxy] }" as Tom suggested or "function () { [native code] }" as Claude suggested. We need progress on the draft spec for F.p.toString reform, including the standardized pattern for the function sources that are not supposed to parse, e.g., "function () { [...stuff...] }”.
I guess I still don’t understand the use case for applying there built-in F.p.toString to any callable. If you are explicitly defining a callable proxy you may want to define a
toString
method for it that does something that makes sense for the specific kind of callable you are creating. But when you expect to do:
evalableString = Function.prototype.toString.call(anyOldCallable);
with the expectation that the result you are going to get will be useful for anything?
Allen
The expectation is not that F.p.toString
will always return something useful; it is that, for any callable object, it will return a string and not throw, because it was so since the dawn of JS.
For example, a random programmer may write:
Function.isGenerator = function () {
return typeof this == "function" && this.toString().match(/^function\s*\*/) != null
}
That function will work (in the sense of: will return an answer; I'm not judging the quality of that answer) with anything reasonable fed to it (where "reasonable" excludes things like (class { static toString() { throw "pwnd!" }})
). Well, ... until a proxy for a function is encountered.
Le 27 oct. 2015 à 09:35, Claude Pache <claude.pache at gmail.com> a écrit :
Le 26 oct. 2015 à 20:20, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
On Oct 26, 2015, at 11:20 AM, Mark Miller <erights at gmail.com> wrote:
I like the idea a function proxy is more encapsulating of its implementation than a function is.
I also like the idea of treating a function proxy as a builtin callable, rather than a function written in JS, and return something like "function () { [function proxy] }" as Tom suggested or "function () { [native code] }" as Claude suggested. We need progress on the draft spec for F.p.toString reform, including the standardized pattern for the function sources that are not supposed to parse, e.g., "function () { [...stuff...] }”.
I guess I still don’t understand the use case for applying there built-in F.p.toString to any callable. If you are explicitly defining a callable proxy you may want to define a
toString
method for it that does something that makes sense for the specific kind of callable you are creating. But when you expect to do:
evalableString = Function.prototype.toString.call(anyOldCallable);
with the expectation that the result you are going to get will be useful for anything?
Allen
The expectation is not that
F.p.toString
will always return something useful; it is that, for any callable object, it will return a string and not throw, because it was so since the dawn of JS.For example, a random programmer may write:
Function.isGenerator = function () { return typeof this == "function" && this.toString().match(/^function\s*\*/) != null }
Naturally, I meant:
Function.isGenerator = function (f) {
return typeof f == "function" && f.toString().match(/^function\s*\*/) != null
}
On 10/27/15 4:35 AM, Claude Pache wrote:
it is that, for any callable object, it will return a string and not throw, because it was so since the dawn of JS.
It's totally false for random "host objects" with a [[Call]] in ES5, per spec and in at least some implementations. As you can tell in Firefox for example:
Function.prototype.toString.call(document.createElement("object"))
(though it does not throw for document.all in Firefox, for interesting implementation reasons).
That function will work (in the sense of: will return an answer; I'm not judging the quality of that answer) with anything reasonable fed to it (where "reasonable" excludes things like
(class { static toString() { throw "pwnd!" }})
).
Won't work with an HTMLObjectElement in at least some browsers. How "reasonable" that is, who knows.
On 10/27/15 9:14 AM, Boris Zbarsky wrote:
On 10/27/15 4:35 AM, Claude Pache wrote:
it is that, for any callable object, it will return a string and not throw, because it was so since the dawn of JS.
It's totally false for random "host objects" with a [[Call]] in ES5, per spec and in at least some implementations. As you can tell in Firefox for example:
Function.prototype.toString.call(document.createElement("object"))
(though it does not throw for document.all in Firefox, for interesting implementation reasons).
Though note that I would be just fine with defining Function.prototype.toString to return something akin to what it does for "host functions" for any "host object" with a [[Call]]...
Le 27 oct. 2015 à 14:14, Boris Zbarsky <bzbarsky at mit.edu> a écrit :
On 10/27/15 4:35 AM, Claude Pache wrote:
it is that, for any callable object, it will return a string and not throw, because it was so since the dawn of JS.
It's totally false for random "host objects" with a [[Call]] in ES5, per spec and in at least some implementations. As you can tell in Firefox for example:
Function.prototype.toString.call(document.createElement("object"))
(though it does not throw for document.all in Firefox, for interesting implementation reasons).
That function will work (in the sense of: will return an answer; I'm not judging the quality of that answer) with anything reasonable fed to it (where "reasonable" excludes things like
(class { static toString() { throw "pwnd!" }})
).Won't work with an HTMLObjectElement in at least some browsers. How "reasonable" that is, who knows.
-Boris
You're right. But since document.createElement("object")
does not inherit from Function.prototype
, the code (f.toString()
) accidentally works after all.
(I've tried not to be too smart in my example by writing f.toString()
instead of Function.prototype.toString.call(f)
. Maybe I should have been even less smart by defining an instance method on Function.prototype
instead of a static method on Function
...)
Notice that whatever we decide on the issue, functionProxy.toString() will work regardless, since you'd be getting the toString method itself through the membrane. functionProxy.toString will be a function proxy for the target.toString method. The invocation on the toString proxy with functionProxy as this will be translated by the membrane back into an invocation of target.toString with target as this.
The issue we're debating is only relevant on an edge case -- when explicitly invoking F.p.toString.call(functionProxy).
Mark: considering that explicitly invoking a builtin prototype method,
expecting a throw, to test for the [[Call]] internal slot, was the branding
approach the committee agreed to preserve when it reaffirmed
Symbol.toStringTag, as an alternative to Object#toString.call
, I think
Claude's point ("The expectation is not that F.p.toString
will always
return something useful; it is that, for any callable object, it will
return a string and not throw, because it was so since the dawn of JS.") is
the one that convinces me.
Shouldn't tests in existing code that rely on Function#toString
not
throwing to indicate that something is callable, or throwing to indicate
that it is not, remain true for a function proxy?
On 10/27/15 12:47 PM, Jordan Harband wrote:
I think Claude's point ("The expectation is not that
F.p.toString
will always return something useful; it is that, for any callable object, it will return a string and not throw, because it was so since the dawn of JS.") is the one that convinces me.
But that statement isn't true, as I pointed out. It just isn't, no matter how much we may wish it.
Le 27 oct. 2015 à 15:52, Mark S. Miller <erights at google.com> a écrit :
Notice that whatever we decide on the issue, functionProxy.toString() will work regardless, since you'd be getting the toString method itself through the membrane. functionProxy.toString will be a function proxy for the target.toString method.
... at the condition that the proxy handler explicitly traps the getting of the toString method. It is certainly the case for impermeable membranes, but not for all proxies. Concretely, new Proxy(function() {}, {}).toString()
does throw in ES2015.
On Oct 27, 2015, at 2:37 AM, Claude Pache <claude.pache at gmail.com> wrote:
Naturally, I meant:
Function.isGenerator = function (f) { return typeof f == "function" && f.toString().match(/^function\s*\*/) != null }
—Claude
That function will work (in the sense of: will return an answer; I'm not judging the quality of that answer) with anything reasonable fed to it (where "reasonable" excludes things like
(class { static toString() { throw "pwnd!" }})
). Well, ... until a proxy for a function is encountered.—Claude
This may have been pragmatically useful but it has never been a reliable test because there was nothing that stops somebody from adding an arbiietrary toString
own property to f or from using dunder photo to change the prototype used to supply toString
.
Also, going back to at lest ES3, F.p.toString was specified to throw a TypeError when applied to an object that “is not Function object”.
That said, I think it is plausible that the predicate in step 2 of tc39.github.io/ecma262/#sec-function.prototype.tostring, tc39.github.io/ecma262/#sec-function.prototype.tostring could be replaced with: IsCallable(func)
Non-membraned proxies are irreparably non-transparent. This case is not worth worrying about.
Le 27 oct. 2015 à 18:41, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
That said, I think it is plausible that the predicate in step 2 of tc39.github.io/ecma262/#sec-function.prototype.tostring, tc39.github.io/ecma262/#sec-function.prototype.tostring could be replaced with: IsCallable(func)
Yes please. From a user's point-of-view, it is not clear why implementations treat differently window.alert
and document.createElement('object')
.
2015-10-27 12:47 GMT-04:00 Jordan Harband <ljharb at gmail.com>:
Shouldn't tests in existing code that rely on
Function#toString
not throwing to indicate that something is callable, or throwing to indicate that it is not, remain true for a function proxy?
Excluding some host objects (as Boris pointed out), I think this does capture the programmer's expectation in the common case.
When I wrote my Proxy shim for ES5 < tvcutsem/harmony-reflect>, I got feedback from
developers to patch F.p.toString() to please unwrap the proxy, as this seemed to be the general expectation (just like Array.isArray should return true for proxies-for-arrays).
I don't have a strong opinion on whether F.p.toString() should drill through the proxy or just return "function() { [...] }", but would really like it not to throw. A function proxy is, for all intents and purposes, a valid function. Having F.p.toString report that it is called on an "incompatible object" just feels wrong.
Even though just calling toString() would work fine on a membraned function proxy, the idiom of storing built-in functions in variables and explicitly .call()-ing those built-in functions is, to my understanding, still very common.
According to developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString
Function.prototype.toString
is supposed to throw aTypeError
exception when used on a function proxy. That's also consistent with how it's defined at www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.tostring but presumably only because the Proxy scenario is not explicitly mentioned.The problem I see with that is that it makes proxies distinguishable from their targets even if they don't specify any traps. At least in cases where the mere existence of a proxy needs to be kept secret, this introduces a severe limitation.
That behavior also goes against the "transparent virtualization" principle as outlined by Axel Rauschmayer (see www.2ality.com/2014/12/es6-proxies.html#transparent_virtualization_and_handler_encapsulation ):
Thomas