High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Skia
DescriptionUninitialized Use in Skia
ComponentSkia
Bug ClassUninitialized Memory
Tracker521491024
Fix commit731888c38ee4 (skia) +85/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/GrDrawingManager.cpp
modified
if
src/gpu/ganesh/SurfaceContext.cpp
modified
FailingFlushCallback
tests/ReadWritePixelsGpuTest.cpp
modified
if
tests/ReadWritePixelsGpuTest.cpp
modified

Files Changed

  • src/gpu/ganesh/GrDrawingManager.cpp
  • src/gpu/ganesh/SurfaceContext.cpp
  • tests/ReadWritePixelsGpuTest.cpp
From 731888c38ee41e5892488f1edb8b45160fa9863c Mon Sep 17 00:00:00 2001
From: Thomas Smith <[email protected]>
Date: Thu, 16 Jul 2026 11:01:22 -0400
Subject: [PATCH] [ganesh] prevent stale readbacks

* Ganesh's SurfaceContext::readPixels did not consider whether the content it was attempting to readback was successfully rendered or not, leading to a scenario where stale texture data could potentially be readback.

* Add some state tracking so that a failed flush is propagated out of the drawing manager and to the surface context

Bug: b/521491024
Change-Id: Idb2b5eaccda7f4ec388e3dc415ee41d329d28533
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1295737
Reviewed-by: Michael Ludwig <[email protected]>
Commit-Queue: Thomas Smith <[email protected]>
---

diff --git a/src/gpu/ganesh/GrDrawingManager.cpp b/src/gpu/ganesh/GrDrawingManager.cpp
index 398bc35..55b58d9 100644
--- a/src/gpu/ganesh/GrDrawingManager.cpp
+++ b/src/gpu/ganesh/GrDrawingManager.cpp
@@ -175,6 +175,7 @@
     }
 
     bool cachePurgeNeeded = false;
+    bool flushSuccessful = false;
 
     if (preFlushSuccessful) {
         bool usingReorderedDAG = false;
@@ -205,8 +206,10 @@
             resourceAllocator.assign();
         }
 
-        cachePurgeNeeded = !resourceAllocator.failedInstantiation() &&
-                           this->executeRenderTasks(&flushState);
+        if (!resourceAllocator.failedInstantiation()) {
+            cachePurgeNeeded = this->executeRenderTasks(&flushState);
+            flushSuccessful = true;
+        }
     }
     this->removeRenderTasks();
 
@@ -226,7 +229,7 @@
     }
     fFlushing = false;
 
-    return true;
+    return flushSuccessful;
 }
 
 bool GrDrawingManager::submitToGpu() {
diff --git a/src/gpu/ganesh/SurfaceContext.cpp b/src/gpu/ganesh/SurfaceContext.cpp
index ba6bb93..efc3f09 100644
--- a/src/gpu/ganesh/SurfaceContext.cpp
+++ b/src/gpu/ganesh/SurfaceContext.cpp
@@ -292,7 +292,12 @@
         pt.fY = flip ? srcSurface->height() - pt.fY - dst.height() : pt.fY;
     }
 
