CVE-2026-14427
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/ops/AtlasTextOp.cpp |
modified | |
iftests/SlugTest.cpp |
modified |
Files Changed
src/gpu/ganesh/ops/AtlasTextOp.cppsrc/text/gpu/SubRunContainer.cpptests/SlugTest.cpp
Patch
From a145861ad1c2650c58dd3ff0dbd92ae6f41f1e1b Mon Sep 17 00:00:00 2001 From: Kaylee Lubick <[email protected]> Date: Fri, 05 Jun 2026 20:51:04 +0000 Subject: [PATCH] Reject Slugs that have creationMatrix with perspective This shouldn't happen during normal use [1] but if the data is corrupted, there are some assumptions that can can cause issues, like the ones linked in the bug. This rejects those and turns one assert into an actual runtime check to provide defense in depth. [1] https://github.com/google/skia/blob/9eecbdc30f7da675edab96974b23174a9d521e0c/src/text/gpu/SubRunContainer.cpp#L1578-L1581 Bug: 520113415 Fixed: 520113415 Change-Id: I6ec23df9a23ea588fa89f7a62dc1f197fe3905fd Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1256016 Commit-Queue: Kaylee Lubick <[email protected]> Reviewed-by: Thomas Smith <[email protected]> --- diff --git a/src/gpu/ganesh/ops/AtlasTextOp.cpp b/src/gpu/ganesh/ops/AtlasTextOp.cpp index 27e919e..51ae890 100644 --- a/src/gpu/ganesh/ops/AtlasTextOp.cpp +++ b/src/gpu/ganesh/ops/AtlasTextOp.cpp @@ -565,11 +565,16 @@ auto& glyphData = subRun.glyphVector().accessBackendData<GlyphData>(); - SkDEBUGCODE(int strideCheck = SkToInt(glyphData.vertexStride(subRun.maskFormat(), - geo->fDrawMatrix))); - SkASSERTF(strideCheck == vertexStride, - "subRun stride: %d vertex buffer stride: %d\n", - strideCheck, vertexStride); + int strideCheck = SkToInt(glyphData.vertexStride(subRun.maskFormat(), geo->fDrawMatrix)); + if (strideCheck != vertexStride) { + // We (unexpectedly) have buffers of different sizes between CPU and GPU. Bail out. + SKIA_LOG_D( + "Warning: stride mismatch detected (subrun stride: %d vertex buffer stride: " + "%d). Aborting draw.\n", + strideCheck, + vertexStride); + return; + } const int subRunEnd = subRun.glyphCount(); diff --git a/src/text/gpu/SubRunContainer.cpp b/src/text/gpu/SubRunContainer.cpp index bd5f827..6d2a7e3 100644 --- a/src/text/gpu/SubRunContainer.cpp +++ b/src/text/gpu/SubRunContainer.cpp @@ -169,6 +169,11 @@ SkMatrix creationMatrix; buffer.readMatrix(&creationMatrix); + // The only valid creationMatrices do not have perspective and many rendering parts assume + // they are non-affine, so reject any malformed matrices here. + if (!buffer.validate(!creationMatrix.hasPerspective())) { + return std::nullopt; + } SkSpan<SkPoint> leftTop = make_points_from_buffer(buffer, alloc); if (leftTop.empty()) { diff --git a/tests/SlugTest.cpp b/tests/SlugTest.cpp index cc1ef79..1c81925 100644 --- a/tests/SlugTest.cpp +++ b/tests/SlugTest.cpp @@ -5,6 +5,8 @@ * found in the LICENSE file. */ +#include "include/core/SkBitmap.h" +#include "include/core/SkCanvas.h" #include "include/core/SkFont.h" #include "include/core/SkFontStyle.h" #include "include/core/SkFontTypes.h" @@ -20,6 +22,8 @@ #include "include/gpu/ganesh/SkSurfaceGanesh.h" #include "include/private/base/SkTDArray.h" #include "include/private/chromium/Slug.h" +#include "src/text/gpu/SlugImpl.h" +#include "src/utils/SkFloatUtils.h" #include "tests/CtsEnforcement.h" #include "tests/Test.h" #include "tools/ToolUtils.h" @@ -65,3 +69,84 @@ sk_sp<sktext::gpu::Slug> slug = sktext::gpu::Slug::ConvertBlob(canvas, *blob, {10, 10}, p); REPORTER_ASSERT(reporter, slug == nullptr); } + +static void set_sdf_options(GrContextOptions* options) { + options->fMinDistanceFieldFontSize = 4; + options->fGlyphsAsPathsFontSize = 256; + options->fSupportBilerpFromGlyphAtlas = true; +} + +DEF_GANESH_TEST_FOR_CONTEXTS(Slug_b520113415, + 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(); + + // This matrix is big enough such that with the font below... + SkMatrix canvasMatrix = SkMatrix::Scale(1.2345f, 6.7890f); + + canvas->save(); + canvas->setMatrix(canvasMatrix); + + // ... it will force SDFT (6.789 x 24 = ~162.9). + auto typeface = ToolUtils::CreatePortableTypeface("serif", SkFontStyle()); + SkFont font(typeface); + font.setSubpixel(true); + font.setSize(24); + + static const char* kText = "A"; + int glyphCount = font.countText(kText, 1, SkTextEncoding::kUTF8); + SkTDArray<SkGlyphID> glyphs; + glyphs.append(glyphCount); + font.textToGlyphs(kText, 1, SkTextEncoding::kUTF8, glyphs); + + SkTextBlobBuilder builder; + const SkTextBlobBuilder::RunBuffer& buf = builder.allocRun(font, glyphs.size(), 0, 0); + memcpy(buf.glyphs, glyphs.begin(), glyphs.size() * sizeof(SkGlyphID)); + auto blob = builder.make(); + + SkPaint paint; + paint.setAntiAlias(true); + + sk_sp<sktext::gpu::Slug> slug = sktext::gpu::Slug::ConvertBlob(canvas, *blob, {0, 0}, paint); + canvas->restore(); + + if (!slug) { + return; + } + + sk_sp<SkData> data = slug->serialize(); + if (!data || data->size() == 0) { + return; + } + + // Copy data to a writable buffer so we can corrupt it + size_t size = data->size(); + std::unique_ptr<uint8_t[]> writableData(new uint8_t[size]); + memcpy(writableData.get(), data->data(), size); + + + // The creationMatrix of VertexFiller inside SDFTSubRun is located 216 bytes in. + // We can overwrite it to have a matrix with perspective. + if (size < 216 + 9 * sizeof(float)) { + ERRORF(reporter, "Serialized Slug is too small to contain creationMatrix!"); + return; + } + float* f = reinterpret_cast<float*>(writableData.get() + 216); + f[0] = 1.0f; f[1] = 0.0f; f[2] = 0.0f; + f[3] = 0.0f; f[4] = 1.0f; f[5] = 0.0f; + f[6] = 0.0078125f; f[7] = 0.0f; f[8] = 1.0f; + + // Deserialize the forged slug. This should return nullptr. Previously, when we went to draw it, + // the perspective 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
Out-of-bounds heap write in Ganesh SDF text rendering path via mismatched predicates
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 heap out-of-bounds write exists in Ganesh’s signed distance field (SDF) text rendering pipeline. A compromised renderer can decouple the perspective predicates used for vertex buffer allocation and CPU-side vertex filling by forging a serialized Slug. This mismatch can cause the GPU process to allocate a vertex buffer with a 16-byte stride but fill it using a 20-byte stride, writing up to 8KB past the buffer boundary.
Affected files:
third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cppthird_party/skia/src/gpu/ganesh/text/GlyphData.cppthird_party/skia/src/gpu/ganesh/effects/GrDistanceFieldGeoProc.cppthird_party/skia/src/text/gpu/SubRunContainer.cppthird_party/skia/src/text/gpu/VertexFiller.cppthird_party/skia/src/gpu/ganesh/Device.cppcc/paint/paint_op_reader.cc
Estimated timestamp from git blame: 2022-10-13
Description
A potential heap out-of-bounds write vulnerability has been identified in Skia’s Ganesh rendering backend, specifically within the signed distance field (SDF) text rendering path. The vulnerability stems from an inconsistency between the perspective predicates used to compute the GeometryProcessor’s vertex-attribute layout and those used to determine the structure size during the CPU-side vertex-fill process.
Root Cause Analysis
-
GeometryProcessor Stride Calculation: In
third_party/skia/src/gpu/ganesh/ops/AtlasTextOp.cpp:195, the perspective flagkPerspective_DistanceFieldEffectFlagis derived fromviewDiffMatrix.hasPerspective():DFGPFlags |= viewDiffMatrix.hasPerspective() ? kPerspective_DistanceFieldEffectFlag : 0;Here,
viewDiffMatrixis calculated as the difference between the drawing-timepositionMatrixand the serializedcreationMatrix(positionMatrix · creationMatrix⁻¹).If the perspective flag is not set,
GrDistanceFieldA8TextGeoProcselects a 2D position attribute (kFloat2_GrVertexAttribType, 8 bytes), resulting in a vertex stride of 16 bytes (8 bytes position + 4 bytes color + 4 bytes UV). -
CPU-Side Vertex Fill Structure Selection: In
third_party/skia/src/gpu/ganesh/text/GlyphData.cpp:299-320, the structure used to write the vertex data is chosen based directly onpositionMatrix.hasPerspective(), rather than theviewDifferencematrix:SkMatrix viewDifference = vf.viewDifference(positionMatrix); if (!positionMatrix.hasPerspective()) { using Quad = Mask2DVertex[4]; // 16B/vertex fill2D(quadData((Quad*)vertexBuffer), color, viewDifference); } else { using Quad = Mask3DVertex[4]; // 20B/vertex fill3D(quadData((Quad*)vertexBuffer), color, viewDifference); }If
positionMatrixcontains perspective,fill3Dwrites vertices asMask3DVertexstructures, which are 20 bytes each. -
De-synchronization of Predicates: A compromised renderer can manipulate both matrices across the IPC boundary. During Slug deserialization in
third_party/skia/src/text/gpu/SubRunContainer.cpp:171, thecreationMatrixis read raw from the shared buffer without validation to verify if it contains perspective:SkMatrix creationMatrix; buffer.readMatrix(&creationMatrix);
Potential Attack Scenario
Note: Our tooling does not currently have the capability to execute code, so this represents a suggested potential attack flow based on static analysis of the codebase.
- A compromised renderer updates the canvas CTM to an arbitrary invertible perspective matrix $P$ via
SetMatrixOp(P). - The renderer crafts and sends a serialized
DrawSlugOpcarrying a forgedSDFTSubRunwherecreationMatrixis also set to $P$. - During playback in the GPU process, the drawing context evaluates
positionMatrixas $P$ (has perspective). - The GPU process computes
viewDiff = positionMatrix * creationMatrix^-1 = P * P^-1 = Identity(no perspective). - Consequently,
calculate_sdf_parametersdoes not apply the perspective flag, setting the GP vertex stride to 16 bytes.target->makeVertexSpaceallocates $16 \times 4 \times N = 64 \times N$ bytes (e.g., 32,768 bytes for $N=512$). - However,
GlyphData::fillVertexDataevaluatespositionMatrix.hasPerspective() == trueand enters theelseblock, writing vertices asMask3DVertex(20-byte stride) at $80 \times N$ bytes (e.g., 40,960 bytes). - This results in up to an 8,192-byte heap out-of-bounds write past the allocated buffer. The written data (coordinates mapped through
viewDifferenceand the color) is fully specified by the attacker.
Suggested Remediation
To remediate this issue, the CPU-side vertex-filling logic should be aligned with the GP-side stride logic by keying the structure selection in GlyphData::fillVertexData on viewDifference.hasPerspective() rather than positionMatrix.hasPerspective(). Alternatively, strict matrix validation can be added during Slug deserialization to reject perspective creationMatrix inputs.
Evaluated with Chrome root at commit: 57b021e1fdae94a215627d29aeb1ccf2eb5b3e91
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.