From 7785fd8191f63f2ccc9316b124fcb1252a863ad2 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 16 Jan 2020 11:52:23 -0800 Subject: [PATCH] browser(firefox): support isolated worlds (#500) https://github.com/dgozman/gecko-dev/commit/211f1f1bff740ecb2a6a9e164c432587e7dc6f45 --- browser_patches/firefox/BUILD_NUMBER | 2 +- .../firefox/patches/bootstrap.diff | 261 ++++++++++++------ 2 files changed, 182 insertions(+), 81 deletions(-) diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index 2d1420d537..9951021cfe 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1 +1 @@ -1012 +1013 diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index d6d67891b8..6aabed2eb4 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -238,6 +238,50 @@ index edda707be08292a767f66d20f2abca98af113796..f7031a8e1fd813a9371b8f6d3a987a32 NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, int32_t aCurSelfProgress, +diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp +index 9e3c1a56d10394d98de9e84fb8cd6ee8e3be5870..8c661de349d6cb64fd8d81d5db9c28f2a2af9138 100644 +--- a/dom/script/ScriptSettings.cpp ++++ b/dom/script/ScriptSettings.cpp +@@ -140,6 +140,30 @@ ScriptSettingsStackEntry::~ScriptSettingsStackEntry() { + MOZ_ASSERT_IF(mGlobalObject, mGlobalObject->HasJSGlobal()); + } + ++static nsIGlobalObject* UnwrapSandboxGlobal(nsIGlobalObject* global) { ++ if (!global) ++ return global; ++ JSObject* globalObject = global->GetGlobalJSObject(); ++ if (!globalObject) ++ return global; ++ JSContext* cx = nsContentUtils::GetCurrentJSContext(); ++ if (!cx) ++ return global; ++ JS::Rooted proto(cx); ++ JS::RootedObject rootedGlobal(cx, globalObject); ++ if (!JS_GetPrototype(cx, rootedGlobal, &proto)) ++ return global; ++ if (!proto || !xpc::IsSandboxPrototypeProxy(proto)) ++ return global; ++ // If this is a sandbox associated with a DOMWindow via a ++ // sandboxPrototype, use that DOMWindow. This supports GreaseMonkey ++ // and JetPack content scripts. ++ proto = js::CheckedUnwrapDynamic(proto, cx, /* stopAtWindowProxy = */ false); ++ if (!proto) ++ return global; ++ return xpc::WindowGlobalOrNull(proto); ++} ++ + // If the entry or incumbent global ends up being something that the subject + // principal doesn't subsume, we don't want to use it. This never happens on + // the web, but can happen with asymmetric privilege relationships (i.e. +@@ -167,7 +191,7 @@ static nsIGlobalObject* ClampToSubject(nsIGlobalObject* aGlobalOrNull) { + NS_ENSURE_TRUE(globalPrin, GetCurrentGlobal()); + if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller() + ->SubsumesConsideringDomain(globalPrin)) { +- return GetCurrentGlobal(); ++ return UnwrapSandboxGlobal(GetCurrentGlobal()); + } + + return aGlobalOrNull; diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp index f0c28cfdae1c9ac33013e9688e0142d161763543..a38ab106e37dbab58e91ef5a873f8954c35881e7 100644 --- a/dom/security/nsCSPUtils.cpp @@ -1780,10 +1824,10 @@ index 0000000000000000000000000000000000000000..2508cce41565023b7fee9c7b85afe8ec + diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js new file mode 100644 -index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e744759b55a5f +index 0000000000000000000000000000000000000000..758871cc245ab5aa30ce54d58e87175f65401d50 --- /dev/null +++ b/testing/juggler/content/PageAgent.js -@@ -0,0 +1,671 @@ +@@ -0,0 +1,714 @@ +"use strict"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const Ci = Components.interfaces; @@ -1795,6 +1839,82 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + +const helper = new Helper(); + ++class FrameData { ++ constructor(agent, frame) { ++ this._agent = agent; ++ this._frame = frame; ++ this._isolatedWorlds = new Map(); ++ this.reset(); ++ } ++ ++ reset() { ++ if (this.mainContext) ++ this._agent._runtime.destroyExecutionContext(this.mainContext); ++ for (const world of this._isolatedWorlds.values()) ++ this._agent._runtime.destroyExecutionContext(world); ++ this._isolatedWorlds.clear(); ++ ++ this.mainContext = this._agent._runtime.createExecutionContext(this._frame.domWindow(), this._frame.domWindow(), { ++ frameId: this._frame.id(), ++ name: '', ++ }); ++ ++ for (const bindingName of this._agent._bindingsToAdd.values()) ++ this.exposeFunction(bindingName); ++ for (const {script, worldName} of this._agent._scriptsToEvaluateOnNewDocument.values()) { ++ const context = worldName ? this.createIsolatedWorld(worldName) : this.mainContext; ++ try { ++ let result = context.evaluateScript(script); ++ if (result && result.objectId) ++ context.disposeObject(result.objectId); ++ } catch (e) { ++ } ++ } ++ } ++ ++ exposeFunction(name) { ++ Cu.exportFunction((...args) => { ++ this._agent._session.emitEvent('Page.bindingCalled', { ++ executionContextId: this.mainContext.id(), ++ name, ++ payload: args[0] ++ }); ++ }, this._frame.domWindow(), { ++ defineAs: name, ++ }); ++ } ++ ++ createIsolatedWorld(name) { ++ const principal = [this._frame.domWindow()]; // extended principal ++ const sandbox = Cu.Sandbox(principal, { ++ sandboxPrototype: this._frame.domWindow(), ++ wantComponents: false, ++ wantExportHelpers: false, ++ wantXrays: true, ++ }); ++ const world = this._agent._runtime.createExecutionContext(this._frame.domWindow(), sandbox, { ++ frameId: this._frame.id(), ++ name, ++ }); ++ this._isolatedWorlds.set(world.id(), world); ++ return world; ++ } ++ ++ unsafeObject(objectId) { ++ if (this.mainContext) { ++ const result = this.mainContext.unsafeObject(objectId); ++ if (result) ++ return result.object; ++ } ++ for (const world of this._isolatedWorlds.values()) { ++ const result = world.unsafeObject(objectId); ++ if (result) ++ return result.object; ++ } ++ throw new Error('Cannot find object with id = ' + objectId); ++ } ++} ++ +class PageAgent { + constructor(session, runtimeAgent, frameTree, scrollbarManager, networkMonitor) { + this._session = session; @@ -1803,7 +1923,7 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + this._networkMonitor = networkMonitor; + this._scrollbarManager = scrollbarManager; + -+ this._frameToExecutionContext = new Map(); ++ this._frameData = new Map(); + this._scriptsToEvaluateOnNewDocument = new Map(); + this._bindingsToAdd = new Set(); + @@ -1866,9 +1986,13 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + docShell.bypassCSPEnabled = enabled; + } + -+ addScriptToEvaluateOnNewDocument({script}) { ++ addScriptToEvaluateOnNewDocument({script, worldName}) { + const scriptId = helper.generateId(); -+ this._scriptsToEvaluateOnNewDocument.set(scriptId, script); ++ this._scriptsToEvaluateOnNewDocument.set(scriptId, {script, worldName}); ++ if (worldName) { ++ for (const frameData of this._frameData.values()) ++ frameData.createIsolatedWorld(worldName); ++ } + return {scriptId}; + } + @@ -1927,10 +2051,10 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + _filePickerShown(inputElement) { + if (inputElement.ownerGlobal.docShell !== this._docShell) + return; -+ const result = this._runtime.rawElementToRemoteObject(inputElement); ++ const frameData = Array.from(this._frameData.values()).find(data => inputElement.ownerDocument === data._frame.domWindow().document); + this._session.emitEvent('Page.fileChooserOpened', { -+ executionContextId: result.executionContextId, -+ element: result.element ++ executionContextId: frameData.mainContext.id(), ++ element: frameData.mainContext.rawValueToRemoteObject(inputElement) + }); + } + @@ -2016,25 +2140,7 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + const frame = this._frameTree.frameForDocShell(docShell); + if (!frame) + return; -+ -+ if (this._frameToExecutionContext.has(frame)) { -+ this._runtime.destroyExecutionContext(this._frameToExecutionContext.get(frame)); -+ this._frameToExecutionContext.delete(frame); -+ } -+ const executionContext = this._ensureExecutionContext(frame); -+ -+ if (!this._scriptsToEvaluateOnNewDocument.size && !this._bindingsToAdd.size) -+ return; -+ for (const bindingName of this._bindingsToAdd.values()) -+ this._exposeFunction(frame, bindingName); -+ for (const script of this._scriptsToEvaluateOnNewDocument.values()) { -+ try { -+ let result = executionContext.evaluateScript(script); -+ if (result && result.objectId) -+ executionContext.disposeObject(result.objectId); -+ } catch (e) { -+ } -+ } ++ this._frameData.get(frame).reset(); + } + + _onFrameAttached(frame) { @@ -2042,26 +2148,16 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + frameId: frame.id(), + parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined, + }); -+ this._ensureExecutionContext(frame); ++ this._frameData.set(frame, new FrameData(this, frame)); + } + + _onFrameDetached(frame) { ++ this._frameData.delete(frame); + this._session.emitEvent('Page.frameDetached', { + frameId: frame.id(), + }); + } + -+ _ensureExecutionContext(frame) { -+ let executionContext = this._frameToExecutionContext.get(frame); -+ if (!executionContext) { -+ executionContext = this._runtime.createExecutionContext(frame.domWindow(), { -+ frameId: frame.id(), -+ }); -+ this._frameToExecutionContext.set(frame, executionContext); -+ } -+ return executionContext; -+ } -+ + dispose() { + helper.removeListeners(this._eventListeners); + } @@ -2127,29 +2223,24 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + if (this._bindingsToAdd.has(name)) + throw new Error(`Binding with name ${name} already exists`); + this._bindingsToAdd.add(name); -+ for (const frame of this._frameTree.frames()) -+ this._exposeFunction(frame, name); ++ for (const frameData of this._frameData.values()) ++ frameData.exposeFunction(name); + } + -+ _exposeFunction(frame, name) { -+ Cu.exportFunction((...args) => { -+ const executionContext = this._ensureExecutionContext(frame); -+ this._session.emitEvent('Page.bindingCalled', { -+ executionContextId: executionContext.id(), -+ name, -+ payload: args[0] -+ }); -+ }, frame.domWindow(), { -+ defineAs: name, -+ }); ++ async adoptNode({frameId, objectId, executionContextId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); ++ const context = this._runtime.findExecutionContext(executionContextId); ++ return { remoteObject: context.rawValueToRemoteObject(unsafeObject) }; + } + + async setFileInputFiles({objectId, frameId, files}) { + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); -+ const executionContext = this._ensureExecutionContext(frame); -+ const unsafeObject = executionContext.unsafeObject(objectId); ++ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject) + throw new Error('Object is not input!'); + const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath))); @@ -2160,8 +2251,7 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); -+ const executionContext = this._ensureExecutionContext(frame); -+ const unsafeObject = executionContext.unsafeObject(objectId); ++ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject.getBoxQuads) + throw new Error('RemoteObject is not a node'); + const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => { @@ -2179,8 +2269,7 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); -+ const executionContext = this._ensureExecutionContext(frame); -+ const unsafeObject = executionContext.unsafeObject(objectId); ++ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject.contentWindow) + return null; + const contentFrame = this._frameTree.frameForDocShell(unsafeObject.contentWindow.docShell); @@ -2191,8 +2280,7 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); -+ const executionContext = this._ensureExecutionContext(frame); -+ const unsafeObject = executionContext.unsafeObject(objectId); ++ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject.getBoxQuads) + throw new Error('RemoteObject is not a node'); + const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}); @@ -2308,8 +2396,7 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + async getFullAXTree({objectId}) { + let unsafeObject = null; + if (objectId) { -+ const executionContext = this._ensureExecutionContext(this._frameTree.mainFrame()); -+ unsafeObject = executionContext.unsafeObject(objectId); ++ unsafeObject = this._frameData.get(this._frameTree.mainFrame()).unsafeObject(objectId); + if (!unsafeObject) + throw new Error(`No object found for id "${objectId}"`); + } @@ -2457,10 +2544,10 @@ index 0000000000000000000000000000000000000000..550860d93891bdeaec54dfccc38e7447 + diff --git a/testing/juggler/content/RuntimeAgent.js b/testing/juggler/content/RuntimeAgent.js new file mode 100644 -index 0000000000000000000000000000000000000000..b43df14b060682711a59221f67fd2c3bba3a0f62 +index 0000000000000000000000000000000000000000..642b8bc1be7eaa6ad46ed38eee10dc58169d44ea --- /dev/null +++ b/testing/juggler/content/RuntimeAgent.js -@@ -0,0 +1,466 @@ +@@ -0,0 +1,465 @@ +"use strict"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -2552,14 +2639,6 @@ index 0000000000000000000000000000000000000000..b43df14b060682711a59221f67fd2c3b + this._enabled = false; + } + -+ rawElementToRemoteObject(node) { -+ const executionContext = Array.from(this._executionContexts.values()).find(context => node.ownerDocument == context._domWindow.document); -+ return { -+ executionContextId: executionContext.id(), -+ element: executionContext.rawValueToRemoteObject(node) -+ }; -+ } -+ + _consoleAPICalled({wrappedJSObject}, topic, data) { + const type = consoleLevelToProtocolType[wrappedJSObject.level]; + if (!type) @@ -2655,14 +2734,21 @@ index 0000000000000000000000000000000000000000..b43df14b060682711a59221f67fd2c3b + pendingPromise.resolve({success: false, obj: null}); + } + -+ createExecutionContext(domWindow, auxData) { -+ const context = new ExecutionContext(this, domWindow, this._debugger.addDebuggee(domWindow), auxData); ++ createExecutionContext(domWindow, contextGlobal, auxData) { ++ const context = new ExecutionContext(this, domWindow, this._debugger.addDebuggee(contextGlobal), auxData); + this._executionContexts.set(context._id, context); + this._windowToExecutionContext.set(domWindow, context); + this._notifyExecutionContextCreated(context); + return context; + } + ++ findExecutionContext(executionContextId) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return executionContext; ++ } ++ + destroyExecutionContext(destroyedContext) { + for (const [promiseID, {reject, executionContext}] of this._pendingPromises) { + if (executionContext === destroyedContext) { @@ -2785,8 +2871,8 @@ index 0000000000000000000000000000000000000000..b43df14b060682711a59221f67fd2c3b + + unsafeObject(objectId) { + if (!this._remoteObjects.has(objectId)) -+ throw new Error('Cannot find object with id = ' + objectId); -+ return this._remoteObjects.get(objectId).unsafeDereference(); ++ return; ++ return { object: this._remoteObjects.get(objectId).unsafeDereference() }; + } + + rawValueToRemoteObject(rawValue) { @@ -3707,10 +3793,10 @@ index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878 +this.NetworkHandler = NetworkHandler; diff --git a/testing/juggler/protocol/PageHandler.js b/testing/juggler/protocol/PageHandler.js new file mode 100644 -index 0000000000000000000000000000000000000000..13e659902758eeb3482d33a7084d8dfd0d330f1e +index 0000000000000000000000000000000000000000..db0648f6bee8035c7750cd810281ad0286540576 --- /dev/null +++ b/testing/juggler/protocol/PageHandler.js -@@ -0,0 +1,281 @@ +@@ -0,0 +1,285 @@ +"use strict"; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -3844,6 +3930,10 @@ index 0000000000000000000000000000000000000000..13e659902758eeb3482d33a7084d8dfd + return await this._contentSession.send('Page.addBinding', options); + } + ++ async adoptNode(options) { ++ return await this._contentSession.send('Page.adoptNode', options); ++ } ++ + async screenshot(options) { + return await this._contentSession.send('Page.screenshot', options); + } @@ -4143,10 +4233,10 @@ index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07 +this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js new file mode 100644 -index 0000000000000000000000000000000000000000..92ce5a8cebfc4290a9f14f398d9ef282d4370fbe +index 0000000000000000000000000000000000000000..0344e2c71a86aff1ab602d9a90c8f2a0817cb66b --- /dev/null +++ b/testing/juggler/protocol/Protocol.js -@@ -0,0 +1,700 @@ +@@ -0,0 +1,711 @@ +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); + +// Protocol-specific types. @@ -4698,6 +4788,7 @@ index 0000000000000000000000000000000000000000..92ce5a8cebfc4290a9f14f398d9ef282 + 'addScriptToEvaluateOnNewDocument': { + params: { + script: t.String, ++ worldName: t.Optional(t.String), + }, + returns: { + scriptId: t.String, @@ -4755,6 +4846,16 @@ index 0000000000000000000000000000000000000000..92ce5a8cebfc4290a9f14f398d9ef282 + boundingBox: t.Nullable(pageTypes.BoundingBox), + }, + }, ++ 'adoptNode': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ executionContextId: t.String, ++ }, ++ returns: { ++ remoteObject: runtimeTypes.RemoteObject, ++ }, ++ }, + 'screenshot': { + params: { + mimeType: t.Enum(['image/png', 'image/jpeg']),