diff --git a/testing/marionette/client/marionette/tests/unit/test_execute_sandboxes.py b/testing/marionette/client/marionette/tests/unit/test_execute_sandboxes.py new file mode 100644 index 00000000000..5bb653594b6 --- /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") diff --git a/testing/marionette/client/marionette/tests/unit/unit-tests.ini b/testing/marionette/client/marionette/tests/unit/unit-tests.ini index 12de65ca1c0..8a1dbe91b92 100644 --- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini +++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini @@ -156,3 +156,5 @@ b2g = false [test_file_upload.py] b2g = false skip-if = os == "win" # http://bugs.python.org/issue14574 + +[test_execute_sandboxes.py] diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js index 9142519c1e0..82b08186676 100644 --- a/testing/marionette/driver.js +++ b/testing/marionette/driver.js @@ -131,7 +131,7 @@ this.GeckoDriver = function(appName, device, emulator) { 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; @@ -715,11 +715,17 @@ GeckoDriver.prototype.getContext = function(cmd, resp) { * @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') { @@ -740,7 +746,7 @@ GeckoDriver.prototype.createExecuteSandbox = function(win, mn, sp) { pow.map(s => loader.loadSubScript(s, sb)); } - return sb; + this.sandboxes[sandboxName] = sb; }; /** @@ -820,6 +826,7 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) { specialPowers, filename, line} = cmd.parameters; + let sandboxName = cmd.parameters.sandbox || 'default'; if (!scriptTimeout) { scriptTimeout = this.scriptTimeout; @@ -836,7 +843,8 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) { timeout: scriptTimeout, specialPowers: specialPowers, filename: filename, - line: line + line: line, + sandboxName: sandboxName }); return; } @@ -860,7 +868,9 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) { } 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, @@ -869,22 +879,23 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) { 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) { @@ -892,7 +903,7 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) { } this.executeScriptInSandbox( resp, - this.sandbox, + this.sandboxes[sandboxName], script, directInject, false /* async */, @@ -951,6 +962,7 @@ GeckoDriver.prototype.executeJSScript = function(cmd, resp) { specialPowers: cmd.parameters.specialPowers, filename: cmd.parameters.filename, line: cmd.parameters.line, + sandboxName: cmd.parameters.sandbox || 'default', }); break; } @@ -980,6 +992,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { specialPowers, filename, line} = cmd.parameters; + let sandboxName = cmd.parameters.sandbox || 'default'; if (!scriptTimeout) { scriptTimeout = this.scriptTimeout; @@ -998,7 +1011,8 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { inactivityTimeout: inactivityTimeout, specialPowers: specialPowers, filename: filename, - line: line + line: line, + sandboxName: sandboxName, }); return; } @@ -1034,7 +1048,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { 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; @@ -1055,7 +1069,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { }; let chromeAsyncFinish = function() { - let res = that.sandbox.generate_results(); + let res = that.sandboxes[sandboxName].generate_results(); chromeAsyncReturnFunc(res); }; @@ -1064,7 +1078,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { chromeAsyncReturnFunc(err); }; - if (!this.sandbox || newSandbox) { + if (newSandbox || !(sandboxName in this.sandboxes)) { let marionette = new Marionette( this, win, @@ -1073,27 +1087,29 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { 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 @@ -1115,8 +1131,8 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { }, 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);" + @@ -1127,7 +1143,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { this.executeScriptInSandbox( resp, - this.sandbox, + this.sandboxes[sandboxName], script, directInject, true /* async */, @@ -1521,10 +1537,6 @@ GeckoDriver.prototype.switchToWindow = function(cmd, resp) { } 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. @@ -2452,6 +2464,7 @@ GeckoDriver.prototype.sessionTearDown = function(cmd, resp) { } this.observing = null; } + this.sandboxes = {}; }; /** diff --git a/testing/marionette/driver/marionette_driver/marionette.py b/testing/marionette/driver/marionette_driver/marionette.py index 75eb888a440..787cd72767a 100644 --- a/testing/marionette/driver/marionette_driver/marionette.py +++ b/testing/marionette/driver/marionette_driver/marionette.py @@ -791,10 +791,12 @@ class Marionette(object): 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"); @@ -826,7 +828,7 @@ class Marionette(object): 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) { @@ -1349,7 +1351,7 @@ class Marionette(object): 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) @@ -1367,7 +1369,7 @@ class Marionette(object): 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). @@ -1382,6 +1384,10 @@ class Marionette(object): 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. @@ -1439,13 +1445,16 @@ class Marionette(object): 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). @@ -1460,6 +1469,10 @@ class Marionette(object): 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. @@ -1489,6 +1502,7 @@ class Marionette(object): script=script, args=args, newSandbox=new_sandbox, + sandbox=sandbox, specialPowers=special_powers, scriptTimeout=script_timeout, line=int(frame[1]), diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index 12266352b5f..cb3769e6ac8 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -46,9 +46,10 @@ 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; @@ -85,7 +86,6 @@ let modalHandler = function() { previousFrame = curFrame; } curFrame = content; - sandbox = null; }; /** @@ -403,7 +403,7 @@ function sendError(err, cmdId) { * Clear test values after completion of test */ function resetValues() { - sandbox = null; + sandboxes = {}; curFrame = content; actions.mouseEventsOnly = false; } @@ -437,7 +437,6 @@ function checkForInterrupted() { //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 @@ -466,7 +465,12 @@ function createExecuteContentSandbox(win, timeout) { 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; @@ -532,7 +536,7 @@ function createExecuteContentSandbox(win, timeout) { sandbox.marionetteScriptFinished = val => sandbox.asyncComplete(val, sandbox.asyncTestCommandId); - return sandbox; + sandboxes[sandboxName] = sandbox; } /** @@ -557,18 +561,22 @@ function executeScript(msg, directInject) { 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()) { @@ -680,23 +688,26 @@ function executeWithCallback(msg, useFinish) { 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() { @@ -1642,7 +1653,7 @@ function switchToFrame(msg) { if(msg.json.focus == true) { curFrame.focus(); } - sandbox = null; + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); return; } @@ -1694,7 +1705,7 @@ function switchToFrame(msg) { if(msg.json.focus == true) { curFrame.focus(); } - sandbox = null; + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); return; } @@ -1716,8 +1727,6 @@ function switchToFrame(msg) { 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']; @@ -1877,7 +1886,7 @@ function runEmulatorShell(args, callback) { function emulatorCmdResult(msg) { let message = msg.json; - if (!sandbox) { + if (!sandboxes[sandboxName]) { return; } let cb = _emu_cbs[message.id];