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

Changed Functions

FunctionChangeNotes
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
if
dom/ipc/BrowserParent.cpp
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
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…