Critical watch CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
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 — ub-class only — no demonstrated corruption path
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.
Tracker516457532
Fix commitf1b8ba877c07 (skia) +44/-1
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-25 (per vendor advisory)
Fix landed2026-05-27 (public git)
Disclosed2026-06-30
Patch gap34 days — fix public in source before the advisory

Background

`SkPicture` deserialization
Skia reconstructs a recorded SkPicture from a byte stream by walking a series of four-byte tags (e.g. SK_PICT_PAINT_BUFFER_TAG, SK_PICT_PATH_BUFFER_TAG, SK_PICT_TEXTBLOB_BUFFER_TAG), each of which triggers tag-specific parsing of the following bytes.
`SkPictureBackedGlyphDrawable`
a glyph representation whose contents are stored as a serialized SkPicture, deserialized in SkGlyph.cpp via SkPicture::MakeFromData from an SkReadBuffer (inferred: the enclosing function name is not in the hunk header).
`SkDeserialProcs` / `fAllowTagsProc`
the deserialization-configuration struct; the patch adds a new callback field fAllowTagsProc (type SkDeserialAllowTagsProc) plus fAllowTagsCtx so callers can allow-list which SkFourByteTag values the picture parser will accept.
`SkFourByteTag`
a 32-bit tag type (four packed characters) used to label each section of the serialized picture stream.

Root Cause Analysis

When a SkPictureBackedGlyphDrawable is rebuilt from an untrusted buffer in SkGlyph.cpp, it called SkPicture::MakeFromData with SkDeserialProcs that only disabled SkSL (procs.fAllowSkSL = false) but otherwise accepted the full set of SkPicture tags. Because a glyph drawable is only ever produced by drawing paths and applying paints (per the commit’s references to the FreeType, DirectWrite and Fontations backends), the deserializer was accepting far richer object types than the format can legitimately contain — including typefaces, textblobs, slugs, vertices and images — thereby exposing that whole parsing surface to attacker-controlled bytes.

The patch adds an allow-list callback (fAllowTagsProc) and installs it in SkGlyph.cpp to permit only SK_PICT_BUFFER_SIZE_TAG, SK_PICT_FACTORY_TAG, SK_PICT_PAINT_BUFFER_TAG, SK_PICT_PATH_BUFFER_TAG and SK_PICT_READER_TAG. Both the stream-tag parser in SkPictureData.cpp and SkPictureData::parseBufferTag now consult this allow-list before dispatching: a disallowed tag returns early (return size == 0; in the stream parser, and buffer.validate(size == 0); return; in parseBufferTag), treating any non-empty disallowed section as invalid. This tightens validation, shrinking the deserialization attack surface for glyph drawables to exactly the tag types the format needs.

Key insight
The single mistake was deserializing glyph-drawable pictures with the general-purpose SkPicture parser, which trusts every tag type even though a glyph drawable can only contain paths and paints; the fix installs a per-tag allow-list (fAllowTagsProc) so disallowed tags (typefaces, textblobs, slugs, vertices, images, SVG fonts) are rejected before their parsers ever run.

Attack Path

  1. Craft a malicious glyph drawable An attacker constructs a serialized SkPictureBackedGlyphDrawable whose embedded SkPicture stream carries a tag that a glyph drawable would never legitimately contain, such as SK_PICT_TEXTBLOB_BUFFER_TAG or a typeface tag, with attacker-chosen payload bytes.
  2. Deliver it to the deserializer The buffer reaches the SkGlyph.cpp deserialization path that calls SkPicture::MakeFromData; pre-patch, the only restriction was fAllowSkSL = false, so any other tag was accepted (inferred: this data crosses a trust boundary via Chrome’s cross-process glyph/font handling, which the diff does not itself show).
  3. Exercise unintended parsing code Before the fix, the stream-tag parser in SkPictureData.cpp and SkPictureData::parseBufferTag dispatched on the attacker’s tag, invoking typeface/textblob/vertices/image deserialization logic that a glyph drawable was never meant to use, all driven by untrusted input.
  4. Trigger a memory-safety defect in that surface (inferred) The bug class is insufficient input validation in Skia; reaching a specific out-of-bounds or type-confusion sink within the newly-forbidden parsers is not demonstrated by this diff and is inferred rather than proven here.

