2

Note: I now believe this question was based on assumptions about the javascript specification which are actually implementation specific.

I am attempting to build a runtime debugging hook system for a complex dynamic javascript application. A series of choices have let me to choose to use javascript Proxy and Reflect metaprogramming constructs to interpose function calls in the application I am debugging, wrapping all incoming functions arguments in Proxy/Reflect constructs.

The approach involves replacing high level application functions with Proxies and using traps and handlers to provide debugging functionality, ultimately passing arguments through to the application in a transparent way. All property get/set and function executions act as normal. However, by wrapping all objects and functions in Proxies allows tracing of the runtime.

I am installing this hook system into Chrome.

(Note: Please do NOT provide an answer suggesting a different methodology for debugging hooks - options have been evaluated extensively.)

The issue is that some javascript methods in the application invoke closures and pass "this" parameters. When "this" parameters are wrapped in a Proxy, the runtime fails to execute a closure, instead throwing an "Illegal Invocation" Exception.

I have tried reengineering the debugging hook system to not wrap arguments for some methods, or selectively wrap arguments. I have not been able to find a way to tell if an argument is intended to be used as a context, making code that tries this approach hardcoded to many possible methods and calling conventions. Ultimately this is too fragile to calling convention edge cases and requires too many case statements.

I have also removed the logic for wrapping arguments before passing them through. This removes the benefit from the debug hooking system, and so I have always reverted the logic to wrap all incoming arguments.

alert.apply(this, [1]);

p = new Proxy(this, {});

try {
    alert.apply(p, [1]);
} catch (e) {
    console.log(e);
}

This throws an "Illegal Invocation" Exception.

typeof this === 'object'
true

But it seems that contexts are objects just like everything else.

I expect that passing a Proxy() through to context should succeed in an invocation. Barring this, I would expect the type of a context to be specific enough to determine whether it should be wrapped in a Proxy() or not.

I have two questions.

(1) What are the semantics of context binding closures in javascript that would cause binding to a Proxy(context) to fail with an illegal invocation?

(2) What type of object are contexts, and how can a javascript method tell one apart from other javascript objects by inspecting its properties at runtime?

  • What exactly is your question now? – Bergi Aug 20 '19 at 17:36
  • "*I have also removed the logic for wrapping arguments before passing them through.*" - can you show us that logic? You might be able to extend it so that it unwraps proxies in the right places. – Bergi Aug 20 '19 at 17:36
  • I do believe that you investigated other methodologies thoroughly, but I'm curious about it. Could you share your evaluations somewhere? (Comments, chat, a blog post, ..., not in the question though probably) – Bergi Aug 20 '19 at 17:38
  • @Bergi I have two questions: (1) What are the semantics of context binding closures in javascript that would cause a Proxy(context) to fail with an illegal invocation? (2) What type of object are contexts, and how can a javascript method tell one apart from other javascript objects by inspecting its properties? Note: I have tried unwrapping Proxies in the right places by coding in edge cases and by blanket/heuristic policies. Both of these approaches fail for their own reasons. I need answers to (1) and (2) to do it formally correctly. – Ross Snider Aug 20 '19 at 18:03
  • `this` is just `window` in that code. `alert` expects `this` to be a window object, not any other object. `alert.apply( window, [1] )` will work, but `alert.apply( x, [1] )` will not where x is anything other than a reference to `window`. – Paul Aug 20 '19 at 18:27
  • 1
    The error is thrown for the same reason `new Proxy( x, {} ) !== x`. You could make a function that behaves the same as alert like this: `function foo ( ) { if ( this !== window ) throw new TypeError( 'Illegal Invocation' ); }` `foo.apply( new Proxy( window, { } ) );` will throw the error, but `foo.apply( window )` will not. – Paul Aug 20 '19 at 18:31
  • @paulpro If I understand you correctly, you're asserting that in the native code of alert(), there is a check that the bound context is === window? How does alert determine whether "window" itself is correct (doesn't it need a scope to access window - and what if window is replaced)? – Ross Snider Aug 20 '19 at 18:59
  • @Paulpro I've reproduced the behavior you've asserted. window.test = function (a) { console.log(a) }; window.test.apply(new Proxy(window, {}), 1) This works in a manner consistent with your assertion. Thank you. – Ross Snider Aug 20 '19 at 19:02
  • @RossSnider no, the `alert` function could have its own closure scope or just query the native DOM representation for the window object. There are always some variables that you cannot replace (like the global object, the `window` variable, the `document` variable, etc). And PaulPro makes another important remark: proxies cannot intercept equality comparisons, they cannot mock an object's very much identity. – Bergi Aug 20 '19 at 19:19

1 Answers1

1

What type of object are contexts, and how can a javascript method tell one apart from other javascript objects by inspecting its properties at runtime?

There is no special type. Every object can become a context by calling a method upon it. Most objects that will become a context of a method call do have that very method as an (inherited) property, but there's no guarantee.

You cannot tell them apart.

What are the semantics of context binding in javascript that would cause binding to a Proxy(context) to fail with an illegal invocation?

When the method is a native one. In user code functions, the this context being a proxy doesn't make a difference, when you access it then it will just behave as a proxy.

The problem is native methods that expect their this argument to be a native object of the respective type. Sure, those objects are still javascript objects, but they may contain private data on internal properties as well. A proxy's target and handler references are implemented through such internal properties as well, for example - you can sometimes inspect them in the debugger. The native methods don't know to unwrap a proxy and use its target instead, they just look at the object and notice that it doesn't have the required internal properties for the method to do its job. You could've passed a plain {} as well.

Examples for such methods can be found as builtins of the ECMAScript runtime:

But also (and even more of them) as host objects supplied by the environment:

  • window.alert/prompt
  • EventTarget.prototype.addEventListener/removeEventListener
  • document.createElement
  • Element.prototype.appendChild/remove/
  • really just anything that's browser-specific
  • but also in other environments, like the nodejs os module

I have tried unwrapping Proxies in the right places by coding in edge cases and by blanket/heuristic policies.

I think the only reasonable approach would be to check whether the called function is a native one, and unwrap all arguments (including the this argument) for them.

Only a few native functions could be whitelisted, such as most of those on the Array.prototype which are explicitly specified in the language standard to work on arbitrary objects.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Is it possible to set an object's properties in a manner that "masquerades" as internal properties for the assumptions/checks of the browser native code? I'm assuming no - and that this would be considered a security issue? – Ross Snider Aug 20 '19 at 19:08
  • @RossSnider No, it's not possible. That's why they're called "internal". They're not properties at all that can be accessed with dot or bracket syntax. And yes, accessing these internal properties would also be a security issue. – Bergi Aug 20 '19 at 19:15
  • This is very helpful, and I think adequately answers both questions. I'm going to give this some time on SE before accepting it in case other information is posted. – Ross Snider Aug 20 '19 at 20:23