Medium watch ✔︎ verified CVSS 3.1 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Skia
DescriptionInsufficient validation of untrusted input in Skia
ComponentSkia
Bug ClassInput Validation
Action tierwatch — dos-class only — no demonstrated corruption path — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: none. Citations verified: ReadPageSizes 179-197 reads SkSize at 191 with no bound; ReadPageCount only checks >INT_MAX (172); filterMask 76-116 sets rowBytes=SkAlign4(width) (83), AllocImage(computeImageSize()) (88), copy loop 96-109. Diff adds kMaxDimension(1<<16) and kMaxWidth(1<<30)+zero-size guards. Page-size->filterMask bounds dataflow and PrintCompositor process are inferred (SkPDFDocument/SkDocument not in material), but both validation gaps are directly confirmed.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker496526419
Fix commit4320748aa7d3 (skia) +23/-7
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-26 (per vendor advisory)
Fix landed2026-03-30 (public git)
Disclosed2026-05-12
Patch gap43 days — fix public in source before the advisory

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 SkSize from an input stream into dstArray.
`SkTableMaskFilterImpl::filterMask`
the mask-filter routine that allocates a destination mask sized from src.fBounds and copies pixels through a 256-entry table.
`computeImageSize`
the SkMask helper 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.

Key insight
Untrusted page dimensions were deserialized and propagated into mask allocation with no upper bound, so an 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

  1. Craft a malicious MSKP An attacker produces a multi-picture document whose per-page SkSize holds extreme width/height values. (inferred)
  2. Deliver it for deserialization The document is fed to ReadPageSizes, which pre-fix stores the unvalidated oversized SkSize into dstArray[i].fSize (SkMultiPictureDocument.cpp:191).
  3. Trigger mask filtering A draw using an SkTableMaskFilter reaches SkTableMaskFilterImpl::filterMask, where SkAlign4(dst->fBounds.width()) overflows and computeImageSize() under-sizes the allocation (SkTableMaskFilter.cpp:83,88).
  4. Overrun the buffer The copy loop at SkTableMaskFilter.cpp:96-109 writes beyond the undersized allocation, crashing the process. (inferred)

Impact Assessment

An attacker supplying a crafted MSKP document could drive an integer overflow in mask sizing and a subsequent out-of-bounds write, which the advisory classifies as insufficient validation of untrusted input in Skia leading to a crash. Vendor severity is medium and the advisory marks it as not exploited in the wild. The reporter situates this in the 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

FunctionChangeNotes
if
src/effects/SkTableMaskFilter.cpp
modified
if
src/utils/SkMultiPictureDocument.cpp
modified
for
src/utils/SkMultiPictureDocument.cpp
modified

Files Changed

  • src/effects/SkTableMaskFilter.cpp
  • src/utils/SkMultiPictureDocument.cpp

Audit Directions

  • Deserialized dimensions
    Treat any width/height/size read from an untrusted stream as needing finiteness and upper-bound checks before it reaches allocation math.
  • Alignment and multiply overflows
    Audit SkAlign4, computeImageSize, and similar size computations for int32_t overflow when fed large attacker-controlled extents.
  • Zero-size allocation then write
    Flag 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.
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;
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/skia/src/pdf/SkPDFDocument.cpp
  • third_party/skia/src/core/SkDocument.cpp
  • components/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:

  1. Deserialization: In third_party/skia/src/utils/SkMultiPictureDocument.cpp, the ReadPageSizes function reads SkSize values (floats) directly from the stream without any validation against upper bounds, infinity, or NaN.
  2. Page Initiation: These unvalidated sizes are passed through PrintCompositorImpl to SkDocument::beginPage in third_party/skia/src/core/SkDocument.cpp.
  3. PDF Generation: In third_party/skia/src/pdf/SkPDFDocument.cpp, the onBeginPage method multiplies the dimensions by fRasterScale. The resulting value is rounded to an integer via toRound(), which saturates to INT32_MAX. This results in an SkPDFDevice being initialized with massive bounds.
  4. Integer Overflows in Filters: If the attacker also includes a drawing command with an SkTableMaskFilter, the large device bounds propagate to SkPDFDevice::internalDrawPathWithFilter. The bounds are used to compute the size of masks. For example, in SkTableMaskFilterImpl::filterMask, the calculation dst->rowBytes() = SkAlign4(dst->fBounds.width()) can overflow int32_t, and dst->computeImageSize() returns 0 due to safeMul32 returning 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)

  1. In a compromised renderer, generate an MSKP file.
  2. Add a page with SkSize set to dimensions that scale to approximately INT32_MAX (e.g., width = 1073741821, height = 2).
  3. Add a path to the page that fills the dimensions, and apply an SkTableMaskFilter to the paint.
  4. Send the MSKP via IPC to the PrintCompositor.
  5. Observe the PrintCompositor process 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.

View on issue tracker