Critical chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Skia
DescriptionInsufficient validation of untrusted input in Skia
ComponentSkia
Bug ClassLogic Error
Tracker516457532
Fix commitf1b8ba877c07 (skia) +44/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

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
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