tor-browser

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

commit 80265519b756dd70a7b2171a121bd38dbbdda56a
parent 269cb6ce97aeb7e2a93748c147a7176925b9309e
Author: Rob Wu <rob@robwu.nl>
Date:   Wed, 24 Dec 2025 00:45:35 +0000

Bug 2004525 - Fix WindowTracker to recognize initial about:blank r=zombie

With the changes from bug 543435, `document.readyState` of initial
about:blank is no longer "uninitialized", but "complete". This change
broke the logic of WindowTracker, because it started to treat
uninitialized windows as completed browser windows, and fails to detect
them as browser windows.

To fix this, add an `isUncommittedInitialDocument` check where needed,
and fix various callers that made the wrong assumptions.

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

Diffstat:
Mbrowser/components/extensions/parent/ext-menus.js | 2+-
Mtoolkit/components/extensions/parent/ext-tabs-base.js | 21+++++++++++++++------
Mtoolkit/components/extensions/test/mochitest/mochitest-common.toml | 2++
Atoolkit/components/extensions/test/mochitest/test_ext_windowtracker_initialization.html | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 145 insertions(+), 7 deletions(-)

diff --git a/browser/components/extensions/parent/ext-menus.js b/browser/components/extensions/parent/ext-menus.js @@ -967,7 +967,7 @@ const libraryTracker = { // See WindowTrackerBase#*browserWindows in ext-tabs-base.js for why we // can't use the enumerator's windowtype filter. for (let window of Services.wm.getEnumerator("")) { - if (window.document.readyState === "complete") { + if (windowTracker.isBrowserWindowInitialized(window)) { if (this.isLibraryWindow(window)) { this.notify(window); } diff --git a/toolkit/components/extensions/parent/ext-tabs-base.js b/toolkit/components/extensions/parent/ext-tabs-base.js @@ -1498,6 +1498,16 @@ class WindowTrackerBase extends EventEmitter { }); } + // Whether the window is sufficiently initialized for isBrowserWindow to + // return a meaningful result. If false, wait for the window's "load" event. + isBrowserWindowInitialized(window) { + const { readyState, isUncommittedInitialDocument } = window.document; + return readyState === "complete" && !isUncommittedInitialDocument; + } + + // Only returns a meaningful result for initialized browser windows. If the + // correctness of the result is important, check isBrowserWindowInitialized, + // and wait for the "load" event if it is false. isBrowserWindow(window) { let { documentElement } = window.document; @@ -1525,7 +1535,7 @@ class WindowTrackerBase extends EventEmitter { for (let window of Services.wm.getEnumerator("")) { let ok = includeIncomplete; - if (window.document.readyState === "complete") { + if (this.isBrowserWindowInitialized(window)) { ok = this.isBrowserWindow(window); } @@ -1621,12 +1631,11 @@ class WindowTrackerBase extends EventEmitter { if ( window && !window.closed && - (window.document.readyState !== "complete" || - this.isBrowserWindow(window)) + // Tolerate incomplete windows because isBrowserWindow is only reliable + // once the window is fully loaded. + (!this.isBrowserWindowInitialized(window) || this.isBrowserWindow(window)) ) { if (!context || context.canAccessWindow(window)) { - // Tolerate incomplete windows because isBrowserWindow is only reliable - // once the window is fully loaded. return window; } } @@ -1661,7 +1670,7 @@ class WindowTrackerBase extends EventEmitter { this._openListeners.add(listener); for (let window of this.browserWindows(true)) { - if (window.document.readyState !== "complete") { + if (!this.isBrowserWindowInitialized(window)) { window.addEventListener("load", this); } } diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.toml b/toolkit/components/extensions/test/mochitest/mochitest-common.toml @@ -623,5 +623,7 @@ skip-if = [ "http3", ] +["test_ext_windowtracker_initialization.html"] + ["test_startup_canary.html"] # test_startup_canary.html is at the bottom to minimize the time spent waiting in the test. diff --git a/toolkit/components/extensions/test/mochitest/test_ext_windowtracker_initialization.html b/toolkit/components/extensions/test/mochitest/test_ext_windowtracker_initialization.html @@ -0,0 +1,127 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Testing extension internals: WindowTracker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// Tests that WindowTracker can detect browser windows even if the open +// register was registered before it was fully initialized. +// +// Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=2004525 +add_task(async function test_WindowTracker_and_incomplete_windows() { + // This extension serves two purposes: + // - A way to open and close a browser window. + // - Ensuring that ExtensionParent.apiManager.global.windowTracker exists, + // (via ext-browser.js on desktop, or ext-android.js on Android), so that + // we can interact with it below. + const extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Extension that opens new browser window" }, + background() { + // About the use of browser.windows vs browser.tabs below: + // browser.windows not supported on Android (bug 1584252), but for the + // purpose of this test, we can use browser.tabs.create/remove instead, + // because each tab is currently rendered in one geckoview.xhtml window. + + let winPromise; + browser.test.onMessage.addListener(async msg => { + if (msg === "openBrowserWindow") { + if (browser.windows) { + winPromise = browser.windows.create({ url: "/manifest.json" }); + } else { + winPromise = browser.tabs.create({ url: "/manifest.json" }); + } + } else if (msg === "closeBrowserWindow") { + let win = await winPromise; + if (browser.windows) { + await browser.windows.remove(win.id); + } else { + await browser.tabs.remove(win.id); + } + browser.test.sendMessage("closedBrowserWindow"); + } + }); + }, + }); + await extension.startup(); + + // This is a very elaborate setup to deterministically peek into incomplete + // windows. In practice this is most likely to happen on browser startup, + // when there is a race between extension framework initialization and + // session restore, and general slowness such that the window initialization + // can take a little bit longer. + const chromeScript = SpecialPowers.loadChromeScript(() => { + const { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" + ); + + // The real WindowTracker has state that we cannot easily shake off. + // To test our behavior of interest, create a new WindowTracker instance. + const WindowTracker = + ExtensionParent.apiManager.global.windowTracker.constructor; + const windowTracker = new WindowTracker(); + const topWindowObserver = win => { + // Sanity check, to see if the conditions from bug 2004525 are met. + Assert.equal(win.location.href, "about:blank", "Initial URL"); + Assert.equal(win.document.readyState, "complete", "Initial readyState"); + Assert.ok( + win.document.isUncommittedInitialDocument, + "Is initial (uncommitted) about:blank" + ); + Assert.ok( + ![...windowTracker.browserWindows()].includes(win), + "windowTracker.browserWindows() does not include incomplete windows" + ); + // Regression test for bug 2004525: This assertion used to fail. + Assert.ok( + [...windowTracker.browserWindows(true)].includes(win), + "windowTracker.browserWindows(true) includes incomplete windows" + ); + // Regression test for bug 2004525: This listener used to not fire. + windowTracker.addOpenListener(openListener); + }; + const openListener = win => { + Assert.ok( + [...windowTracker.browserWindows()].includes(win), + "windowTracker.browserWindows() includes window from addOpenListener" + ); + windowTracker.removeOpenListener(openListener); + this.sendAsyncMessage("windowChecked"); + }; + Services.obs.addObserver(topWindowObserver, "toplevel-window-ready"); + this.addMessageListener("cleanup", () => { + Services.obs.removeObserver(topWindowObserver, "toplevel-window-ready"); + windowTracker.removeOpenListener(openListener); + }); + this.sendAsyncMessage("setup"); + }); + SimpleTest.registerCurrentTaskCleanupFunction(async () => { + await chromeScript.sendQuery("cleanup"); + chromeScript.destroy(); + }); + await chromeScript.promiseOneMessage("setup"); + const windowCheckedPromise = chromeScript.promiseOneMessage("windowChecked"); + + info("Opening new browser window"); + extension.sendMessage("openBrowserWindow"); + + info("Waiting for windowTracker checks to complete"); + await windowCheckedPromise; + + extension.sendMessage("closeBrowserWindow"); + await extension.awaitMessage("closedBrowserWindow"); + + await extension.unload(); +}); + +</script> + +</body> +</html>