CVE-2026-16417
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/GrDrawingManager.cpp |
modified | |
ifsrc/gpu/ganesh/SurfaceContext.cpp |
modified | |
FailingFlushCallbacktests/ReadWritePixelsGpuTest.cpp |
modified | |
iftests/ReadWritePixelsGpuTest.cpp |
modified |
Files Changed
src/gpu/ganesh/GrDrawingManager.cppsrc/gpu/ganesh/SurfaceContext.cpptests/ReadWritePixelsGpuTest.cpp
Patch
From 731888c38ee41e5892488f1edb8b45160fa9863c Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
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 <michaelludwig@google.com>
Commit-Queue: Thomas Smith <thomsmit@google.com>
---
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);
+ }
+}
+
Original Bug Report
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.cppthird_party/skia/src/gpu/ganesh/GrDrawingManager.cppthird_party/skia/src/gpu/ganesh/ops/AtlasPathRenderer.cppthird_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 queuedfillRectToRectWithFPcopy task. flush()still returnstrue(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.)
- 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’sfScratchMap. - 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
AtlasPathRendereror allocating extremely large SharedImages to induce memory pressure). - The attacker issues a
ReadbackARGBImagePixelsINTERNALIPC command on the raster command buffer, settingdst_sk_alpha_typetokUnpremul_SkAlphaTypeanddst_sk_color_typetokRGBA_8888_SkColorTypeto forcecanvas2DFastPathduringreadPixels. - Skia’s
readPixelsrecycles the victim’s scratch texture but fails to run the draw-overwrite task because the pre-flush/allocator failure aborts render task execution. - 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.