CVE-2026-74948
Overview
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.
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
- Supply a crafted image
An attacker delivers image data whose header declares a
stridesmaller thanwidth * BytesPerPixel, or a non-positivewidth/height. - Reach the size calculation
Decoding or surface handling invokes
CalculateBufferSizeForImage, which computes an underflowedpaddingand therefore an inconsistentusedBufferSize. - 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.
- Over-read of memory The mismatch causes an out-of-bounds read of adjacent heap memory into the image surface.
- Disclosure The over-read contents become observable to script or rendered output, leaking sensitive process memory.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifdom/base/nsContentUtils.cpp |
modified |
Files Changed
dom/base/nsContentUtils.cpp
Audit Directions
- Unsigned checked subtractionReview any
CheckedInt<size_t>subtraction where the subtrahend is attacker-influenced; unsigned underflow yields a huge valid-looking value, so prefer signedCheckedInt32/explicit ordering checks. - Unvalidated stride/dimension inputsAudit graphics and image buffer-size helpers for missing
width > 0,height > 0, andstride >= width * BytesPerPixelinvariants before arithmetic. - Buffer size divergenceLook for code that derives both a max and a used size and consumes them separately, ensuring both are validated together so a valid
requiredBytescannot mask an invalidusedBytes.
Patch
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;