Impact Assessment

An attacker who can supply a crafted serialized glyph drawable gains the ability to drive Skia’s picture deserializer through parsing paths (typefaces, textblobs, slugs, vertices, images) that legitimate glyph drawables never use, all within the process that performs the deserialization. The precondition is delivering attacker-controlled bytes to the SkPictureBackedGlyphDrawable deserialization path (inferred: reachable across a Chrome process boundary that handles fonts/glyphs). The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. Any memory corruption, information disclosure, or further escalation beyond the broadened parsing surface is (inferred) and not established by the patch.

Changed Functions

FunctionChangeNotes
switch
src/core/SkPictureData.cpp
modified
if
src/core/SkReadBuffer.h
modified

Files Changed

  • include/core/SkSerialProcs.h
  • src/core/SkDebugUtils.h
  • src/core/SkGlyph.cpp
  • src/core/SkPictureData.cpp
  • src/core/SkPictureData.h
  • src/core/SkReadBuffer.h

Audit Directions

  • General-purpose deserializers on constrained inputs
    Look for other callers of SkPicture::MakeFromData / SkReadBuffer that only set narrow flags (like fAllowSkSL = false) but accept the full tag/flattenable set even though the data format is known to contain only a small subset of object types.
  • Trust boundaries in glyph/font/typeface paths
    Audit serialization used across Chrome process boundaries for fonts, glyphs and typefaces (e.g. SkTypeface and SkTextBlob deserialization), where untrusted producers can embed richer object graphs than the consumer needs.
  • Early-exit validation semantics
    Review the return size == 0 / buffer.validate(size == 0) skip logic on disallowed tags to confirm every rejected-tag path both stops parsing and correctly marks the buffer invalid when payload bytes are present, so a skipped tag cannot leave a partially-consumed stream in a trusted state.
From f1b8ba877c07cccc080451fc4f6b17c27fa76581 Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Wed, 27 May 2026 11:55:53 -0400
Subject: [PATCH] Restrict deserial types further in SkGlyph and SkCustomTypeface

Importantly, we don't want typefaces or textblobs in
SkPictureBackedGlyphDrawable. In fact, we can look at
the code used to make the drawables by our various backends
[1][2][3][4] and see we only draw paths and use paints. Thus, we can
restrict deserializing these to just those (and the helper tags).

The SVG fonts could be a bit more complex [5] but chromium doesn't
use those (SkGraphics::SetOpenTypeSVGDecoderFactory is never called)

See also http://graphviz/#57cc93d0c3c73546055bec64fa8e27cb

[1] https://skia.googlesource.com/skia/+/9da67e212e59fbe4f144a92315ca6f8b876c9c01/src/ports/SkFontHost_FreeType_common.cpp#1567
[2] https://skia.googlesource.com/skia/+/9da67e212e59fbe4f144a92315ca6f8b876c9c01/src/ports/SkFontHost_FreeType_common.cpp#1078
[3] https://skia.googlesource.com/skia/+/9da67e212e59fbe4f144a92315ca6f8b876c9c01/src/ports/SkScalerContext_win_dw.cpp#677
[4] https://skia.googlesource.com/skia/+/9da67e212e59fbe4f144a92315ca6f8b876c9c01/src/ports/SkTypeface_fontations.cpp#1336

[4] https://skia.googlesource.com/skia/+/9da67e212e59fbe4f144a92315ca6f8b876c9c01/modules/svg/src/SkSVGOpenTypeSVGDecoder.cpp#160

Bug: 516457532, 513820666
Fixed: 516457532
Change-Id: If852aac417e04f5574dd073138abbe74f792734d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1245617
Reviewed-by: Florin Malita <fmalita@google.com>
---

