Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Skia
DescriptionUninitialized Use in Skia
ComponentSkia
Bug ClassUninitialized Memory
Tracker513780208
Fix commite202cf3ef8e1 (skia) +95/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
DEF_TEST
tests/SkRemoteGlyphCacheTest.cpp
modified

Files Changed

  • src/core/SkGlyph.cpp
  • src/core/SkScalerContext.cpp
  • tests/SkRemoteGlyphCacheTest.cpp
From e202cf3ef8e18c0f47f1d7098fb96efbdc1953c1 Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <[email protected]>
Date: Fri, 29 May 2026 15:16:50 -0400
Subject: [PATCH] Address potential MSAN issue in SkScalerContext

If malformed data was passed to SkStrikeClient, an unexpected
mask would pass through asserts in a release build and lead
to a mask being allocated that was 4x bigger than what
was written to.

To defend against this problem, we 1) reject masks that
aren't of the three formats SkScalerContext::GenerateImageFromPath
expects; 2) zero out the whole mask, regardless of how big
it is instead of relying on how big an A8 mask would be.

Additionally, I noticed that in the intermediateDst case
(e.g. for LCD text when we draw into an A8 and then later unpack
it to be LCD16) we weren't zeroing that intermediate buffer
which could be a problem if the glyph itself was small (but
the bounds were corrupted to be big). Thus, we zero that
intermediate A8 buffer too.

Change-Id: Ib7080fc45eb77ea19b3e733570da33cd339a51f8
Bug: 513780208
Fixed: 513780208
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1248536
Commit-Queue: Kaylee Lubick <[email protected]>
Auto-Submit: Kaylee Lubick <[email protected]>
Reviewed-by: Florin Malita <[email protected]>
---

diff --git a/src/core/SkGlyph.cpp b/src/core/SkGlyph.cpp
index 26c8004..7f440a4 100644
--- a/src/core/SkGlyph.cpp
+++ b/src/core/SkGlyph.cpp
@@ -406,6 +406,12 @@
         const bool pathIsHairline = buffer.readBool();
         const bool pathIsModified = buffer.readBool();
         if (auto path = buffer.readPath()) {
+            if (fMaskFormat != SkMask::kBW_Format &&
+                fMaskFormat != SkMask::kA8_Format &&
+                fMaskFormat != SkMask::kLCD16_Format) {
+                buffer.validate(false);
+                return 0;
+            }
             if (this->setPath(alloc, &path.value(), pathIsHairline, pathIsModified)) {
                 memoryIncrease += path->approximateBytesUsed();
             }
diff --git a/src/core/SkScalerContext.cpp b/src/core/SkScalerContext.cpp
index 85edc26..2d68c14 100644
--- a/src/core/SkScalerContext.cpp
+++ b/src/core/SkScalerContext.cpp
@@ -572,10 +572,11 @@
             sk_bzero(dstMask.image(), dstMask.computeImageSize());
             return;
         }
+        sk_bzero(dst.writable_addr(), dst.computeByteSize());
     } else {
         dst.reset(info, dstMask.image(), dstMask.fRowBytes);
     }
-    sk_bzero(dst.writable_addr(), dst.computeByteSize());
+    sk_bzero(dstMask.image(), dstMask.computeImageSize());
 
     skcpu::Draw draw;
     draw.fBlitterChooser = SkA8Blitter_Choose;
@@ -600,6 +601,7 @@
             pack4xHToMask(dst, dstMask, maskPreBlend, doBGR, verticalLCD);
             break;
         default:
+            SkUNREACHABLE;
             break;
     }
 }
diff --git a/tests/SkRemoteGlyphCacheTest.cpp b/tests/SkRemoteGlyphCacheTest.cpp
index 2b437a2..f357fce 100644
--- a/tests/SkRemoteGlyphCacheTest.cpp
+++ b/tests/SkRemoteGlyphCacheTest.cpp
@@ -13,6 +13,7 @@
 #include "include/core/SkColorType.h"
 #include "include/core/SkData.h"
 #include "include/core/SkFont.h"
+#include "include/core/SkFontMetrics.h"
 #include "include/core/SkFontStyle.h"
 #include "include/core/SkFontTypes.h"
 #include "include/core/SkGraphics.h"
@@ -33,9 +34,12 @@
 #include "include/private/base/SkMutex.h"
 #include "include/private/chromium/SkChromeRemoteGlyphCache.h"
 #include "include/private/chromium/Slug.h"
