CVE-2026-13781
Overview
Background
- `SkPicture` deserialization
- Skia reconstructs a recorded
SkPicturefrom 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 inSkGlyph.cppviaSkPicture::MakeFromDatafrom anSkReadBuffer(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(typeSkDeserialAllowTagsProc) plusfAllowTagsCtxso callers can allow-list whichSkFourByteTagvalues 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.
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
- Craft a malicious glyph drawable
An attacker constructs a serialized
SkPictureBackedGlyphDrawablewhose embeddedSkPicturestream carries a tag that a glyph drawable would never legitimately contain, such asSK_PICT_TEXTBLOB_BUFFER_TAGor a typeface tag, with attacker-chosen payload bytes. - Deliver it to the deserializer
The buffer reaches the
SkGlyph.cppdeserialization path that callsSkPicture::MakeFromData; pre-patch, the only restriction wasfAllowSkSL = 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). - Exercise unintended parsing code
Before the fix, the stream-tag parser in
SkPictureData.cppandSkPictureData::parseBufferTagdispatched 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. - 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
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
| Function | Change | Notes |
|---|---|---|
switchsrc/core/SkPictureData.cpp |
modified | |
ifsrc/core/SkReadBuffer.h |
modified |
Files Changed
include/core/SkSerialProcs.hsrc/core/SkDebugUtils.hsrc/core/SkGlyph.cppsrc/core/SkPictureData.cppsrc/core/SkPictureData.hsrc/core/SkReadBuffer.h
Audit Directions
- General-purpose deserializers on constrained inputsLook for other callers of
SkPicture::MakeFromData/SkReadBufferthat only set narrow flags (likefAllowSkSL = 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 pathsAudit serialization used across Chrome process boundaries for fonts, glyphs and typefaces (e.g.
SkTypefaceandSkTextBlobdeserialization), where untrusted producers can embed richer object graphs than the consumer needs. - Early-exit validation semanticsReview 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.
Patch
From f1b8ba877c07cccc080451fc4f6b17c27fa76581 Mon Sep 17 00:00:00 2001 From: Kaylee Lubick <[email protected]> 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 <[email protected]> --- 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.
Original Bug Report
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.cppthird_party/skia/src/core/SkPictureData.cppgpu/command_buffer/service/raster_decoder.ccgpu/command_buffer/service/service_font_manager.ccthird_party/skia/src/text/gpu/SkChromeRemoteGlyphCache.cppthird_party/skia/src/core/SkStrike.cppthird_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)
- A compromised renderer prepares a malicious
SkPicturepayload containing anSK_PICT_TYPEFACE_TAGentry with an embedded, malformed font stream. - The renderer transmits this payload inside an OOP-raster command via the
glRasterCHROMIUMshared memory font transfer buffer. - The GPU process processes the command and invokes
ServiceFontManager::Deserializeto ingest the strike data. - The strike client processes drawable glyphs, calling
SkPictureBackedGlyphDrawable::MakeFromBuffer. - During picture deserialization, Skia falls back to
SkTypeface::MakeDeserializebecause nofTypefaceStreamProcis set. - 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.