tor-browser

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

commit 3b08e71bb5bef3f2ad259f0ab86f93aef85cd149
parent 38ad18e48ec3aba49a6e6a523d0e167c5654f783
Author: Mike Conley <mconley@mozilla.com>
Date:   Wed,  1 Oct 2025 18:39:30 +0000

Bug 1991490 - Cancel in-process XPIInstalls on quit-application-granted. r=rpl

Normally, the network:offline-about-to-go-offline topic would satisfy here - however,
in cases where we have an AsyncShutdown blocker that's preventing us from reaching
network:offline-about-to-go-offline AND that blocker is awaiting on an XPIInstall,
the quit-application-granted topic can help break that circular dependency.

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

Diffstat:
Mtoolkit/mozapps/extensions/internal/XPIInstall.sys.mjs | 16+++++++++++-----
Mtoolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 68 insertions(+), 7 deletions(-)

diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs @@ -99,6 +99,8 @@ const PREF_XPI_WEAK_SIGNATURES_ALLOWED = const PREF_SELECTED_THEME = "extensions.activeThemeID"; const TOOLKIT_ID = "toolkit@mozilla.org"; +const TOPIC_GOING_OFFLINE = "network:offline-about-to-go-offline"; +const TOPIC_QUIT_GRANTED = "quit-application-granted"; ChromeUtils.defineLazyGetter(lazy, "MOZ_UNSIGNED_SCOPES", () => { let result = 0; @@ -2424,9 +2426,11 @@ var DownloadAddonInstall = class extends AddonInstall { } } - observe() { - // Network is going offline - this.cancel(); + observe(_subject, topic, _data) { + if (topic == TOPIC_GOING_OFFLINE || topic == TOPIC_QUIT_GRANTED) { + // Network is going offline, or we're shutting down + this.cancel(); + } } /** @@ -2515,7 +2519,8 @@ var DownloadAddonInstall = class extends AddonInstall { } this.channel.asyncOpen(listener); - Services.obs.addObserver(this, "network:offline-about-to-go-offline"); + Services.obs.addObserver(this, TOPIC_GOING_OFFLINE); + Services.obs.addObserver(this, TOPIC_QUIT_GRANTED); } catch (e) { logger.warn( "Failed to start download for addon " + this.sourceURI.spec, @@ -2632,7 +2637,8 @@ var DownloadAddonInstall = class extends AddonInstall { this.stream.close(); this.channel = null; this.badCerthandler = null; - Services.obs.removeObserver(this, "network:offline-about-to-go-offline"); + Services.obs.removeObserver(this, TOPIC_GOING_OFFLINE); + Services.obs.removeObserver(this, TOPIC_QUIT_GRANTED); let crypto = this.crypto; this.crypto = null; diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js @@ -22,6 +22,9 @@ class TestListener { } onStartRequest(request) { + if (this.listener.onStartRequest) { + this.listener.onStartRequest(request); + } this.origListener.onStartRequest(request); } @@ -37,7 +40,7 @@ function startListener(listener) { let observer = { observe(subject) { let channel = subject.QueryInterface(Ci.nsIHttpChannel); - if (channel.URI.spec === "http://example.com/addons/test.xpi") { + if (channel.URI.spec.endsWith("/addons/test.xpi")) { let channelListener = new TestListener(listener); channelListener.origListener = subject .QueryInterface(Ci.nsITraceableChannel) @@ -49,7 +52,7 @@ function startListener(listener) { Services.obs.addObserver(observer, "http-on-modify-request"); } -add_task(async function setup() { +add_setup(async function setup() { let xpi = AddonTestUtils.createTempWebExtensionFile({ manifest: { name: "Test", @@ -109,3 +112,55 @@ add_task(async function test_install_user_cancelled() { "wait for temp file to be removed" ); }); + +add_task(async function test_install_cancelled_on_quit_or_offline() { + const TOPICS = [ + "quit-application-granted", + "network:offline-about-to-go-offline", + ]; + + const server = AddonTestUtils.createHttpServer({ + hosts: ["slow.example.com"], + }); + let pendingResponses = []; + server.registerPathHandler("/addons/test.xpi", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/x-xpinstall"); + pendingResponses.push(response); + response.processAsync(); + response.write("Here are some bytes"); + }); + const cleanupResponses = () => { + pendingResponses.forEach(res => res.finish()); + pendingResponses = []; + }; + registerCleanupFunction(cleanupResponses); + + for (let topic of TOPICS) { + info(`Verify DownloadAddonInstall cancelled on topic ${topic}`); + + let url = "http://slow.example.com/addons/test.xpi"; + let install = await AddonManager.getInstallForURL(url, { + name: "Test", + version: "1.0", + }); + let downloadStarted = new Promise(resolve => { + startListener({ + onStartRequest() { + resolve(); + }, + }); + }); + let installPromise = install.install(); + await downloadStarted; + equal(install.state, AddonManager.STATE_DOWNLOADING, "install was started"); + Services.obs.notifyObservers(null, topic); + await Assert.rejects( + installPromise, + /Install failed: onDownloadCancelled/, + "Got the expected install onDownloadCancelled failure" + ); + equal(install.state, AddonManager.STATE_CANCELLED, "install was cancelled"); + cleanupResponses(); + } +});