Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionDenial-of-service in the Widget component
ComponentDOM
Bug ClassLogic Error
Tracker2051788
Fix commite30801fa1ccd (firefox) +111/-2
CISA KEVNot listed
CreditedRiski Muhammad Ivan
Disclosed2026-08-18

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 LayoutDeviceInt coordinates.

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.

Key insight
The core mistake was treating content-supplied resize dimensions as trustworthy sizes — allowing both a 32-bit integer overflow in the delta arithmetic and an unbounded final size unrelated to the physical screen. The fix does the arithmetic in 64 bits and clamps every request to twice the available screen size at the parent-process boundary, so no request can produce buffers too large to allocate.

Attack Path

  1. Open a script-controllable window A malicious page opens a popup (or uses its own window) that it can call resize APIs on.
  2. Request an enormous size The page calls win.resizeTo(99999999, 99999999) or win.resizeBy(99999999, 99999999), supplying dimensions far beyond the screen.
  3. Overflow or over-large size propagates The delta add in ResizeByOuter overflows 32-bit arithmetic and/or the raw dimensions flow through IPC to RecvSetDimensions with no screen-relative bound.
  4. 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

An attacker gains a denial-of-service: content-driven resize requests can force the browser to attempt window/compositor buffers that exceed graphics-memory limits, leading to failure or a crash. This manifests in the parent (chrome) process via the widget layer, triggered from an ordinary web page in a content process. The only precondition is the ability to run script that calls 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

FunctionChangeNotes
if
dom/ipc/BrowserParent.cpp
modified
add_task
browser/base/content/test/popups/browser_popup_resize_clamp.js
modified
for
browser/base/content/test/popups/browser_popup_resize_clamp.js
modified

Files Changed

  • browser/base/content/test/popups/browser.toml
  • browser/base/content/test/popups/browser_popup_resize_clamp.js
  • dom/base/nsGlobalWindowOuter.cpp
  • dom/ipc/BrowserParent.cpp

Audit Directions

  • Content-controlled arithmetic
    Audit 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 bounds
    Look 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 boundary
    Verify 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.
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);
+});
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…