Bug 1481021: Part 2 - Stop loading SpecialPowers into frame script scopes. r?bz,jmaher,aswan draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 04 Aug 2018 12:36:34 -0700
changeset 827018 1fd314414cd44abca36f7c6085acc9105363b6df
parent 826719 e1defb5e242c9946e282195803b1c259aba9cd94
child 828377 57b3bf1ab55270eec274b7cac8e44bd0b8355484
push id118440
push usermaglione.k@gmail.com
push dateMon, 06 Aug 2018 18:25:25 +0000
reviewersbz, jmaher, aswan
bugs1481021
milestone63.0a1
Bug 1481021: Part 2 - Stop loading SpecialPowers into frame script scopes. r?bz,jmaher,aswan Loading SpecialPowers into frame scripts has side-effects, detailed in part 1, which are undesirable. The main side-effect that I'm trying to get rid of here is the force-enabling of permissive COWs in frame script scopes, which is blocking changes that I need to make elsewhere. But both that and the scope pollution it causes are likely to allow code to work when running in automation which fails in real world usage. This patch changes our special powers frame scripts to load specialpowers.js and specialpowersAPI.js as JSMs, which run in their own global, but define most of the same properties on our frame script globals. Most other callers still load those scripts via <script> tags or the subscript loader, and should ideally migrated in a follow-up. But even so, this patch still gives us a cleaner separation of the frame script and non-frame-script loading code. MozReview-Commit-ID: CR226gCDaGY
browser/base/content/test/performance/browser_startup_content.js
docshell/test/chrome/bug89419_window.xul
testing/mochitest/tests/SimpleTest/setup.js
testing/specialpowers/content/SpecialPowersObserver.jsm
testing/specialpowers/content/specialpowers.js
testing/specialpowers/content/specialpowersAPI.js
testing/specialpowers/content/specialpowersFrameScript.js
testing/specialpowers/moz.build
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -19,16 +19,18 @@ const kDumpAllStacks = false;
 
 const whitelist = {
   components: new Set([
     "ContentProcessSingleton.js",
     "extension-process-script.js",
   ]),
   modules: new Set([
     "chrome://mochikit/content/ShutdownLeaksCollector.jsm",
+    "resource://specialpowers/specialpowers.js",
+    "resource://specialpowers/specialpowersAPI.js",
 
     // General utilities
     "resource://gre/modules/AppConstants.jsm",
     "resource://gre/modules/AsyncShutdown.jsm",
     "resource://gre/modules/DeferredTask.jsm",
     "resource://gre/modules/PromiseUtils.jsm",
     "resource://gre/modules/Services.jsm", // bug 1464542
     "resource://gre/modules/Timer.jsm",
--- a/docshell/test/chrome/bug89419_window.xul
+++ b/docshell/test/chrome/bug89419_window.xul
@@ -5,18 +5,16 @@
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
         onload="setTimeout(nextTest,0);"
         title="bug 89419 test">
 
   <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
   <script type="text/javascript"
-          src="chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js"/>
-  <script type="text/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SpecialPowersObserverAPI.js"/>
   <script type="text/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/specialpowers.js"/>
   <script type="application/javascript" src="docshell_helpers.js" />
   <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
 
   <script type="application/javascript"><![CDATA[
     // Define the generator-iterator for the tests.
--- a/testing/mochitest/tests/SimpleTest/setup.js
+++ b/testing/mochitest/tests/SimpleTest/setup.js
@@ -100,17 +100,17 @@ if (params.repeat) {
 }
 
 if (params.runUntilFailure) {
   TestRunner.runUntilFailure = true;
 }
 
 // closeWhenDone tells us to close the browser when complete
 if (params.closeWhenDone) {
-  TestRunner.onComplete = SpecialPowers.quit;
+  TestRunner.onComplete = SpecialPowers.quit.bind(SpecialPowers);
 }
 
 if (params.failureFile) {
   TestRunner.setFailureFile(params.failureFile);
 }
 
 // Breaks execution and enters the JS debugger on a test failure
 if (params.debugOnFailure) {
--- a/testing/specialpowers/content/SpecialPowersObserver.jsm
+++ b/testing/specialpowers/content/SpecialPowersObserver.jsm
@@ -11,18 +11,17 @@
 
 /* import-globals-from SpecialPowersObserverAPI.js */
 
 var EXPORTED_SYMBOLS = ["SpecialPowersObserver"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 Cu.importGlobalProperties(["File"]);
 
-const CHILD_SCRIPT = "resource://specialpowers/specialpowers.js";
-const CHILD_SCRIPT_API = "resource://specialpowers/specialpowersAPI.js";
+const CHILD_SCRIPT_API = "resource://specialpowers/specialpowersFrameScript.js";
 const CHILD_LOGGER_SCRIPT = "resource://specialpowers/MozillaLogger.js";
 
 
 // Glue to add in the observer API to this object.  This allows us to share code with chrome tests
 Services.scriptloader.loadSubScript("resource://specialpowers/SpecialPowersObserverAPI.js");
 
 /* XPCOM gunk */
 function SpecialPowersObserver() {
@@ -77,17 +76,16 @@ SpecialPowersObserver.prototype._loadFra
     this._messageManager.addMessageListener("SPUnloadExtension", this);
     this._messageManager.addMessageListener("SPExtensionMessage", this);
     this._messageManager.addMessageListener("SPCleanUpSTSData", this);
     this._messageManager.addMessageListener("SPRequestDumpCoverageCounters", this);
     this._messageManager.addMessageListener("SPRequestResetCoverageCounters", this);
 
     this._messageManager.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
     this._messageManager.loadFrameScript(CHILD_SCRIPT_API, true);
-    this._messageManager.loadFrameScript(CHILD_SCRIPT, true);
     this._isFrameScriptLoaded = true;
     this._createdFiles = null;
   }
 };
 
 SpecialPowersObserver.prototype._sendAsyncMessage = function(msgname, msg) {
   this._messageManager.broadcastAsyncMessage(msgname, msg);
 };
@@ -144,17 +142,16 @@ SpecialPowersObserver.prototype.uninit =
     this._messageManager.removeMessageListener("SPUnloadExtension", this);
     this._messageManager.removeMessageListener("SPExtensionMessage", this);
     this._messageManager.removeMessageListener("SPCleanUpSTSData", this);
     this._messageManager.removeMessageListener("SPRequestDumpCoverageCounters", this);
     this._messageManager.removeMessageListener("SPRequestResetCoverageCounters", this);
 
     this._messageManager.removeDelayedFrameScript(CHILD_LOGGER_SCRIPT);
     this._messageManager.removeDelayedFrameScript(CHILD_SCRIPT_API);
-    this._messageManager.removeDelayedFrameScript(CHILD_SCRIPT);
     this._isFrameScriptLoaded = false;
   }
 };
 
 SpecialPowersObserver.prototype._addProcessCrashObservers = function() {
   if (this._processCrashObserversRegistered) {
     return;
   }
--- a/testing/specialpowers/content/specialpowers.js
+++ b/testing/specialpowers/content/specialpowers.js
@@ -1,21 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* This code is loaded in every child process that is started by mochitest in
  * order to be used as a replacement for UniversalXPConnect
  */
 
-/* import-globals-from specialpowersAPI.js */
-/* globals addMessageListener, removeMessageListener, sendSyncMessage, sendAsyncMessage */
+/* globals bindDOMWindowUtils, SpecialPowersAPI */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-function SpecialPowers(window) {
+Services.scriptloader.loadSubScript("resource://specialpowers/MozillaLogger.js", this);
+
+var EXPORTED_SYMBOLS = ["SpecialPowers", "attachSpecialPowersToWindow"];
+
+ChromeUtils.import("resource://specialpowers/specialpowersAPI.js", this);
+
+Cu.forcePermissiveCOWs();
+
+function SpecialPowers(window, mm) {
+  this.mm = mm;
+
   this.window = Cu.getWeakReference(window);
   this._windowID = window.windowUtils.currentInnerWindowID;
   this._encounteredCrashDumpFiles = [];
   this._unexpectedCrashDumpFiles = { };
   this._crashDumpDir = null;
   this.DOMWindowUtils = bindDOMWindowUtils(window);
   Object.defineProperty(this, "Components", {
       configurable: true, enumerable: true, value: this.getFullComponents()
@@ -42,28 +51,28 @@ function SpecialPowers(window) {
                             "SPPingService",
                             "SPLoadExtension",
                             "SPProcessCrashManagerWait",
                             "SPStartupExtension",
                             "SPUnloadExtension",
                             "SPExtensionMessage",
                             "SPRequestDumpCoverageCounters",
                             "SPRequestResetCoverageCounters"];
-  addMessageListener("SPPingService", this._messageListener);
-  addMessageListener("SpecialPowers.FilesCreated", this._messageListener);
-  addMessageListener("SpecialPowers.FilesError", this._messageListener);
+  mm.addMessageListener("SPPingService", this._messageListener);
+  mm.addMessageListener("SpecialPowers.FilesCreated", this._messageListener);
+  mm.addMessageListener("SpecialPowers.FilesError", this._messageListener);
   let self = this;
   Services.obs.addObserver(function onInnerWindowDestroyed(subject, topic, data) {
     var id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
     if (self._windowID === id) {
       Services.obs.removeObserver(onInnerWindowDestroyed, "inner-window-destroyed");
       try {
-        removeMessageListener("SPPingService", self._messageListener);
-        removeMessageListener("SpecialPowers.FilesCreated", self._messageListener);
-        removeMessageListener("SpecialPowers.FilesError", self._messageListener);
+        mm.removeMessageListener("SPPingService", self._messageListener);
+        mm.removeMessageListener("SpecialPowers.FilesCreated", self._messageListener);
+        mm.removeMessageListener("SpecialPowers.FilesError", self._messageListener);
       } catch (e) {
         // Ignore the exception which the message manager has been destroyed.
         if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE) {
           throw e;
         }
       }
     }
   }, "inner-window-destroyed");
@@ -78,43 +87,44 @@ SpecialPowers.prototype.sanityCheck = fu
 SpecialPowers.prototype.DOMWindowUtils = undefined;
 SpecialPowers.prototype.Components = undefined;
 SpecialPowers.prototype.IsInNestedFrame = false;
 
 SpecialPowers.prototype._sendSyncMessage = function(msgname, msg) {
   if (!this.SP_SYNC_MESSAGES.includes(msgname)) {
     dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
   }
-  return sendSyncMessage(msgname, msg);
+  let result = this.mm.sendSyncMessage(msgname, msg);
+  return Cu.cloneInto(result, this);
 };
 
 SpecialPowers.prototype._sendAsyncMessage = function(msgname, msg) {
   if (!this.SP_ASYNC_MESSAGES.includes(msgname)) {
     dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
   }
-  sendAsyncMessage(msgname, msg);
+  this.mm.sendAsyncMessage(msgname, msg);
 };
 
 SpecialPowers.prototype._addMessageListener = function(msgname, listener) {
-  addMessageListener(msgname, listener);
-  sendAsyncMessage("SPPAddNestedMessageListener", { name: msgname });
+  this.mm.addMessageListener(msgname, listener);
+  this.mm.sendAsyncMessage("SPPAddNestedMessageListener", { name: msgname });
 };
 
 SpecialPowers.prototype._removeMessageListener = function(msgname, listener) {
-  removeMessageListener(msgname, listener);
+  this.mm.removeMessageListener(msgname, listener);
 };
 
 SpecialPowers.prototype.registerProcessCrashObservers = function() {
-  addMessageListener("SPProcessCrashService", this._messageListener);
-  sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
+  this.mm.addMessageListener("SPProcessCrashService", this._messageListener);
+  this.mm.sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
 };
 
 SpecialPowers.prototype.unregisterProcessCrashObservers = function() {
-  removeMessageListener("SPProcessCrashService", this._messageListener);
-  sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
+  this.mm.removeMessageListener("SPProcessCrashService", this._messageListener);
+  this.mm.sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
 };
 
 SpecialPowers.prototype._messageReceived = function(aMessage) {
   switch (aMessage.name) {
     case "SPProcessCrashService":
       if (aMessage.json.type == "crash-observed") {
         for (let e of aMessage.json.dumpIDs) {
           this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
@@ -134,17 +144,17 @@ SpecialPowers.prototype._messageReceived
       }
       break;
 
     case "SpecialPowers.FilesCreated":
       var createdHandler = this._createFilesOnSuccess;
       this._createFilesOnSuccess = null;
       this._createFilesOnError = null;
       if (createdHandler) {
-        createdHandler(aMessage.data);
+        createdHandler(Cu.cloneInto(aMessage.data, this.mm.content));
       }
       break;
 
     case "SpecialPowers.FilesError":
       var errorHandler = this._createFilesOnError;
       this._createFilesOnSuccess = null;
       this._createFilesOnError = null;
       if (errorHandler) {
@@ -152,43 +162,43 @@ SpecialPowers.prototype._messageReceived
       }
       break;
   }
 
   return true;
 };
 
 SpecialPowers.prototype.quit = function() {
-  sendAsyncMessage("SpecialPowers.Quit", {});
+  this.mm.sendAsyncMessage("SpecialPowers.Quit", {});
 };
 
 // fileRequests is an array of file requests. Each file request is an object.
 // A request must have a field |name|, which gives the base of the name of the
 // file to be created in the profile directory. If the request has a |data| field
 // then that data will be written to the file.
 SpecialPowers.prototype.createFiles = function(fileRequests, onCreation, onError) {
   if (this._createFilesOnSuccess || this._createFilesOnError) {
     onError("Already waiting for SpecialPowers.createFiles() to finish.");
     return;
   }
 
   this._createFilesOnSuccess = onCreation;
   this._createFilesOnError = onError;
-  sendAsyncMessage("SpecialPowers.CreateFiles", fileRequests);
+  this.mm.sendAsyncMessage("SpecialPowers.CreateFiles", fileRequests);
 };
 
 // Remove the files that were created using |SpecialPowers.createFiles()|.
 // This will be automatically called by |SimpleTest.finish()|.
 SpecialPowers.prototype.removeFiles = function() {
-  sendAsyncMessage("SpecialPowers.RemoveFiles", {});
+  this.mm.sendAsyncMessage("SpecialPowers.RemoveFiles", {});
 };
 
 SpecialPowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) {
   this._pongHandlers.push(aCallback);
-  sendAsyncMessage("SPPingService", { op: "ping" });
+  this.mm.sendAsyncMessage("SPPingService", { op: "ping" });
 };
 
 SpecialPowers.prototype.nestedFrameSetup = function() {
   let self = this;
   Services.obs.addObserver(function onRemoteBrowserShown(subject, topic, data) {
     let frameLoader = subject;
     // get a ref to the app <iframe>
     let frame = frameLoader.ownerElement;
@@ -210,71 +220,50 @@ SpecialPowers.prototype.nestedFrameSetup
         });
       });
       mm.addMessageListener("SPPAddNestedMessageListener", function(msg) {
         self._addMessageListener(msg.json.name, function(aMsg) {
           mm.sendAsyncMessage(aMsg.name, aMsg.data);
           });
       });
 
-      mm.loadFrameScript("resource://specialpowers/MozillaLogger.js", false);
-      mm.loadFrameScript("resource://specialpowers/specialpowersAPI.js", false);
-      mm.loadFrameScript("resource://specialpowers/specialpowers.js", false);
+      mm.loadFrameScript("resource://specialpowers/specialpowersFrameScript.js", false);
 
       let frameScript = "SpecialPowers.prototype.IsInNestedFrame=true;";
       mm.loadFrameScript("data:," + frameScript, false);
     }
   }, "remote-browser-shown");
 };
 
 SpecialPowers.prototype.isServiceWorkerRegistered = function() {
   var swm = Cc["@mozilla.org/serviceworkers/manager;1"]
               .getService(Ci.nsIServiceWorkerManager);
   return swm.getAllRegistrations().length != 0;
 };
 
 // Attach our API to the window.
-function attachSpecialPowersToWindow(aWindow) {
+function attachSpecialPowersToWindow(aWindow, mm) {
   try {
     if ((aWindow !== null) &&
         (aWindow !== undefined) &&
         (aWindow.wrappedJSObject) &&
         !(aWindow.wrappedJSObject.SpecialPowers)) {
-      let sp = new SpecialPowers(aWindow);
+      let sp = new SpecialPowers(aWindow, mm);
       aWindow.wrappedJSObject.SpecialPowers = sp;
       if (sp.IsInNestedFrame) {
         sp.addPermission("allowXULXBL", true, aWindow.document);
       }
     }
   } catch (ex) {
     dump("TEST-INFO | specialpowers.js |  Failed to attach specialpowers to window exception: " + ex + "\n");
   }
 }
 
-// This is a frame script, so it may be running in a content process.
-// In any event, it is targeted at a specific "tab", so we listen for
-// the DOMWindowCreated event to be notified about content windows
-// being created in this context.
-
-function SpecialPowersManager() {
-  addEventListener("DOMWindowCreated", this, false);
-}
-
-SpecialPowersManager.prototype = {
-  handleEvent: function handleEvent(aEvent) {
-    var window = aEvent.target.defaultView;
-    attachSpecialPowersToWindow(window);
-  }
-};
-
-
-var specialpowersmanager = new SpecialPowersManager();
-
 this.SpecialPowers = SpecialPowers;
 this.attachSpecialPowersToWindow = attachSpecialPowersToWindow;
 
 // In the case of Chrome mochitests that inject specialpowers.js as
 // a regular content script
 if (typeof window != "undefined") {
   window.addMessageListener = function() {};
   window.removeMessageListener = function() {};
-  window.wrappedJSObject.SpecialPowers = new SpecialPowers(window);
+  window.wrappedJSObject.SpecialPowers = new SpecialPowers(window, window);
 }
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -3,23 +3,27 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* This code is loaded in every child process that is started by mochitest in
  * order to be used as a replacement for UniversalXPConnect
  */
 
 "use strict";
 
 /* import-globals-from MozillaLogger.js */
-/* globals XPCNativeWrapper, content */
+/* globals XPCNativeWrapper */
 
-var global = this;
+var EXPORTED_SYMBOLS = ["SpecialPowersAPI", "bindDOMWindowUtils"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+Services.scriptloader.loadSubScript("resource://specialpowers/MozillaLogger.js", this);
+
+ChromeUtils.defineModuleGetter(this, "setTimeout",
+                               "resource://gre/modules/Timer.jsm");
 ChromeUtils.defineModuleGetter(this, "MockFilePicker",
                                "resource://specialpowers/MockFilePicker.jsm");
 ChromeUtils.defineModuleGetter(this, "MockColorPicker",
                                "resource://specialpowers/MockColorPicker.jsm");
 ChromeUtils.defineModuleGetter(this, "MockPermissionPrompt",
                                "resource://specialpowers/MockPermissionPrompt.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -577,16 +581,19 @@ SpecialPowersAPI.prototype = {
         this._removeMessageListener("SPChromeScriptMessage", chromeScript);
         this._removeMessageListener("SPChromeScriptAssert", chromeScript);
       },
 
       receiveMessage: (aMessage) => {
         let messageId = aMessage.json.id;
         let name = aMessage.json.name;
         let message = aMessage.json.message;
+        if (this.mm) {
+          message = new StructuredCloneHolder(message).deserialize(this.mm.content);
+        }
         // Ignore message from other chrome script
         if (messageId != id)
           return;
 
         if (aMessage.name == "SPChromeScriptMessage") {
           listeners.filter(o => (o.name == name))
                    .forEach(o => o.listener(message));
         } else if (aMessage.name == "SPChromeScriptAssert") {
@@ -749,34 +756,34 @@ SpecialPowersAPI.prototype = {
   },
 
   _setTimeout(callback) {
     // for mochitest-browser
     if (typeof window != "undefined")
       setTimeout(callback, 0);
     // for mochitest-plain
     else
-      content.window.setTimeout(callback, 0);
+      this.mm.content.setTimeout(callback, 0);
   },
 
   _delayCallbackTwice(callback) {
-     function delayedCallback() {
-       function delayAgain(aCallback) {
+     let delayedCallback = () => {
+       let delayAgain = (aCallback) => {
          // Using this._setTimeout doesn't work here
          // It causes failures in mochtests that use
          // multiple pushPrefEnv calls
          // For chrome/browser-chrome mochitests
          if (typeof window != "undefined")
            setTimeout(aCallback, 0);
          // For mochitest-plain
          else
-           content.window.setTimeout(aCallback, 0);
-       }
-       delayAgain(delayAgain(callback));
-     }
+           this.mm.content.setTimeout(aCallback, 0);
+       };
+       delayAgain(delayAgain.bind(this, callback));
+     };
      return delayedCallback;
   },
 
   /* apply permissions to the system and when the test case is finished (SimpleTest.finish())
      we will revert the permission back to the original.
 
      inPermissions is an array of objects where each object has a type, action, context, ex:
      [{'type': 'SystemXHR', 'allow': 1, 'context': document},
@@ -1456,20 +1463,20 @@ SpecialPowersAPI.prototype = {
   isBackButtonEnabled(window) {
     return !this._getTopChromeWindow(window).document
                                       .getElementById("Browser:Back")
                                       .hasAttribute("disabled");
   },
   // XXX end of problematic APIs
 
   addChromeEventListener(type, listener, capture, allowUntrusted) {
-    addEventListener(type, listener, capture, allowUntrusted);
+    this.mm.addEventListener(type, listener, capture, allowUntrusted);
   },
   removeChromeEventListener(type, listener, capture) {
-    removeEventListener(type, listener, capture);
+    this.mm.removeEventListener(type, listener, capture);
   },
 
   // Note: each call to registerConsoleListener MUST be paired with a
   // call to postConsoleSentinel; when the callback receives the
   // sentinel it will unregister itself (_after_ calling the
   // callback).  SimpleTest.expectConsoleMessages does this for you.
   // If you register more than one console listener, a call to
   // postConsoleSentinel will zap all of them.
@@ -1747,17 +1754,17 @@ SpecialPowersAPI.prototype = {
     return Services.focus.focusedWindow;
   },
 
   focus(aWindow) {
     // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
     // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
     if (aWindow)
       aWindow.focus();
-    var mm = global;
+    var mm = this.mm;
     if (aWindow) {
       let windowMM = aWindow.docShell.messageManager;
       if (windowMM) {
         mm = windowMM;
       }
       /*
        * Otherwise (e.g. XUL chrome windows from mochitest-chrome which won't
        * have a message manager) just stick with "global".
@@ -1770,17 +1777,17 @@ SpecialPowersAPI.prototype = {
     if (whichClipboard === undefined)
       whichClipboard = Services.clipboard.kGlobalClipboard;
 
     var xferable = Cc["@mozilla.org/widget/transferable;1"].
                    createInstance(Ci.nsITransferable);
     // in e10s b-c tests |content.window| is a CPOW whereas |window| works fine.
     // for some non-e10s mochi tests, |window| is null whereas |content.window|
     // works fine.  So we take whatever is non-null!
-    xferable.init(this._getDocShell(typeof(window) == "undefined" ? content.window : window)
+    xferable.init(this._getDocShell(typeof(window) == "undefined" ? this.mm.content.window : window)
                       .QueryInterface(Ci.nsILoadContext));
     xferable.addDataFlavor(flavor);
     Services.clipboard.getData(xferable, whichClipboard);
     var data = {};
     try {
       xferable.getTransferData(flavor, data, {});
     } catch (e) {}
     data = data.value || null;
@@ -2090,17 +2097,17 @@ SpecialPowersAPI.prototype = {
         } else if (msg.data.type == "extensionFailed") {
           state = "failed";
           rejectStartup("startup failed");
         } else if (msg.data.type == "extensionUnloaded") {
           this._extensionListeners.delete(listener);
           state = "unloaded";
           resolveUnload();
         } else if (msg.data.type in handler) {
-          handler[msg.data.type](...msg.data.args);
+          handler[msg.data.type](...Cu.cloneInto(msg.data.args, this.mm.content));
         } else {
           dump(`Unexpected: ${msg.data.type}\n`);
         }
       }
     };
 
     this._extensionListeners.add(listener);
     return extension;
@@ -2111,17 +2118,17 @@ SpecialPowersAPI.prototype = {
   },
 
   allowMedia(window, enable) {
     this._getDocShell(window).allowMedia = enable;
   },
 
   createChromeCache(name, url) {
     let principal = this._getPrincipalFromArg(url);
-    return wrapIfUnwrapped(new content.window.CacheStorage(name, principal));
+    return wrapIfUnwrapped(new this.mm.content.CacheStorage(name, principal));
   },
 
   loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
     const BinaryInputStream =
         Components.Constructor("@mozilla.org/binaryinputstream;1",
                                "nsIBinaryInputStream",
                                "setInputStream");
 
new file mode 100644
--- /dev/null
+++ b/testing/specialpowers/content/specialpowersFrameScript.js
@@ -0,0 +1,26 @@
+"use strict";
+
+/* globals attachSpecialPowersToWindow */
+
+let mm = this;
+
+ChromeUtils.import("resource://specialpowers/specialpowersAPI.js", this);
+ChromeUtils.import("resource://specialpowers/specialpowers.js", this);
+
+// This is a frame script, so it may be running in a content process.
+// In any event, it is targeted at a specific "tab", so we listen for
+// the DOMWindowCreated event to be notified about content windows
+// being created in this context.
+
+function SpecialPowersManager() {
+  addEventListener("DOMWindowCreated", this, false);
+}
+
+SpecialPowersManager.prototype = {
+  handleEvent: function handleEvent(aEvent) {
+    var window = aEvent.target.defaultView;
+    attachSpecialPowersToWindow(window, mm);
+  }
+};
+
+var specialpowersmanager = new SpecialPowersManager();
--- a/testing/specialpowers/moz.build
+++ b/testing/specialpowers/moz.build
@@ -18,14 +18,15 @@ FINAL_TARGET_FILES += [
 FINAL_TARGET_FILES.content += [
     '../modules/Assert.jsm',
     'content/MockColorPicker.jsm',
     'content/MockFilePicker.jsm',
     'content/MockPermissionPrompt.jsm',
     'content/MozillaLogger.js',
     'content/specialpowers.js',
     'content/specialpowersAPI.js',
+    'content/specialpowersFrameScript.js',
     'content/SpecialPowersObserver.jsm',
     'content/SpecialPowersObserverAPI.js',
 ]
 
 with Files("**"):
     BUG_COMPONENT = ("Testing", "Mochitest")