gh-14843: Prevent accessing old media data (gh-14888)

This commit is contained in:
mr. m
2026-08-08 20:24:28 +02:00
committed by GitHub
parent 73ed322695
commit 2a887a5f2e
12 changed files with 440 additions and 36 deletions

View File

@@ -0,0 +1,136 @@
diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -32,11 +32,11 @@
*/
@media (-moz-gtk-csd-transparency-available) {
:root[customtitlebar] {
background-color: transparent;
- &[sizemode="normal"]:not([gtktiledwindow]) {
+ &[sizemode="normal"]:not([tiled]) {
/* Firefox draws its contents to a child window, while GTK takes care of
* drawing the toplevel (which in most cases is just the window
* decorations).
*
* Due to how X11 child windows work, pixels painted by a child window will
diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css
--- a/browser/themes/shared/browser-shared.css
+++ b/browser/themes/shared/browser-shared.css
@@ -433,18 +433,18 @@
}
}
@media (-moz-platform: linux) and (-moz-gtk-csd-reversed-placement: 0) {
:root:not([sizemode="normal"]) &[type="pre-tabs"],
- :root[gtktiledwindow] &[type="pre-tabs"] {
+ :root[tiled] &[type="pre-tabs"] {
display: none;
}
}
@media (-moz-gtk-csd-reversed-placement) {
:root:not([sizemode="normal"]) &[type="post-tabs"],
- :root[gtktiledwindow] &[type="post-tabs"] {
+ :root[tiled] &[type="post-tabs"] {
display: none;
}
}
@media (max-width: 500px) {
diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -3683,11 +3683,11 @@
#ifdef ACCESSIBILITY
DispatchRestoreEventAccessible();
#endif // ACCESSIBILITY
}
- mIsTiled = aEvent->new_window_state & GDK_WINDOW_STATE_TILED;
+ SetIsTiled(aEvent->new_window_state & GDK_WINDOW_STATE_TILED);
LOG("\tTiled: %d\n", int(mIsTiled));
mResizableEdges = [&] {
Sides result;
if (mSizeMode != nsSizeMode_Normal) {
return result;
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -1379,10 +1379,12 @@
protected:
// Returns whether compositing should use an external surface size.
virtual bool UseExternalCompositingSurface() const { return false; }
+ void SetIsTiled(bool);
+
/**
* Starts the OMTC compositor destruction sequence.
*
* When this function returns, the compositor should not be
* able to access the opengl context anymore.
diff --git a/widget/nsIWidget.cpp b/widget/nsIWidget.cpp
--- a/widget/nsIWidget.cpp
+++ b/widget/nsIWidget.cpp
@@ -327,10 +327,16 @@
void nsIWidget::QuitIME() {
IMEStateManager::WidgetOnQuit(this);
this->mIMEHasQuit = true;
}
+void nsIWidget::SetIsTiled(bool aIsTiled) {
+ // TODO: Do we want to report an event here or something when stuff changes?
+ // For now this is propagated in a somewhat out-of-band way via AppWindow.
+ mIsTiled = aIsTiled;
+}
+
void nsIWidget::DestroyCompositor() {
RevokeTransactionIdAllocator();
// We release this before releasing the compositor, since it may hold the
// last reference to our ClientLayerManager. ClientLayerManager's dtor can
diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -1512,11 +1512,10 @@
Atom("gamma", "gamma"),
Atom("glyphRef", "glyphRef"),
Atom("grad", "grad"),
Atom("gradientTransform", "gradientTransform"),
Atom("gradientUnits", "gradientUnits"),
- Atom("gtktiledwindow", "gtktiledwindow"),
Atom("hardLight", "hard-light"),
Atom("hue", "hue"),
Atom("hueRotate", "hueRotate"),
Atom("identity", "identity"),
Atom("image_rendering", "image-rendering"),
@@ -1950,10 +1949,11 @@
Atom("superscriptshift", "superscriptshift"),
Atom("symmetric", "symmetric"),
Atom("tanh", "tanh"),
Atom("tan", "tan"),
Atom("tendsto", "tendsto"),
+ Atom("tiled", "tiled"),
Atom("times", "times"),
Atom("transpose", "transpose"),
Atom("union_", "union"),
Atom("uplimit", "uplimit"),
Atom("variance", "variance"),
diff --git a/xpfe/appshell/AppWindow.cpp b/xpfe/appshell/AppWindow.cpp
--- a/xpfe/appshell/AppWindow.cpp
+++ b/xpfe/appshell/AppWindow.cpp
@@ -1992,11 +1992,11 @@
aRootElement.SetAttr(nsGkAtoms::sizemode, sizeString, IgnoreErrors());
if (aShouldPersist && aPersistString.Find(u"sizemode") >= 0) {
(void)SetPersistentValue(nsGkAtoms::sizemode, sizeString);
}
}
- aRootElement.SetBoolAttr(nsGkAtoms::gtktiledwindow, mWindow->IsTiled());
+ aRootElement.SetBoolAttr(nsGkAtoms::tiled, mWindow->IsTiled());
}
void AppWindow::SavePersistentAttributes(
const PersistentAttributes aAttributes) {
// can happen when the persistence timer fires at an inopportune time

View File

@@ -0,0 +1,18 @@
diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -6423,10 +6423,13 @@
// Skip window size change events below on minimization.
return;
}
}
+ // Recompute tiled state.
+ SetIsTiled(mWnd && ::IsWindowArranged(mWnd));
+
// Notify visibility change when window is activated.
if (!(wp->flags & SWP_NOACTIVATE) && NeedsToTrackWindowOcclusionState()) {
WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
this, mFrameState->GetSizeMode() != nsSizeMode_Minimized);
}

View File

@@ -0,0 +1,72 @@
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -70,10 +70,11 @@
#include "nsDocShell.h"
#include "gfxPlatform.h"
#include "qcms.h"
+#import <objc/runtime.h>
#include "mozilla/AutoRestore.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/dom/Document.h"
#include "mozilla/Maybe.h"
#include "mozilla/NativeKeyBindingsType.h"
@@ -6948,10 +6949,31 @@
return nsSizeMode_Maximized;
}
return nsSizeMode_Normal;
}
+// The tiling state lives in a private NSWindow ivar "_tilingStateController"
+// (an _NSWMWindowTilingStateController). That controller's -tilingState returns
+// a non-nil _WMWindowTilingState only while the window is tiled by the system
+// window manager. None of this is public API, so it is all guarded and degrades
+// to false if the internals ever change.
+static bool WindowIsTiled(NSWindow* aWindow) {
+ if (!aWindow) {
+ return false;
+ }
+ static Ivar sControllerIvar =
+ class_getInstanceVariable([NSWindow class], "_tilingStateController");
+ if (!sControllerIvar) {
+ return false;
+ }
+ id controller = object_getIvar(aWindow, sControllerIvar);
+ if (![controller respondsToSelector:@selector(tilingState)]) {
+ return false;
+ }
+ return [controller performSelector:@selector(tilingState)] != nil;
+}
+
void nsCocoaWindow::ReportMoveEvent() {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
// Prevent recursion, which can become infinite (see bug 708278). This
// can happen when the call to [NSWindow setFrameTopLeftPoint:] in
@@ -6961,10 +6983,11 @@
return;
}
mInReportMoveEvent = true;
UpdateBounds();
+ SetIsTiled(WindowIsTiled(mWindow));
// The zoomed state can change when we're moving, in which case we need to
// update our internal mSizeMode. This can happen either if we're maximized
// and then moved, or if we're not maximized and moved back to zoomed state.
if (mWindow && (mSizeMode == nsSizeMode_Maximized) ^ mWindow.isZoomed) {
@@ -7055,10 +7078,11 @@
void nsCocoaWindow::ReportSizeEvent() {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
UpdateBounds();
+ SetIsTiled(WindowIsTiled(mWindow));
LayoutDeviceIntRect innerBounds = GetClientBounds();
if (mWidgetListener) {
mWidgetListener->WindowResized(this, innerBounds.Size());
}
if (mAttachedWidgetListener) {

View File

@@ -39,5 +39,14 @@
"type": "phabricator",
"id": "D291714",
"name": "gh-12979 Clip dirty_rect to device_size"
},
{
"type": "phabricator",
"ids": [
"D312028",
"D312036",
"D312091"
],
"name": "Expose tiled attribute to all platforms"
}
]

View File

@@ -22,27 +22,64 @@ class ZenSidebarNotification extends MozLitElement {
static properties = {
headingL10nId: { type: String, fluent: true },
links: { type: Array },
autoHideMs: { type: Number },
};
constructor({ headingL10nId = "", links = [] } = {}) {
#autoHideAnimation = null;
constructor({ headingL10nId = "", links = [], autoHideMs = 0 } = {}) {
super();
this.headingL10nId = headingL10nId;
this.links = links;
this.autoHideMs = autoHideMs;
// Hold the countdown while the user is reading.
this.addEventListener("mouseenter", () => this.#autoHideAnimation?.pause());
this.addEventListener("mouseleave", () => this.#autoHideAnimation?.play());
}
connectedCallback() {
super.connectedCallback();
if (this.parentElement) {
this.#animateIn();
this.#animateIn().then(() => this.#startAutoHide());
}
}
remove() {
if (this.#autoHideAnimation?.playState !== "finished") {
// Stop a running countdown; a finished one must keep its fill so
// the bar doesn't snap back to full during the fade-out.
this.#autoHideAnimation?.cancel();
}
this.#autoHideAnimation = null;
this.#animateOut().then(() => {
super.remove();
});
}
async #startAutoHide() {
if (!this.autoHideMs || !this.isConnected) {
return;
}
await this.updateComplete;
const bar = this.shadowRoot?.querySelector(
".zen-sidebar-notification-progress-bar"
);
if (!bar || this.#autoHideAnimation) {
return;
}
this.#autoHideAnimation = bar.animate(
{ transform: ["scaleX(1)", "scaleX(0)"] },
{ duration: this.autoHideMs, easing: "linear", fill: "forwards" }
);
this.#autoHideAnimation.finished.then(
() => this.remove(),
() => {}
);
if (this.matches(":hover")) {
this.#autoHideAnimation.pause();
}
}
render() {
return html`
<link
@@ -62,6 +99,15 @@ class ZenSidebarNotification extends MozLitElement {
<img src="chrome://browser/skin/zen-icons/close.svg" />
</div>
</div>
${
this.autoHideMs
? html`
<div class="zen-sidebar-notification-progress">
<div class="zen-sidebar-notification-progress-bar"></div>
</div>
`
: null
}
<div class="zen-sidebar-notification-body">
${this.links.map(
link => html`

View File

@@ -7,6 +7,7 @@ import createSidebarNotification from "chrome://browser/content/zen-components/Z
const ZEN_UPDATE_PREF = "zen.updates.last-version";
const ZEN_BUILD_ID_PREF = "zen.updates.last-build-id";
const ZEN_UPDATE_SHOW = "zen.updates.show-update-notification";
const ZEN_UPDATE_NOTIFICATION_TIMEOUT_MS = 15000;
export default function checkForZenUpdates() {
const version = Services.appinfo.version;
@@ -25,6 +26,7 @@ export default function checkForZenUpdates() {
);
createSidebarNotification({
headingL10nId: "zen-sidebar-notification-updated-heading",
autoHideMs: ZEN_UPDATE_NOTIFICATION_TIMEOUT_MS,
links: [
{
url: Services.urlFormatter.formatURL(

View File

@@ -77,6 +77,18 @@
}
}
.zen-sidebar-notification-progress {
height: 2px;
overflow: hidden;
transform: translateY(-1px);
& .zen-sidebar-notification-progress-bar {
height: 100%;
background: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
transform-origin: left center;
}
}
.zen-sidebar-notification-body {
padding: 6px;
display: flex;

View File

@@ -12,7 +12,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
// Maximum number of cards shown in the stack; extra cards stay hidden until
// a slot frees up.
const MAX_STACKED_CARDS = 5;
const MAX_STACKED_CARDS = 3;
// How many background cards visually peek out at the top of the stack.
// Deeper cards hide perfectly behind the last peeking one, so the stack
// doesn't take more room with every extra card.
@@ -412,7 +412,25 @@ class ZenMediaCard {
this.#tabTimeout = null;
}
this.element.remove();
const { element } = this;
if (element.hidden) {
element.remove();
} else {
// Animate the card out instead of popping it away. It leaves the
// flex flow immediately (so the remaining cards re-slot right away)
// but stays anchored where it currently is while it sinks and fades.
const barBottom =
this.manager.mediaControlBar.getBoundingClientRect().bottom;
element.style.bottom =
barBottom - element.getBoundingClientRect().bottom + "px";
element.setAttribute("zen-removing", "true");
// Resolves once the exit transitions finish or get cancelled, and
// immediately if they never start (e.g. reduced motion).
Promise.allSettled(
element.getAnimations().map(animation => animation.finished)
).then(() => element.remove());
}
this.manager.onCardDestroyed(this);
}
}
@@ -452,21 +470,28 @@ class nsZenMediaController {
window.addEventListener("DOMAudioPlaybackStarted", event => {
const browser = event.target;
// Only create the card if the media is still playing a moment
// later, so short sounds (e.g. notification pings) never produce
// one.
// The card is created right away (while the controller is fresh)
// but only shown if the media is still playing a moment later, so
// short sounds (e.g. notification pings) never reveal it.
setTimeout(() => {
const mediaController = browser.browsingContext?.mediaController;
if (!mediaController?.isPlaying) {
const card = this.#cardForBrowser(browser);
if (!card || card.isSharing) {
return;
}
this.activateMediaControls(mediaController, browser);
const card = this.#cardForBrowser(browser);
if (card && !card.isSharing) {
if (card.controller.isPlaying) {
card.refreshVisibility();
} else if (card.element.hidden) {
// The sound was over before ever being shown (e.g. a
// notification ping): drop the card instead of keeping a ghost
// around.
card.destroy();
}
}, 1000);
this.activateMediaControls(
browser.browsingContext.mediaController,
browser
);
});
window.addEventListener("DOMAudioPlaybackStopped", () => {
@@ -623,18 +648,31 @@ class nsZenMediaController {
card => !card.element.hidden
);
// Re-slot the indices without transitions first: the index transform
// only compensates the flex-order shift, so a reindexed card keeps
// its visual position through this step.
visibleCards.forEach((card, index) => {
card.element.toggleAttribute(
"stack-overflow",
index >= MAX_STACKED_CARDS
);
card.element.toggleAttribute("stacked-behind", index > 0);
card.element.setAttribute("zen-reindexing", "true");
card.element.style.setProperty("--zen-media-card-index", index);
card.element.style.zIndex = 100 - index;
});
// Flush styles so the re-slotting lands instantly...
this.mediaControlBar.getBoundingClientRect();
// ...then animate only the peek level: demoted cards slide up from
// behind the front card instead of dropping in from above.
visibleCards.forEach((card, index) => {
card.element.removeAttribute("zen-reindexing");
card.element.style.setProperty(
"--zen-media-card-peek-level",
Math.min(index, MAX_PEEK_LEVELS)
);
card.element.style.zIndex = 100 - index;
});
const stackedCount = Math.min(visibleCards.length, MAX_STACKED_CARDS);

View File

@@ -9,6 +9,7 @@
--button-spacing: 2px;
--zen-media-stack-peek: 8px;
--zen-media-stack-gap: 6px;
--zen-media-stack-easing: cubic-bezier(0.25, 1, 0.5, 1);
display: flex;
min-width: 0;
@@ -88,7 +89,7 @@
animation: zen-back-and-forth-text 10s infinite ease-in-out;
}
& .zen-media-card {
& .zen-media-card:not([zen-removing]) {
transform: none;
opacity: 1;
}
@@ -114,10 +115,10 @@
padding: 0 6px;
pointer-events: none;
transition:
max-height 0.2s ease,
opacity 0.2s ease,
transform 0.2s ease,
padding 0.2s ease;
max-height 0.3s var(--zen-media-stack-easing),
opacity 0.3s var(--zen-media-stack-easing),
transform 0.3s var(--zen-media-stack-easing),
padding 0.3s var(--zen-media-stack-easing);
}
.zen-media-current-time,
@@ -149,12 +150,39 @@
)
)
scale(calc(1 - 0.04 * var(--zen-media-card-peek-level, 0)));
opacity: calc(1 - 0.4 * var(--zen-media-card-peek-level, 0));
opacity: calc(1 - 0.3 * var(--zen-media-card-peek-level, 0));
filter: blur(0);
transition:
transform 0.1s ease-out,
opacity 0.2s ease-out,
transform 0.35s var(--zen-media-stack-easing),
opacity 0.3s var(--zen-media-stack-easing),
filter 0.3s var(--zen-media-stack-easing),
padding 0.3s ease-out;
@starting-style {
opacity: 0;
transform: translateY(0.5rem);
filter: blur(4px);
}
&[zen-reindexing] {
transition: none;
}
&[zen-removing] {
position: absolute;
left: 0;
width: 100%;
transform: none;
translate: 0 0.5rem;
opacity: 0;
filter: blur(2px);
pointer-events: none;
transition:
translate 0.3s var(--zen-media-stack-easing),
opacity 0.3s var(--zen-media-stack-easing),
filter 0.3s var(--zen-media-stack-easing);
}
&[stack-overflow] {
display: none;
}
@@ -249,7 +277,7 @@
overflow: visible;
position: relative;
z-index: 2;
transition: height 0.2s ease-out;
transition: height 0.3s var(--zen-media-stack-easing);
}
}

View File

@@ -26,6 +26,10 @@ XPCOMUtils.defineLazyServiceGetter(
// comfortably above click jitter or clicks in the region get lost.
const DRAG_START_THRESHOLD_PX = 4;
// Un-snapping a maximized or tiled window is more disruptive, so those
// need a firmer gesture, mirroring how native titlebars behave.
const SNAPPED_DRAG_START_THRESHOLD_PX = 50;
// Content that drives its own mouse interaction without being
// interactive HTML content in the spec sense.
const kAppContentTags = new Set(["audio", "canvas", "video"]);
@@ -63,6 +67,9 @@ export class ZenWindowDragChild extends JSWindowActorChild {
#dragging = false;
#startScreenX = 0;
#startScreenY = 0;
// 0 while waiting for the ZenWindowDrag:IsSnapped answer; no drag can
// start until the proper threshold is known.
#dragThresholdPx = 0;
handleEvent(event) {
// Never let pages spoof the gesture with synthetic events.
@@ -145,8 +152,18 @@ export class ZenWindowDragChild extends JSWindowActorChild {
const { screenX, screenY } = this.#screenPoint(event);
this.#startScreenX = screenX;
this.#startScreenY = screenY;
this.#dragThresholdPx = 0;
this.#tracking = true;
this.#addGestureListeners();
this.sendQuery("ZenWindowDrag:IsSnapped")
.then(snapped => {
if (this.#tracking) {
this.#dragThresholdPx = snapped
? SNAPPED_DRAG_START_THRESHOLD_PX
: DRAG_START_THRESHOLD_PX;
}
})
.catch(() => this.#reset());
}
#onMouseMove(event) {
@@ -164,9 +181,12 @@ export class ZenWindowDragChild extends JSWindowActorChild {
event.preventDefault();
return;
}
if (!this.#dragThresholdPx) {
return;
}
const point = this.#screenPoint(event);
const threshold =
DRAG_START_THRESHOLD_PX * this.contentWindow.devicePixelRatio;
this.#dragThresholdPx * this.contentWindow.devicePixelRatio;
if (
Math.hypot(
point.screenX - this.#startScreenX,

View File

@@ -15,19 +15,42 @@ XPCOMUtils.defineLazyServiceGetter(
export class ZenWindowDragParent extends JSWindowActorParent {
receiveMessage(message) {
if (message.name !== "ZenWindowDrag:StartDrag") {
return;
}
const win = this.browsingContext.topChromeWindow;
if (!win || win.closed || win.windowState === win.STATE_FULLSCREEN) {
return;
if (!win || win.closed) {
return undefined;
}
if (Cu.isInAutomation) {
// Tests can't exercise a real OS drag session; let them observe the
// decision instead.
Services.obs.notifyObservers(win, "zen-window-drag-started");
return;
switch (message.name) {
case "ZenWindowDrag:StartDrag": {
if (win.windowState === win.STATE_FULLSCREEN) {
break;
}
if (Cu.isInAutomation) {
// Tests can't exercise a real OS drag session; let them observe
// the decision instead.
Services.obs.notifyObservers(win, "zen-window-drag-started");
break;
}
lazy.zenDragAndDropService.beginNativeWindowMove(win);
break;
}
case "ZenWindowDrag:IsSnapped": {
return this.#isSnapped(win);
}
}
lazy.zenDragAndDropService.beginNativeWindowMove(win);
return undefined;
}
/**
* Whether the window is maximized or tiled to an edge. The tiled
* attribute is kept in sync with the widget by AppWindow, the same way
* sizemode is.
*
* @param {ChromeWindow} win
*/
#isSnapped(win) {
return (
win.windowState === win.STATE_MAXIMIZED ||
win.document.documentElement.hasAttribute("tiled")
);
}
}

View File

@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.21.12b",
"displayVersion": "1.21.13b",
"github": {
"repo": "zen-browser/desktop"
},