High firefox Info Disclosure 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionInformation disclosure in the Graphics component
ComponentDOM
Bug ClassInfo Disclosure
Tracker2060106
Fix commitd7420de29623 (firefox) +15/-10
CISA KEVNot listed
CreditedYaqoub Aldurayhim
Disclosed2026-08-18

Background

`nsContentUtils::CalculateBufferSizeForImage`
a helper that computes the maximum and used byte sizes of a pixel buffer from an image’s stride, dimensions, and surface format.
`stride`
the number of bytes per pixel row, which is greater than or equal to the visible row width because rows can carry trailing alignment padding.
`CheckedInt<T>`
a Mozilla integer wrapper that tracks overflow so that arithmetic producing an out-of-range result is flagged as invalid rather than silently wrapping.
`BytesPerPixel`
a graphics helper returning the byte count of a single pixel for a given SurfaceFormat.

Root Cause Analysis

The vulnerable path computed padding as CheckedSize(aStride) - (CheckedSize(aImageSize.width) * BytesPerPixel(aFormat)) using an unsigned CheckedInt<size_t>, and then derived usedBytes = requiredBytes - padding without ever validating that aImageSize.width and aImageSize.height were positive or that aStride was at least as large as the real row width. Because size_t is unsigned, a width * BytesPerPixel product exceeding aStride (or a negative dimension coerced into a huge unsigned value) caused padding to underflow into an enormous value, so usedBytes could be computed incorrectly while requiredBytes still appeared valid. The invariant violated is that stride >= width * BytesPerPixel and that both dimensions are strictly positive, which the code assumed but never enforced.

The fix rejects non-positive dimensions up front, recomputes rowBytes with a signed CheckedInt32, explicitly requires stride.value() >= rowBytes.value(), and rewrites usedBytes as requiredBytes - stride + rowBytes, so an inconsistent stride/width/height combination now returns NS_ERROR_FAILURE instead of yielding a bogus buffer size.

Key insight
The single core mistake was trusting caller-supplied stride and dimensions and doing the padding subtraction in unsigned arithmetic without a stride >= rowBytes or positive-dimension check, letting an underflow produce a mismatched usedBufferSize. The fix restores the invariant by validating dimensions and the stride-versus-row-width relationship before any subtraction, using signed checked math.

Attack Path

  1. Supply a crafted image An attacker delivers image data whose header declares a stride smaller than width * BytesPerPixel, or a non-positive width/height.
  2. Reach the size calculation Decoding or surface handling invokes CalculateBufferSizeForImage, which computes an underflowed padding and therefore an inconsistent usedBufferSize.
  3. Buffer mismatch Downstream graphics code allocates or bounds against these sizes and copies pixel rows using the miscomputed used-size, exceeding the true valid extent of the buffer.
  4. Over-read of memory The mismatch causes an out-of-bounds read of adjacent heap memory into the image surface.
  5. Disclosure The over-read contents become observable to script or rendered output, leaking sensitive process memory.

Impact Assessment

An attacker gains an out-of-bounds read that can disclose adjacent heap memory contents through the graphics/DOM surface-handling path, consistent with the reported high-severity information disclosure. The precondition is that the attacker controls image stride and dimension values passed into CalculateBufferSizeForImage, for example via a crafted or malicious image source processed by the content process. No memory write or code execution is supported by the diff; the exposure is limited to reading memory that should not have been included in the buffer.

Changed Functions

FunctionChangeNotes
if
dom/base/nsContentUtils.cpp
modified

Files Changed

  • dom/base/nsContentUtils.cpp

Audit Directions

  • Unsigned checked subtraction
    Review any CheckedInt<size_t> subtraction where the subtrahend is attacker-influenced; unsigned underflow yields a huge valid-looking value, so prefer signed CheckedInt32/explicit ordering checks.
  • Unvalidated stride/dimension inputs
    Audit graphics and image buffer-size helpers for missing width > 0, height > 0, and stride >= width * BytesPerPixel invariants before arithmetic.
  • Buffer size divergence
    Look for code that derives both a max and a used size and consumes them separately, ensuring both are validated together so a valid requiredBytes cannot mask an invalid usedBytes.
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
index 2cd762c75b0..3024ca4f25e 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -10008,19 +10008,24 @@ nsresult nsContentUtils::CalculateBufferSizeForImage(
     const uint32_t& aStride, const IntSize& aImageSize,
     const SurfaceFormat& aFormat, size_t* aMaxBufferSize,
     size_t* aUsedBufferSize) {
-  using CheckedSize = CheckedInt<size_t>;
-
-  CheckedSize padding = CheckedSize(aStride) - (CheckedSize(aImageSize.width) *
-                                                BytesPerPixel(aFormat));
-  CheckedSize requiredBytes =
-      CheckedSize(aStride) * CheckedSize(aImageSize.height);
-  CheckedSize usedBytes = requiredBytes - padding;
-  if (!usedBytes.isValid()) {
+  if (aImageSize.width <= 0 || aImageSize.height <= 0) {
+    return NS_ERROR_FAILURE;
+  }
+
+  CheckedInt32 rowBytes =
+      CheckedInt32(aImageSize.width) * BytesPerPixel(aFormat);
+  CheckedInt32 stride(aStride);
+  if (!rowBytes.isValid() || !stride.isValid() ||
+      stride.value() < rowBytes.value()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  CheckedInt32 requiredBytes = stride * CheckedInt32(aImageSize.height);
+  CheckedInt32 usedBytes = requiredBytes - stride + rowBytes;
+  if (!requiredBytes.isValid() || !usedBytes.isValid()) {
     return NS_ERROR_FAILURE;
   }
 
-  MOZ_ASSERT(requiredBytes.isValid(), "requiredBytes should be valid");
-  MOZ_ASSERT(padding.isValid(), "padding should be valid");
   *aMaxBufferSize = requiredBytes.value();
   *aUsedBufferSize = usedBytes.value();
   return NS_OK;
Loading diff…