Bug 1445551: Part 1a - Add uses-unsafe-cpows annotation to mochitest harness. r?mconley draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 13 Mar 2018 19:11:10 -0700
changeset 767654 60d643b3b46d51341fd5042cbf93dcd944c0acd5
parent 767653 4ea43e2a1cfe95e606cfe4ff358cd61fc4cb3596
child 767655 89d766a17dce0dc3d81decdd78f5e22778cbdcc0
push id102654
push usermaglione.k@gmail.com
push dateWed, 14 Mar 2018 21:41:22 +0000
reviewersmconley
bugs1445551
milestone61.0a1
Bug 1445551: Part 1a - Add uses-unsafe-cpows annotation to mochitest harness. r?mconley This allows us to specifically whitelist browser mochitests which still rely on unsafe CPOWs, and run them in a separate Sandbox global with permissive CPOWs enabled. The test harness and most of the in-tree tests will run with permissive CPOWs disabled, like the rest of the browser. MozReview-Commit-ID: CxIkuxr5PXJ
testing/mochitest/browser-harness.xul
testing/mochitest/browser-test.js
testing/mochitest/manifestLibrary.js
testing/mochitest/runtests.py
testing/mochitest/tests/SimpleTest/EventUtils.js
--- a/testing/mochitest/browser-harness.xul
+++ b/testing/mochitest/browser-harness.xul
@@ -105,16 +105,17 @@
         setTimeout(runTests, 0);
     }
 
     var gErrorCount = 0;
 
     function browserTest(aTestFile) {
       this.path = aTestFile['url'];
       this.expected = aTestFile['expected'];
+      this.usesUnsafeCPOWs = aTestFile['uses-unsafe-cpows'] || false;
       this.dumper = gDumper;
       this.results = [];
       this.scope = null;
       this.duration = 0;
       this.unexpectedTimeouts = 0;
       this.lastOutputTime = 0;
     }
     browserTest.prototype = {
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -374,16 +374,26 @@ function takeInstrumentation() {
 function Tester(aTests, structuredLogger, aCallback) {
   this.structuredLogger = structuredLogger;
   this.tests = aTests;
   this.callback = aCallback;
 
   this._scriptLoader = Services.scriptloader;
   this.EventUtils = {};
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.EventUtils);
+
+  // In order to allow existing tests to continue using unsafe CPOWs
+  // with EventUtils, we need to load a separate copy into a sandbox
+  // which has unsafe CPOW usage whitelisted.
+  this.cpowSandbox = Cu.Sandbox(window, {sandboxPrototype: window});
+  Cu.permitCPOWsInScope(this.cpowSandbox);
+
+  this.cpowEventUtils = new this.cpowSandbox.Object();
+  this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.cpowEventUtils);
+
   var simpleTestScope = {};
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SpecialPowersObserverAPI.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/MemoryStats.js", simpleTestScope);
   this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", simpleTestScope);
   this.SimpleTest = simpleTestScope.SimpleTest;
@@ -965,80 +975,80 @@ Tester.prototype = {
 
     this.SimpleTest.reset();
 
     // Load the tests into a testscope
     let currentScope = this.currentTest.scope = new testScope(this, this.currentTest, this.currentTest.expected);
     let currentTest = this.currentTest;
 
     // Import utils in the test scope.
-    this.currentTest.scope.EventUtils = this.EventUtils;
-    this.currentTest.scope.SimpleTest = this.SimpleTest;
-    this.currentTest.scope.gTestPath = this.currentTest.path;
-    this.currentTest.scope.Task = this.Task;
-    this.currentTest.scope.ContentTask = this.ContentTask;
-    this.currentTest.scope.BrowserTestUtils = this.BrowserTestUtils;
-    this.currentTest.scope.TestUtils = this.TestUtils;
-    this.currentTest.scope.ExtensionTestUtils = this.ExtensionTestUtils;
+    let {scope} = this.currentTest;
+    scope.EventUtils = this.currentTest.usesUnsafeCPOWs ? this.cpowEventUtils : this.EventUtils;
+    scope.SimpleTest = this.SimpleTest;
+    scope.gTestPath = this.currentTest.path;
+    scope.Task = this.Task;
+    scope.ContentTask = this.ContentTask;
+    scope.BrowserTestUtils = this.BrowserTestUtils;
+    scope.TestUtils = this.TestUtils;
+    scope.ExtensionTestUtils = this.ExtensionTestUtils;
     // Pass a custom report function for mochitest style reporting.
-    this.currentTest.scope.Assert = new this.Assert(function(err, message, stack) {
+    scope.Assert = new this.Assert(function(err, message, stack) {
       currentTest.addResult(new testResult(err ? {
         name: err.message,
         ex: err.stack,
         stack: err.stack,
         allowFailure: currentTest.allowFailure,
       } : {
         name: message,
         pass: true,
         stack,
         allowFailure: currentTest.allowFailure,
       }));
     }, true);
 
     this.ContentTask.setTestScope(currentScope);
 
     // Allow Assert.jsm methods to be tacked to the current scope.
-    this.currentTest.scope.export_assertions = function() {
+    scope.export_assertions = function() {
       for (let func in this.Assert) {
         this[func] = this.Assert[func].bind(this.Assert);
       }
     };
 
     // Override SimpleTest methods with ours.
     SIMPLETEST_OVERRIDES.forEach(function(m) {
       this.SimpleTest[m] = this[m];
-    }, this.currentTest.scope);
+    }, scope);
 
     //load the tools to work with chrome .jar and remote
     try {
-      this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", this.currentTest.scope);
+      this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", scope);
     } catch (ex) { /* no chrome-harness tools */ }
 
     // Import head.js script if it exists.
     var currentTestDirPath =
       this.currentTest.path.substr(0, this.currentTest.path.lastIndexOf("/"));
     var headPath = currentTestDirPath + "/head.js";
     try {
-      this._scriptLoader.loadSubScript(headPath, this.currentTest.scope);
+      this._scriptLoader.loadSubScript(headPath, scope);
     } catch (ex) {
       // Ignore if no head.js exists, but report all other errors.  Note this
       // will also ignore an existing head.js attempting to import a missing
       // module - see bug 755558 for why this strategy is preferred anyway.
       if (!/^Error opening input stream/.test(ex.toString())) {
        this.currentTest.addResult(new testResult({
          name: "head.js import threw an exception",
          ex,
        }));
       }
     }
 
     // Import the test script.
     try {
-      this._scriptLoader.loadSubScript(this.currentTest.path,
-                                       this.currentTest.scope);
+      this._scriptLoader.loadSubScript(this.currentTest.path, scope);
       // Run the test
       this.lastStartTime = Date.now();
       if (this.currentTest.scope.__tasks) {
         // This test consists of tasks, added via the `add_task()` API.
         if ("test" in this.currentTest.scope) {
           throw "Cannot run both a add_task test and a normal test at the same time.";
         }
         let Promise = this.Promise;
@@ -1086,18 +1096,18 @@ Tester.prototype = {
                 }));
               }
             }
             PromiseTestUtils.assertNoUncaughtRejections();
             this.SimpleTest.info("Leaving test " + task.name);
           }
           this.finish();
         }.bind(currentScope));
