"use strict"; const { ExtensionPermissions } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionPermissions.sys.mjs" ); const { ExtensionTestCommon } = ChromeUtils.importESModule( "resource://testing-common/ExtensionTestCommon.sys.mjs" ); const { ExtensionUninstallTracker } = ChromeUtils.importESModule( "resource://testing-common/ExtensionTestCommon.sys.mjs" ); AddonTestUtils.init(this); AddonTestUtils.overrideCertDB(); AddonTestUtils.createAppInfo( "xpcshell@tests.mozilla.org", "XPCShell", "1", "42" ); // This test doesn't need the test extensions to be detected as privileged, // disabling it to avoid having to keep the list of expected "internal:*" // permissions that are added automatically to privileged extensions // and already covered by other tests. AddonTestUtils.usePrivilegedSignatures = false; // Look up the cached permissions, if any. async function getCachedPermissions(extensionId) { const NotFound = Symbol("extension ID not found in permissions cache"); try { return await ExtensionParent.StartupCache.permissions.get( extensionId, () => { // Throw error to prevent the key from being created. throw NotFound; } ); } catch (e) { if (e === NotFound) { return null; } throw e; } } // Look up the permissions from the file. Internal methods are used to avoid // inadvertently changing the permissions in the cache or the database. async function getStoredPermissions(extensionId) { if (await ExtensionPermissions._has(extensionId)) { return ExtensionPermissions._get(extensionId); } return null; } add_setup(async function setup() { // Bug 1646182: Force ExtensionPermissions to run in rkv mode, the legacy // storage mode will run in xpcshell-legacy-ep.toml await ExtensionPermissions._uninit(); optionalPermissionsPromptHandler.init(); optionalPermissionsPromptHandler.acceptPrompt = true; await AddonTestUtils.promiseStartupManager(); registerCleanupFunction(async () => { await AddonTestUtils.promiseShutdownManager(); }); }); // This test must run before any restart of the addonmanager so the // ExtensionAddonObserver works. add_task(async function test_permissions_removed() { let extension = ExtensionTestUtils.loadExtension({ manifest: { optional_permissions: ["idle"], }, background() { browser.test.onMessage.addListener(async (msg, arg) => { if (msg == "request") { try { let result = await browser.permissions.request(arg); browser.test.sendMessage("request.result", result); } catch (err) { browser.test.sendMessage("request.result", err.message); } } }); }, useAddonManager: "temporary", }); await extension.startup(); await withHandlingUserInput(extension, async () => { extension.sendMessage("request", { permissions: ["idle"], origins: [] }); let result = await extension.awaitMessage("request.result"); equal(result, true, "request() for optional permissions succeeded"); }); let id = extension.id; let perms = await ExtensionPermissions.get(id); equal( perms.permissions.length, 1, `optional permission added (${JSON.stringify(perms.permissions)})` ); Assert.deepEqual( await getCachedPermissions(id), { permissions: ["idle"], origins: [], data_collection: [], }, "Optional permission added to cache" ); Assert.deepEqual( await getStoredPermissions(id), { permissions: ["idle"], origins: [], data_collection: [], }, "Optional permission added to persistent file" ); await extension.unload(); // Directly read from the internals instead of using ExtensionPermissions.get, // because the latter will lazily cache the extension ID. Assert.deepEqual( await getCachedPermissions(id), null, "Cached permissions removed" ); Assert.deepEqual( await getStoredPermissions(id), null, "Stored permissions removed" ); perms = await ExtensionPermissions.get(id); equal( perms.permissions.length, 0, `no permissions after uninstall (${JSON.stringify(perms.permissions)})` ); equal( perms.origins.length, 0, `no origin permissions after uninstall (${JSON.stringify(perms.origins)})` ); // The public ExtensionPermissions.get method should not store (empty) // permissions in the persistent database. Polluting the cache is not ideal, // but acceptable since the cache will eventually be cleared, and non-test // code is not likely to call ExtensionPermissions.get() for non-installed // extensions anyway. Assert.deepEqual(await getCachedPermissions(id), perms, "Permissions cached"); Assert.deepEqual( await getStoredPermissions(id), null, "Permissions not saved" ); }); // Regression test for bug 1902011: Verifies that a slow write does not result // in the extension permissions continuing to be around post extension unload. add_task(async function test_simulate_slow_storage() { await ExtensionPermissions._uninit(); const interceptedStorePutCalls = []; const deferredPut = Promise.withResolvers(); let store = ExtensionPermissions._getStore(); let originalPut = store.put; store.put = async function (extensionId, permissions) { // _setupStartupPermissions in Extension.sys.mjs will call us indirectly as // part as granting host_permissions of the MV3 test extension. interceptedStorePutCalls.push({ extensionId, permissions }); info("Intercepted and suspended store.put call"); await deferredPut.promise; info("Continuing store.put call"); return originalPut.apply(this, arguments); }; let extension = ExtensionTestUtils.loadExtension({ manifest: { manifest_version: 3, host_permissions: ["*://slow.example.com/*"], }, useAddonManager: "temporary", }); await extension.startup(); const id = extension.id; // We are simulating slow writes, but don't delay reads. As part of loading // an extension for the first time there is an ExtensionPermissions.get call // that initializes the cached permissions (before host_permissions are // granted via _setupStartupPermissions in Extension.sys.mjs). Assert.deepEqual( await getCachedPermissions(id), { permissions: [], origins: [], data_collection: [] }, "Cached permissions should be present despite simulated slow storage" ); Assert.deepEqual( await getStoredPermissions(id), null, "No stored permissions due to the simulated slow storage" ); let cleanupDone = false; let removeAllDone = false; Management.once("cleanupAfterUninstall", (_, addonId, tasks) => { equal(addonId, id, "Got cleanupAfterUninstall for expected extension"); ok(!cleanupDone, "Extension cleanup has not finished yet"); const name = `Clear ExtensionPermissions for ${addonId}`; let removeAllPromise = tasks.find(t => t.name === name).promise; removeAllPromise.then(() => { removeAllDone = true; }); // Delay for a bit to allow any other async tasks to potentially complete // and potentially pollute any state. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(() => { ok(!removeAllDone, "ExtensionPermissions.removeAll() is still pending"); ok(!cleanupDone, "Extension cleanup has still not finished yet"); deferredPut.resolve(); }, 1000); }); const uninstallTracker = new ExtensionUninstallTracker(id); await extension.unload(); let unrelatedExt = ExtensionTestUtils.loadExtension({ manifest: { manifest_version: 2, permissions: ["tabs", "*://example.com/*"], optional_permissions: ["webNavigation"], browser_specific_settings: { gecko: { id: "unrelated@ext-id" } }, }, useAddonManager: "temporary", }); // As explained earlier, ExtensionPermissions.get is called for the first // startup. unrelatedExt's startup should not be affected by the write // operation of |extension|. info("Unrelated extension can start up without delay"); await unrelatedExt.startup(); // Note: waitForUninstallCleanupDone() depends on removeAll()'s completion, // which should not resolve until we call deferredPut.resolve(). ok(!removeAllDone, "ExtensionPermissions.removeAll() not done yet"); await uninstallTracker.waitForUninstallCleanupDone(); cleanupDone = true; ok(removeAllDone, "ExtensionPermissions.removeAll() completed"); Assert.deepEqual( await getCachedPermissions(id), null, "No cached permissions past extension uninstall" ); Assert.deepEqual( await getStoredPermissions(id), null, "No stored permissions past extension uninstall" ); store.put = originalPut; Assert.deepEqual( interceptedStorePutCalls, [ { extensionId: id, permissions: { permissions: [], origins: ["*://slow.example.com/*"], data_collection: [], }, }, ], "Observed attempt to write host_permissions through ExtensionPermissions" ); // Now verify that the unrelated extension can unload as usual now that we // have the usual non-delayed writes. const uninstallTracker2 = new ExtensionUninstallTracker(unrelatedExt.id); await unrelatedExt.unload(); info("Unrelated extension unloaded, waiting for cleanup"); await uninstallTracker2.waitForUninstallCleanupDone(); info("Unrelated extension managed to uninstall and cleanup"); }); // Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1974419 // When an extension package is parsed with ExtensionData, e.g. via // loadManifestFromWebManifest as part of extension installation prompt, // no data should be written anywhere. add_task(async function test_ExtensionData_does_not_write_permissions() { const ID = "extension-id@that-is-never-installed"; let generatedExt = ExtensionTestCommon.generate({ manifest: { host_permissions: ["*://example.com/*"], permissions: ["tabs", "webNavigation", "unlimitedStorage"], browser_specific_settings: { gecko: { id: ID } }, }, }); const extensionData = new ExtensionData(generatedExt.rootURI); await extensionData.loadManifest(); equal( await getCachedPermissions(ID), null, "ExtensionData should not write any permissions to StartupCache" ); await generatedExt.cleanupGeneratedFile(); });