Limitations of chrome scripts

This page describes patterns that used to work in the chrome process that will no longer work in multiprocess Firefox. These are the sorts of things that will break an old add-on in multiprocess Firefox. The fix is generally some variant of "do that in a frame script loaded into the content process".

This is one of a pair of articles: the other one lists limitations of frame scripts.

Compatibility shims

For many of the patterns described here we've implemented compatibility shims so the patterns still work. For example: whenever extensions try to access web content from the chrome process, the browser will return a Cross Process Object Wrapper that gives the chrome code synchronous access to the content.

You'll get the shims for your add-on by default, unless you set the multiprocessCompatible flag in your add-on's install manifest.

However, these shims are not a substitute for migrating extensions:

  • they are only a temporary measure, and will be removed eventually
  • they can have a bad effect on responsiveness
  • there are likely to be edge cases in which they don't work properly

You can see all the places where your add-on uses compatibility shims by setting the dom.ipc.shims.enabledWarnings preference and watching the browser console as you use the add-on.

The rest of this page documents patterns that don't work in multiprocess Firefox as well as how are shims try to paper over the problem. For each pattern we've noted:

  • whether a shim exists and what kind of behavior it provides
  • how to update your add-on so you don't need the shim

gBrowser.contentWindow, window.content...

Without the shim

All APIs in the chrome process that provide direct access to content objects will no longer work. For example:

// chrome code

gBrowser.contentWindow;                    // null

gBrowser.contentDocument;                  // null

gBrowser.selectedBrowser.contentWindow;    // null

window.content;                            // null

content;                                   // null

As a special note, docshells live in the content process, so they are also inaccessible:

gBrowser.docShell;                         // null

gBrowser.selectedBrowser.docShell;         // null

With the shim

The shim will give you a CPOW for the content object in these situations.

In some situations, the content process may not be initialized when an add-on asks for access to its content. In this case, the shim will return a JavaScript object that looks somewhat like a window or a document for about:blank. However, this "dummy" object is completely static and only exposes a few of the normal properties that windows and documents have. For this reason, add-ons that try to access content objects in fresh <browser> elements may run into trouble.

To make the shim unnecessary: factor the code that needs to access content into a separate script, load that script into the content process as a frame script, and communicate between the chrome script and the frame script using the message-passing APIs. See the article on using the message manager.

Limitations of CPOWs

Cross process object wrappers (CPOWs) are a migration aid giving chrome code synchronous access to content objects. However, there are various limitations on the kinds of things you can do with a CPOW.

Note that from Firefox 47 onwards, unsafe CPOW usage is no longer permitted in browser code. If browser code tries an unsafe CPOW operation, the browser will throw an exception and you'll see an "unsafe CPOW usage forbidden” error in the Browser Console.

Add-on code is still allowed to use CPOWs "unsafely". However, if an add-on passes a CPOW into a platform API, and that platform API then attempts an unsafe operation on it, this will throw an exception. So as a general rule, add-ons should avoid passing CPOWs into non-addon code.

nsIContentPolicy

Without the shim

In multiprocess Firefox, if you register the nsIContentPolicy in the chrome process then it will never see any attempts to load web content, because they happen in the content process.

With the shim

The shim enables you to add content policies in the chrome process. It transparently registers an nsIContentPolicy in the content process, whose shouldLoad just forwards to the chrome process. The content to check is forwarded as a CPOW. The chrome process then checks the content against the policy supplied by the add-on, and forwards the response back to the child to be enforced.

To make the shim unnecessary: define and register nsIContentPolicy in the content process. If you need to ensure that the policy is only registered once, use a process script to register the policy.

nsIWebProgressListener

This API will work in the chrome process. There is a shim that gives you access to the DOMWindow property of the nsIWebProgress object passed into onStateChange. However, the DOMWindow is passed asynchronously, so by the time the chrome process receives it, the DOM might have changed (for example, because code running in the content process modified it or we navigated to a different page). We're working on fixing this issue in bug 1118880.

Also note that unlike other shims, this shim is always active.

Alternatively, you can use nsIWebProgressListener in the content process.

Observers in the chrome process

Depending on the topic, you need to register observers in either the chrome process or in a frame script.

For most topics you need to register observers in the chrome process.

However, you must listen to content-document-global-created and document-element-inserted in a frame script. Observers for these topics get content objects as the aSubject argument to observe(), so notifications are not sent to the chrome process.

