CVE-2026-74977
Overview
Background
- `NVImage`
- a Gecko image type in the layers subsystem holding NV12/NV21-style semi-planar YCbCr video frame data.
- `CheckedInt32`
- a Mozilla helper type that performs 32-bit integer arithmetic while tracking overflow, exposing
isValid()andvalue(). - `SourceSurface`
- an abstract drawing surface produced from image data for compositing or readback.
- Integer overflow
- an arithmetic result that exceeds the range of its integer type, wrapping to an incorrect (often small) value.
Root Cause Analysis
Both NVImage::GetAsSourceSurface and NVImage::BuildSurfaceDescriptorBuffer computed the destination buffer size as ySize.height * mData.mYStride + cbcrSize.height * cbcrSize.width * 2 using plain int arithmetic. Because the YDataSize, CbCrDataSize, and stride values are attacker-influenced image dimensions, this multiply-and-add could overflow a signed 32-bit int, yielding a bufferLength far smaller than the data actually copied into the buffer. The invariant that the allocation passed to MakeUnique<uint8_t[]> must be at least as large as the bytes subsequently written was thus violated, allowing a heap buffer of undersized length to be populated with full-size plane data.
The fix recomputes bufferLength with CheckedInt32, checks isValid(), and bails out (return nullptr or NS_ERROR_FAILURE) before allocating when the size overflows, so the allocation is only performed with a correct, in-range length.
int without overflow detection; the fix wraps the computation in CheckedInt32 and refuses to allocate when the result is not representable.Attack Path
- Craft oversized image An attacker supplies a video/image frame whose YCbCr dimensions and stride are large enough that the byte-length computation wraps a signed 32-bit integer.
- Trigger surface conversion
Content causes Gecko to convert the
NVImageto a surface viaGetAsSourceSurfaceorBuildSurfaceDescriptorBuffer. - Undersized allocation
The overflowed
bufferLengthproduces a heap allocation much smaller than the real plane size. - Heap overflow write Copying the Y and CbCr plane data into the too-small buffer writes past its bounds, corrupting adjacent heap memory.
Impact Assessment
NVImage. Exploitation requires the attacker to control image dimensions and to induce a surface conversion, and per the metadata the severity/impact is rated low.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/layers/ImageContainer.cpp |
modified |
Files Changed
gfx/layers/ImageContainer.cpp
Audit Directions
- Unchecked size arithmeticSearch image/video/graphics code for
width * height * stride-style products in plainint/size_tfeeding allocation sizes, and requireCheckedIntwrappers. - Allocation vs. copy mismatchVerify every
MakeUnique<uint8_t[]>/buffer allocation derives its length from the same validated dimensions used by the subsequent copy loops. - Attacker-controlled dimensionsAudit paths where decoded media dimensions and strides reach allocation math without prior range validation.
Patch
diff --git a/gfx/layers/ImageContainer.cpp b/gfx/layers/ImageContainer.cpp
index b0db3a93327..5e9b8316dda 100644
--- a/gfx/layers/ImageContainer.cpp
+++ b/gfx/layers/ImageContainer.cpp
@@ -1043,9 +1043,14 @@ already_AddRefed<SourceSurface> NVImage::GetAsSourceSurface() {
// logics in PlanarYCbCrImage::GetAsSourceSurface().
auto ySize = mData.YDataSize();
auto cbcrSize = mData.CbCrDataSize();
- const int bufferLength =
- ySize.height * mData.mYStride + cbcrSize.height * cbcrSize.width * 2;
- auto buffer = MakeUnique<uint8_t[]>(bufferLength);
+ auto bufferLength = CheckedInt32(ySize.height) * mData.mYStride +
+ CheckedInt32(cbcrSize.height) * cbcrSize.width * 2;
+ if (!bufferLength.isValid()) {
+ NS_ERROR("Image buffer length exceeds integer limits.");
+ return nullptr;
+ }
+
+ auto buffer = MakeUnique<uint8_t[]>(bufferLength.value());
Data aData = mData;
aData.mCbCrStride = cbcrSize.width;
@@ -1118,9 +1123,15 @@ nsresult NVImage::BuildSurfaceDescriptorBuffer(
RefPtr<gfx::DataSourceSurface> sourceSurface = mSourceSurface.Get();
if (!sourceSurface) {
- const int bufferLength =
- ySize.height * mData.mYStride + cbcrSize.height * cbcrSize.width * 2;
- buffer = MakeUnique<uint8_t[]>(bufferLength);
+ auto bufferLength = CheckedInt32(ySize.height) * mData.mYStride +
+ CheckedInt32(cbcrSize.height) * cbcrSize.width * 2;
+
+ if (!bufferLength.isValid()) {
+ NS_ERROR("Image buffer length exceeds integer limits.");
+ return NS_ERROR_FAILURE;
+ }
+
+ buffer = MakeUnique<uint8_t[]>(bufferLength.value());
aData.mYChannel = buffer.get();
if (mData.mCbChannel < mData.mCrChannel) { // NV12