High firefox Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionInternally found bugs present in Firefox ESR 115.38, Firefox ESR 140.13, Firefox ESR 153.0 and Firefox 153. Some of these bugs showed evidence of memory corruption or another security-relevant defect and we presume that with enough effort some of these could have been exploited.
ComponentDOM
Bug ClassMemory Corruption
Tracker2045774
Fix commitba3797fd94e3 (firefox) +22/-15
CISA KEVNot listed
CreditedChristian Holler, Jan de Mooij, Tom Ritter and the Mozilla Fuzzing Team
Disclosed2026-08-18

Background

Picture region
the gfx::IntRect describing which sub-rectangle of a decoded video frame is actually displayed after cropping.
Stride
the byte (or sample) distance from one row of a plane to the next, which is >= the plane width because of alignment padding.
Crop parameters
the WebM container fields crop_left, crop_right, crop_top, and crop_bottom that trim the coded frame down to the picture region.
`CheckedUint32`
a Mozilla wrapper type that flags arithmetic overflow so a bounds check can reject invalid sizes.

Root Cause Analysis

Attacker-controlled WebM metadata flowed through WebMDemuxer::ReadMetadata, where the crop width was computed as params.width - cropH using 32-bit unsigned arithmetic, so a cropH larger than params.width wrapped to a huge value instead of being rejected. That inflated pictureRect was then bounds-checked in ValidateBufferAndPicture, but the check compared the picture’s horizontal extent against aBuffer.mPlanes[0].mStride (the padded row span) rather than mWidth (the real sample count), so a picture wider than the actual pixel data could still pass validation. Finally, CopyPlane asserted only that rowBytes <= aStride for the packed case and ignored the interleaved (aSkip != 0) path, whose per-row reach is larger, allowing the copy to read past the source plane. The invariant that a picture region must lie entirely within the decoded plane’s real sample bounds was therefore violated at three layers.

The fix computes crop in 64-bit and only applies it when it leaves a non-empty in-frame region, validates against mWidth instead of mStride, and recomputes the true row span including aSkip before the release assertion.

Key insight
The single core mistake was validating and copying a video picture region against padded/packed row metrics (mStride, packed rowBytes) and 32-bit crop math rather than against the plane’s true sample dimensions; the fix realigns every bound to the real width/height in 64-bit arithmetic so an over-wide picture is rejected before any copy.

Attack Path

  1. Craft malicious WebM An attacker builds a WebM file whose track headers specify crop values (crop_left/crop_right) summing to more than the coded width.
  2. Overflow the crop math In WebMDemuxer::ReadMetadata, params.width - cropH underflows in 32-bit unsigned arithmetic, producing an oversized pictureRect.
  3. Bypass validation ValidateBufferAndPicture accepts the region because it checks the extent against the larger mStride rather than mWidth.
  4. Trigger the copy CopyPlane copies the frame, and on the interleaved path reads beyond the true plane bounds because the assertion ignored aSkip, producing out-of-bounds memory access.

Impact Assessment

An attacker who gets a victim to load a crafted WebM video causes out-of-bounds memory access during frame copying, which is memory corruption that could potentially be leveraged toward code execution. This occurs in the media/decoding path of the content process. The only precondition is that the victim decodes the attacker-supplied WebM stream, which any web page can serve.

Changed Functions

FunctionChangeNotes
if
dom/media/webm/WebMDemuxer.cpp
modified
if
gfx/layers/ImageContainer.cpp
modified

Files Changed

  • dom/media/MediaData.cpp
  • dom/media/webm/WebMDemuxer.cpp
  • gfx/layers/ImageContainer.cpp

Audit Directions

  • Stride vs. width confusion
    Flag any bounds check that validates a picture/copy extent against a plane’s stride or padded row size when the real constraint is the sample width or height.
  • Container-controlled size arithmetic
    Audit demuxers for width/height/crop math done in native-width unsigned integers, and require CheckedUint32/64-bit computation with explicit non-empty-region checks.
  • Assertion-only bounds on copy paths
    Review CopyPlane-style routines whose safety rests on a MOZ_RELEASE_ASSERT, ensuring the asserted span matches every branch (packed vs. interleaved/aSkip) the copy can take.
diff --git a/dom/media/MediaData.cpp b/dom/media/MediaData.cpp
index 85a6b1ca0fa..040da354eb6 100644
--- a/dom/media/MediaData.cpp
+++ b/dom/media/MediaData.cpp
@@ -229,9 +229,10 @@ static MediaResult ValidateBufferAndPicture(
   }
   // Ensure the picture size specified in the headers can be extracted out of
   // the frame we've been supplied without indexing out of bounds.
+  // The picture extent and the plane width are both measured in samples.
   CheckedUint32 xLimit = aPicture.x + CheckedUint32(aPicture.width);
   CheckedUint32 yLimit = aPicture.y + CheckedUint32(aPicture.height);
-  if (!xLimit.isValid() || xLimit.value() > aBuffer.mPlanes[0].mStride ||
+  if (!xLimit.isValid() || xLimit.value() > aBuffer.mPlanes[0].mWidth ||
       !yLimit.isValid() || yLimit.value() > aBuffer.mPlanes[0].mHeight) {
     // The specified picture dimensions can't be contained inside the video
     // frame, we'll stomp memory if we try to copy it. Fail.
diff --git a/dom/media/webm/WebMDemuxer.cpp b/dom/media/webm/WebMDemuxer.cpp
index 3bf19b09cde..dfa3a39a28f 100644
--- a/dom/media/webm/WebMDemuxer.cpp
+++ b/dom/media/webm/WebMDemuxer.cpp
@@ -527,19 +527,19 @@ nsresult WebMDemuxer::ReadMetadata() {
       mInfo.mVideo.mHDRMetadata = ParseWebMMasteringMetadata(params);
 
       // Picture region, taking into account cropping, before scaling
-      // to the display size.
-      unsigned int cropH = params.crop_right + params.crop_left;
-      unsigned int cropV = params.crop_bottom + params.crop_top;
-      gfx::IntRect pictureRect(params.crop_left, params.crop_top,
-                               params.width - cropH, params.height - cropV);
-
-      // If the cropping data appears invalid then use the frame data
-      if (pictureRect.width <= 0 || pictureRect.height <= 0 ||
-          pictureRect.x < 0 || pictureRect.y < 0) {
-        pictureRect.x = 0;
-        pictureRect.y = 0;
-        pictureRect.width = params.width;
-        pictureRect.height = params.height;
+      // to the display size. Default to the full frame and apply cropping only
+      // when it leaves a non-empty region within the frame.
+      gfx::IntRect pictureRect(0, 0, AssertedCast<int32_t>(params.width),
+                               AssertedCast<int32_t>(params.height));
+      uint64_t cropH =
+          static_cast<uint64_t>(params.crop_left) + params.crop_right;
+      uint64_t cropV =
+          static_cast<uint64_t>(params.crop_top) + params.crop_bottom;
+      if (cropH < params.width && cropV < params.height) {
+        pictureRect.x = AssertedCast<int32_t>(params.crop_left);
+        pictureRect.y = AssertedCast<int32_t>(params.crop_top);
+        pictureRect.width = AssertedCast<int32_t>(params.width - cropH);
+        pictureRect.height = AssertedCast<int32_t>(params.height - cropV);
       }
 
       // Validate the container-reported frame and pictureRect sizes. This
diff --git a/gfx/layers/ImageContainer.cpp b/gfx/layers/ImageContainer.cpp
index 5cab470b982..901ced77a29 100644
--- a/gfx/layers/ImageContainer.cpp
+++ b/gfx/layers/ImageContainer.cpp
@@ -894,7 +894,13 @@ static void CopyPlane(uint8_t* aDst, const uint8_t* aSrc,
   int32_t width = aSize.width;
   const int32_t rowBytes = width * aBytesPerElement;
 
-  MOZ_RELEASE_ASSERT(rowBytes <= aStride);
+  // The interleaved (aSkip != 0) path steps over skipped elements between
+  // pixels, so a row reaches further than the packed width. Computed with
+  // 64-bit arithmetic.
+  const int64_t srcRowSpan =
+      (static_cast<int64_t>(width) + static_cast<int64_t>(width - 1) * aSkip) *
+      aBytesPerElement;
+  MOZ_RELEASE_ASSERT(srcRowSpan <= aStride);
 
   if (!aSkip) {
     // Fast path: planar input.
Loading diff…