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:
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) {