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 <kjlubick@google.com>
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 <kjlubick@google.com>
Reviewed-by: Jorge Betancourt <jmbetancourt@google.com>
---
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.