+#include "src/core/SkFontMetricsPriv.h"
 #include "src/core/SkFontPriv.h"
 #include "src/core/SkGlyph.h"
 #include "src/core/SkReadBuffer.h"
+#include "src/core/SkScalerContext.h"
+#include "src/core/SkStrikeCache.h"
 #include "src/core/SkStrikeSpec.h"
 #include "src/core/SkTHash.h"
 #include "src/core/SkTextBlobPriv.h"
@@ -1406,3 +1410,85 @@
 
     SkGraphics::SetTypefaceCacheCountLimit(prev1);  // restore orig
 }
+
+DEF_TEST(SkRemoteGlyphCache_b513780208, reporter) {
+    // Legitimate path glyphs should only use kBW_Format, kA8_Format, or kLCD16_Format.
+    // If a malicious strike provided a path with kARGB32_Format, it could trigger an
+    // uninitialized memory issue in SkScalerContext::GenerateImageFromPath.
+    //
+    // SkGlyph::addPathFromBuffer now rejects any path whose format is not an allowed type.
+
+    constexpr int kW = 200;
+    constexpr int kH = 4;
+    constexpr SkTypefaceID kServerTypefaceID = 0x65000000u;  // arbitrary
+    constexpr uint32_t kPackedGlyphID = 0x42u;               // arbitrary
+
+    auto BuildMaliciousStrikeData = [&]() {
+        SkBinaryWriteBuffer buffer{nullptr, 0, {}};
+
+        // --- typefaces ---
+        buffer.writeInt(1);  // typefaceCount
+        buffer.writeUInt(kServerTypefaceID);
+        buffer.writeInt(256);
+        buffer.write32(0);
+        buffer.writeBool(false);
+        buffer.writeBool(false);
+
+        // --- strikes ---
+        buffer.writeInt(1);                   // strikeCount
+        buffer.writeUInt(kServerTypefaceID);  // serverTypefaceID
+        buffer.writeUInt(1);                  // discardableHandleID
+
+        // SkDescriptor: Craft a Rec that triggers GenerateImageFromPath (fFrameWidth >= 0).
+        {
+            SkScalerContextRec rec;
+            rec.fTypefaceID = kServerTypefaceID;
+            rec.fTextSize = 16.0f;
+            rec.fPreScaleX = 1.0f;
+            rec.fPost2x2[0][0] = 1.0f;
+            rec.fPost2x2[1][1] = 1.0f;
+            rec.fFrameWidth = 0.0f;  // triggers fGenerateImageFromPath
+            rec.fMaskFormat = SkMask::kA8_Format;
+            SkAutoDescriptor ad{SkDescriptor::ComputeOverhead(1) + sizeof(rec)};
+            SkDescriptor* desc = ad.getDesc();
+            desc->addEntry(kRec_SkDescriptorTag, sizeof(rec), &rec);
+            desc->computeChecksum();
+            desc->flatten(buffer);
+        }
+
+        buffer.writeBool(false);  // fontMetricsInitialized == false
+        {
+            SkFontMetrics fm;
+            SkFontMetricsPriv::Flatten(buffer, fm);
+        }
+
+        buffer.writeInt(0);  // imagesCount
+
+        // Craft a path glyph with an invalid mask format (kARGB32_Format).
+        buffer.writeInt(1);  // pathsCount
+        buffer.writeUInt(kPackedGlyphID);
+        buffer.writePoint(SkPoint::Make(static_cast<float>(kW), 0.0f));
+        buffer.writeUInt((static_cast<uint32_t>(kW) << 16) | kH);
+        buffer.writeUInt(0);
+        buffer.writeUInt(static_cast<uint32_t>(SkMask::kARGB32_Format));
+        buffer.writeBool(true);  // hasPath
+        buffer.writeBool(false);
+        buffer.writeBool(false);
+        buffer.writePath(SkPath::Rect(SkRect::MakeXYWH(0, 0, 1, 1)));
+
+        buffer.writeInt(0);  // drawablesCount
+
+        return buffer.snapshotAsData();
+    };
+
+    sk_sp<SkData> blob = BuildMaliciousStrikeData();
+    REPORTER_ASSERT(reporter, blob);
+
+    SkStrikeCache cache;
+    auto discardableManager = sk_make_sp<DiscardableManager>();
+    SkStrikeClient client(discardableManager, /*isLogging=*/false, &cache);
+
+    // This should fail to read the strike data because SkGlyph::addPathFromBuffer
+    // now validates that path glyphs have an expected mask format.
+    REPORTER_ASSERT(reporter, !client.readStrikeData(blob->data(), blob->size()));
+}
Loading diff…

