Bug 1149618 - Add a sandbox parameter to execute, r=dburns
authorJonathan Griffin <jgriffin@mozilla.com>
Thu, 23 Apr 2015 13:39:38 -0700
changeset 273678 82a9bf38481bcf9492e14abc7c1dec2c614ec776
parent 273677 73ec15af7075580103103974c8c4f495e83ff4d5
child 273679 b491b0ab6c2ebc5a16f76e81f34543762cd92cad
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdburns
bugs1149618
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1149618 - Add a sandbox parameter to execute, r=dburns
testing/marionette/client/marionette/tests/unit/test_execute_sandboxes.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/driver.js
testing/marionette/driver/marionette_driver/marionette.py
testing/marionette/listener.js
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_sandboxes.py
@@ -0,0 +1,72 @@
+# 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/.
+
+from marionette import MarionetteTestCase
+from marionette_driver.errors import JavascriptException
+
+
+class TestExecuteSandboxes(MarionetteTestCase):
+    def setUp(self):
+        super(TestExecuteSandboxes, self).setUp()
+
+    def test_execute_system_sandbox(self):
+        # Test that 'system' sandbox has elevated privileges in execute_script
+        result = self.marionette.execute_script("""
+            return Components.interfaces.nsIPermissionManager.ALLOW_ACTION;
+            """, sandbox='system')
+        self.assertEqual(result, 1)
+
+    def test_execute_async_system_sandbox(self):
+        # Test that 'system' sandbox has elevated privileges in
+        # execute_async_script.
+        result = self.marionette.execute_async_script("""
+            let result = Components.interfaces.nsIPermissionManager.ALLOW_ACTION;
+            marionetteScriptFinished(result);
+            """, sandbox='system')
+        self.assertEqual(result, 1)
+
+    def test_execute_switch_sandboxes(self):
+        # Test that sandboxes are retained when switching between them
+        # for execute_script.
+        self.marionette.execute_script("foo = 1;", sandbox='1')
+        self.marionette.execute_script("foo = 2;", sandbox='2')
+        foo = self.marionette.execute_script("return foo;", sandbox='1',
+                                             new_sandbox=False)
+        self.assertEqual(foo, 1)
+        foo = self.marionette.execute_script("return foo;", sandbox='2',
+                                             new_sandbox=False)
+        self.assertEqual(foo, 2)
+
+    def test_execute_new_sandbox(self):
+        # Test that clearing a sandbox does not affect other sandboxes
+        self.marionette.execute_script("foo = 1;", sandbox='1')
+        self.marionette.execute_script("foo = 2;", sandbox='2')
+        self.assertRaises(JavascriptException,
+                          self.marionette.execute_script,
+                          "return foo;", sandbox='1', new_sandbox=True)
+        foo = self.marionette.execute_script("return foo;", sandbox='2',
+                                             new_sandbox=False)
+        self.assertEqual(foo, 2)
+
+    def test_execute_async_switch_sandboxes(self):
+        # Test that sandboxes are retained when switching between them
+        # for execute_async_script.
+        self.marionette.execute_async_script("foo = 1; marionetteScriptFinished()",
+                                             sandbox='1')
+        self.marionette.execute_async_script("foo = 2; marionetteScriptFinished()",
+                                             sandbox='2')
+        foo = self.marionette.execute_async_script("marionetteScriptFinished(foo);",
+                                                   sandbox='1',
+                                                   new_sandbox=False)
+        self.assertEqual(foo, 1)
+        foo = self.marionette.execute_async_script("marionetteScriptFinished(foo);",
+                                                   sandbox='2',
+                                                   new_sandbox=False)
+        self.assertEqual(foo, 2)
+
+
+class TestExecuteSandboxesChrome(TestExecuteSandboxes):
+    def setUp(self):
+        super(TestExecuteSandboxesChrome, self).setUp()
+        self.marionette.set_context("chrome")
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -151,8 +151,10 @@ b2g = false
 [test_key_actions.py]
 [test_mouse_action.py]
 b2g = false
 [test_teardown_context_preserved.py]
 b2g = false
 [test_file_upload.py]
 b2g = false
 skip-if = os == "win" # http://bugs.python.org/issue14574