diff --git a/include/core/SkSerialProcs.h b/include/core/SkSerialProcs.h
index 020945d..c5d3850 100644
--- a/include/core/SkSerialProcs.h
+++ b/include/core/SkSerialProcs.h
@@ -8,6 +8,7 @@
 #ifndef SkSerialProcs_DEFINED
 #define SkSerialProcs_DEFINED
 
+#include "include/core/SkFourByteTag.h"
 #include "include/core/SkRefCnt.h"
 #include "include/private/base/SkAPI.h"
 
@@ -86,6 +87,8 @@
 using SkDeserialTypefaceStreamProc = sk_sp<SkTypeface> (*)(SkStream&, void* ctx);
 using SkDeserialTypefaceProc = sk_sp<SkTypeface> (*)(const void* data, size_t length, void* ctx);
 
+using SkDeserialAllowTagsProc = bool(*)(SkFourByteTag, void* ctx);
+
 struct SK_API SkSerialProcs {
     SkSerialPictureProc fPictureProc = nullptr;
     void*               fPictureCtx = nullptr;
@@ -115,6 +118,10 @@
     // parameters and returns a bool). Given that there are only two valid implementations of that
     // proc, we just insert the bool directly.
     bool                         fAllowSkSL = true;
+
+    // Can be used to restrict the tags that will be deserialized from SkPictures.
+    SkDeserialAllowTagsProc      fAllowTagsProc = nullptr;
+    void*                        fAllowTagsCtx = nullptr;
 };
 
 #endif
diff --git a/src/core/SkDebugUtils.h b/src/core/SkDebugUtils.h
index a32b2e4..8f5012f 100644
--- a/src/core/SkDebugUtils.h
+++ b/src/core/SkDebugUtils.h
@@ -8,6 +8,7 @@
 #ifndef SkDebugUtils_DEFINED
 #define SkDebugUtils_DEFINED
 
+#include "include/core/SkFourByteTag.h"
 #include "include/core/SkTileMode.h"
 #include "include/private/base/SkAssert.h"
 #include "include/private/base/SkDebug.h"
@@ -51,5 +52,9 @@
 inline void SkDumpBuffer(uint8_t*, int, int, int) {}
 #endif
 
+static inline void SkDumpTag(SkFourByteTag tag) {
+    SkDebugf("%c%c%c%c", (char)(tag >> 24), (char)(tag >> 16) & 0xFF, (char)(tag >> 8) & 0xFF, (char)tag & 0xFF);
+}
+
 
 #endif // SkDebugUtils_DEFINED
diff --git a/src/core/SkGlyph.cpp b/src/core/SkGlyph.cpp
index 192868d..26c8004 100644
--- a/src/core/SkGlyph.cpp
+++ b/src/core/SkGlyph.cpp
@@ -19,6 +19,7 @@
 #include "include/private/base/SkTo.h"
 #include "src/base/SkArenaAlloc.h"
 #include "src/base/SkBezierCurves.h"
+#include "src/core/SkPictureData.h"
 #include "src/core/SkReadBuffer.h"
 #include "src/core/SkScalerContext.h"
 #include "src/core/SkWriteBuffer.h"
@@ -48,6 +49,13 @@
     // purposes (see b/40064011, b/40064341)
     SkDeserialProcs procs;
     procs.fAllowSkSL = false;
