CVE-2026-74990
Overview
Background
- Picture region
- the
gfx::IntRectdescribing 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, andcrop_bottomthat 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.
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
- Craft malicious WebM
An attacker builds a WebM file whose track headers specify crop values (
crop_left/crop_right) summing to more than the codedwidth. - Overflow the crop math
In
WebMDemuxer::ReadMetadata,params.width - cropHunderflows in 32-bit unsigned arithmetic, producing an oversizedpictureRect. - Bypass validation
ValidateBufferAndPictureaccepts the region because it checks the extent against the largermStriderather thanmWidth. - Trigger the copy
CopyPlanecopies the frame, and on the interleaved path reads beyond the true plane bounds because the assertion ignoredaSkip, producing out-of-bounds memory access.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/media/webm/WebMDemuxer.cpp |
modified | |
ifgfx/layers/ImageContainer.cpp |
modified |
Files Changed
dom/media/MediaData.cppdom/media/webm/WebMDemuxer.cppgfx/layers/ImageContainer.cpp
Audit Directions
- Stride vs. width confusionFlag 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 arithmeticAudit 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 pathsReview
CopyPlane-style routines whose safety rests on aMOZ_RELEASE_ASSERT, ensuring the asserted span matches every branch (packed vs. interleaved/aSkip) the copy can take.
Patch
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.