+
+[test_execute_sandboxes.py]
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -126,17 +126,17 @@ this.GeckoDriver = function(appName, dev
   this.importedScripts = FileUtils.getFile("TmpD", ["marionetteChromeScripts"]);
   this.importedScriptHashes = {};
   this.importedScriptHashes[Context.CONTENT] = [];
   this.importedScriptHashes[Context.CHROME] = [];
   this.currentFrameElement = null;
   this.testName = null;
   this.mozBrowserClose = null;
   this.enabled_security_pref = false;
-  this.sandbox = null;
+  this.sandboxes = {};
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
   this.actions = new ActionChain(utils);
 
   this.sessionCapabilities = {
     // Mandated capabilities
@@ -710,21 +710,27 @@ GeckoDriver.prototype.getContext = funct
  * @param {Object} args
  *     Arguments given by client.
  * @param {boolean} sp
  *     True to enable special powers in the sandbox, false not to.
  *
  * @return {nsIXPCComponents_utils_Sandbox}
  *     Returns the sandbox.
  */
-GeckoDriver.prototype.createExecuteSandbox = function(win, mn, sp) {
-  let sb = new Cu.Sandbox(win,
+GeckoDriver.prototype.createExecuteSandbox = function(win, mn, sp, sandboxName) {
+  let principal = win;
+  if (sandboxName == 'system') {
+    principal = Cc["@mozilla.org/systemprincipal;1"].
+                createInstance(Ci.nsIPrincipal);
+  }
+  let sb = new Cu.Sandbox(principal,
       {sandboxPrototype: win, wantXrays: false, sandboxName: ""});
   sb.global = sb;
   sb.testUtils = utils;
+  sb.proto = win;
 
   mn.exports.forEach(function(fn) {
     if (typeof mn[fn] === 'function') {
       sb[fn] = mn[fn].bind(mn);
     } else {
       sb[fn] = mn[fn];
     }
   });
@@ -735,17 +741,17 @@ GeckoDriver.prototype.createExecuteSandb
     let pow = [
       "chrome://specialpowers/content/specialpowersAPI.js",
       "chrome://specialpowers/content/SpecialPowersObserverAPI.js",
       "chrome://specialpowers/content/ChromePowers.js",
     ];
     pow.map(s => loader.loadSubScript(s, sb));
   }
 
-  return sb;
+  this.sandboxes[sandboxName] = sb;
 };
 
 /**
  * Apply arguments sent from the client to the current (possibly reused)
  * execution sandbox.
  */
 GeckoDriver.prototype.applyArgumentsToSandbox = function(win, sb, args) {
   sb.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args, win);
@@ -815,33 +821,35 @@ GeckoDriver.prototype.execute = function
   let {inactivityTimeout,
        scriptTimeout,
        script,
        newSandbox,
        args,
        specialPowers,
        filename,
        line} = cmd.parameters;
+  let sandboxName = cmd.parameters.sandbox || 'default';
 
   if (!scriptTimeout) {
     scriptTimeout = this.scriptTimeout;
   }
   if (typeof newSandbox == "undefined") {
     newSandbox = true;
   }
 
   if (this.context == Context.CONTENT) {
     resp.value = yield this.listener.executeScript({
       script: script,
       args: args,
       newSandbox: newSandbox,
       timeout: scriptTimeout,
       specialPowers: specialPowers,
       filename: filename,
-      line: line
+      line: line,
+      sandboxName: sandboxName
     });
     return;
   }
 
   // handle the inactivity timeout
   let that = this;
   if (inactivityTimeout) {
     let setTimer = function() {
@@ -855,49 +863,52 @@ GeckoDriver.prototype.execute = function
     setTimer();
     this.heartbeatCallback = function() {
       that.inactivityTimer.cancel();
       setTimer();
     };
   }
 
   let win = this.getCurrentWindow();
-  if (!this.sandbox || newSandbox) {
+  if (newSandbox ||
+      !(sandboxName in this.sandboxes) ||
+      (this.sandboxes[sandboxName].proto != win)) {
     let marionette = new Marionette(
         this,
         win,
         "chrome",
         this.marionetteLog,
         scriptTimeout,
         this.heartbeatCallback,
         this.testName);
-    this.sandbox = this.createExecuteSandbox(
+    this.createExecuteSandbox(
         win,
         marionette,
-        specialPowers);
-    if (!this.sandbox) {
+        specialPowers,
+        sandboxName);
+    if (!this.sandboxes[sandboxName]) {
       return;
     }
   }
-  this.applyArgumentsToSandbox(win, this.sandbox, args);
+  this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
 
   try {
-    this.sandbox.finish = () => {
+    this.sandboxes[sandboxName].finish = () => {
       if (this.inactivityTimer !== null) {
         this.inactivityTimer.cancel();
       }
-      return this.sandbox.generate_results();
+      return this.sandboxes[sandboxName].generate_results();
     };
 
     if (!directInject) {
       script = "let func = function() { " + script + " }; func.apply(null, __marionetteParams);";
     }
     this.executeScriptInSandbox(
         resp,
-        this.sandbox,
+        this.sandboxes[sandboxName],
         script,
         directInject,
         false /* async */,
         scriptTimeout);
   } catch (e) {
     throw new JavaScriptError(e, "execute_script", filename, line, script);
   }
 };
@@ -946,16 +957,17 @@ GeckoDriver.prototype.executeJSScript = 
         newSandbox: cmd.parameters.newSandbox,
         async: cmd.parameters.async,
         timeout: cmd.parameters.scriptTimeout ?
             cmd.parameters.scriptTimeout : this.scriptTimeout,
         inactivityTimeout: cmd.parameters.inactivityTimeout,
         specialPowers: cmd.parameters.specialPowers,
         filename: cmd.parameters.filename,
         line: cmd.parameters.line,
+        sandboxName: cmd.parameters.sandbox || 'default',
       });
       break;
  }
 };
 
 /**
  * This function is used by executeAsync and executeJSScript to execute
  * a script in a sandbox.
@@ -975,16 +987,17 @@ GeckoDriver.prototype.executeWithCallbac
   let {script,
       args,
       newSandbox,
       inactivityTimeout,
       scriptTimeout,
       specialPowers,
       filename,
       line} = cmd.parameters;
+  let sandboxName = cmd.parameters.sandbox || 'default';
 
   if (!scriptTimeout) {
     scriptTimeout = this.scriptTimeout;
   }
   if (typeof newSandbox == "undefined") {
     newSandbox = true;
   }
 
@@ -993,17 +1006,18 @@ GeckoDriver.prototype.executeWithCallbac
       script: script,
       args: args,
       id: cmd.id,
       newSandbox: newSandbox,
       timeout: scriptTimeout,
       inactivityTimeout: inactivityTimeout,
       specialPowers: specialPowers,
       filename: filename,
-      line: line
+      line: line,
+      sandboxName: sandboxName,
     });
     return;
   }
 
   // handle the inactivity timeout
   let that = this;
   if (inactivityTimeout) {
     this.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@@ -1029,17 +1043,17 @@ GeckoDriver.prototype.executeWithCallbac
 
   let res = yield new Promise(function(resolve, reject) {
     let chromeAsyncReturnFunc = function(val) {
       if (that.emulator.cbs.length > 0) {
         that.emulator.cbs = [];
         throw new WebDriverError("Emulator callback still pending when finish() called");
       }
 
-      if (cmd.id == that.sandbox.command_id) {
+      if (cmd.id == that.sandboxes[sandboxName].command_id) {
         if (that.timer !== null) {
           that.timer.cancel();
           that.timer = null;
         }
 
         win.onerror = origOnError;
 
         if (error.isError(val)) {
@@ -1050,55 +1064,57 @@ GeckoDriver.prototype.executeWithCallbac
       }
 
       if (that.inactivityTimer !== null) {
         that.inactivityTimer.cancel();
       }
     };
 
     let chromeAsyncFinish = function() {
-      let res = that.sandbox.generate_results();
+      let res = that.sandboxes[sandboxName].generate_results();
       chromeAsyncReturnFunc(res);
     };
 
     let chromeAsyncError = function(e, func, file, line, script) {
       let err = new JavaScriptError(e, func, file, line, script);
       chromeAsyncReturnFunc(err);
     };
 
-    if (!this.sandbox || newSandbox) {
+    if (newSandbox || !(sandboxName in this.sandboxes)) {
       let marionette = new Marionette(
           this,
           win,
           "chrome",
           this.marionetteLog,
           scriptTimeout,
           this.heartbeatCallback,
           this.testName);
-      this.sandbox = this.createExecuteSandbox(win, marionette, specialPowers);
-      if (!this.sandbox) {
-        return;
-      }
+      this.createExecuteSandbox(win, marionette,
+                                specialPowers, sandboxName);
     }
-    this.sandbox.command_id = cmd.id;
-    this.sandbox.runEmulatorCmd = (cmd, cb) => {
+    if (!this.sandboxes[sandboxName]) {
+      return;
+    }
+
+    this.sandboxes[sandboxName].command_id = cmd.id;
+    this.sandboxes[sandboxName].runEmulatorCmd = (cmd, cb) => {
       let ecb = new EmulatorCallback();
       ecb.onresult = cb;
       ecb.onerror = chromeAsyncError;
       this.emulator.pushCallback(ecb);
       this.emulator.send({emulator_cmd: cmd, id: ecb.id});
     };
-    this.sandbox.runEmulatorShell = (args, cb) => {
+    this.sandboxes[sandboxName].runEmulatorShell = (args, cb) => {
       let ecb = new EmulatorCallback();
       ecb.onresult = cb;
       ecb.onerror = chromeAsyncError;
       this.emulator.pushCallback(ecb);
       this.emulator.send({emulator_shell: args, id: ecb.id});
     };
-    this.applyArgumentsToSandbox(win, this.sandbox, args);
+    this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
 
     // NB: win.onerror is not hooked by default due to the inability to
     // differentiate content exceptions from chrome exceptions. See bug
     // 1128760 for more details. A debug_script flag can be set to
     // reenable onerror hooking to help debug test scripts.
     if (cmd.parameters.debug_script) {
       win.onerror = function(msg, url, line) {
         let err = new JavaScriptError(`${msg} at: ${url} line: ${line}`);
@@ -1110,29 +1126,29 @@ GeckoDriver.prototype.executeWithCallbac
     try {
       this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
       if (this.timer !== null) {
         this.timer.initWithCallback(function() {
           chromeAsyncReturnFunc(new ScriptTimeoutError("timed out"));
         }, that.timeout, Ci.nsITimer.TYPE_ONE_SHOT);
       }
 
-      this.sandbox.returnFunc = chromeAsyncReturnFunc;
-      this.sandbox.finish = chromeAsyncFinish;
+      this.sandboxes[sandboxName].returnFunc = chromeAsyncReturnFunc;
+      this.sandboxes[sandboxName].finish = chromeAsyncFinish;
 
       if (!directInject) {
         script =  "__marionetteParams.push(returnFunc);" +
             "let marionetteScriptFinished = returnFunc;" +
             "let __marionetteFunc = function() {" + script + "};" +
             "__marionetteFunc.apply(null, __marionetteParams);";
       }
 
       this.executeScriptInSandbox(
           resp,
-          this.sandbox,
+          this.sandboxes[sandboxName],
           script,
           directInject,
           true /* async */,
           scriptTimeout);
     } catch (e) {
       chromeAsyncError(e, "execute_async_script", filename, line, script);
     }
   }.bind(this));
@@ -1516,20 +1532,16 @@ GeckoDriver.prototype.switchToWindow = f
       if (byNameOrId(win.name, outerId)) {
         found = {win: win, outerId: outerId};
         break;
       }
     }
   }
 
   if (found) {
-    // As in content, switching to a new window invalidates a sandbox
-    // for reuse.
-    this.sandbox = null;
-
     // Initialise Marionette if browser has not been seen before,
     // otherwise switch to known browser and activate the tab if it's a
     // content browser.
     if (!(found.outerId in this.browsers)) {
       let registerBrowsers, browserListening;
       if (found.contentId) {
         registerBrowsers = this.registerPromise();
         browserListening = this.listeningPromise();
@@ -2447,16 +2459,17 @@ GeckoDriver.prototype.sessionTearDown = 
   this.deleteFile("marionetteContentScripts");
 
   if (this.observing !== null) {
     for (let topic in this.observing) {
       Services.obs.removeObserver(this.observing[topic], topic);
     }
     this.observing = null;
   }
+  this.sandboxes = {};
 };
 
 /**
  * Processes the "deleteSession" request from the client by tearing down
  * the session and responding "ok".
  */
 GeckoDriver.prototype.deleteSession = function(cmd, resp) {
   this.sessionTearDown();
--- a/testing/marionette/driver/marionette_driver/marionette.py
+++ b/testing/marionette/driver/marionette_driver/marionette.py
@@ -786,20 +786,22 @@ class Marionette(object):
                 for i in range(len(val)):
                     typing.append(val[i])
         return typing
 
     def push_permission(self, perm_type, allow):
         with self.using_context('content'):
             perm = self.execute_script("""
                 let allow = arguments[0];
-                if (allow)
+                if (allow) {
                   allow = Components.interfaces.nsIPermissionManager.ALLOW_ACTION;
-                else
+                }
+                else {
                   allow = Components.interfaces.nsIPermissionManager.DENY_ACTION;
+                }
                 let perm_type = arguments[1];
 
                 Components.utils.import("resource://gre/modules/Services.jsm");
                 window.wrappedJSObject.permChanged = false;
                 window.wrappedJSObject.permObserver = function(subject, topic, data) {
                   if (topic == "perm-changed") {
                     let permission = subject.QueryInterface(Components.interfaces.nsIPermission);
                     if (perm_type == permission.type) {
@@ -821,17 +823,17 @@ class Marionette(object):
                 return value;
                 """, script_args=[allow, perm_type], sandbox='system')
 
         with self.using_context('chrome'):
             waiting = self.execute_script("""
                 Components.utils.import("resource://gre/modules/Services.jsm");
                 let perm = arguments[0];
                 let secMan = Services.scriptSecurityManager;
-                let principal = secMan.getAppCodebasePrincipal(Services.io.newURI(perm.url, null, null), 
+                let principal = secMan.getAppCodebasePrincipal(Services.io.newURI(perm.url, null, null),
                                 perm.appId, perm.isInBrowserElement);
                 let testPerm = Services.perms.testPermissionFromPrincipal(principal, perm.type, perm.action);
                 if (testPerm == perm.action) {
                   return false;
                 }
                 Services.perms.addFromPrincipal(principal, perm.type, perm.action);
                 return true;
                 """, script_args=[perm])
@@ -1344,17 +1346,17 @@ class Marionette(object):
         else:
             unwrapped = value
 
         return unwrapped
 
     def execute_js_script(self, script, script_args=None, async=True,
                           new_sandbox=True, special_powers=False,
                           script_timeout=None, inactivity_timeout=None,
-                          filename=None):
+                          filename=None, sandbox='default'):
         if script_args is None:
             script_args = []
         args = self.wrapArguments(script_args)
         response = self._send_message('executeJSScript',
                                       'value',
                                       script=script,
                                       args=args,
                                       async=async,
@@ -1362,31 +1364,35 @@ class Marionette(object):
                                       specialPowers=special_powers,
                                       scriptTimeout=script_timeout,
                                       inactivityTimeout=inactivity_timeout,
                                       filename=filename,
                                       line=None)
         return self.unwrapValue(response)
 
     def execute_script(self, script, script_args=None, new_sandbox=True,
-                       special_powers=False, script_timeout=None):
+                       special_powers=False, sandbox='default', script_timeout=None):
         '''
         Executes a synchronous JavaScript script, and returns the result (or None if the script does return a value).
 
         The script is executed in the context set by the most recent
         set_context() call, or to the CONTEXT_CONTENT context if set_context()
         has not been called.
 
         :param script: A string containing the JavaScript to execute.
         :param script_args: A list of arguments to pass to the script.
         :param special_powers: Whether or not you want access to SpecialPowers
          in your script. Set to False by default because it shouldn't really
          be used, since you already have access to chrome-level commands if you
          set context to chrome and do an execute_script. This method was added
          only to help us run existing Mochitests.
+        :param sandbox: A tag referring to the sandbox you wish to use; if
+         you specify a new tag, a new sandbox will be created.  If you use the
+         special tag 'system', the sandbox will be created using the system
+         principal which has elevated privileges.
         :param new_sandbox: If False, preserve global variables from the last
          execute_*script call. This is True by default, in which case no
          globals are preserved.
 
         Simple usage example:
 
         ::
 
@@ -1434,37 +1440,44 @@ class Marionette(object):
         args = self.wrapArguments(script_args)
         stack = traceback.extract_stack()
         frame = stack[-2:-1][0] # grab the second-to-last frame
         response = self._send_message('executeScript',
                                       'value',
                                       script=script,
                                       args=args,
                                       newSandbox=new_sandbox,
+                                      sandbox=sandbox,
                                       specialPowers=special_powers,
                                       scriptTimeout=script_timeout,
                                       line=int(frame[1]),
                                       filename=os.path.basename(frame[0]))
         return self.unwrapValue(response)
 
-    def execute_async_script(self, script, script_args=None, new_sandbox=True, special_powers=False, script_timeout=None, debug_script=False):
+    def execute_async_script(self, script, script_args=None, new_sandbox=True,
+                             sandbox='default', script_timeout=None,
+                             special_powers=False, debug_script=False):
         '''
         Executes an asynchronous JavaScript script, and returns the result (or None if the script does return a value).
 
         The script is executed in the context set by the most recent
         set_context() call, or to the CONTEXT_CONTENT context if set_context()
         has not been called.
 
         :param script: A string containing the JavaScript to execute.
         :param script_args: A list of arguments to pass to the script.
         :param special_powers: Whether or not you want access to SpecialPowers
          in your script. Set to False by default because it shouldn't really
          be used, since you already have access to chrome-level commands if you
          set context to chrome and do an execute_script. This method was added
          only to help us run existing Mochitests.
+        :param sandbox: A tag referring to the sandbox you wish to use; if
+         you specify a new tag, a new sandbox will be created.  If you use the
+         special tag 'system', the sandbox will be created using the system
+         principal which has elevated privileges.
         :param new_sandbox: If False, preserve global variables from the last
          execute_*script call. This is True by default, in which case no
          globals are preserved.
         :param debug_script: Capture javascript exceptions when in
          CONTEXT_CHROME context.
 
         Usage example:
 
@@ -1484,16 +1497,17 @@ class Marionette(object):
         args = self.wrapArguments(script_args)
         stack = traceback.extract_stack()
         frame = stack[-2:-1][0] # grab the second-to-last frame
         response = self._send_message('executeAsyncScript',
                                       'value',
                                       script=script,
                                       args=args,
                                       newSandbox=new_sandbox,
+                                      sandbox=sandbox,
                                       specialPowers=special_powers,
                                       scriptTimeout=script_timeout,
                                       line=int(frame[1]),
                                       filename=os.path.basename(frame[0]),
                                       debug_script=debug_script)
         return self.unwrapValue(response)
 
     def find_element(self, method, target, id=None):
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -41,19 +41,20 @@ let listenerId = null; // unique ID of t
 let curFrame = content;
 let isRemoteBrowser = () => curFrame.contentWindow !== null;
 let previousFrame = null;
 let elementManager = new ElementManager([]);
 let accessibility = new Accessibility();
 let actions = new ActionChain(utils, checkForInterrupted);
 let importedScripts = null;
 
-// The sandbox we execute test scripts in. Gets lazily created in
-// createExecuteContentSandbox().
-let sandbox;
+// A dict of sandboxes used this session
+let sandboxes = {};
+// The name of the current sandbox
+let sandboxName = 'default';
 
 // the unload handler
 let onunload;
 
 // Flag to indicate whether an async script is currently running or not.
 let asyncTestRunning = false;
 let asyncTestCommandId;
 let asyncTestTimeoutId;
@@ -80,17 +81,16 @@ logger.info("loaded listener.js");
 let modalHandler = function() {
   // This gets called on the system app only since it receives the mozbrowserprompt event
   sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true });
   let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
   if (isLocal) {
     previousFrame = curFrame;
   }
   curFrame = content;