+    procs.fAllowTagsProc = [](SkFourByteTag tag, void*) -> bool {
+        return tag == SK_PICT_BUFFER_SIZE_TAG ||
+               tag == SK_PICT_FACTORY_TAG ||
+               tag == SK_PICT_PAINT_BUFFER_TAG ||
+               tag == SK_PICT_PATH_BUFFER_TAG ||
+               tag == SK_PICT_READER_TAG;
+    };
     sk_sp<SkPicture> picture = SkPicture::MakeFromData(pictureData.get(), &procs);
     if (!buffer.validate(picture != nullptr)) {
         return nullptr;
diff --git a/src/core/SkPictureData.cpp b/src/core/SkPictureData.cpp
index 3382e63..6a1adca 100644
--- a/src/core/SkPictureData.cpp
+++ b/src/core/SkPictureData.cpp
@@ -18,6 +18,7 @@
 #include "include/private/base/SkTemplates.h"
 #include "include/private/base/SkTo.h"
 #include "src/base/SkAutoMalloc.h"
+#include "src/core/SkDebugUtils.h"
 #include "src/core/SkPicturePriv.h"
 #include "src/core/SkPictureRecord.h"
 #include "src/core/SkPtrRecorder.h"
@@ -309,6 +310,11 @@
                                    const SkDeserialProcs& procs,
                                    SkTypefacePlayback* topLevelTFPlayback,
                                    int recursionLimit) {
+    if (procs.fAllowTagsProc && !procs.fAllowTagsProc(tag, procs.fAllowTagsCtx)) {
+        // Typefaces are always set but if there's 0 of them, we won't mark the stream as invalid
+        // if the typeface tag is not allowed.
+        return size == 0;
+    }
     switch (tag) {
         case SK_PICT_READER_TAG:
             SkASSERT(nullptr == fOpData);
@@ -456,6 +462,15 @@
 }
 
 void SkPictureData::parseBufferTag(SkReadBuffer& buffer, uint32_t tag, uint32_t size) {
+#if defined(SK_DUMP_TAGS)
+    SkDebugf("parseBufferTag ");
+    SkDumpTag(tag);
+    SkDebugf(" size %u\n", size);
+#endif
+    if (!buffer.allowTags(tag)) {
+        buffer.validate(size == 0);
+        return;
+    }
     switch (tag) {
         case SK_PICT_PAINT_BUFFER_TAG: {
             if (!buffer.validate(SkTFitsIn<int>(size))) {
diff --git a/src/core/SkPictureData.h b/src/core/SkPictureData.h
index ecfef8a..4626fc5 100644
--- a/src/core/SkPictureData.h
+++ b/src/core/SkPictureData.h
@@ -71,7 +71,7 @@
 #define SK_PICT_PAINT_BUFFER_TAG    SkSetFourByteTag('p', 'n', 't', ' ')
 #define SK_PICT_PATH_BUFFER_TAG     SkSetFourByteTag('p', 't', 'h', ' ')
 #define SK_PICT_TEXTBLOB_BUFFER_TAG SkSetFourByteTag('b', 'l', 'o', 'b')
-#define SK_PICT_SLUG_BUFFER_TAG SkSetFourByteTag('s', 'l', 'u', 'g')
+#define SK_PICT_SLUG_BUFFER_TAG     SkSetFourByteTag('s', 'l', 'u', 'g')
 #define SK_PICT_VERTICES_BUFFER_TAG SkSetFourByteTag('v', 'e', 'r', 't')
 #define SK_PICT_IMAGE_BUFFER_TAG    SkSetFourByteTag('i', 'm', 'a', 'g')
 
diff --git a/src/core/SkReadBuffer.h b/src/core/SkReadBuffer.h
index 7d2c92e..948b55c 100644
--- a/src/core/SkReadBuffer.h
+++ b/src/core/SkReadBuffer.h
@@ -11,6 +11,7 @@
 #include "include/core/SkColor.h"
 #include "include/core/SkColorFilter.h"
 #include "include/core/SkFlattenable.h"
+#include "include/core/SkFourByteTag.h"
 #include "include/core/SkImageFilter.h"
 #include "include/core/SkPaint.h"
 #include "include/core/SkPathEffect.h"
@@ -186,6 +187,13 @@
     bool allowSkSL() const { return fAllowSkSL; }
     void setAllowSkSL(bool allow) { fAllowSkSL = allow; }
 
+    bool allowTags(SkFourByteTag tag) const {
+        if (!fProcs.fAllowTagsProc) {
+            return true;
+        }
+        return fProcs.fAllowTagsProc(tag, fProcs.fAllowTagsCtx);
+    }
+
     /**
      *  If isValid is false, sets the buffer to be "invalid". Returns true if the buffer
      *  is still valid.
Loading diff…

Original Bug Report

reported by vm...@google.com

GPU font-parsing boundary bypass via deserialization of SkPicture-backed glyphs

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: When deserializing SkPicture-backed glyph drawables, the GPU process uses deserialization procs that leave the typeface stream proc as null. This potentially allows a compromised renderer to bypass font-parsing isolation boundaries by embedding raw font bytes inside serialized SkPictures. This fallback mechanism dispatches the untrusted stream directly to in-process font engines such as FreeType.

Affected files:

  • third_party/skia/src/core/SkGlyph.cpp
  • third_party/skia/src/core/SkPictureData.cpp
  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/service_font_manager.cc
  • third_party/skia/src/text/gpu/SkChromeRemoteGlyphCache.cpp
  • third_party/skia/src/core/SkStrike.cpp
  • third_party/skia/src/core/SkTypeface.cpp

Estimated timestamp from git blame: 2022-02-08

Potential Security Boundary Bypass in GPU Font Deserialization

Summary

There is a potential security boundary bypass in the OOP-rasterization path. A compromised renderer could potentially bypass the font-parsing isolation boundary and pass raw, unsanitized font bytes directly to in-process font parsers (like FreeType) running inside the GPU process. This bypasses the intended SkTypefaceProxy design, which is structured to ensure that typeface data crosses the process boundary solely as metadata while glyphs arrive pre-rasterized.

Mechanism and Code Walkthrough

When the GPU-side out-of-process (OOP) raster strike client deserializes a glyph drawable, it constructs an SkPicture from renderer-supplied bytes via SkPictureBackedGlyphDrawable::MakeFromBuffer (third_party/skia/src/core/SkGlyph.cpp, line 37).

SkDeserialProcs procs;
procs.fAllowSkSL = false;
sk_sp<SkPicture> picture = SkPicture::MakeFromData(pictureData.get(), &procs);

However, procs.fTypefaceStreamProc is left as nullptr.

During deserialization in SkPictureData::parseStreamTag (third_party/skia/src/core/SkPictureData.cpp, line 340), when handling the SK_PICT_TYPEFACE_TAG section, the code checks if a custom deserialization proc is present:

if (procs.fTypefaceStreamProc) {
    tf = procs.fTypefaceStreamProc(*stream, procs.fTypefaceCtx);
} else {
    tf = SkTypeface::MakeDeserialize(stream, nullptr);   // <-- Fallback
}

Because fTypefaceStreamProc is null, Skia falls back to calling SkTypeface::MakeDeserialize(stream, nullptr) directly on the untrusted stream. This method parses an SkFontDescriptor containing an embedded font-stream blob and an attacker-controlled factoryId, and dispatches it directly to one of the compiled-in decoders, such as FreeType (SkTypeface_FreeType::MakeFromStream at third_party/skia/src/ports/SkFontHost_FreeType.cpp, line 2047).

This behavior exposes the GPU process to raw font parsing of attacker-controlled files. On platforms like Android where the GPU process is unsandboxed, a parser-level vulnerability (e.g., in FreeType) could lead to a direct sandbox escape.

Potential Attack Path

(Note: These are suggested steps based on static code analysis; our tools cannot execute code or verify this with a live PoC)

  1. A compromised renderer prepares a malicious SkPicture payload containing an SK_PICT_TYPEFACE_TAG entry with an embedded, malformed font stream.
  2. The renderer transmits this payload inside an OOP-raster command via the glRasterCHROMIUM shared memory font transfer buffer.
  3. The GPU process processes the command and invokes ServiceFontManager::Deserialize to ingest the strike data.
  4. The strike client processes drawable glyphs, calling SkPictureBackedGlyphDrawable::MakeFromBuffer.
  5. During picture deserialization, Skia falls back to SkTypeface::MakeDeserialize because no fTypefaceStreamProc is set.
  6. The embedded stream is matched against FreeType or other registered decoders, parsing the malformed font bytes in the GPU process context.

Suggested Remediation

To prevent fallback typeface deserialization, SkPictureBackedGlyphDrawable::MakeFromBuffer should explicitly register a restrictive fTypefaceStreamProc callback inside its SkDeserialProcs struct that rejects raw stream deserialization or returns an empty typeface, ensuring that untrusted font bytes are never dispatched to in-process font parsers.

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker