CVE-2026-74982
Overview
Background
- `window.resizeTo()` / `window.resizeBy()`
- Web-content DOM APIs that request an absolute or relative change to a window’s outer size.
- `nsGlobalWindowOuter::ResizeByOuter`
- Gecko’s implementation backing
resizeBy(), which converts the current device-pixel size to CSS pixels and applies the content-supplied delta. - `BrowserParent::RecvSetDimensions`
- The parent-process IPC handler that receives a resize request from a content process and applies it to the real chrome window.
- `widget::Screen::GetAvailRect`
- Widget API returning the available (usable) rectangle of the screen a window is on, in
LayoutDeviceIntcoordinates.
Root Cause Analysis
In nsGlobalWindowOuter::ResizeByOuter, the content-controlled deltas aWidthDif and aHeightDif were added directly to the current CSS size (cssSize.width += aWidthDif) using 32-bit arithmetic, so a large delta could overflow int32_t and produce a wrapped or unbounded target size. More fundamentally, no layer of the resize path constrained the requested dimensions to anything near the actual screen size, so a page could drive the window to a size whose painting/compositing buffers exceed practical graphics limits.
The fix adds two independent guards: the arithmetic in ResizeByOuter is now performed in 64 bits and clamped to [0, INT32_MAX] via std::clamp<int64_t>, eliminating the overflow, and BrowserParent::RecvSetDimensions now clamps the requested width and height to twice the available screen size before applying them. The screen-relative clamp is the durable invariant: regardless of how a request arrives, the resulting size stays in a range the window can actually be given (twice the screen size is used because reported screen sizes are unreliable, notably on Wayland). Together they ensure a content resize request cannot escape a sane, screen-proportional bound.
Attack Path
- Open a script-controllable window A malicious page opens a popup (or uses its own window) that it can call resize APIs on.
- Request an enormous size
The page calls
win.resizeTo(99999999, 99999999)orwin.resizeBy(99999999, 99999999), supplying dimensions far beyond the screen. - Overflow or over-large size propagates
The delta add in
ResizeByOuteroverflows 32-bit arithmetic and/or the raw dimensions flow through IPC toRecvSetDimensionswith no screen-relative bound. - Widget attempts an oversized window The parent process tries to realize a window whose painting buffers do not fit in available graphics memory, causing allocation failure or a crash.
Impact Assessment
resizeTo()/resizeBy() on a window, e.g. a popup the page itself opened; impact is rated low with no memory-corruption or code-execution primitive shown in the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/ipc/BrowserParent.cpp |
modified | |
add_taskbrowser/base/content/test/popups/browser_popup_resize_clamp.js |
modified | |
forbrowser/base/content/test/popups/browser_popup_resize_clamp.js |
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
Audit Directions
- Content-controlled arithmeticAudit any place a value from web content is added to or multiplied with a size/coordinate in fixed-width integers; widen to 64 bits and clamp before narrowing.
- Missing screen-relative boundsLook for widget/window sizing paths (resize, move, fullscreen, PiP) that apply dimensions without constraining them to the target screen’s available rectangle.
- Trust across the IPC boundaryVerify that dimension and geometry requests crossing from a content process into
BrowserParent/widget code are re-validated in the parent, not assumed to be sanitized in the child.
Patch
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());
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);
+});
Regression Test / PoC
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);
+});