CVE-2026-14429
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
iftests/SlugTest.cpp |
modified |
Files Changed
src/text/gpu/SubRunContainer.cpptests/SlugTest.cpp
Patch
From eac15152bf4bea57c0720d8e9c16702393a2b24b Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <kjlubick@google.com>
Date: Mon, 08 Jun 2026 18:12:57 +0000
Subject: [PATCH] Avoid improper mask formats for SDFT runs
SDFTSubRun has a hard assumption of the kA8 mask format and
if the vertex filler differs, there will be a memory mismatch.
This catches it when deserializing the Slug and changes the
debug-only assert to be runtime to make sure we don't miss other
places.
Bug: 520571816
Fixed: 520571816
Change-Id: I9ae3e509397e23d38d621cb7eb8bb5b955a43d7d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1257397
Commit-Queue: Kaylee Lubick <kjlubick@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Thomas Smith <thomsmit@google.com>
---
diff --git a/src/text/gpu/SubRunContainer.cpp b/src/text/gpu/SubRunContainer.cpp
index 6d2a7e3..6465c50 100644
--- a/src/text/gpu/SubRunContainer.cpp
+++ b/src/text/gpu/SubRunContainer.cpp
@@ -892,7 +892,7 @@
, fUseLCDText{useLCDText}
, fAntiAliased{antiAliased}
, fMatrixRange{matrixRange} {
- SkASSERT(fVertexFiller.maskFormat() == MaskFormat::kA8);
+ SkASSERT_RELEASE(fVertexFiller.maskFormat() == MaskFormat::kA8);
}
static SubRunOwner Make(SkZip<const SkPackedGlyphID, const SkPoint> accepted,
@@ -943,6 +943,8 @@
skglyph::kSDFT, kGlyphInsetting);
if (!buffer.validate(vertexFiller.has_value())) { return nullptr; }
+ if (!buffer.validate(vertexFiller->maskFormat() == MaskFormat::kA8)) { return nullptr; }
+
return alloc->makeUnique<SDFTSubRun>(useLCD,
isAntiAliased,
matrixRange,
diff --git a/tests/SlugTest.cpp b/tests/SlugTest.cpp
index 1c81925..9e14972 100644
--- a/tests/SlugTest.cpp
+++ b/tests/SlugTest.cpp
@@ -22,6 +22,7 @@
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
#include "include/private/base/SkTDArray.h"
#include "include/private/chromium/Slug.h"
+#include "src/gpu/MaskFormat.h"
#include "src/text/gpu/SlugImpl.h"
#include "src/utils/SkFloatUtils.h"
#include "tests/CtsEnforcement.h"
@@ -150,3 +151,65 @@
sktext::gpu::Slug::Deserialize(writableData.get(), size, nullptr);
REPORTER_ASSERT(reporter, forgedSlug == nullptr);
}
+
+DEF_GANESH_TEST_FOR_CONTEXTS(Slug_b520571816,
+ skgpu::IsRenderingContext,
+ reporter,
+ ctxInfo,
+ set_sdf_options,
+ CtsEnforcement::kNextRelease) {
+ auto dContext = ctxInfo.directContext();
+
+ SkImageInfo info = SkImageInfo::MakeN32Premul(256, 256);
+ auto surface = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, info);
+ REPORTER_ASSERT(reporter, surface);
+ auto canvas = surface->getCanvas();
+
+ // Scale enough to force SDFT.
+ SkMatrix canvasMatrix = SkMatrix::Scale(10.0f, 10.0f);
+ canvas->save();
+ canvas->setMatrix(canvasMatrix);
+
+ auto typeface = ToolUtils::CreatePortableTypeface("serif", SkFontStyle());
+ SkFont font(typeface);
+ font.setSize(24);
+
+ static const char* kText = "A";
+ SkTextBlobBuilder builder;
+ const SkTextBlobBuilder::RunBuffer& runBuf = builder.allocRun(font, 1, 0, 0);
+ font.textToGlyphs(kText, 1, SkTextEncoding::kUTF8, SkSpan(runBuf.glyphs, 1));
+ auto blob = builder.make();
+
+ SkPaint paint;
+ paint.setAntiAlias(true);
+
+ sk_sp<sktext::gpu::Slug> slug = sktext::gpu::Slug::ConvertBlob(canvas, *blob, {0, 0}, paint);
+ SkASSERT_RELEASE(slug);
+ canvas->restore();
+
+ sk_sp<SkData> data = slug->serialize();
+ SkASSERT_RELEASE(data && data->size());
+
+ size_t size = data->size();
+ std::unique_ptr<uint8_t[]> writableData(new uint8_t[size]);
+ memcpy(writableData.get(), data->data(), size);
+
+ // Corrupt the maskFormat of VertexFiller inside SDFTSubRun at offset 208 from
+ // kA8 (0) to kARGB (2).
+ if (size < 212) {
+ ERRORF(reporter, "Serialized Slug is too small to contain maskFormat!");
+ return;
+ }
+ int32_t* formatPtr = reinterpret_cast<int32_t*>(writableData.get() + 208);
+ if (*formatPtr != 0) {
+ ERRORF(reporter, "maskFormat is not kA8 at expected offset 208!");
+ return;
+ }
+ *formatPtr = static_cast<int32_t>(skgpu::MaskFormat::kARGB);
+
+ // Since our validation fix will be active, this must fail and return nullptr. Previously, when
+ // we went to draw it, the unexpected mask format would cause issues.
+ sk_sp<sktext::gpu::Slug> forgedSlug =
+ sktext::gpu::Slug::Deserialize(writableData.get(), size, nullptr);
+ REPORTER_ASSERT(reporter, forgedSlug == nullptr);
+}
Original Bug Report
GPU Process Info Leak via Forged MaskFormat in Ganesh SDFTSubRun Deserialization
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 compromised renderer can potentially deserialize a forged SDFTSubRun with a mask format of kARGB, which is normally reserved for color bitmap text. This causes a stride mismatch between the geometry processor (which expects a 16-byte or 20-byte stride) and the CPU-side vertex filler (which writes 12-byte or 16-byte structs). As a result, uninitialized memory from a shared GPU-process staging buffer pool is uploaded and rendered to the GPU, potentially leaking cross-origin graphics data.
Affected files:
third_party/skia/src/text/gpu/SubRunContainer.cppthird_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cppthird_party/skia/src/gpu/ganesh/text/GlyphData.cpp
Estimated timestamp from git blame: 2022-02-23
Detailed Description
A security vulnerability potentially exists in Skia’s Ganesh text-rendering pipeline when deserializing Slug objects from a compromised renderer. Specifically, the deserialization of SDFTSubRun does not validate that the maskFormat is restricted to kA8. This leads to a mismatch between the GPU vertex stride (defined by the Geometry Processor) and the CPU-side vertex fill stride, resulting in an uninitialized memory disclosure from the GPU process.
Analysis of the Root Cause
-
Deserialization Validation Deficit: In
third_party/skia/src/text/gpu/SubRunContainer.cpp,SDFTSubRun::MakeFromBufferis used to deserialize SDF subruns from an untrusted renderer:static SubRunOwner MakeFromBuffer(SkReadBuffer& buffer, ...)It reads the
VertexFillerusingmake_vertex_filler_from_buffer:MaskFormat maskFormat = buffer.read32LE(MaskFormat::kLast);Since
MaskFormat::kLastisMaskFormat::kARGB(value 2), a compromised renderer can supplykARGBand pass deserialization. The constructor ofSDFTSubRuncontains only a debug-only assertion (SkASSERT) checking thatmaskFormat == MaskFormat::kA8, which is compiled out of release/production builds of Chrome. -
GPU Allocation (16-Byte Stride): When drawing the subrun,
AtlasTextOp::Makeexecutes. BecauseglyphParams().isSDFis unconditionally true for anSDFTSubRun, it routes to the distance-field path.AtlasTextOpbuilds aGrDistanceFieldA8TextGeoProcwhose vertex layout is unconditionally composed of:inPosition: 8 bytes (kFloat2_GrVertexAttribTypefor non-perspective)inColor: 4 bytes (kUByte4_norm_GrVertexAttribType)inTextureCoords: 4 bytes (kUShort2_GrVertexAttribType)
This results in a total vertex stride of exactly 16 bytes (or 20 bytes if perspective is active). The vertex buffer is then allocated from the shared pool
GrVertexBufferAllocPoolusing this 16-byte stride. -
CPU Fill (12-Byte Stride): During the draw preparation,
GlyphData::fillVertexDatahandles the vertex data population on the CPU side. It determines the struct size based onvf.maskFormat():if (!positionMatrix.hasPerspective()) { if (vf.maskFormat() == MaskFormat::kARGB) { using Quad = ARGB2DVertex[4]; ...Because the attacker forged
maskFormattokARGB, the code usesARGB2DVertex, which is only 12 bytes in size (since theGrColorfield is discarded). Thus, Skia writes only 12 bytes per vertex but advances the buffer allocation index using the 16-byte stride requested by the geometry processor. -
Uninitialized Memory Leak: This mismatch leaves 4 bytes (or 8 bytes under perspective variations) uninitialized at the tail of each vertex. Because the vertex buffers are allocated with
ZeroInit::kNofrom a shared staging pool reused across multiple contexts and origins, these uninitialized bytes contain stale graphics data from prior draws. This stale data is uploaded to the GPU, rendered via the distance field shader, and can potentially be retrieved by the attacker via canvas readback (getImageData) or texture copying.
Potential Steps to Reproduce
Since our automated tooling does not currently have the capability to execute proof-of-concept code, these are theoretical steps demonstrating how an attacker could trigger the mismatch:
- From a compromised renderer, construct a serialized
DrawSlugOppaint-op. - In the
SDFTSubRunpayload, set theVertexFiller’smaskFormatfield to2(MaskFormat::kARGB). - Submit the paint op over the IPC channel to the GPU process to be rasterized into an OOP-R tile backing or an accelerated 2D canvas.
- Read back the rasterized output from the renderer side (e.g., via
CanvasRenderingContext2D.getImageData) and examine the returned pixel data for artifacts corresponding to leaked cross-origin GPU memory.
Suggested Fix
Validate the maskFormat inside SDFTSubRun::MakeFromBuffer in third_party/skia/src/text/gpu/SubRunContainer.cpp. If the format is not MaskFormat::kA8, the buffer should be marked invalid and deserialization should fail:
static SubRunOwner MakeFromBuffer(SkReadBuffer& buffer,
SubRunAllocator* alloc,
const SkStrikeClient* client) {
...
auto vertexFiller = make_vertex_filler_from_buffer(buffer, alloc, &glyphVector.value(),
skglyph::kSDFT, kGlyphInsetting);
if (!buffer.validate(vertexFiller.has_value())) { return nullptr; }
// Ensure the deserialized mask format is kA8 for SDFT subruns
if (!buffer.validate(vertexFiller->maskFormat() == MaskFormat::kA8)) { return nullptr; }
return alloc->makeUnique<SDFTSubRun>(useLCD,
isAntiAliased,
matrixRange,
std::move(*vertexFiller),
std::move(*glyphVector));
}
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
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.