-  sandbox = null;
 };
 
 /**
  * Called when listener is first started up.
  * The listener sends its unique window ID and its current URI to the actor.
  * If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
@@ -398,17 +398,17 @@ function sendLog(msg) {
 function sendError(err, cmdId) {
   sendToServer("Marionette:error", null, {error: err}, cmdId);
 }
 
 /**
  * Clear test values after completion of test
  */
 function resetValues() {
-  sandbox = null;
+  sandboxes = {};
   curFrame = content;
   actions.mouseEventsOnly = false;
 }
 
 /**
  * Dump a logline to stdout. Prepends logline with a timestamp.
  */
 function dumpLog(logline) {
@@ -432,17 +432,16 @@ function wasInterrupted() {
 }
 
 function checkForInterrupted() {
     if (wasInterrupted()) {
       if (previousFrame) {
         //if previousFrame is set, then we're in a single process environment
         curFrame = actions.frame = previousFrame;
         previousFrame = null;
-        sandbox = null;
       }
       else {
         //else we're in OOP environment, so we'll switch to the original OOP frame
         sendSyncMessage("Marionette:switchToModalOrigin");
       }
       sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
     }
 }
@@ -461,17 +460,22 @@ function createExecuteContentSandbox(win
       "content",
       marionetteLogObj,
       timeout,
       heartbeatCallback,
       marionetteTestName);
   mn.runEmulatorCmd = (cmd, cb) => this.runEmulatorCmd(cmd, cb);
   mn.runEmulatorShell = (args, cb) => this.runEmulatorShell(args, cb);
 
-  let sandbox = new Cu.Sandbox(win, {sandboxPrototype: win});
+  let principal = win;
+  if (sandboxName == 'system') {
+    principal = Cc["@mozilla.org/systemprincipal;1"].
+                createInstance(Ci.nsIPrincipal);
+  }
+  let sandbox = new Cu.Sandbox(principal, {sandboxPrototype: win});
   sandbox.global = sandbox;
   sandbox.window = win;
   sandbox.document = sandbox.window.document;
   sandbox.navigator = sandbox.window.navigator;
   sandbox.testUtils = utils;
   sandbox.asyncTestCommandId = asyncTestCommandId;
   sandbox.marionette = mn;
 
@@ -527,17 +531,17 @@ function createExecuteContentSandbox(win
       sandbox.asyncComplete(mn.generate_results(), sandbox.asyncTestCommandId);
     } else {
       return mn.generate_results();
     }
   };
   sandbox.marionetteScriptFinished = val =>
       sandbox.asyncComplete(val, sandbox.asyncTestCommandId);
 