-    dContext->priv().flushSurface(srcProxy.get());
+    bool hasPendingTasks =
+            dContext->priv().drawingManager()->getLastRenderTask(srcProxy.get()) != nullptr;
+    GrSemaphoresSubmitted flushResult = dContext->priv().flushSurface(srcProxy.get());
+    if (flushResult == GrSemaphoresSubmitted::kNo && hasPendingTasks) {
+        return false;
+    }
     dContext->submit();
     if (!dContext->priv().getGpu()->readPixels(srcSurface,
                                                SkIRect::MakePtSize(pt, dst.dimensions()),
diff --git a/tests/ReadWritePixelsGpuTest.cpp b/tests/ReadWritePixelsGpuTest.cpp
index 143ff72..c64b79e 100644
--- a/tests/ReadWritePixelsGpuTest.cpp
+++ b/tests/ReadWritePixelsGpuTest.cpp
@@ -43,7 +43,9 @@
 #include "src/gpu/ganesh/GrCaps.h"
 #include "src/gpu/ganesh/GrDataUtils.h"
 #include "src/gpu/ganesh/GrDirectContextPriv.h"
+#include "src/gpu/ganesh/GrDrawingManager.h"
 #include "src/gpu/ganesh/GrImageInfo.h"
+#include "src/gpu/ganesh/GrOnFlushResourceProvider.h"
 #include "src/gpu/ganesh/GrPixmap.h"
 #include "src/gpu/ganesh/GrSamplerState.h"
 #include "src/gpu/ganesh/GrSurfaceProxy.h"
@@ -243,6 +245,19 @@
 template <typename T>
 using GpuReadDstFn = SkAutoPixmapStorage(const T&);
 
+class FailingFlushCallback : public GrOnFlushCallbackObject {
+public:
+    bool preFlush(GrOnFlushResourceProvider*) override {
+        ++fCount;
+        return false;
+    }
+
+    int count() const { return fCount; }
+
+private:
+    int fCount = 0;
+};
+
 }  // anonymous namespace
 
 SkPixmap make_pixmap_have_valid_alpha_type(SkPixmap pm) {
@@ -1513,3 +1528,61 @@
 
     ComparePixels(syncResult.pixmap(), asyncResult, tol, error);
 }
+
+// readPixels() that uses an intermediate scratch surface should not return stale recycled scratch
+// contents if the implicit flush of the queued draw into that surface drops its render tasks.
+DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(ReadPixelsIntermediateFailedFlush,
+                                       reporter,
+                                       ctxInfo,
+                                       CtsEnforcement::kNextRelease) {
+    auto dContext = ctxInfo.directContext();
+    if (dContext->supportsProtectedContent()) {
+        return;
+    }
+
+    static constexpr int kSize = 100;
+    static constexpr SkColor kStaleColor = 0xFF112233;
+    static constexpr SkColor kSrcColor = 0xFF008800;
+
+    auto srcII = SkImageInfo::Make({kSize, kSize}, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
+    auto dstII = srcII.makeAlphaType(kUnpremul_SkAlphaType);
+    auto surf = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kYes, srcII);
+    if (!surf) {
+        return;
+    }
+
+    SkAutoPixmapStorage pixels;
+    pixels.alloc(dstII);
+
+    // Reading the unpremul destination from the premul source draws through an intermediate
+    // approx-fit surface. Do this once so a matching scratch surface lands in the resource cache
+    // with known stale contents.
+    surf->getCanvas()->clear(kStaleColor);
+    if (!surf->readPixels(pixels, 0, 0)) {
+        return;
+    }
+
+    surf->getCanvas()->clear(kSrcColor);
+    dContext->flushAndSubmit(GrSyncCpu::kYes);
+
+    // From here on the flush-time callback fails so any queued render tasks are discarded.
+    FailingFlushCallback failingCallback;
+    dContext->priv().addOnFlushCallbackObject(&failingCallback);
+
+    pixels.erase(SkColors::kTransparent);
+    bool ok = surf->readPixels(pixels, 0, 0);
+
+    dContext->priv().drawingManager()->testingOnly_removeOnFlushCallbackObject(&failingCallback);
+
+    if (ok) {
+        SkColor result = pixels.getColor(0, 0);
+        REPORTER_ASSERT(reporter,
+                        result == kSrcColor,
+                        "readPixels reported success but returned 0x%08x, expected 0x%08x",
+                        result,
+                        kSrcColor);
+    } else {
+        REPORTER_ASSERT(reporter, failingCallback.count() > 0);
+    }
+}
+
Loading diff…

Original Bug Report

reported by [email protected]

Potential GPU-Process Cross-Origin Texture Disclosure via Ganesh SurfaceContext::readPixels

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 logic vulnerability in Skia’s Ganesh backend could allow a compromised renderer to read cross-origin texture data. When executing a synchronous readback via the fast-path copy branch, Skia eagerly instantiates a scratch render target which may contain stale texture contents from another origin. If the subsequent drawing manager flush fails due to GPU memory pressure or atlas allocation issues, the copy operation is silently dropped while the readback still returns success with the stale data.

Affected files:

  • third_party/skia/src/gpu/ganesh/SurfaceContext.cpp
  • third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp
  • third_party/skia/src/gpu/ganesh/ops/AtlasPathRenderer.cpp
  • third_party/skia/src/gpu/ganesh/GrResourceAllocator.cpp

Estimated timestamp from git blame: 2019-05-10

Description

A potential information disclosure vulnerability exists in Skia’s Ganesh backend, specifically within SurfaceContext::readPixels. When a readback utilizes the fast-path copy branch (canvas2DFastPath or kCopyToTexture2D), Skia allocates an internal kApprox scratch render target to perform the readback. Under certain circumstances where a subsequent flush fails, this can allow a compromised renderer to read stale recycled-texture contents (cross-origin pixel bytes) from the global, shared cache instead of the correct source pixels.

Root Cause Analysis

In third_party/skia/src/gpu/ganesh/SurfaceContext.cpp:

if (readFlag == GrCaps::SurfaceReadPixelsSupport::kCopyToTexture2D || canvas2DFastPath) {
    ...
    auto sfc = dContext->priv().makeSFC(
            tempInfo, "SurfaceContext_ReadPixels", SkBackingFit::kApprox);   // 1. Deferred approx RT proxy
    ...
    sfc->fillRectToRectWithFP(..., std::move(fp));                           // 2. Queues OpsTask in shared DAG
    pt = {0, 0};
    tempCtx = std::move(sfc);
    ...
    return tempCtx->readPixels(dContext, dst, pt);                           // 3. Recurse on temp surface
}

The recursive call re-enters readPixels with srcProxy equal to the temporary SFC’s proxy. Because this proxy is non-lazy, it is immediately instantiated:

if (!srcProxy->instantiate(dContext->priv().resourceProvider())) {
    return false;
}

Since SkBackingFit::kApprox is used, the resource provider queries the shared GrResourceCache for a matching scratch texture. Because the GrResourceCache is shared globally on the GPU thread across all renderers on the same context, the returned texture may contain another origin’s last-rendered pixels.

Following instantiation, the code reaches the direct-read path:

dContext->priv().flushSurface(srcProxy.get());          // 4. flushSurface() return value is IGNORED
dContext->submit();                                      // 5. submit() return value is IGNORED
if (!dContext->priv().getGpu()->readPixels(srcSurface, ..., readDst, readRB)) {
    return false;
}

When flushSurface() invokes GrDrawingManager::flush(), several gates are checked. If any GrOnFlushCallbackObject::preFlush() returns false (e.g., AtlasPathRenderer::preFlush on atlas-texture allocation failure under GPU memory pressure) or if GrResourceAllocator::assign() fails to instantiate any other co-pending lazy proxy in the shared DAG:

  • The entire executeRenderTasks() execution is skipped.
  • The drawing manager calls removeRenderTasks(), which discards all tasks in the DAG, including the attacker’s queued fillRectToRectWithFP copy task.
  • flush() still returns true (signaling success but executing no tasks).

Because the copy task was discarded, the recycled scratch texture is never overwritten. The subsequent call to GrGpu::readPixels proceeds to copy the stale, recycled scratch texture contents containing the victim’s cross-origin pixel bytes into the caller’s shared memory buffer and reports success.

Potential Step-by-Step Attack Path

(Note: These are potential steps as our tooling does not currently have the capability to run code or compile a working PoC.)

  1. A compromised renderer causes a victim origin to perform GPU rasterization of a specific, predictable approximate size (e.g., $256 \times 256$ pixels), ensuring a dirty texture of that size is released and recycled into GrResourceCache’s fScratchMap.
  2. The attacker’s command buffer channel queues work in the shared DAG that is guaranteed to fail during flush (e.g., complex clipped paths targeting AtlasPathRenderer or allocating extremely large SharedImages to induce memory pressure).
  3. The attacker issues a ReadbackARGBImagePixelsINTERNAL IPC command on the raster command buffer, setting dst_sk_alpha_type to kUnpremul_SkAlphaType and dst_sk_color_type to kRGBA_8888_SkColorType to force canvas2DFastPath during readPixels.
  4. Skia’s readPixels recycles the victim’s scratch texture but fails to run the draw-overwrite task because the pre-flush/allocator failure aborts render task execution.
  5. The driver maps the un-overwritten GPU texture and copies the raw stale pixel data directly into the attacker’s shared memory buffer, returning success.

Suggested Fix

Propagate flush execution failures or require that any temporary scratch target allocated with SkBackingFit::kApprox is explicitly zero-initialized or cleared before reading back if the associated drawing tasks were skipped. Alternatively, ensure that SurfaceContext::readPixels validates that the required render tasks successfully executed before copying data from the GPU texture.

Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf


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