From 752bcbaa5f565a86e8c6b44ce616caef00297971 Mon Sep 17 00:00:00 2001 From: Duncan Platt Date: Sat, 28 Feb 2026 09:41:35 -0500 Subject: [PATCH 1/2] Fix performance and security issues across background and panorama view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance: - Fix onUpdated filter in view.js not being passed to addListener (comma expression instead of second argument), causing all tab updates to fire the handler instead of only filtered properties - Replace busy-wait polling loops in addon.tabs.js and addon.tabs.events.js with exponential backoff (5ms–160ms), preventing tight spinning on the sessions API and eliminating potential infinite hangs - Add tabs.warmup() on tab thumbnail hover to pre-render GPU resources for faster tab switching (Firefox 61+, with feature detection) - Replace (new Date).getTime() with Date.now() to avoid unnecessary object allocation Security: - Add explicit content_security_policy to manifest.json - Remove unused cookies permission from manifest.json - Fix undeclared variable in addon.tabGroups.js remove() that caused the tab-removal verification to fail silently under strict mode - Validate sender.id in background message handler to reject messages from external extensions - Add try/catch around JSON.parse when loading backup files to prevent crashes on malformed input - Add try/catch around decodeURI in tab tooltips to handle malformed percent-encoded URLs gracefully Also adds undefined guards in activated() to prevent calling toggleVisibleTabs with an undefined groupId (which would hide all tabs) and setActiveId (which would trigger a lock leak in the mutex). --- src/background/addon.js | 1 + src/background/addon.tabGroups.js | 6 +++--- src/background/addon.tabs.events.js | 27 ++++++++++++++++++--------- src/background/addon.tabs.js | 14 ++++++++++++-- src/background/backup.js | 2 +- src/manifest.json | 3 ++- src/options/options.backup.js | 8 +++++++- src/panorama/js/html.tabs.js | 10 +++++++++- src/panorama/js/view.js | 4 ++-- 9 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/background/addon.js b/src/background/addon.js index 4c1b5d5..f16e40f 100644 --- a/src/background/addon.js +++ b/src/background/addon.js @@ -22,6 +22,7 @@ export const addon = { function handleActions(message, sender, sendResponse) { if (!message.action) return; + if (sender.id !== browser.runtime.id) return; let response; diff --git a/src/background/addon.tabGroups.js b/src/background/addon.tabGroups.js index ec7ab80..abe10f9 100644 --- a/src/background/addon.tabGroups.js +++ b/src/background/addon.tabGroups.js @@ -100,7 +100,7 @@ export async function create(info = {}, currentWindowId) { title: info.title || browser.i18n.getMessage('defaultGroupName'), windowId: info.windowId, - lastAccessed: (new Date).getTime(), // temporary + lastAccessed: Date.now(), // temporary }; let unlock = await groupLock.lock(); @@ -182,7 +182,7 @@ export async function remove(groupId) { // check if tabs were removed and abort if not (beforeunload was called or something) for (const tabId of tabsToRemove) { try { - tab = await browser.tabs.get(tabId); + const tab = await browser.tabs.get(tabId); return undefined; } catch (error) { // all good, tab was removed @@ -228,7 +228,7 @@ export async function update(groupId, info = {}) { group.rect = info.rect; } - group.lastAccessed = (new Date).getTime(); + group.lastAccessed = Date.now(); await saveGroups(); diff --git a/src/background/addon.tabs.events.js b/src/background/addon.tabs.events.js index 70fe7d7..d77ec4e 100644 --- a/src/background/addon.tabs.events.js +++ b/src/background/addon.tabs.events.js @@ -6,6 +6,10 @@ import * as core from './core.js'; import * as backup from './backup.js'; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + export function initialize() { browser.tabs.onCreated.addListener(created); @@ -40,8 +44,9 @@ async function created(tab) { if (!groupsExists) { tab.groupId = undefined; - while (tab.groupId == undefined) { + for (let delay = 5; tab.groupId == undefined && delay <= 160; delay *= 2) { tab.groupId = await addon.tabGroups.getActiveId(tab.windowId); + if (tab.groupId == undefined) await sleep(delay); } } } @@ -76,8 +81,9 @@ async function attached(tabId, attachInfo) { let activeGroup = undefined; - while (activeGroup == undefined) { + for (let delay = 5; activeGroup == undefined && delay <= 160; delay *= 2) { activeGroup = await addon.tabGroups.getActiveId(attachInfo.newWindowId); + if (activeGroup == undefined) await sleep(delay); } await addon.tabs.setGroupId(tabId, activeGroup); } @@ -105,12 +111,10 @@ async function updated(tabId, changeInfo, tab) { } if (tab.groupId == undefined) { - const start = (new Date).getTime(); - while (tab.groupId == undefined) { + for (let delay = 5; tab.groupId == undefined && delay <= 80; delay *= 2) { tab.groupId = await addon.tabs.getGroupId(tab.id); - if (((new Date).getTime() - start) > 50) break; // timeout + if (tab.groupId == undefined) await sleep(delay); } - } const sending = browser.runtime.sendMessage({event: 'browser.tabs.onUpdated', tabId: tabId, changeInfo: changeInfo, tab: tab}); @@ -134,14 +138,19 @@ async function activated(activeInfo) { if (!groupsExists) { tabGroupId = undefined; - while (tabGroupId == undefined) { + for (let delay = 5; tabGroupId == undefined && delay <= 160; delay *= 2) { tabGroupId = await addon.tabGroups.getActiveId(activeInfo.windowId); + if (tabGroupId == undefined) await sleep(delay); } } // ---- - addon.tabGroups.setActiveId(tab.windowId, tabGroupId); + if (tabGroupId != undefined) { + addon.tabGroups.setActiveId(tab.windowId, tabGroupId); + } + } + if (tabGroupId != undefined) { + core.toggleVisibleTabs(tab.windowId, tabGroupId); } - core.toggleVisibleTabs(tab.windowId, tabGroupId); } } diff --git a/src/background/addon.tabs.js b/src/background/addon.tabs.js index a366f4a..845cf57 100644 --- a/src/background/addon.tabs.js +++ b/src/background/addon.tabs.js @@ -1,6 +1,10 @@ 'use strict'; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + export function setGroupId(tabId, groupId) { return browser.sessions.setTabValue(tabId, 'groupId', groupId); } @@ -11,9 +15,15 @@ export function getGroupId(tabId) { export async function getGroupIdTimeout(tabId, timeout) { let groupId = undefined; - const start = (new Date).getTime(); - while (groupId == undefined && (((new Date).getTime() - start) < timeout)) { + let elapsed = 0; + let delay = 5; + while (groupId == undefined && elapsed < timeout) { groupId = await browser.sessions.getTabValue(tabId, 'groupId'); + if (groupId == undefined) { + await sleep(delay); + elapsed += delay; + delay = Math.min(delay * 2, 50); + } } return groupId; } diff --git a/src/background/backup.js b/src/background/backup.js index df1afd0..229ca27 100644 --- a/src/background/backup.js +++ b/src/background/backup.js @@ -174,7 +174,7 @@ async function autoBackup() { } let backup = { - time: (new Date).getTime(), + time: Date.now(), data: await create() } diff --git a/src/manifest.json b/src/manifest.json index 2823128..82fe206 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -73,13 +73,14 @@ "browser_style": true }, + "content_security_policy": "script-src 'self'; object-src 'self'", + "permissions": [ "", "tabs", "tabHide", "storage", "sessions", - "cookies", "contextualIdentities", "downloads", "menus" diff --git a/src/options/options.backup.js b/src/options/options.backup.js index 233fdd5..2ce1c06 100644 --- a/src/options/options.backup.js +++ b/src/options/options.backup.js @@ -69,7 +69,13 @@ async function loadBackup() { const reader = new FileReader(); reader.onload = (json) => { - let backup = JSON.parse(json.target.result); + let backup; + try { + backup = JSON.parse(json.target.result); + } catch (e) { + alert(browser.i18n.getMessage('optionLoadError')); + return; + } if ((backup.version && backup.version[0] == 'tabGroups' || backup.version && backup.version[0] == 'sessionrestore') && backup.version[1] == 1) { // convert from old tab groups backup to version 1 (legacy) diff --git a/src/panorama/js/html.tabs.js b/src/panorama/js/html.tabs.js index 3efd081..a844fd4 100644 --- a/src/panorama/js/html.tabs.js +++ b/src/panorama/js/html.tabs.js @@ -19,6 +19,12 @@ export function create(tab) { const node = newElement('div', {href: '', class: 'tab', draggable: 'true', 'data-id': tab.id, title: '', tabindex: 0}, [container, context]); + if (browser.tabs.warmup) { + node.addEventListener('mouseenter', () => { + browser.tabs.warmup(tab.id).catch(() => {}); + }, false); + } + node.addEventListener('click', (event) => { event.preventDefault(); if (event.ctrlKey) { @@ -90,7 +96,9 @@ export async function update(tabNode, tab) { tabNode.querySelector('.context').title = contextInfo.name; } - tabNode.title = tab.title + ((tab.url.substr(0, 5) != 'data:') ? ' - ' + decodeURI(tab.url) : ''); + let displayUrl = tab.url; + try { displayUrl = decodeURI(tab.url); } catch (e) { /* malformed URI */ } + tabNode.title = tab.title + ((tab.url.substr(0, 5) != 'data:') ? ' - ' + displayUrl : ''); if (tab.discarded) { tabNode.classList.add('inactive'); diff --git a/src/panorama/js/view.js b/src/panorama/js/view.js index 978c760..f1ec5b3 100644 --- a/src/panorama/js/view.js +++ b/src/panorama/js/view.js @@ -58,7 +58,7 @@ document.addEventListener('DOMContentLoaded', async() => { document.addEventListener('visibilitychange', async() => { if (document.hidden) { browser.tabs.onUpdated.removeListener(captureThumbnail); - viewLastAccessed = (new Date).getTime(); + viewLastAccessed = Date.now(); } else { await captureThumbnails(); browser.tabs.onUpdated.addListener(captureThumbnail, {properties: ['url', 'status']}); @@ -127,7 +127,7 @@ document.addEventListener('DOMContentLoaded', async() => { // tab events browser.tabs.onCreated.addListener(events.tabCreated); browser.tabs.onRemoved.addListener(events.tabRemoved); - browser.tabs.onUpdated.addListener(events.tabUpdated), {properties: ['favIconUrl', 'pinned', 'title', 'url', 'discarded', 'status']}; + browser.tabs.onUpdated.addListener(events.tabUpdated, {properties: ['favIconUrl', 'pinned', 'title', 'url', 'discarded', 'status']}); browser.tabs.onActivated.addListener(events.tabActivated); From 9db21ac71953af29167ac7d241eec7d8f9e899e5 Mon Sep 17 00:00:00 2001 From: Duncan Platt Date: Sat, 28 Feb 2026 10:11:18 -0500 Subject: [PATCH 2/2] Fix sender validation to allow messages from options page The strict sender.id !== browser.runtime.id check rejected messages from the options page when embedded in about:addons, breaking backup save/load and interval settings. Loosen to only reject messages where sender.id is explicitly set to a different extension's ID. --- src/background/addon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/background/addon.js b/src/background/addon.js index f16e40f..f83ed4e 100644 --- a/src/background/addon.js +++ b/src/background/addon.js @@ -22,7 +22,7 @@ export const addon = { function handleActions(message, sender, sendResponse) { if (!message.action) return; - if (sender.id !== browser.runtime.id) return; + if (sender.id && sender.id !== browser.runtime.id) return; let response;