no-bug: Adress possible memory leaks (gh-13242)

This commit is contained in:
mr. m
2026-04-14 01:03:47 +02:00
committed by GitHub
parent d540c6cddf
commit 7ed7b63b08
4 changed files with 82 additions and 44 deletions

View File

@@ -15,7 +15,7 @@
value: "alt" # ctrl, alt, shift
- name: zen.glance.animation-duration
value: 350 # in milliseconds
value: 300 # in milliseconds
- name: zen.glance.deactivate-docshell-during-animation
value: true

View File

@@ -318,7 +318,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
data.width,
data.height
);
return await this.#imageBitmapToBase64(
return await this.#imageBitmapToObjectURL(
await window.browsingContext.currentWindowGlobal.drawSnapshot(
rect,
1,
@@ -785,7 +785,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
// Batch all style/attribute writes together to avoid interleaved
// read/write layout thrashing.
this.browserWrapper.style.transformOrigin = "";
this.browserWrapper.style.height = "100%";
this.browserWrapper.style.width = "80%";
this.browserWrapper.removeAttribute("animate");
@@ -987,16 +986,31 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
}
}
async #imageBitmapToBase64(imageBitmap) {
// Use OffscreenCanvas + blob URL to avoid blocking the main thread
// with synchronous base64 encoding from toDataURL().
async #imageBitmapToObjectURL(imageBitmap) {
// OffscreenCanvas + convertToBlob avoids the synchronous PNG re-encode
// and base64 string copy that toDataURL performs on the main thread.
// Callers must release the URL via #deleteGlance when the glance entry
// is removed so the blob can be freed.
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(imageBitmap, 0, 0);
const blob = await canvas.convertToBlob({ type: "image/png" });
imageBitmap.close();
return URL.createObjectURL(blob);
}
#deleteGlance(glanceID) {
const entry = this.#glances.get(glanceID);
if (!entry) {
return;
}
this.#glances.delete(glanceID);
const url = entry.elementData ?? entry.elementImageData;
if (typeof url === "string") {
URL.revokeObjectURL(url);
}
}
/**
* Animate parent background restoration
*
@@ -1196,7 +1210,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
*/
#resetGlanceState(setNewID) {
this.#currentParentTab.removeAttribute("glance-id");
this.#glances.delete(this.#currentGlanceID);
this.#deleteGlance(this.#currentGlanceID);
this.#currentGlanceID = setNewID;
this.#duringOpening = false;
}
@@ -1545,7 +1559,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
this.animatingFullOpen = false;
const glanceID = this.#currentGlanceID;
this.closeGlance({ noAnimation: true, skipPermitUnload: true });
this.#glances.delete(glanceID);
this.#deleteGlance(glanceID);
}
/**

View File

@@ -22,13 +22,15 @@ export class ZenGlanceParent extends JSWindowActorParent {
break;
}
case "ZenGlance:CloseGlance": {
const params = {
// Explicitly allowlist fields from content; never forward
// skipPermitUnload or other privileged flags.
const { noAnimation, setNewID, hasFocused } = message.data ?? {};
this.browsingContext.topChromeWindow.gZenGlanceManager.closeGlance({
onTabClose: true,
...message.data,
};
this.browsingContext.topChromeWindow.gZenGlanceManager.closeGlance(
params
);
noAnimation: !!noAnimation,
setNewID: typeof setNewID === "string" ? setNewID : null,
hasFocused: !!hasFocused,
});
break;
}
case "ZenGlance:RecordLinkClickData": {

View File

@@ -7,6 +7,10 @@ import {
nsZenMultiWindowFeature,
} from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
const DOT_RE = /\./g;
const WHITESPACE_RE = /\s/g;
const NON_NAME_RE = /[^A-Za-z_-]+/g;
/**
* Zen Mods Manager, handles downloading, updating and applying Zen Mods.
*
@@ -171,47 +175,57 @@ class nsZenMods extends nsZenPreloadedFeature {
}
#writeToDom(modsWithPreferences) {
for (const browser of nsZenMultiWindowFeature.browsers) {
// Precompute per-mod data once; values are global prefs and sanitized
// names don't vary per browser window, so this hoists O(windows) work.
const prepared = modsWithPreferences.map(
// eslint-disable-next-line no-shadow
for (const { enabled, preferences, name } of modsWithPreferences) {
const sanitizedName = this.sanitizeModName(name);
({ enabled, preferences, name }) => ({
enabled,
sanitizedName: this.sanitizeModName(name),
prefs: preferences.map(({ property, type }) => ({
property,
type,
sanitizedProperty: property?.replaceAll(DOT_RE, "-"),
value:
enabled === undefined || enabled
? Services.prefs.getStringPref(property, "")
: "",
})),
})
);
for (const browser of nsZenMultiWindowFeature.browsers) {
const doc = browser.document;
const root = doc.documentElement;
for (const { enabled, sanitizedName, prefs } of prepared) {
if (enabled !== undefined && !enabled) {
const element = browser.document.getElementById(sanitizedName);
const element = doc.getElementById(sanitizedName);
if (element) {
element.remove();
}
for (const { property } of preferences.filter(
({ type }) => type !== "checkbox"
)) {
const sanitizedProperty = property?.replaceAll(/\./g, "-");
browser.document
.querySelector(":root")
.style.removeProperty(`--${sanitizedProperty}`);
for (const { type, sanitizedProperty } of prefs) {
if (type === "checkbox") {
continue;
}
root.style.removeProperty(`--${sanitizedProperty}`);
}
continue;
}
for (const { property, type } of preferences) {
const value = Services.prefs.getStringPref(property, "");
const sanitizedProperty = property?.replaceAll(/\./g, "-");
for (const { type, sanitizedProperty, value } of prefs) {
switch (type) {
case "dropdown": {
if (value !== "") {
let element = browser.document.getElementById(sanitizedName);
let element = doc.getElementById(sanitizedName);
if (!element) {
element = browser.document.createElement("div");
element = doc.createElement("div");
element.style.display = "none";
element.setAttribute("id", sanitizedName);
browser.document.body.appendChild(element);
doc.body.appendChild(element);
}
element.setAttribute(sanitizedProperty, value);
@@ -221,13 +235,9 @@ class nsZenMods extends nsZenPreloadedFeature {
case "string": {
if (value === "") {
browser.document
.querySelector(":root")
.style.removeProperty(`--${sanitizedProperty}`);
root.style.removeProperty(`--${sanitizedProperty}`);
} else {
browser.document
.querySelector(":root")
.style.setProperty(`--${sanitizedProperty}`, value);
root.style.setProperty(`--${sanitizedProperty}`, value);
}
break;
}
@@ -308,6 +318,19 @@ class nsZenMods extends nsZenPreloadedFeature {
}
async #downloadUrlToFile(url, path, maxRetries = 3, retryDelayMs = 500) {
// Mod assets must come over HTTPS. Without a signing/hash scheme this
// is the minimum guard against MITM or a store-hosted HTTP redirect
// serving attacker-controlled CSS/JSON into the chrome profile.
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`[ZenMods]: Invalid mod asset URL: ${url}`);
}
if (parsed.protocol !== "https:") {
throw new Error(`[ZenMods]: Refusing non-HTTPS mod asset URL: ${url}`);
}
let attempt = 0;
while (attempt < maxRetries) {
@@ -377,8 +400,7 @@ class nsZenMods extends nsZenPreloadedFeature {
sanitizeModName(aName) {
// Do not change to "mod-" for backwards compatibility
// eslint-disable-next-line no-shadow
return `theme-${aName?.replaceAll(/\s/g, "-")?.replaceAll(/[^A-Za-z_-]+/g, "")}`;
return `theme-${aName?.replaceAll(WHITESPACE_RE, "-")?.replaceAll(NON_NAME_RE, "")}`;
}
get updatePref() {