-      } else if (typeof this.currentTest.scope.test == "function") {
-        this.currentTest.scope.test();
+      } else if (typeof scope.test == "function") {
+        scope.test();
       } else {
         throw "This test didn't call add_task, nor did it define a generatorTest() function, nor did it define a test() function, so we don't know how to run it.";
       }
     } catch (ex) {
       if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) {
         this.currentTest.addResult(new testResult({
           name: "Exception thrown",
           pass: this.SimpleTest.isExpectingUncaughtException(),
@@ -1367,16 +1377,28 @@ function testScope(aTester, aTest, expec
   };
 
   this.requestCompleteLog = function test_requestCompleteLog() {
     self.__tester.structuredLogger.deactivateBuffering();
     self.registerCleanupFunction(function() {
       self.__tester.structuredLogger.activateBuffering();
     })
   };
+
+  // If we're running a test that requires unsafe CPOWs, create a
+  // separate sandbox scope, with CPOWS whitelisted, for that test, and
+  // mirror all of our properties onto it. Test files will be loaded
+  // into this sandbox.
+  //
+  // Otherwise, load test files directly into the testScope instance.
+  if (aTest.usesUnsafeCPOWs) {
+    let sandbox = this._createSandbox();
+    Cu.permitCPOWsInScope(sandbox);
+    return sandbox;
+  }
 }
 
 function decorateTaskFn(fn) {
   fn = fn.bind(this);
   fn.skip = () => fn.__skipMe = true;
   fn.only = () => this.__runOnlyThisTask = fn;
   return fn;
 }
@@ -1395,16 +1417,39 @@ testScope.prototype = {
   SimpleTest: {},
   Task: null,
   ContentTask: null,
   BrowserTestUtils: null,
   TestUtils: null,
   ExtensionTestUtils: null,
   Assert: null,
 
+  _createSandbox() {
+    let sandbox = Cu.Sandbox(window, {sandboxPrototype: window});
+
+    for (let prop in this) {
+      if (typeof this[prop] == "function") {
+        sandbox[prop] = this[prop].bind(this);
+      } else {
+        Object.defineProperty(sandbox, prop, {
+          configurable: true,
+          enumerable: true,
+          get: () => {
+            return this[prop];
+          },
+          set: (value) => {
+            this[prop] = value;
+          }
+        });
+      }
+    }
+
+    return sandbox;
+  },
+
   /**
    * Add a test function which is a Task function.
    *
    * Task functions are functions fed into Task.jsm's Task.spawn(). They are
    * generators that emit promises.
    *
    * If an exception is thrown, an assertion fails, or if a rejected
    * promise is yielded, the test function aborts immediately and the test is
--- a/testing/mochitest/manifestLibrary.js
+++ b/testing/mochitest/manifestLibrary.js
@@ -21,20 +21,20 @@ function parseTestManifest(testManifest,
     var path = obj['path'];
     // Note that obj.disabled may be "". We still want to skip in that case.
     if ("disabled" in obj) {
       dump("TEST-SKIPPED | " + path + " | " + obj.disabled + "\n");
       continue;
     }
     if (params.testRoot != 'tests' && params.testRoot !== undefined) {
       name = params.baseurl + '/' + params.testRoot + '/' + path;
-      links[name] = {'test': {'url': name, 'expected': obj['expected']}};
+      links[name] = {'test': {'url': name, 'expected': obj['expected'], 'uses-unsafe-cpows': obj['uses-unsafe-cpows']}};
     } else {
       name = params.testPrefix + path;
-      paths.push({'test': {'url': name, 'expected': obj['expected']}});
+      paths.push({'test': {'url': name, 'expected': obj['expected'], 'uses-unsafe-cpows': obj['uses-unsafe-cpows']}});
     }
   }
   if (paths.length > 0) {
     callback(paths);
   } else {
     callback(links);
   }
 }
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -1519,16 +1519,18 @@ toolbar#nav-bar {
                                "set the `prefs` key".format(manifest_relpath))
                 sys.exit(1)
 
             testob = {'path': tp, 'manifest': manifest_relpath}
             if 'disabled' in test:
                 testob['disabled'] = test['disabled']
             if 'expected' in test:
                 testob['expected'] = test['expected']
+            if 'uses-unsafe-cpows' in test:
+                testob['uses-unsafe-cpows'] = test['uses-unsafe-cpows'] == 'true'
             if 'scheme' in test:
                 testob['scheme'] = test['scheme']
             if options.failure_pattern_file:
                 pat_file = os.path.join(os.path.dirname(test['manifest']),
                                         options.failure_pattern_file)
                 patterns = self.getFailurePatterns(pat_file, test['name'])
                 if patterns:
                     testob['expected'] = patterns
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -171,16 +171,20 @@ function sendMouseEvent(aEvent, aTarget,
   var buttonArg        = computeButton(aEvent);
   var relatedTargetArg = aEvent.relatedTarget || null;
 
   event.initMouseEvent(typeArg, canBubbleArg, cancelableArg, viewArg, detailArg,
                        screenXArg, screenYArg, clientXArg, clientYArg,
                        ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
                        buttonArg, relatedTargetArg);
 
+  // If documentURIObject exists or `window` is a stub object, we're in
+  // a chrome scope, so don't bother trying to go through SpecialPowers.
+  if (!window.document || window.document.documentURIObject)
+    return aTarget.dispatchEvent(event);
   return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
 }
 
 function isHidden(aElement) {
   var box = aElement.getBoundingClientRect();
   return box.width == 0 && box.height == 0;
 }
 
@@ -1235,16 +1239,24 @@ function disableNonTestMouseEvents(aDisa
 function _getDOMWindowUtils(aWindow = window)
 {
   // Leave this here as something, somewhere, passes a falsy argument
   // to this, causing the |window| default argument not to get picked up.
   if (!aWindow) {
     aWindow = window;
   }
 
+  // If documentURIObject exists or `window` is a stub object, we're in
+  // a chrome scope, so don't bother trying to go through SpecialPowers.
+  if (!window.document || window.document.documentURIObject) {
+    return aWindow
+        .QueryInterface(_EU_Ci.nsIInterfaceRequestor)
+        .getInterface(_EU_Ci.nsIDOMWindowUtils);
+  }
+
   // we need parent.SpecialPowers for:
   //  layout/base/tests/test_reftests_with_caret.html
   //  chrome: toolkit/content/tests/chrome/test_findbar.xul
   //  chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
   if ("SpecialPowers" in window && window.SpecialPowers != undefined) {
     return SpecialPowers.getDOMWindowUtils(aWindow);
   }
   if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) {