Firefox · DOM
CVE-2026-74982
Logic Error in DOM
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
add_taskbrowser/base/content/test/popups/browser_popup_resize_clamp.js |
modified | |
forbrowser/base/content/test/popups/browser_popup_resize_clamp.js |
modified | |
ifdom/ipc/BrowserParent.cpp |
modified |
Files Changed
browser/base/content/test/popups/browser.tomlbrowser/base/content/test/popups/browser_popup_resize_clamp.jsdom/base/nsGlobalWindowOuter.cppdom/ipc/BrowserParent.cpp
Patch
diff --git a/browser/base/content/test/popups/browser.toml b/browser/base/content/test/popups/browser.toml
index 2bd80a35be1..fe386eec0ad 100644
--- a/browser/base/content/test/popups/browser.toml
+++ b/browser/base/content/test/popups/browser.toml
@@ -94,6 +94,8 @@ skip-if = [
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && !headless", # outdated current sizes
]
+["browser_popup_resize_clamp.js"]
+
["browser_popup_resize_instant.js"]
skip-if = [
"os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland' && !headless", # outdated current sizes
diff --git a/browser/base/content/test/popups/browser_popup_resize_clamp.js b/browser/base/content/test/popups/browser_popup_resize_clamp.js
new file mode 100644
index 00000000000..3b99e16ee4a
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_clamp.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// window.resizeTo() and window.resizeBy() from content should not be able to
+// make a window much larger than the screen it is on. Without that bound a page
+// can ask for a window whose painting buffers do not fit in graphics memory.
+// The bound is twice the screen size, since the size reported for a screen is
+// not always accurate.
+add_task(async function test_resize_clamped_to_screen() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.net"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ info("Opening popup.");
+ let win = this.content.open(
+ "https://example.net",
+ "",
+ "width=200,height=200"
+ );
+
+ await ContentTaskUtils.waitForEvent(win, "load");
+
+ // Let a resize round-trip to the parent process and back.
+ let settle = async () => {
+ for (let i = 0; i < 30; i++) {
+ await new Promise(r => win.requestAnimationFrame(r));
+ }
+ };
+
+ let maxWidth = 2 * win.screen.width;
+ let maxHeight = 2 * win.screen.height;
+ info(
+ `Screen is ${win.screen.width}x${win.screen.height}, so the bound is ` +
+ `${maxWidth}x${maxHeight}.`
+ );
+
+ // A size that fits on the screen is honored as-is.
+ win.resizeTo(400, 400);
+ await settle();
+ is(win.outerWidth, 400, "Width within the screen is honored.");
+ is(win.outerHeight, 400, "Height within the screen is honored.");
+
+ // A size far beyond the screen is clamped to the bound, rather than to some
+ // much larger graphics limit such as the maximum texture size.
+ win.resizeTo(99999999, 99999999);
+ await settle();
+ Assert.lessOrEqual(
+ win.outerWidth,
+ maxWidth,
+ "resizeTo() far beyond the screen is clamped in width."
+ );
+ Assert.lessOrEqual(
+ win.outerHeight,
+ maxHeight,
+ "resizeTo() far beyond the screen is clamped in height."
+ );
+
+ // The same bound applies to resizeBy(), whose deltas are added to the
+ // current size and could otherwise overflow.
+ win.resizeTo(400, 400);
+ await settle();
+ win.resizeBy(99999999, 99999999);
+ await settle();
+ Assert.lessOrEqual(
+ win.outerWidth,
+ maxWidth,
+ "resizeBy() far beyond the screen is clamped in width."
+ );
+ Assert.lessOrEqual(
+ win.outerHeight,
+ maxHeight,
+ "resizeBy() far beyond the screen is clamped in height."
+ );
+
+ info("Closing popup.");
+ win.close();
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index 0f705861707..30d5d848397 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -5409,8 +5409,12 @@ void nsGlobalWindowOuter::ResizeByOuter(int32_t aWidthDif, int32_t aHeightDif,
auto scale = CSSToDevScaleForBaseWindow(treeOwnerAsWin);
CSSIntSize cssSize = RoundedToInt(size / scale);
- cssSize.width += aWidthDif;
- cssSize.height += aHeightDif;
+ // The deltas come from content and can be large enough to overflow a 32-bit
+ // add, so do the arithmetic in 64 bits and keep the result in range.
+ cssSize.width = int32_t(
+ std::clamp<int64_t>(int64_t(cssSize.width) + aWidthDif, 0, INT32_MAX));
+ cssSize.height = int32_t(
+ std::clamp<int64_t>(int64_t(cssSize.height) + aHeightDif, 0, INT32_MAX));
if (mBrowsingContext->GetIsDocumentPiP()) {
if (Maybe<CSSIntRect> screen =
diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp
index 892e47db528..4c746d5ecc6 100644
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -1093,6 +1093,27 @@ mozilla::ipc::IPCResult BrowserParent::RecvSetDimensions(
aRequest.mHeight.apply(rescaleFunc);
}
+ // Nothing further down keeps the size near the size of the screen that the
+ // window is on, so do it here. We allow twice the screen size because the
+ // size we get for a screen is not always accurate, on Wayland in particular,
+ // and all we need is to keep the size in a range the window can be given. For
+ // a request that carries inner dimensions this is a looser bound than it
+ // looks, since the outer size is larger.
+ nsCOMPtr<nsIWidget> mainWidget;
+ treeOwnerAsWin->GetMainWidget(getter_AddRefs(mainWidget));
+ if (mainWidget) {
+ if (RefPtr<widget::Screen> screen = mainWidget->GetWidgetScreen()) {
+ const LayoutDeviceIntSize availSize = screen->GetAvailRect().Size();
+ auto clampTo = [](Maybe<LayoutDeviceIntCoord>& aValue, int32_t aMax) {
+ if (aValue) {
+ *aValue = std::min<int32_t>(*aValue, aMax);
+ }
+ };
+ clampTo(aRequest.mWidth, 2 * availSize.width);
+ clampTo(aRequest.mHeight, 2 * availSize.height);
+ }
+ }
+
// treeOwner is the chrome tree owner, but we wan't the content tree owner.
nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = do_GetInterface(treeOwner);
NS_ENSURE_TRUE(webBrowserChrome, IPC_OK());
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/browser/base/content/test/popups/browser.toml b/browser/base/content/test/popups/browser.toml
index 2bd80a35be1..fe386eec0ad 100644
--- a/browser/base/content/test/popups/browser.toml
+++ b/browser/base/content/test/popups/browser.toml
@@ -94,6 +94,8 @@ skip-if = [
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && !headless", # outdated current sizes
]
+["browser_popup_resize_clamp.js"]
+
["browser_popup_resize_instant.js"]
skip-if = [
"os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland' && !headless", # outdated current sizes
diff --git a/browser/base/content/test/popups/browser_popup_resize_clamp.js b/browser/base/content/test/popups/browser_popup_resize_clamp.js
new file mode 100644
index 00000000000..3b99e16ee4a
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_resize_clamp.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// window.resizeTo() and window.resizeBy() from content should not be able to
+// make a window much larger than the screen it is on. Without that bound a page
+// can ask for a window whose painting buffers do not fit in graphics memory.
+// The bound is twice the screen size, since the size reported for a screen is
+// not always accurate.
+add_task(async function test_resize_clamped_to_screen() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.net"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ info("Opening popup.");
+ let win = this.content.open(
+ "https://example.net",
+ "",
+ "width=200,height=200"
+ );
+
+ await ContentTaskUtils.waitForEvent(win, "load");
+
+ // Let a resize round-trip to the parent process and back.
+ let settle = async () => {
+ for (let i = 0; i < 30; i++) {
+ await new Promise(r => win.requestAnimationFrame(r));
+ }
+ };
+
+ let maxWidth = 2 * win.screen.width;
+ let maxHeight = 2 * win.screen.height;
+ info(
+ `Screen is ${win.screen.width}x${win.screen.height}, so the bound is ` +
+ `${maxWidth}x${maxHeight}.`
+ );
+
+ // A size that fits on the screen is honored as-is.
+ win.resizeTo(400, 400);
+ await settle();
+ is(win.outerWidth, 400, "Width within the screen is honored.");
+ is(win.outerHeight, 400, "Height within the screen is honored.");
+
+ // A size far beyond the screen is clamped to the bound, rather than to some
+ // much larger graphics limit such as the maximum texture size.
+ win.resizeTo(99999999, 99999999);
+ await settle();
+ Assert.lessOrEqual(
+ win.outerWidth,
+ maxWidth,
+ "resizeTo() far beyond the screen is clamped in width."
+ );
+ Assert.lessOrEqual(
+ win.outerHeight,
+ maxHeight,
+ "resizeTo() far beyond the screen is clamped in height."
+ );
+
+ // The same bound applies to resizeBy(), whose deltas are added to the
+ // current size and could otherwise overflow.
+ win.resizeTo(400, 400);
+ await settle();
+ win.resizeBy(99999999, 99999999);
+ await settle();
+ Assert.lessOrEqual(
+ win.outerWidth,
+ maxWidth,
+ "resizeBy() far beyond the screen is clamped in width."
+ );
+ Assert.lessOrEqual(
+ win.outerHeight,
+ maxHeight,
+ "resizeBy() far beyond the screen is clamped in height."
+ );
+
+ info("Closing popup.");
+ win.close();
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
Loading diff…
References
On This Page