CVE-2026-8579
Overview
Background
- MSKP / `SkMultiPictureDocument`
- Skia’s multi-picture document format whose header stores a page count followed by one
SkSize(two floats) per page. - `ReadPageSizes`
- the deserializer that reads each page’s
SkSizefrom an input stream intodstArray. - `SkTableMaskFilterImpl::filterMask`
- the mask-filter routine that allocates a destination mask sized from
src.fBoundsand copies pixels through a 256-entry table. - `computeImageSize`
- the
SkMaskhelper that multiplies row bytes by height to size the mask image allocation.
Root Cause Analysis
Pre-fix, ReadPageSizes (src/utils/SkMultiPictureDocument.cpp:179-197) read each SkSize with stream->read(&s, sizeof(s)) at line 191 and accepted whatever float values arrived, with no bound, finiteness, or emptiness check, and ReadPageCount only bounded the count by pageCount > INT_MAX (line 172). Those unvalidated dimensions then flow downstream to mask computation. In SkTableMaskFilterImpl::filterMask (src/effects/SkTableMaskFilter.cpp:76-116), dst->rowBytes() is set to SkAlign4(dst->fBounds.width()) at line 83 and the image is allocated from dst->computeImageSize() at line 88; for a width near INT32_MAX, SkAlign4 overflows and computeImageSize() can return 0, yielding an undersized/zero allocation that the copy loop at lines 96-109 then overruns.
The fix adds validation at both ends: ReadPageSizes now rejects s.isEmpty() || s.width() >= kMaxDimension || s.height() >= kMaxDimension (with kMaxDimension = 1 << 16), tightens the count check to pageCount > (INT_MAX / sizeof(SkSize)), and rejects streams too short to hold all sizes; filterMask now rejects src.fBounds.width() > kMaxWidth (1 << 30) before the SkAlign4 and bails when computeImageSize() returns 0 before allocating. Both checks TIGHTEN the code by adding previously-absent upper bounds.
SkAlign4/computeImageSize integer overflow produced a tiny or zero buffer that was then written past; the fix caps the dimensions at read time and independently guards the overflow-prone arithmetic in the mask filter.Attack Path
- Craft a malicious MSKP
An attacker produces a multi-picture document whose per-page
SkSizeholds extreme width/height values. (inferred) - Deliver it for deserialization
The document is fed to
ReadPageSizes, which pre-fix stores the unvalidated oversizedSkSizeintodstArray[i].fSize(SkMultiPictureDocument.cpp:191). - Trigger mask filtering
A draw using an
SkTableMaskFilterreachesSkTableMaskFilterImpl::filterMask, whereSkAlign4(dst->fBounds.width())overflows andcomputeImageSize()under-sizes the allocation (SkTableMaskFilter.cpp:83,88). - Overrun the buffer
The copy loop at
SkTableMaskFilter.cpp:96-109writes beyond the undersized allocation, crashing the process. (inferred)
Impact Assessment
PrintCompositor utility process reachable from a compromised renderer, but this material does not include that IPC path, so the specific process and reachability are (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/effects/SkTableMaskFilter.cpp |
modified | |
ifsrc/utils/SkMultiPictureDocument.cpp |
modified | |
forsrc/utils/SkMultiPictureDocument.cpp |
modified |
Files Changed
src/effects/SkTableMaskFilter.cppsrc/utils/SkMultiPictureDocument.cpp
Audit Directions
- Deserialized dimensionsTreat any width/height/size read from an untrusted stream as needing finiteness and upper-bound checks before it reaches allocation math.
- Alignment and multiply overflowsAudit
SkAlign4,computeImageSize, and similar size computations forint32_toverflow when fed large attacker-controlled extents. - Zero-size allocation then writeFlag code that allocates from a computed size and then writes a loop bounded by the original dimensions, where a saturated/zeroed size yields an undersized buffer.
Patch
From 4320748aa7d3dc82b7064b23625a78d621f21963 Mon Sep 17 00:00:00 2001 From: Kaylee Lubick <[email protected]> Date: Mon, 30 Mar 2026 08:44:58 -0700 Subject: [PATCH] Validate sizes in mskp reading and SkTableMaskFilter The max dimension for mskps is approximately the sqrt of INT32_MAX which felt "big enough" Bug: b/496526419 Change-Id: I6850c28e18f7427d85ea0ea724c5d056a4682970 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1198616 Commit-Queue: Kaylee Lubick <[email protected]> Reviewed-by: Jorge Betancourt <[email protected]> --- diff --git a/src/effects/SkTableMaskFilter.cpp b/src/effects/SkTableMaskFilter.cpp index d422ce0..ad42372 100644 --- a/src/effects/SkTableMaskFilter.cpp +++ b/src/effects/SkTableMaskFilter.cpp @@ -78,14 +78,22 @@ if (src.fFormat != SkMask::kA8_Format) { return false; } - + // SkAlign4 overflows when too close to INT32_MAX, so reject when too big. + constexpr int32_t kMaxWidth = 1 << 30; + if (src.fBounds.width() > kMaxWidth) { + return false; + } dst->bounds() = src.fBounds; dst->rowBytes() = SkAlign4(dst->fBounds.width()); dst->format() = SkMask::kA8_Format; dst->image() = nullptr; if (src.fImage) { - dst->image() = SkMaskBuilder::AllocImage(dst->computeImageSize()); + auto imgSize = dst->computeImageSize(); + if (imgSize == 0) { + return false; + } + dst->image() = SkMaskBuilder::AllocImage(imgSize); const uint8_t* srcP = src.fImage; uint8_t* dstP = dst->image(); diff --git a/src/utils/SkMultiPictureDocument.cpp b/src/utils/SkMultiPictureDocument.cpp index 5f327e6..72377d0 100644 --- a/src/utils/SkMultiPictureDocument.cpp +++ b/src/utils/SkMultiPictureDocument.cpp @@ -50,7 +50,9 @@ static constexpr char kEndPage[] = "SkMultiPictureEndPage"; -const uint32_t kVersion = 2; +static constexpr uint32_t kVersion = 2; + +static constexpr uint32_t kMaxDimension = 1 << 16; static SkSize join(const TArray<SkSize>& sizes) { SkSize joined = {0, 0}; @@ -169,28 +171,34 @@ return 0; } uint32_t pageCount; - if (!src->readU32(&pageCount) || pageCount > INT_MAX) { + if (!src->readU32(&pageCount) || pageCount > (INT_MAX / sizeof(SkSize))) { return 0; } // leave stream position right here. return SkTo<int>(pageCount); } -bool ReadPageSizes(SkStreamSeekable* stream, +bool ReadPageSizes(SkStreamSeekable* src, SkDocumentPage* dstArray, int dstArrayCount) { if (!dstArray || dstArrayCount < 1) { return false; } - int pageCount = ReadPageCount(stream); + int pageCount = ReadPageCount(src); if (pageCount < 1 || pageCount != dstArrayCount) { return false; } + if (src->hasLength() && src->getLength() < (static_cast<size_t>(pageCount) * sizeof(SkSize))) { + return false; // not enough memory to read all the sizes + } for (int i = 0; i < pageCount; ++i) { SkSize& s = dstArray[i].fSize; - if (sizeof(s) != stream->read(&s, sizeof(s))) { + if (sizeof(s) != src->read(&s, sizeof(s))) { return false; } + if (s.isEmpty() || s.width() >= kMaxDimension || s.height() >= kMaxDimension) { + return false; // invalid sizes + } } // leave stream position right here. return true;
Original Bug Report
Unbounded page dimensions in SkMultiPictureDocument lead to DoS in PrintCompositor
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: SkMultiPictureDocument::ReadPageSizes deserializes page dimensions without checking for excessively large values. A compromised renderer can send a malicious MSKP file with huge page sizes, causing massive memory allocations and integer overflows in Skia, leading to a Denial of Service in the PrintCompositor process.
Affected files:
third_party/skia/src/utils/SkMultiPictureDocument.cppthird_party/skia/src/pdf/SkPDFDocument.cppthird_party/skia/src/core/SkDocument.cppcomponents/services/print_compositor/print_compositor_impl.cc
Estimated timestamp from git blame: 2025-03-24
Summary
The SkMultiPictureDocument::ReadPageSizes function in Skia lacks upper-bound validation for page dimensions read from its input stream. This enables a compromised renderer to specify extremely large page sizes that are subsequently used by the PrintCompositor process. This issue leads to a Denial of Service via memory exhaustion and triggers integer overflows in downstream Skia components like mask filters.
Technical Details
When processing a SkMultiPictureDocument (MSKP) in the PrintCompositor utility process, the following data flow occurs:
- Deserialization: In
third_party/skia/src/utils/SkMultiPictureDocument.cpp, theReadPageSizesfunction readsSkSizevalues (floats) directly from the stream without any validation against upper bounds, infinity, or NaN. - Page Initiation: These unvalidated sizes are passed through
PrintCompositorImpltoSkDocument::beginPageinthird_party/skia/src/core/SkDocument.cpp. - PDF Generation: In
third_party/skia/src/pdf/SkPDFDocument.cpp, theonBeginPagemethod multiplies the dimensions byfRasterScale. The resulting value is rounded to an integer viatoRound(), which saturates toINT32_MAX. This results in anSkPDFDevicebeing initialized with massive bounds. - Integer Overflows in Filters: If the attacker also includes a drawing command with an
SkTableMaskFilter, the large device bounds propagate toSkPDFDevice::internalDrawPathWithFilter. The bounds are used to compute the size of masks. For example, inSkTableMaskFilterImpl::filterMask, the calculationdst->rowBytes() = SkAlign4(dst->fBounds.width())can overflowint32_t, anddst->computeImageSize()returns 0 due tosafeMul32returning 0 for sizes> INT32_MAX. This leads to a 0-byte allocation followed by a massive linear out-of-bounds write of >2GB, causing an immediate segmentation fault.
Impact
An attacker with control over a renderer process can send a malicious MSKP via the DidPrintContentParams.metafile_data_region IPC. This reachability allows the attacker to reliably crash the sandboxed PrintCompositor utility process by triggering massive out-of-bounds writes (which are guaranteed to hit unmapped guard pages) or OOMs, resulting in a Denial of Service.
Reproduction Steps (Suggested)
- In a compromised renderer, generate an MSKP file.
- Add a page with
SkSizeset to dimensions that scale to approximatelyINT32_MAX(e.g., width =1073741821, height =2). - Add a path to the page that fills the dimensions, and apply an
SkTableMaskFilterto the paint. - Send the MSKP via IPC to the
PrintCompositor. - Observe the
PrintCompositorprocess crash due to an OOM or a segmentation fault when Skia attempts to allocate or write to the massive mask buffer.
Suggested Fix
In third_party/skia/src/utils/SkMultiPictureDocument.cpp, ReadPageSizes should validate the deserialized SkSize values. If the width or height is negative, non-finite, or exceeds a reasonable maximum document size, the function should return false to reject the malformed document.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.