-  return sandbox;
+  sandboxes[sandboxName] = sandbox;
 }
 
 /**
  * Execute the given script either as a function body (executeScript)
  * or directly (for mochitest like JS Marionette tests).
  */
 function executeScript(msg, directInject) {
   // Set up inactivity timeout.
@@ -552,28 +556,32 @@ function executeScript(msg, directInject
     heartbeatCallback = function() {
       curFrame.clearTimeout(inactivityTimeoutId);
       setTimer();
     };
   }
 
   asyncTestCommandId = msg.json.command_id;
   let script = msg.json.script;
+  sandboxName = msg.json.sandboxName;
 
-  if (msg.json.newSandbox || !sandbox) {
-    sandbox = createExecuteContentSandbox(curFrame,
-                                          msg.json.timeout);
-    if (!sandbox) {
+  if (msg.json.newSandbox ||
+      !(sandboxName in sandboxes) ||
+      (sandboxes[sandboxName].window != curFrame)) {
+    createExecuteContentSandbox(curFrame, msg.json.timeout);
+    if (!sandboxes[sandboxName]) {
       sendError(new WebDriverError("Could not create sandbox!"), asyncTestCommandId);
       return;
     }
   } else {
-    sandbox.asyncTestCommandId = asyncTestCommandId;
+    sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
   }
 
+  let sandbox = sandboxes[sandboxName];
+
   try {
     if (directInject) {
       if (importedScripts.exists()) {
         let stream = Components.classes["@mozilla.org/network/file-input-stream;1"].
                       createInstance(Components.interfaces.nsIFileInputStream);
         stream.init(importedScripts, -1, 0, 0);
         let data = NetUtil.readInputStreamToString(stream, stream.available());
         stream.close();
@@ -675,33 +683,36 @@ function executeWithCallback(msg, useFin
     heartbeatCallback = function() {
       curFrame.clearTimeout(inactivityTimeoutId);
       setTimer();
     };
   }
 
   let script = msg.json.script;
   asyncTestCommandId = msg.json.command_id;
+  sandboxName = msg.json.sandboxName;
 
   onunload = function() {
     sendError(new JavaScriptError("unload was called"), asyncTestCommandId);
   };
   curFrame.addEventListener("unload", onunload, false);
 
-  if (msg.json.newSandbox || !sandbox) {
-    sandbox = createExecuteContentSandbox(curFrame,
-                                          msg.json.timeout);
-    if (!sandbox) {
+  if (msg.json.newSandbox ||
+      !(sandboxName in sandboxes) ||
+      (sandboxes[sandboxName].window != curFrame)) {
+    createExecuteContentSandbox(curFrame, msg.json.timeout);
+    if (!sandboxes[sandboxName]) {
       sendError(new JavaScriptError("Could not create sandbox!"), asyncTestCommandId);
       return;
     }
   }
   else {
-    sandbox.asyncTestCommandId = asyncTestCommandId;
+    sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
   }
+  let sandbox = sandboxes[sandboxName];
   sandbox.tag = script;
 
   asyncTestTimeoutId = curFrame.setTimeout(function() {
     sandbox.asyncComplete(new ScriptTimeoutError("timed out"), asyncTestCommandId);
   }, msg.json.timeout);
 
   originalOnError = curFrame.onerror;
   curFrame.onerror = function errHandler(msg, url, line) {
@@ -1637,17 +1648,17 @@ function switchToFrame(msg) {
   if ((msg.json.id === null || msg.json.id === undefined) && (msg.json.element == null)) {
     // returning to root frame
     sendSyncMessage("Marionette:switchedToFrame", { frameValue: null });
 
     curFrame = content;
     if(msg.json.focus == true) {
       curFrame.focus();
     }
-    sandbox = null;
+
     checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
     return;
   }
   if (msg.json.element != undefined) {
     if (elementManager.seenItems[msg.json.element] != undefined) {
       let wantedFrame;
       try {
         wantedFrame = elementManager.getKnownElement(msg.json.element, curFrame); //Frame Element
@@ -1689,17 +1700,17 @@ function switchToFrame(msg) {
         else {
           // If foundFrame is null at this point then we have the top level browsing
           // context so should treat it accordingly.
           sendSyncMessage("Marionette:switchedToFrame", { frameValue: null});
           curFrame = content;
           if(msg.json.focus == true) {
             curFrame.focus();
           }
-          sandbox = null;
+
           checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
           return;
         }
       } catch (e) {
         // Since window.frames does not return OOP frames it will throw
         // and we land up here. Let's not give up and check if there are
         // iframes and switch to the indexed frame there
         let iframes = curFrame.document.getElementsByTagName("iframe");
@@ -1711,18 +1722,16 @@ function switchToFrame(msg) {
     }
   }
 
   if (foundFrame === null) {
     sendError(new NoSuchFrameError("Unable to locate frame: " + (msg.json.id || msg.json.element)), command_id);
     return true;
   }
 
-  sandbox = null;
-
   // send a synchronous message to let the server update the currently active
   // frame element (for getActiveFrame)
   let frameValue = elementManager.wrapValue(curFrame.wrappedJSObject)['ELEMENT'];
   sendSyncMessage("Marionette:switchedToFrame", { frameValue: frameValue });
 
   let rv = null;
   if (curFrame.contentWindow === null) {
     // The frame we want to switch to is a remote/OOP frame;
@@ -1872,17 +1881,17 @@ function runEmulatorShell(args, callback
     _emu_cbs[_emu_cb_id] = callback;
   }
   sendAsyncMessage("Marionette:runEmulatorShell", {emulator_shell: args, id: _emu_cb_id});
   _emu_cb_id += 1;
 }
 
 function emulatorCmdResult(msg) {
   let message = msg.json;
-  if (!sandbox) {
+  if (!sandboxes[sandboxName]) {
     return;
   }
   let cb = _emu_cbs[message.id];
   delete _emu_cbs[message.id];
   if (!cb) {
     return;
   }
   try {