Original Bug Report

reported by [email protected]

Potential GPU process uninitialized heap disclosure in Skia's GenerateImageFromPath

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: A potential vulnerability in Skia’s glyph rasterization could allow a compromised renderer to disclose uninitialized heap memory from the GPU process. This occurs due to an incorrect buffer size calculation during the zero-initialization of glyph images in the ARGB32 format.

Affected files:

  • third_party/skia/src/core/SkScalerContext.cpp
  • third_party/skia/src/core/SkImageInfo.cpp
  • third_party/skia/src/core/SkGlyph.cpp
  • third_party/skia/src/text/gpu/SkChromeRemoteGlyphCache.cpp
  • third_party/skia/src/gpu/ganesh/text/GrAtlasManager.cpp

Estimated timestamp from git blame: 2021-02-08

Summary

A potential uninitialized heap memory disclosure vulnerability has been identified in the Skia library, affecting the Chromium GPU process. The issue is located in SkScalerContext::GenerateImageFromPath in third_party/skia/src/core/SkScalerContext.cpp. When rasterizing a glyph with the kARGB32_Format mask format (which requires 4 bytes per pixel), the function incorrectly calculates the number of bytes to zero-initialize. This results in the trailing portion of the glyph’s image buffer remaining uninitialized, potentially containing sensitive stale data from the GPU process heap.

Root Cause Analysis

In SkScalerContext::GenerateImageFromPath, when rasterizing a path into a glyph’s destination buffer, the code creates an intermediate SkImageInfo and SkAutoPixmapStorage to manage the drawing:

const SkImageInfo info = SkImageInfo::MakeA8(dstW, dstH);
SkAutoPixmapStorage dst;
// ...
dst.reset(info, dstMask.image(), dstMask.fRowBytes);
sk_bzero(dst.writable_addr(), dst.computeByteSize());

The vulnerability stems from using SkImageInfo::MakeA8 (which represents 1 byte per pixel) while the destination mask (dstMask) may be in kARGB32_Format (4 bytes per pixel). Even though the correct fRowBytes (which is 4 * width for ARGB32) is passed to dst.reset(), the dst.computeByteSize() call uses the A8 format for its calculation.

In third_party/skia/src/core/SkImageInfo.cpp, computeByteSize is implemented as: size_t bytes = (height - 1) * rowBytes + (width * bytesPerPixel)

For an A8 format, bytesPerPixel is 1. Thus, if the destination is ARGB32 with rowBytes = 4 * width, computeByteSize returns (height - 1) * 4 * width + width. However, the total buffer allocated for the ARGB32 glyph is height * 4 * width. This mismatch causes sk_bzero to leave the last 3 * width bytes of the buffer uninitialized.

In production release builds, the SkASSERT calls intended to prevent kARGB32_Format from reaching this path are removed, allowing a compromised renderer to trigger the vulnerable code.

Potential Trigger Path

  1. Strike Injection: A compromised renderer sends a crafted SkDescriptor via font-related IPC to the GPU process. By setting fFrameWidth to 0.0f (or any non-negative value), it forces the GPU-side SkScalerContext to set fGenerateImageFromPath = true.
  2. Glyph Definition: The renderer defines a glyph within this strike using fMaskFormat = SkMask::kARGB32_Format and provides an SkPath for the glyph.
  3. Vulnerable Rasterization: The renderer triggers a draw operation for this glyph. The GPU process allocates an uninitialized buffer for the image and calls GenerateImageFromPath. Due to the logic described above, the buffer is only partially zeroed.
  4. Memory Disclosure: The rasterized glyph, containing uninitialized GPU process heap memory, is uploaded to the shared glyph atlas. The renderer then draws this glyph to a canvas and reads back the pixels (e.g., via getImageData), disclosing the stale memory.

Suggested Fix

Ensure that SkScalerContext::GenerateImageFromPath uses the correct buffer size for zero-initialization, regardless of the mask format. A simple fix is to use the destination mask’s own size calculation:

// Suggested fix in third_party/skia/src/core/SkScalerContext.cpp
sk_bzero(dstMask.image(), dstMask.computeImageSize());

Alternatively, the SkImageInfo used for the pixmap should be initialized with the correct SkColorType corresponding to dstMask.fFormat.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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