tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 663f98a4556cc917036850648cb494133b30e197
parent 05e11564b0f449829262ded7f2fc2809eb3dc5ce
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date:   Fri, 19 Dec 2025 15:09:26 +0000

Bug 2005938 - [devtools] Retrieve all globals from target actors process in makeSideeffectFreeDebugger. r=devtools-reviewers,ochameau.

Since an evaluation can call functions from other globals,
for example using iframeElement.contentWindow, we need to
track all those globals to be able to detect stateful evaluations.

Differential Revision: https://phabricator.services.mozilla.com/D276993

Diffstat:
Mdevtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mdevtools/server/actors/targets/window-global.js | 35+++++++++++++++++++++++++++++++++--
Mdevtools/server/actors/utils/make-debugger.js | 4++--
Mdevtools/server/actors/webconsole/eval-with-debugger.js | 21+++++++++++++++------
4 files changed, 141 insertions(+), 25 deletions(-)

diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js @@ -4,21 +4,23 @@ "use strict"; -const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html> -<script> -let x = 3, y = 4; -function zzyzx() { - x = 10; -} -function zzyzx2() { - x = 10; -} -var obj = {propA: "A", propB: "B"}; -var array = [1, 2, 3]; -var $$ = 42; -</script> -<h1>title</h1> -`; +const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(` + <!DOCTYPE html> + <script> + let x = 3, y = 4; + function zzyzx() { + x = 10; + } + function zzyzx2() { + x = 10; + } + var obj = {propA: "A", propB: "B"}; + var array = [1, 2, 3]; + var $$ = 42; + </script> + <h1>title</h1> + <iframe src="https://example.com/document-builder.sjs?html=in iframe"></iframe> +`)}`; const EAGER_EVALUATION_PREF = "devtools.webconsole.input.eagerEvaluation"; @@ -263,6 +265,80 @@ add_task(async function () { setInputValue(hud, "await 1; 2 + 3;"); await waitForNoEagerEvaluationResult(hud); ok(true, "instant evaluation is disabled for top-level await expressions"); + + info( + "Check that effect-less expression are eagerly evaluated even when going through contentWindow" + ); + setInputValue( + hud, + `document.querySelector("iframe").contentWindow.Function("return location.search")()` + ); + await waitForEagerEvaluationResult(hud, `"?html=in%20iframe"`); + ok(true, "contentWindow expression was eagerly evaluated"); + + info( + "Check that effectful expression are not eagerly evaluated when going through contentWindow" + ); + setInputValue( + hud, + `document.querySelector("iframe").contentWindow.Function("globalThis.x = 10; return globalThis.x")()` + ); + await waitForNoEagerEvaluationResult(hud); + ok(true, "effectful contentWindow expression was not eagerly evaluated"); + // double check that the evaluation wasn't done + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + is( + content.document.querySelector("iframe").contentWindow.x, + undefined, + "iframe global x property wasn't set" + ); + }); + + info( + "Check that effect-less expression are eagerly evaluated even when going through window.parent" + ); + // First, select the iframe as the evaluation target + selectTargetInContextSelector( + hud, + "https://example.com/document-builder.sjs?html=in%20iframe" + ); + setInputValue( + hud, + ` + // sanity check to make sure we do evaluate this from the iframe document + if (globalThis.parent === globalThis) { + throw new Error("unexpected") + } + globalThis.parent.Function("return array")() + ` + ); + await waitForEagerEvaluationResult(hud, "Array(3) [ 1, 2, 3 ]"); + ok(true, "window.parent expression was eagerly evaluated"); + + info( + "Check that effectful expression are not eagerly evaluated when going through window.parent" + ); + setInputValue( + hud, + ` + // sanity check to make sure we do evaluate this from the iframe document + if (globalThis.parent === globalThis) { + throw new Error("unexpected") + } + globalThis.parent.Function("return array.push(4)")() + ` + ); + await waitForNoEagerEvaluationResult(hud); + ok(true, "effectful window.parent expression was not eagerly evaluated"); + + // double check that the evaluation wasn't done + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + Assert.deepEqual( + content.wrappedJSObject.array, + [1, 2, 3], + "array property wasn't modified" + ); + }); }); // Test that the currently selected autocomplete result is eagerly evaluated. diff --git a/devtools/server/actors/targets/window-global.js b/devtools/server/actors/targets/window-global.js @@ -137,6 +137,34 @@ function getChildDocShells(parentDocShell) { exports.getChildDocShells = getChildDocShells; /** + * Helper to retrieve all the windows (parent, children, or browsing context own window) + * that exists in the same process than the given browsing context. + * + * @param {BrowsingContext} browsingContext + * @returns {Array<Window>} + */ +function getAllSameProcessGlobalsFromBrowsingContext(browsingContext) { + const windows = []; + const topBrowsingContext = browsingContext.top; + for (const bc of topBrowsingContext.getAllBrowsingContextsInSubtree()) { + // Filter out browsingContext which don't expose any docshell (e.g. remote frame) + if (!bc.docShell) { + continue; + } + try { + windows.push(bc.docShell.domWindow); + } catch (e) { + // docShell.domWindow may throw when the docshell is being destroyed. + // Ignore them. We can't use docShell.isBeingDestroyed as it + // is flagging too early. e.g it's already true when hitting a breakpoint + // in the unload event. + } + } + + return windows; +} + +/** * Browser-specific actors. */ @@ -300,13 +328,16 @@ class WindowGlobalTargetActor extends BaseTargetActor { this._shouldAddNewGlobalAsDebuggee.bind(this); this.makeDebugger = makeDebugger.bind(null, { - findDebuggees: () => { + findDebuggees: (dbg, includeAllSameProcessGlobals) => { const result = []; const inspectUAWidgets = Services.prefs.getBoolPref( "devtools.inspector.showAllAnonymousContent", false ); - for (const win of this.windows) { + const windows = includeAllSameProcessGlobals + ? getAllSameProcessGlobalsFromBrowsingContext(this.browsingContext) + : this.windows; + for (const win of windows) { result.push(win); // Only expose User Agent internal (like <video controls>) when the // related pref is set. diff --git a/devtools/server/actors/utils/make-debugger.js b/devtools/server/actors/utils/make-debugger.js @@ -95,8 +95,8 @@ module.exports = function makeDebugger({ dbg.addDebuggees(); dbg.onNewGlobalObject = onNewGlobalObject; }; - dbg.findDebuggees = function () { - return findDebuggees(dbg); + dbg.findDebuggees = function (includeAllSameProcessGlobals) { + return findDebuggees(dbg, includeAllSameProcessGlobals); }; return dbg; diff --git a/devtools/server/actors/webconsole/eval-with-debugger.js b/devtools/server/actors/webconsole/eval-with-debugger.js @@ -451,12 +451,21 @@ function makeSideeffectFreeDebugger(targetActorDbg) { // made via this debugger will be ignored by all debuggers except this one. dbg.exclusiveDebuggerOnEval = true; - // We need to register all target actor's globals. - // In most cases, this will be only one global, except for the browser toolbox, - // where process target actors may interact with many. - // On the browser toolbox, we may have many debuggees and this is important to register - // them in order to detect native call made from/to these others globals. - for (const global of targetActorDbg.findDebuggees()) { + // We need to register all JS globals that the evaluation may use. + // By default WindowGlobalTarget (with "EFT" mode enabled by default) is specific to + // only one JS global, related to the WindowGlobal it relates to. + // But there is two edgecases: + // - the browser toolbox WindowGlobalTarget still have EFT turned off and each target + // may involve many WindowGlobal and so many JS globals. + // - if the current target's document has some iframes (or is an iframe) running in the + // same process, the evaluation may use `globalThis.contentWindow` or + // `iframeElement.(top|parent)` and so involve any of these same-process JS globals. + // + // While we don't have to do this for regular evaluations, it is important for + // side-effect-free one as onNativeCall is only called for these registered globals. + // If we miss one, we would prevent detecting side-effect calls in the missed global. + const globals = targetActorDbg.findDebuggees(true); + for (const global of globals) { try { dbg.addDebuggee(global); } catch (e) {