Page 1 of 1

proxifying modifyGetContext

Posted: Mon May 03, 2021 10:53 am
by skriptimaahinen
Been looking at this function a while and didn't realize this https://github.com/hackademix/nscl/comm ... 7946672329. Sigh. Oh well, never did say I understand regex ;)

Anyhow... Lets tackle some error messages.

Btw, can't move the call to getContext to the front because if we return null we can't have the canvas initialized to webgl as it still needs to be able to provide other contexts like 2d. i.e.

Code: Select all

// Native behaviour. Assume webgl disabled from about:config. We perform these two consecutive calls:
canvas.getContext("webgl") -> null
canvas.getContext("2d") -> 2d context

// Assume webgl enabled.
canvas.getContext("webgl") -> webgl context
canvas.getContext("2d") -> null
So to achive this, getCanvas should not be called when the type is webgl. We just must make best effort to not return null when there should be an error.

Different types of errors I have encountered:

Code: Select all

TypeError: 'getContext' called on an object that does not implement interface HTMLCanvasElement.    HTMLCanvasElement.prototype.getContext()
TypeError: HTMLCanvasElement.getContext: At least 1 argument required, but only 0 passed            canvas.getContext()
TypeError: getContext is not a constructor                                                          new canvas.getContext
The first one should be handled by checking that the object is instance of HTMLCanvasElement. Using proxy will take care of the two others.

webglHook.js:

Code: Select all

function modifyGetContext(scope, env) {
  let dispatchEvent = EventTarget.prototype.dispatchEvent;
  let { Event } = scope;
  for (let canvas of ["HTMLCanvasElement", "OffscreenCanvas"]) {
    if (!(canvas in scope)) continue;
    
    const getContext = scope[canvas].prototype.getContext;

    const handler = cloneInto({
      apply: function(targetObj, thisArg, argumentsList) {
        if (thisArg instanceof HTMLCanvasElement && /webgl/i.test(argumentsList[0])) {
          let target = canvas === "HTMLCanvasElement" && document.contains(thisArg) ? thisArg : scope;
          env.port.postMessage("webgl", target);
          return null;
        }
        return getContext.call(thisArg, ...argumentsList);
      }
    }, scope, {cloneFunctions: true});
    
    const proxy = new scope.Proxy(getContext, handler);
    scope[canvas].prototype.getContext = proxy;
  }
}
The need to use a proxy stems mainly from the error message thrown by new operator. For reason unknown, if used on exported getContext, it seems the error gets initialized in contentscript context and while propagating back to the page, causes another error (DOMException).

Generating the error in the exported function for the right scope works around this, but there is no way to make the error message completely correct as the message should contain the line that is executed and this info is not available in the exported function. e.g.

Code: Select all

new canvas.getContext           -> TypeError: canvas.getContext is not a constructor
new windows.canvas.getContext   -> TypeError: windows.canvas.getContext is not a constructor
So proxy seems must. Note that the proxy is created in the page scope. Otherwise the error message from "new" will still be initialized in the contentscript context. Only the proxy handler is exported.

Also HTMLCanvasElement.prototype.getContext.length is now 1 as it should be.


Oh! Does line "let target = canvas === "HTMLCanvasElement" && document.contains(thisArg) ? thisArg : scope;" have some planned use? postMessage does not appear to use the target for anything.

Re: proxifying modifyGetContext

Posted: Mon May 03, 2021 8:05 pm
by Giorgio Maone
skriptimaahinen wrote:
Mon May 03, 2021 10:53 am
Oh! Does line "let target = canvas === "HTMLCanvasElement" && document.contains(thisArg) ? thisArg : scope;" have some planned use? postMessage does not appear to use the target for anything.
It uses it to retrieve the canvas element and create its placeholder, if needed (i.e. if the node is still attached to the document and it's not an OffscreenCanvas).

Re: proxifying modifyGetContext

Posted: Tue May 04, 2021 6:48 am
by skriptimaahinen
Should the target be passed to the fire() in postMessage()?

Also little patch for my patch:

"thisArg instanceof HTMLCanvasElement" test should be "thisArg instanceof window[canvas]" so it catches Offscreencanvas too.

Re: proxifying modifyGetContext

Posted: Tue May 04, 2021 7:05 am
by Giorgio Maone
skriptimaahinen wrote:
Tue May 04, 2021 6:48 am
Should the target be passed to the fire() in postMessage()
That's correct, thanks.

Re: proxifying modifyGetContext

Posted: Wed May 05, 2021 7:51 am
by skriptimaahinen

Code: Select all

-   const getContext = scope[canvas].prototype.getContext;
+   const CanvasClass = window[canvas];
+   const getContext = CanvasClass.prototype.getContext;
Unfortunately this change affects the "target" getContext for Proxy which needs to be from page context, otherwise there will be e.g. "Permission denied" error when trying to run getContext.call() on the page.

Though even if just the "target" getContext is from page context, using page script like:

HTMLCanvasElement.prototype.getContext()

causes a

TypeError: 'getContext' called on an object that does not implement interface HTMLCanvasElement. (line 23: return getContext.call...)

As it should. But since it's thrown by the getContext from the privileged context, it will cause a DOMException:InvalidStateError when propagating to the page.

Is there a compelling reason to use privileged getContext?