There is a shim that will forward these two topics to the chrome process, sending CPOWs as the aSubject argument.

HTTP requests

You can't observe HTTP requests in the content process. If you do, you'll get an error.

If you do so in the chrome process, it will mostly work. The subject for the observer notification will be an nsIHttpChannel as you would expect.

A common pattern here is to use the notificationCallbacks property of the nsIHttpChannel to get the DOM window that initiated the load, like this:

observe: function (subject, topic, data) {
  if (topic == "http-on-modify-request") {
    var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
    var domWindow = httpChannel.notificationCallbacks.getInterface(Ci.nsIDOMWindow);
  }
}

Or this:

observe: function (subject, topic, data) {
  if (topic == "http-on-modify-request") {
    var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
    var domWindow = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
  }
}

In multiprocess Firefox these patterns will no longer work: the getInterface call will fail.

In multiprocess Firefox, notificationCallbacks is a special object that tries to emulate the single-process notificationsCallbacks object as best it can. It will return a dummy nsILoadContext when asked, but any attempt to get a window out of it will fail.

There is an outstanding bug (bug 1108827) to implement a shim here that will make notificationCallbacks a CPOW for the objects in the content process.

The correct way to access the DOM window is through a message manager. In an HTTP observer, you can get the browser message manager for the window using code like this:

observe: function (subject, topic, data) {
  if (topic == "http-on-modify-request") {
    var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
    var loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
    // topFrameElement is the <browser> element
    var topFrameElement = loadContext.topFrameElement;
    var browserMM = topFrameElement.messageManager;
    console.log("browserMM: " + browserMM);
  }
}

However, before Firefox 38, this technique will not work if multiprocess Firefox is disabled: specifically, topFrameElement will be null. This means that if you need to write code that works before Firefox 38 and on both multiprocess and non-multiprocess variants, you need to implement both paths:

  • test whether topFrameElement is null
  • if it is, you're running in single-process Firefox, and should use the old way
  • if it isn't, you're running in multiprocess Firefox and should use the new way

From Firefox 38 onwards, the topFrameElement approach always works.

DOM Events

Without the shim

In multiprocess Firefox, if you want to register an event listener on some content DOM node, that needs to happen in the content process.

It used to be that if you registered a listener on the XUL <browser> or <tab> element that hosted some DOM content, then events in the content would bubble up to the XUL and you could handle them there. This no longer happens in multiprocess Firefox.

With the shim

The shim intercepts chrome process code that adds listeners to XUL elements and sets up listeners in the content process, relaying the result back to the chrome process. The Event object itself is relayed to the chrome process as a CPOW.

To make the shim unnecessary: register event listeners on the global object inside a frame script. For example:

addEventListener("load", handler, true) // for example
If you need to contact the chrome process when that happens, send it a message.

Sandboxes

You can create sandboxes in the chrome or the content process. Sandboxes are often used as a safe way to manipulate web content, and if that's your goal, create the sandbox in the content process.
There is a shim for sandboxes: if you make a sandbox in the chrome process and give it content principals (by passing a CPOW as the first argument to Components.utils.Sandbox) then we'll actually make it in the content process.

nsIAboutModule

By default, custom about: pages registered using nsIAboutModule are loaded in the chrome process. This means that you can't access their content from the content process (via XHR, for example).

You can change this default in the code you use to register the about: URI. See about: and chrome: URIs.

JavaScript code modules (JSMs)

In single-process Firefox, you can use JavaScript code modules (JSMs) to maintain global state. In multiprocess Firefox, a JSM loaded into one process does not share any state with the same JSM loaded into a different process: so you can't use a JSM to share state between the chrome and content processes.
If an add-on wants to use a JSM to share state in this way, it's best to load the JSM in the chrome process, and have frame scripts store and access the JSM's state by sending messages to the chrome process using the message manager.

nsIProtocolHandler

In multiprocess Firefox, code which registers a custom protocol using nsIProtocolHandler must register the custom protocol in the child content process.
If the custom protocol is registered only in the chrome process, no iframes or tabs will be able to load any resource from the custom procotol.
The solution is to register the custom protocol in the content process. You can do this by loading a frame script or, with the Add-on SDK, you can use remoteRequire() in the "remote/parent" module.